UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

496 lines (495 loc) 20.6 kB
/** * ExplorationTracker — 统一的 AI 探索生命周期控制器 * * 合并了三个原本各自为政的系统: * 1. PhaseRouter (ContextWindow.js) — 阶段状态机 * 2. 探索进度追踪 (原内联逻辑) — 信息增量检测 * 3. ReasoningLayer 行为控制部分 — 反思/规划/停滞 nudge * * 职责(拆分后的编排层): * - 阶段状态机: phase 持有 + 转换规则 * - 信号收集: 委托 SignalDetector * - Nudge 生成: 委托 NudgeGenerator * - 计划跟踪: 委托 PlanTracker * - Graceful exit: 管理轮次耗尽后的优雅退出流程 * * 不拥有的职责: * - 推理链数据收集 → ReasoningTrace (纯数据,不影响行为) * - 上下文压缩 → ContextWindow * - 工具注册与执行 → ToolRegistry * - 跨对话记忆 → Memory / WorkingMemory * * @module ExplorationTracker */ import Logger from '#infra/logging/Logger.js'; import { createBootstrapStrategy, STRATEGY_ANALYST, STRATEGY_PRODUCER, } from './exploration/ExplorationStrategies.js'; import { NudgeGenerator } from './exploration/NudgeGenerator.js'; import { PlanTracker } from './exploration/PlanTracker.js'; import { SEARCH_TOOLS, SignalDetector } from './exploration/SignalDetector.js'; // ─── ExplorationTracker 主类 ───────────────────────────── export class ExplorationTracker { /** 策略配置 */ #strategy; /** 预算配置 */ #budget; /** 当前阶段 */ #phase; /** 日志器 */ #logger; /** 信号总线(可选) */ #signalBus; // ── 子模块 ── #signalDetector; #nudgeGenerator; #planTracker; // ── 信号指标 ── #metrics = { uniqueFiles: new Set(), uniquePatterns: new Set(), uniqueQueries: new Set(), totalToolCalls: 0, submitCount: 0, roundsSinceNewInfo: 0, roundsSinceSubmit: 0, iteration: 0, searchRoundsInPhase: 0, phaseRounds: 0, consecutiveIdleRounds: 0, }; // ── 阶段控制 ── /** 是否刚完成阶段转换(用于 pending nudge) */ #justTransitioned = false; /** 转换前的旧阶段 */ #transitionFromPhase = null; // ── Graceful exit 控制 ── /** 进入 graceful exit 的轮次 */ #gracefulExitRound = null; /** tick 是否已调用(用于 rollback) */ #ticked = false; /** 提交工具名(用于 nudge 文本生成) */ #submitToolName = 'submit_knowledge'; /** 管线类型标识 — 统一场景判别(替代 submitToolName / strategy.name 字符串比较) */ #pipelineType; /** 当前阶段开始时间(用于 dwell time 统计) */ #phaseStartTime = Date.now(); /** * @param strategy 策略配置对象 * @param budget 预算配置 { maxIterations, searchBudget, ... } */ constructor(strategy, budget) { this.#strategy = strategy; this.#budget = { maxIterations: 24, searchBudget: 18, searchBudgetGrace: 10, maxSubmits: 10, softSubmitLimit: 8, idleRoundsToExit: 3, ...budget, }; this.#submitToolName = budget.submitToolName || 'submit_knowledge'; // pipelineType 显式传入 > 从策略名推断默认值 this.#pipelineType = budget.pipelineType || (strategy.name === 'analyst' ? 'analyst' : 'bootstrap'); this.#phase = strategy.phases[0]; this.#logger = Logger.getInstance(); this.#signalBus = budget.signalBus ?? null; // 初始化子模块 this.#signalDetector = new SignalDetector(this.#metrics); this.#nudgeGenerator = new NudgeGenerator(); this.#planTracker = new PlanTracker(); } // ─── 静态工厂 ───────────────────────────────────────── /** * 根据调用参数解析应使用的策略 * @param opts AgentRuntime execute 的选项 * @param budget 预算配置 * @returns User 模式返回 null */ static resolve(opts, budget) { const { source = 'user', strategy: strategyName, dimensionMeta } = opts; const isSystem = source === 'system'; if (!isSystem) { return null; } let resolvedStrategy; if (strategyName === 'analyst') { resolvedStrategy = STRATEGY_ANALYST; } else if (strategyName === 'producer') { resolvedStrategy = STRATEGY_PRODUCER; } else { const isSkillOnly = dimensionMeta?.outputType === 'skill'; resolvedStrategy = createBootstrapStrategy(isSkillOnly); } return new ExplorationTracker(resolvedStrategy, budget); } // ─── 核心 API:主循环调用点 ──────────────────────────── /** 每轮迭代开始时调用 — 递增计数 */ tick() { this.#metrics.iteration++; this.#metrics.phaseRounds++; this.#ticked = true; this.#justTransitioned = false; } /** 撤销 tick(AI 调用失败或空响应时,不计入迭代) */ rollbackTick() { if (this.#ticked) { this.#metrics.iteration--; this.#metrics.phaseRounds--; this.#ticked = false; } } /** 提交工具名 */ get submitToolName() { return this.#submitToolName; } /** 管线类型标识 */ get pipelineType() { return this.#pipelineType; } /** 是否应退出主循环 */ shouldExit() { // Scan 管线: SUMMARIZE 无消费方,直接退出 if (this.#isTerminalPhase() && this.#pipelineType === 'scan') { this.#emitExitSignal('scan_terminal'); return true; } // 终结阶段 + 已给了 3 轮 grace → 退出 if (this.#isTerminalPhase() && this.#metrics.phaseRounds >= 3) { this.#emitExitSignal('grace_exhausted'); return true; } // 硬上限兜底 if (this.#metrics.iteration >= this.#budget.maxIterations + 2) { this.#emitExitSignal('hard_limit'); return true; } // 达到 maxIterations 但未在终结阶段 → 强制转入终结阶段 if (this.#metrics.iteration >= this.#budget.maxIterations && !this.#isTerminalPhase()) { this.#logger.info(`[ExplorationTracker] maxIterations reached (${this.#metrics.iteration}/${this.#budget.maxIterations}), forcing → ${this.#getTerminalPhase()}`); this.#transitionTo(this.#getTerminalPhase()); this.#justTransitioned = false; this.#gracefulExitRound = this.#metrics.iteration; return false; } return false; } #emitExitSignal(reason) { if (this.#signalBus) { this.#signalBus.send('exploration', 'ExplorationTracker.exit', 0, { metadata: { totalIterations: this.#metrics.iteration, reason }, }); } } /** * 获取本轮的 Nudge(每轮最多一条) * @param trace 推理链 * @returns |null} */ getNudge(trace) { // 委托 NudgeGenerator const nudge = this.#nudgeGenerator.generate(this.#buildNudgeState(), trace); if (nudge) { // 日志 (保持原有行为) if (nudge.type === 'convergence') { this.#logger.info(`[ExplorationTracker] 📊 Exploration saturated at iter ${this.#metrics.iteration}/${this.#budget.maxIterations} — ` + `files=${this.#metrics.uniqueFiles.size}, patterns=${this.#metrics.uniquePatterns.size}, staleRounds=${this.#metrics.roundsSinceNewInfo}`); } else if (nudge.type === 'budget_warning') { this.#logger.info(`[ExplorationTracker] 📌 Budget warning at ${this.#metrics.iteration}/${this.#budget.maxIterations}`); } else if (nudge.type === 'reflection') { this.#logger.info(`[ExplorationTracker] 💭 reflection triggered at iteration ${this.#metrics.iteration}`); } return nudge; } // NudgeGenerator 不处理 planning — 委托 PlanTracker if (this.#strategy.enablePlanning) { const planningNudge = this.#planTracker.checkPlanning(this.#buildNudgeState(), trace); if (planningNudge) { this.#logger.info(`[ExplorationTracker] 📋 ${planningNudge.type} triggered at iteration ${this.#metrics.iteration}`); return planningNudge; } } return null; } /** 获取当前阶段的上下文状态行(注入 systemPrompt 尾部) */ getPhaseContext() { return this.#nudgeGenerator.getPhaseContext(this.#buildNudgeState()); } /** 获取当前阶段的 toolChoice */ getToolChoice() { if (this.isGracefulExit) { return 'none'; } return this.#strategy.getToolChoice(this.#phase, this.#metrics, this.#budget); } /** * 记录一次工具调用结果,更新内部指标 * * @returns } */ recordToolCall(toolName, args, result) { this.#metrics.totalToolCalls++; const isNew = this.#signalDetector.detect(toolName, args, result); // Submit 追踪 if (toolName === 'submit_knowledge' || toolName === 'submit_with_check' || toolName === 'collect_scan_recipe') { const status = typeof result === 'object' ? result?.status : 'ok'; const isRejected = status === 'rejected'; const isError = status === 'error'; if (!isRejected && !isError) { this.#metrics.submitCount++; this.#metrics.roundsSinceSubmit = 0; } } return { isNew }; } /** * 结束本轮迭代 — 更新轮次级指标 + 检查阶段转换 * * @returns |null} 阶段转换 nudge */ endRound({ hasNewInfo = false, submitCount = 0, toolNames = [], skipped = false, } = {}) { this.#ticked = false; if (skipped) { return null; } // 1. 更新轮次级指标 if (hasNewInfo) { this.#metrics.roundsSinceNewInfo = 0; } else { this.#metrics.roundsSinceNewInfo++; } if (submitCount > 0) { this.#metrics.roundsSinceSubmit = 0; } else { this.#metrics.roundsSinceSubmit++; } // 2. 搜索轮次计数 const hasSearchTool = toolNames.some((t) => SEARCH_TOOLS.has(t)); if (hasSearchTool) { this.#metrics.searchRoundsInPhase++; } // 2.5 连续空闲轮次追踪(无任何工具调用 = 真正空转,有工具调用 = 活跃工作) if (toolNames.length === 0) { this.#metrics.consecutiveIdleRounds++; } else { this.#metrics.consecutiveIdleRounds = 0; } // 3. 检查 metrics 驱动的阶段转换 this.#checkMetricsTransition(); // 4. 如果发生了转换,生成 nudge if (this.#justTransitioned) { this.#justTransitioned = false; // Scan 管线: skip SUMMARIZE nudge if (this.#pipelineType === 'scan' && this.#isTerminalPhase()) { this.#logger.info(`[ExplorationTracker] scan pipeline: skip SUMMARIZE nudge, will exit on next tick (submits=${this.#metrics.submitCount})`); return null; } return { type: 'phase_transition', text: this.#nudgeGenerator.buildTransitionNudge(this.#buildNudgeState()), }; } return null; } /** * 处理 AI 返回纯文本响应(无工具调用) * @returns } */ onTextResponse() { const m = this.#metrics; const transitioned = this.#checkTextTransition(); if (transitioned) { this.#justTransitioned = false; } const isTerminal = this.#isTerminalPhase(); if (isTerminal && !transitioned) { return { isFinalAnswer: true, needsDigestNudge: false, shouldContinue: false, nudge: null }; } if (isTerminal && transitioned) { const submitCount = m.submitCount; // Scan 管线: 所有结果在 toolCalls 中,无需文本总结 if (this.#pipelineType === 'scan') { return { isFinalAnswer: true, needsDigestNudge: false, shouldContinue: false, nudge: null }; } // Analyst 管线: Markdown 分析报告 // Bootstrap 管线: dimensionDigest JSON const nudge = this.#pipelineType === 'analyst' ? `请**停止调用工具**,直接输出你的完整分析报告。用 Markdown 格式,包含具体文件路径、类名和代码模式。至少涵盖 3 个核心发现。\n\n**现在开始输出你的分析报告。**\n⚠️ 严禁在回复中复制本条指令文字,只输出你自己的分析。` : `请在回复中直接输出 dimensionDigest JSON 总结(用 \`\`\`json 包裹):\n` + `\`\`\`json\n{"dimensionDigest":{"summary":"分析总结(100-200字)","candidateCount":${submitCount},"keyFindings":["关键发现"],"crossRefs":{},"gaps":["未覆盖方面"],"remainingTasks":[{"signal":"未处理的信号/主题","reason":"未完成原因","priority":"high|medium|low","searchHints":["建议搜索词"]}]}}\n\`\`\`\n> 如果所有信号都已覆盖,remainingTasks 留空数组 \`[]\`。\n` + `⚠️ 严禁在回复中复制本条指令文字,只输出 JSON。`; return { isFinalAnswer: false, needsDigestNudge: true, shouldContinue: true, nudge, }; } // 非终结阶段收到文本 if (this.#phase === 'PRODUCE' || this.#phase === 'EXPLORE') { const nudge = this.#phase === 'PRODUCE' && this.#pipelineType !== 'scan' ? `你的分析很好。请继续调用 ${this.#submitToolName} 提交你发现的知识候选,每个值得记录的模式/实践都应该提交。` : null; return { isFinalAnswer: false, needsDigestNudge: false, shouldContinue: true, nudge }; } return { isFinalAnswer: false, needsDigestNudge: false, shouldContinue: true, nudge: null }; } /** 记录被截断的工具调用数量 */ recordTruncatedCalls(count) { if (count > 0) { this.#logger.warn(`[ExplorationTracker] ${count} tool calls truncated (MAX_TOOL_CALLS_PER_ITER)`); } } // ─── 状态查询 ───────────────────────────────────────── get isGracefulExit() { return this.#gracefulExitRound != null; } get isHardExit() { return (this.#gracefulExitRound != null && this.#metrics.iteration >= this.#gracefulExitRound + 2); } get phase() { return this.#phase; } get iteration() { return this.#metrics.iteration; } get totalSubmits() { return this.#metrics.submitCount; } get strategyName() { return this.#strategy.name; } getMetrics() { return { iteration: this.#metrics.iteration, phase: this.#phase, phaseRounds: this.#metrics.phaseRounds, submitCount: this.#metrics.submitCount, uniqueFiles: this.#metrics.uniqueFiles.size, uniquePatterns: this.#metrics.uniquePatterns.size, uniqueQueries: this.#metrics.uniqueQueries.size, totalToolCalls: this.#metrics.totalToolCalls, roundsSinceNewInfo: this.#metrics.roundsSinceNewInfo, }; } get metrics() { return this.getMetrics(); } getPlanProgress() { return this.#planTracker.progress; } /** 更新计划进度 — 委托 PlanTracker */ updatePlanProgress(trace) { if (!this.#strategy.enablePlanning) { return; } this.#planTracker.updatePlanProgress(trace); } /** * 推理质量评分 — 委托 PlanTracker * @returns } */ getQualityMetrics(trace) { return this.#planTracker.getQualityMetrics(trace); } // ─── 阶段路由内部方法 ────────────────────────────────── #checkMetricsTransition() { const transitions = this.#strategy.transitions; const nextPhaseIndex = this.#strategy.phases.indexOf(this.#phase) + 1; if (nextPhaseIndex >= this.#strategy.phases.length) { return; } const nextPhase = this.#strategy.phases[nextPhaseIndex]; const transKey = `${this.#phase}→${nextPhase}`; const rule = transitions[transKey]; if (!rule) { return; } const condition = typeof rule === 'function' ? rule : rule.onMetrics; if (condition?.(this.#metrics, this.#budget)) { this.#transitionTo(nextPhase); } } #checkTextTransition() { const transitions = this.#strategy.transitions; const nextPhaseIndex = this.#strategy.phases.indexOf(this.#phase) + 1; if (nextPhaseIndex >= this.#strategy.phases.length) { return false; } const nextPhase = this.#strategy.phases[nextPhaseIndex]; const transKey = `${this.#phase}→${nextPhase}`; const rule = transitions[transKey]; if (!rule) { return false; } let shouldTransition = false; if (typeof rule === 'object' && rule.onTextResponse !== undefined) { if (typeof rule.onTextResponse === 'function') { shouldTransition = rule.onTextResponse(this.#metrics, this.#budget); } else { shouldTransition = !!rule.onTextResponse; } } if (shouldTransition) { this.#transitionTo(nextPhase); return true; } return false; } #transitionTo(newPhase) { const oldPhase = this.#phase; const dwellMs = Date.now() - this.#phaseStartTime; this.#transitionFromPhase = oldPhase; this.#phase = newPhase; this.#phaseStartTime = Date.now(); this.#metrics.phaseRounds = 0; this.#metrics.searchRoundsInPhase = 0; // 重置停滞计数器 — 防止跨阶段累积导致级联式过早转换 // (SCAN 阶段的 roundsSinceNewInfo 不应影响 EXPLORE→VERIFY 的判定) this.#metrics.roundsSinceNewInfo = 0; this.#metrics.roundsSinceSubmit = 0; this.#metrics.consecutiveIdleRounds = 0; this.#justTransitioned = true; this.#logger.info(`[ExplorationTracker] ${oldPhase} → ${newPhase} (iter=${this.#metrics.iteration}, submits=${this.#metrics.submitCount}, ` + `dwellMs=${dwellMs}, files=${this.#metrics.uniqueFiles.size}, patterns=${this.#metrics.uniquePatterns.size})`); // Phase 3: 发射阶段转换信号 if (this.#signalBus) { const terminalPhase = this.#getTerminalPhase(); const value = newPhase === terminalPhase ? 1.0 : 0.5; this.#signalBus.send('exploration', 'ExplorationTracker.phase', value, { metadata: { from: oldPhase, to: newPhase, iteration: this.#metrics.iteration }, }); } } #isTerminalPhase() { return this.#phase === this.#getTerminalPhase(); } #getTerminalPhase() { return this.#strategy.phases[this.#strategy.phases.length - 1]; } /** 构建 NudgeState 供 NudgeGenerator / PlanTracker 使用 */ #buildNudgeState() { return { phase: this.#phase, metrics: this.#metrics, budget: this.#budget, strategy: this.#strategy, gracefulExitRound: this.#gracefulExitRound, submitToolName: this.#submitToolName, pipelineType: this.#pipelineType, isTerminalPhase: this.#isTerminalPhase(), transitionFromPhase: this.#transitionFromPhase, }; } } export default ExplorationTracker;