UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

419 lines (418 loc) 19.6 kB
/** * PipelineStrategy — 顺序多阶段执行策略 * * 从 strategies.js 提取的独立模块。 * 每个阶段可以有不同的 Capability 和 Budget, * 阶段间可插入质量门控 (Quality Gate)。 * * 等价于 Anthropic 的 "Prompt Chaining" + "Evaluator-Optimizer"。 * * 增强特性 (v3): * - Gate 支持自定义 evaluator 函数 (三态: pass/retry/degrade) * - Gate retry: 失败时回退重新执行前一阶段 * - Stage 支持 promptBuilder(context), systemPrompt, onToolCall * - Per-stage 硬超时保护 * - 阶段隔离 (ContextWindow/ExplorationTracker 状态) * * @module PipelineStrategy */ import Logger from '#infra/logging/Logger.js'; import { AgentEventBus, AgentEvents } from './AgentEventBus.js'; import { ExplorationTracker } from './context/ExplorationTracker.js'; import { Strategy, StrategyRegistry } from './strategies.js'; const _pipelineLogger = Logger.getInstance(); export class PipelineStrategy extends Strategy { #stages; /** 最大重试次数 (Gate 失败时全局兜底) */ #maxRetries; constructor({ stages = [], maxRetries = 1, } = {}) { super(); this.#stages = stages; this.#maxRetries = maxRetries; } get name() { return 'pipeline'; } async execute(runtime, message, opts = {}) { const bus = AgentEventBus.getInstance(); const ctx = { phaseResults: {}, strategyContext: (opts.strategyContext || {}), totalToolCalls: [], totalTokenUsage: { input: 0, output: 0 }, totalIterations: 0, gateArtifact: null, degraded: false, execStageCount: 0, lastExecutedStageName: null, }; for (let i = 0; i < this.#stages.length; i++) { const stage = this.#stages[i]; // ── Quality Gate 阶段 ── if (stage.gate) { if (ctx.degraded) { continue; } const gateAction = this.#processGate(stage, i, ctx, bus); if (gateAction === 'break') { break; } if (gateAction === 'continue') { continue; } if (typeof gateAction === 'number') { i = gateAction; // retry: jump back continue; } break; // unknown action fallback } // ── 执行阶段 ── if (ctx.degraded && stage.skipOnDegrade !== false) { continue; } await this.#executeStage(runtime, message, stage, ctx, bus); } // 最终回复 = 最后一个执行阶段的输出 const lastStage = Object.values(ctx.phaseResults) .filter((r) => r != null && typeof r === 'object' && 'reply' in r) .pop(); return { reply: lastStage?.reply || '', toolCalls: ctx.totalToolCalls, tokenUsage: ctx.totalTokenUsage, iterations: ctx.totalIterations, phases: ctx.phaseResults, degraded: ctx.degraded, }; } // ═══════════════════════════════════════════════════════════ // Private: Gate 处理 // ═══════════════════════════════════════════════════════════ /** * 处理 Quality Gate 阶段 * * @returns break/continue 或 retry 回退索引 (i-1) */ #processGate(stage, stageIndex, ctx, bus) { const { phaseResults, strategyContext } = ctx; if (!stage.gate) { return 'continue'; } const gate = stage.gate; const sourceName = (stage.source || this.#prevStageName(stage)); const source = phaseResults[sourceName]; let gateResult; // v3: 自定义评估器 (Bootstrap 用) if (typeof gate.evaluator === 'function') { gateResult = gate.evaluator(source, phaseResults, strategyContext); if (!gateResult.action) { gateResult.action = gateResult.pass ? 'pass' : 'retry'; } } else { // 向后兼容: 阈值评估 const legacyResult = this.#evaluateGate(gate, phaseResults, sourceName); gateResult = { action: legacyResult.pass ? 'pass' : 'retry', pass: legacyResult.pass, reason: legacyResult.reason, }; } bus.publish(AgentEvents.PROGRESS, { type: 'quality_gate', pass: gateResult.action === 'pass', action: gateResult.action, reason: gateResult.reason, stage: stage.name || 'gate', }); // 存储 gate 结果和产物 phaseResults[stage.name || 'gate'] = { pass: gateResult.action === 'pass', action: gateResult.action, reason: gateResult.reason || '', artifact: gateResult.artifact || null, }; if (gateResult.artifact) { ctx.gateArtifact = gateResult.artifact; } // 三态处理 if (gateResult.action === 'pass') { return 'continue'; } if (gateResult.action === 'degrade') { ctx.degraded = true; return 'break'; } if (gateResult.action === 'retry') { const maxRetries = gate.maxRetries ?? this.#maxRetries; const retryKey = `_retries_${stage.name || 'gate'}`; phaseResults[retryKey] = (phaseResults[retryKey] || 0) + 1; if (phaseResults[retryKey] <= maxRetries) { const prevIdx = this.#findPrevExecStageIdx(stageIndex); if (prevIdx >= 0) { const retryTargetStage = this.#stages[prevIdx]; phaseResults._retryContext = { reason: gateResult.reason, artifact: gateResult.artifact, }; phaseResults[`_was_retry_${retryTargetStage.name}`] = true; return prevIdx - 1; // 循环 i++ 后回到 prevIdx } } // 重试次数耗尽 if (stage.skipOnFail !== false) { return 'break'; } return 'continue'; } // 兜底: 未知 action if (stage.skipOnFail !== false) { return 'break'; } return 'continue'; } // ═══════════════════════════════════════════════════════════ // Private: Stage 执行 // ═══════════════════════════════════════════════════════════ /** 执行单个 Pipeline 阶段 */ async #executeStage(runtime, message, stage, ctx, bus) { const { phaseResults, strategyContext } = ctx; bus.publish(AgentEvents.PROGRESS, { type: 'pipeline_stage_start', stage: stage.name, capabilities: stage.capabilities?.map((c) => typeof c === 'string' ? c : c.name), }); // 构建阶段 prompt const stagePrompt = await this.#buildStagePrompt(stage, message, phaseResults, strategyContext, ctx); // Budget (retry 时使用 retryBudget) const isRetry = !!phaseResults[`_was_retry_${stage.name}`]; const effectiveBudget = isRetry && stage.retryBudget ? stage.retryBudget : stage.budget; delete phaseResults[`_was_retry_${stage.name}`]; // 阶段隔离 (ContextWindow + ExplorationTracker) const ctxWin = (strategyContext.contextWindow || null); const isNewStage = ctx.lastExecutedStageName !== stage.name; if (ctxWin && ctx.execStageCount > 0 && isNewStage) { ctxWin.resetForNewStage(); } else if (ctxWin && ctx.execStageCount > 0 && !isNewStage) { _pipelineLogger.info(`[PipelineStrategy] ♻️ Retry stage "${stage.name}" — preserving ContextWindow (${ctxWin.tokenCount || 0} tokens)`); } // ExplorationTracker (per-stage) const stageTracker = this.#resolveStageTracker(stage, ctx, strategyContext, effectiveBudget); ctx.lastExecutedStageName = stage.name; ctx.execStageCount++; const submitToolName = (stage.submitToolName || strategyContext.submitToolName || undefined); _pipelineLogger.info(`[PipelineStrategy] ▶ Stage "${stage.name}"${isRetry ? ' (retry)' : ''} — ` + `budget: ${effectiveBudget?.maxIterations || '∞'} iters, ` + `timeout: ${effectiveBudget?.timeoutMs ? `${effectiveBudget.timeoutMs / 1000}s` : '∞'}, ` + `tracker: ${stageTracker?.constructor?.name || 'none'}` + `${submitToolName ? `, submitTool: ${submitToolName}` : ''}`); // 执行 reactLoop (含 per-stage 硬超时保护) let stageResult = await this.#runWithTimeout(runtime, stagePrompt, message, stage, effectiveBudget, ctxWin, stageTracker, strategyContext, phaseResults, bus); // ── 超时零输出快速重试 ── // 当阶段 hard timeout 且 0 tool calls(LLM 完全卡住), // 如果有 retryBudget 且本次非 retry,立即以降级预算重跑一次, // 跳过 gate 往返,争取在更短时限内拿到输出。 if (stageResult.timedOut && !stageResult.toolCalls?.length && !isRetry && stage.retryBudget) { _pipelineLogger.info(`[PipelineStrategy] ♻️ Stage "${stage.name}" timed out with 0 tool calls — fast-retrying with retryBudget`); bus.publish(AgentEvents.PROGRESS, { type: 'pipeline_stage_fast_retry', stage: stage.name, }); // 重置 ContextWindow (清空上一轮的空消息) if (ctxWin) { ctxWin.resetForNewStage(); } // 重建 tracker — 用 retryBudget 的更短限制 const retryTracker = this.#resolveStageTracker(stage, ctx, strategyContext, stage.retryBudget); // 构建简化 prompt(如果有 retryPromptBuilder 则使用) let retryPrompt = stagePrompt; if (typeof stage.retryPromptBuilder === 'function') { retryPrompt = stage.retryPromptBuilder({ reason: 'Stage hard timeout with 0 tool calls', artifact: null }, message.content, phaseResults); } stageResult = await this.#runWithTimeout(runtime, retryPrompt, message, stage, stage.retryBudget, ctxWin, retryTracker, strategyContext, phaseResults, bus); } // 累计结果 phaseResults[stage.name] = stageResult; ctx.totalToolCalls.push(...(stageResult.toolCalls || [])); ctx.totalIterations += stageResult.iterations || 0; if (stageResult.tokenUsage) { ctx.totalTokenUsage.input += stageResult.tokenUsage.input || 0; ctx.totalTokenUsage.output += stageResult.tokenUsage.output || 0; } _pipelineLogger.info(`[PipelineStrategy] ✅ Stage "${stage.name}" done — ` + `${stageResult.iterations || 0} iters, ${stageResult.toolCalls?.length || 0} tool calls, ` + `reply: ${stageResult.reply?.length || 0} chars${stageResult.timedOut ? ' (TIMED OUT)' : ''}`); bus.publish(AgentEvents.PROGRESS, { type: 'pipeline_stage_done', stage: stage.name, iterations: stageResult.iterations, }); } // ═══════════════════════════════════════════════════════════ // Private: Helpers // ═══════════════════════════════════════════════════════════ /** 构建阶段 prompt (优先级: retryPromptBuilder > promptBuilder > promptTransform > 原始) */ async #buildStagePrompt(stage, message, phaseResults, strategyContext, ctx) { let prompt; if (phaseResults._retryContext && stage.retryPromptBuilder) { const retryCtx = phaseResults._retryContext; prompt = stage.retryPromptBuilder(retryCtx, message.content, phaseResults); delete phaseResults._retryContext; } else if (stage.promptBuilder) { prompt = await stage.promptBuilder({ message: message.content, phaseResults, gateArtifact: ctx.gateArtifact, ...strategyContext, }); } else if (stage.promptTransform) { prompt = stage.promptTransform(message.content, phaseResults); } else { prompt = message.content; } // 清除已消费的 retryContext if (phaseResults._retryContext) { delete phaseResults._retryContext; } return prompt; } /** 为阶段解析 ExplorationTracker */ #resolveStageTracker(stage, ctx, strategyContext, effectiveBudget) { let stageTracker = (strategyContext.tracker || null); const submitToolName = (stage.submitToolName || strategyContext.submitToolName || undefined); const pipelineType = (stage.pipelineType || strategyContext.pipelineType || undefined); if (stageTracker && ctx.execStageCount > 0) { const trackerStrategy = stage.name === 'produce' || stage.name === 'producer' ? 'producer' : 'analyst'; stageTracker = ExplorationTracker.resolve({ source: strategyContext.source || 'system', strategy: trackerStrategy }, { ...(effectiveBudget || {}), ...(submitToolName ? { submitToolName } : {}), ...(pipelineType ? { pipelineType } : {}), }); } else if (stageTracker && ctx.execStageCount === 0 && submitToolName) { if (stageTracker.submitToolName !== submitToolName) { stageTracker = ExplorationTracker.resolve({ source: strategyContext.source || 'system', strategy: 'analyst' }, { ...(effectiveBudget || {}), submitToolName, ...(pipelineType ? { pipelineType } : {}), }); } } return stageTracker; } /** 执行 reactLoop 并添加硬超时保护 */ async #runWithTimeout(runtime, stagePrompt, message, stage, effectiveBudget, ctxWin, stageTracker, strategyContext, phaseResults, bus) { // 创建 AbortController — hard timeout 时取消进行中的 LLM 请求 const abortController = new AbortController(); const reactPromise = runtime.reactLoop(stagePrompt, { history: message.history, context: { ...(message.metadata.context || {}), pipelinePhase: stage.name, previousPhases: phaseResults, }, capabilityOverride: stage.capabilities, budgetOverride: effectiveBudget, systemPromptOverride: stage.systemPrompt, onToolCall: stage.onToolCall, contextWindow: ctxWin, tracker: stageTracker, trace: strategyContext.trace || null, memoryCoordinator: strategyContext.memoryCoordinator || null, sharedState: strategyContext.sharedState || null, source: strategyContext.source || null, abortSignal: abortController.signal, }); const stageTimeoutMs = effectiveBudget?.timeoutMs; if (!stageTimeoutMs) { return reactPromise; } // 硬超时 = budget.timeoutMs + 30s 缓冲 const hardLimitMs = stageTimeoutMs + 30_000; let hardTimer; return Promise.race([ reactPromise, new Promise((_, reject) => { hardTimer = setTimeout(() => { // 先中止进行中的 LLM HTTP 请求,再触发 reject abortController.abort(); reject(new Error('__STAGE_HARD_TIMEOUT__')); }, hardLimitMs); }), ]) .catch((err) => { if (err instanceof Error && err.message === '__STAGE_HARD_TIMEOUT__') { runtime.logger?.info?.(`[PipelineStrategy] ⏰ Stage "${stage.name}" hard timeout (${hardLimitMs}ms) — continuing pipeline`); bus.publish(AgentEvents.PROGRESS, { type: 'pipeline_stage_timeout', stage: stage.name, timeoutMs: hardLimitMs, }); return { reply: '', toolCalls: [], iterations: 0, tokenUsage: { input: 0, output: 0 }, timedOut: true, }; } throw err; }) .finally(() => clearTimeout(hardTimer)); } /** 质量门控评估 (向后兼容: 阈值模式) */ #evaluateGate(gateConfig, phaseResults, sourceName) { const source = phaseResults[sourceName]; if (!source?.reply) { return { pass: false, reason: `No output from stage "${sourceName}"` }; } const reply = source.reply; const reasons = []; if (gateConfig.minEvidenceLength && reply.length < gateConfig.minEvidenceLength) { reasons.push(`分析长度不足: ${reply.length} < ${gateConfig.minEvidenceLength}`); } if (gateConfig.minFileRefs) { const fileRefCount = (reply.match(/[\w/]+\.\w+/g) || []).length; if (fileRefCount < gateConfig.minFileRefs) { reasons.push(`文件引用不足: ${fileRefCount} < ${gateConfig.minFileRefs}`); } } if (gateConfig.minToolCalls) { const toolCalls = source.toolCalls?.length || 0; if (toolCalls < gateConfig.minToolCalls) { reasons.push(`工具调用不足: ${toolCalls} < ${gateConfig.minToolCalls}`); } } if (gateConfig.custom && typeof gateConfig.custom === 'function') { const customResult = gateConfig.custom(source); if (!customResult.pass) { reasons.push(customResult.reason ?? ''); } } return reasons.length === 0 ? { pass: true } : { pass: false, reason: reasons.join('; ') }; } /** 找到当前 gate 之前最近的执行阶段索引 (用于 retry 回退) */ #findPrevExecStageIdx(currentIdx) { for (let j = currentIdx - 1; j >= 0; j--) { if (!this.#stages[j].gate) { return j; } } return -1; } #prevStageName(currentStage) { const idx = this.#stages.indexOf(currentStage); for (let i = idx - 1; i >= 0; i--) { if (!this.#stages[i].gate && this.#stages[i].name) { return this.#stages[i].name; } } return null; } } // 自注册: 避免 strategies.js ↔ PipelineStrategy.js 循环依赖 StrategyRegistry.register('pipeline', PipelineStrategy);