/** * LoopKernel — KCode harness 的统一循环引擎。 * * 编排 Reason → Gate → Act → Observe → Update → Evaluate 六步循环。 * * 六步循环详解: * 1. Reason(推理):Reasoner 回调分析当前状态,决定下一步 ActionPlan。 * 2. Gate(门禁):GateRunner 在 pre-tool checkpoint 评估是否允许执行。 * 3. Act(执行):ToolPipeline 执行实际工具调用。 * 4. Observe(观察):捕获工具执行结果、证据路径、源码引用。 * 5. Update(更新):通过 loopReducer 纯函数更新不可变 LoopState。 * 6. Evaluate(评估):检查 CompletionPromise、StopHook、预算是否满足停止条件。 * * 设计决策: * - Reasoner 是外部回调,LoopKernel 不感知决策逻辑。 * - 状态转换通过 loopReducer 纯函数,支持确定性重放和时间旅行调试。 * - StopHook 在每个 Evaluate 步骤检查,防止无限循环和 stuck。 * * @see reducers.ts — 纯状态转换函数 * @see action-plan.ts — ActionPlan 类型定义 * @see ../gates/gate-runner.ts — GateRunner 门禁执行器 * @see ../tools/tool-pipeline.ts — ToolPipeline 工具管道 */ import type { ActiveRun, KdPhase } from "../types.ts"; import type { GateDecision } from "../gates/findings.ts"; import type { GateRunner } from "../gates/gate-runner.ts"; import type { ToolPipeline, ToolPipelineResult } from "../tools/tool-pipeline.ts"; import type { ActionPlan } from "./action-plan.ts"; import { type LoopEvent, type LoopObservation, type LoopState, type LoopStatus, loopReducer, createInitialLoopState, replayLoopEvents, } from "./reducers.ts"; import { generateTraceId } from "../kernel/trace.ts"; // ── LoopBudget ─────────────────────────────────────────────────────────────── /** * 循环预算约束。 * * 内核强制执行这些限制,当任何一项超限时停止循环。 * 预算机制防止无限循环和资源耗尽。 */ export interface LoopBudget { /** 最大 Reason→Gate→Act→Observe→Update→Evaluate 迭代次数。 */ maxIterations: number; /** 最大总工具调用次数(跨所有迭代)。 */ maxToolCalls?: number; /** 最大委派调用次数。 */ maxDelegations?: number; } // ── StopHook ───────────────────────────────────────────────────────────────── /** * 停止钩子:检查循环是否应因非正常完成原因终止(如 stuck、用户中止)。 * * StopHookDetector 在每个 Evaluate 步骤后调用。 * 第一个返回非 null 决策的钩子会停止循环。 */ export interface StopDecision { /** 循环停止的原因。 */ reason: string; /** 停止原因分类。 */ hook: "stuck" | "budget_exhausted" | "user_abort" | "error"; } /** * 停止钩子检测器接口。 * * 实现类检查 stuck 模式、预算超限或外部中止信号。 */ export interface StopHookDetector { /** * 检查循环是否应停止。 * * @param state 当前循环状态。 * @param budget 循环预算(用于预算超限检查)。 * @returns StopDecision(若应停止),null(继续运行)。 */ check(state: LoopState, budget: LoopBudget): StopDecision | null; } // ── CompletionPromise ──────────────────────────────────────────────────────── /** * CompletionPromise — 循环成功完成的条件。 * * 当条件满足时,循环可以正常停止。 * 每个 ActionPlan 通过 completionPromiseId 绑定到一个 CompletionPromise。 * 内核在每个 Evaluate 步骤后检查是否满足。 */ export interface CompletionPromise { /** 唯一标识符。 */ readonly id: string; /** * 检查承诺是否已满足。 * * @param state 当前循环状态。 * @returns true 表示承诺已满足。 */ isFulfilled(state: LoopState): boolean; /** 人类可读的满足条件描述。 */ message(): string; } // ── Reasoner callback ──────────────────────────────────────────────────────── /** * Reasoner 回调:分析当前循环状态,决定下一步行动。 * * 这是循环的"Reason"步骤。作为外部回调提供, * 使 LoopKernel 与决策逻辑解耦。 * 返回 null 表示无更多行动,循环应正常完成。 * * @param state 当前循环状态。 * @param run 活跃运行上下文。 * @returns ActionPlan(下一步行动),或 null(停止循环)。 */ export type Reasoner = ( state: LoopState, run: ActiveRun, ) => ActionPlan | null | Promise; // ── LoopInput ──────────────────────────────────────────────────────────────── /** LoopKernel.run() 的输入参数。 */ export interface LoopInput { /** 活跃运行上下文。 */ run: ActiveRun; /** 当前运行的工作目录。 */ cwd: string; /** 循环预算约束。 */ loopBudget: LoopBudget; /** 决定下一步行动的 Reasoner 回调。 */ reasoner: Reasoner; /** 可选的完成承诺(用于检测成功完成条件)。 */ completionPromise?: CompletionPromise; /** 可选的初始阶段(默认使用 run.phase)。 */ phase?: KdPhase; } // ── LoopResult ─────────────────────────────────────────────────────────────── /** 循环运行的结果。 */ export interface LoopResult { /** 最终循环状态。 */ status: LoopStatus; /** 停止钩子原因(若由 StopHook 停止)。 */ stopHook?: string; /** 循环期间收集的所有证据路径。 */ evidencePaths: string[]; /** 循环期间做出的所有门禁决策。 */ gateDecisions: GateDecision[]; /** 循环步骤的有序追踪记录。 */ trace: LoopTraceStep[]; /** 人类可读的摘要消息。 */ message: string; /** 最终循环状态对象。 */ state: LoopState; } /** 循环追踪中的单个步骤。 */ export interface LoopTraceStep { /** 迭代编号。 */ turn: number; /** 执行的动作。 */ action: string; /** 门禁决策 trace ID(若运行了门禁)。 */ gateDecisionTraceId?: string; /** 门禁是否允许。 */ gateAllowed?: boolean; /** 观察摘要(若动作已执行)。 */ observation?: string; /** ISO 时间戳。 */ timestamp: string; } // ── LoopKernel ─────────────────────────────────────────────────────────────── /** * The unified loop engine. * * Drives the Reason → Gate → Act → Observe → Update → Evaluate cycle. * Each step is a distinct phase; the kernel orchestrates them in order * and loops back to Reason unless a stop condition is met. * * @example * ```ts * const kernel = createLoopKernel(gateRunner, toolPipeline, stopHookDetector); * const result = await kernel.run({ * run: activeRun, * cwd: "/path/to/project", * loopBudget: { maxIterations: 50 }, * reasoner: (state, run) => decideNextAction(state, run), * completionPromise: myPromise, * }); * ``` */ export class LoopKernel { /** 门禁执行器,用于 pre/post-action 门禁检查。 */ private readonly gateRunner: GateRunner; /** 工具管道,用于执行实际工具调用。 */ private readonly toolPipeline: ToolPipeline; /** 停止钩子检测器,用于检测 stuck、预算超限等。 */ private readonly stopHookDetector: StopHookDetector; /** 可选的默认完成承诺。 */ private readonly completionPromise?: CompletionPromise; constructor( gateRunner: GateRunner, toolPipeline: ToolPipeline, stopHookDetector: StopHookDetector, completionPromise?: CompletionPromise, ) { this.gateRunner = gateRunner; this.toolPipeline = toolPipeline; this.stopHookDetector = stopHookDetector; this.completionPromise = completionPromise; } /** * 运行循环直到满足停止条件。 * * 停止条件(在 Evaluate 步骤中按顺序检查): * 1. CompletionPromise 满足 → status "completed" * 2. StopHook 触发 → status 由 hook 决定 * 3. 预算耗尽 → status "budget_exhausted" * 4. Reasoner 返回 null → status "completed" * 5. 门禁阻塞 → status "blocked" * * @param input 循环配置和上下文。 * @returns 包含最终状态、追踪记录和证据的循环结果。 */ async run(input: LoopInput): Promise { const { run, cwd, loopBudget, reasoner, completionPromise, phase, } = input; const effectivePromise = completionPromise ?? this.completionPromise; const initialPhase = phase ?? run.phase; const traceId = generateTraceId("loop"); // 初始化状态 let state = createInitialLoopState(initialPhase); const trace: LoopTraceStep[] = []; // ── 主循环 ──────────────────────────────────────────────────── while (state.status === "running") { // ── 预算检查(迭代前) ────────────────────────────────── // 提前检查预算,避免在超限后仍执行 Reason 步骤 if (state.turn >= loopBudget.maxIterations) { state = loopReducer(state, { type: "loop.budget_exhausted", payload: { maxIterations: loopBudget.maxIterations }, }); break; } // ── 步骤 1: Reason(推理) ─────────────────────────────── // Reasoner 回调分析当前状态,返回下一步 ActionPlan let actionPlan: ActionPlan | null; try { actionPlan = await reasoner(state, run); } catch (error) { // Reasoner 抛出异常 → 视为 stuck 停止 state = loopReducer(state, { type: "loop.stuck", payload: { reason: `Reasoner error: ${error instanceof Error ? error.message : String(error)}`, turnsAtSameState: 0, }, }); break; } // Reasoner 返回 null → 循环正常完成(无更多行动) if (!actionPlan) { state = loopReducer(state, { type: "loop.completed", payload: { message: "Reasoner returned null — no more actions." }, }); break; } // ── 步骤 2: Gate(门禁,pre-action) ──────────────────── // 在执行前检查门禁,决定是否允许该行动 let gateDecision: GateDecision | undefined; try { gateDecision = await this.gateRunner.runGate({ cwd, checkpoint: "pre-tool", run, toolName: actionPlan.kind, payload: actionPlan, }); } catch (error) { // Gate evaluation failed — treat as blocked. state = loopReducer(state, { type: "loop.blocked", payload: { reason: `Gate error: ${error instanceof Error ? error.message : String(error)}`, action: actionPlan, }, }); trace.push({ turn: state.turn, action: actionPlan.intent, observation: `Gate error: ${error instanceof Error ? error.message : String(error)}`, timestamp: new Date().toISOString(), }); break; } // 门禁阻塞 → 停止循环 if (!gateDecision.allowed) { state = loopReducer(state, { type: "loop.blocked", payload: { reason: `Gate blocked action: ${gateDecision.findings.map((f) => f.message).join("; ")}`, gateDecision, action: actionPlan, }, }); trace.push({ turn: state.turn, action: actionPlan.intent, gateDecisionTraceId: gateDecision.traceId, gateAllowed: false, observation: "Blocked by gate.", timestamp: new Date().toISOString(), }); break; } // ── 步骤 3: Act(执行) ────────────────────────────────── let pipelineResult: ToolPipelineResult | undefined; let observation: LoopObservation | undefined; // 仅对工具可调用的 action kind 通过 ToolPipeline 执行 // "stop" 和 "ask-user" 不经过管道 if (this.isToolCallable(actionPlan.kind)) { try { pipelineResult = await this.toolPipeline.execute({ toolName: this.actionKindToToolName(actionPlan.kind), args: actionPlan.parameters, cwd, run, }); // ── 步骤 4: Observe(观察) ────────────────────────────── // 捕获工具执行结果、证据路径和源码引用 observation = { actionPlanId: actionPlan.planId, toolName: this.actionKindToToolName(actionPlan.kind), success: pipelineResult.contract.status === "success", summary: pipelineResult.contract.summary, output: pipelineResult.output.slice(0, 2000), evidencePaths: pipelineResult.contract.evidencePath ? [pipelineResult.contract.evidencePath] : [], sourceRefs: pipelineResult.contract.sourceRefs ?? [], timestamp: new Date().toISOString(), }; } catch (error) { // Tool execution failed — capture as failed observation. observation = { actionPlanId: actionPlan.planId, toolName: this.actionKindToToolName(actionPlan.kind), success: false, summary: `Execution error: ${error instanceof Error ? error.message : String(error)}`, output: "", evidencePaths: [], sourceRefs: [], timestamp: new Date().toISOString(), }; } } // ── 步骤 5: Update(更新状态) ──────────────────────────── // 通过 loopReducer 纯函数更新不可变 LoopState state = loopReducer(state, { type: "loop.step", payload: { action: actionPlan, gateDecision, observation, }, }); // Record trace step. trace.push({ turn: state.turn, action: actionPlan.intent, gateDecisionTraceId: gateDecision?.traceId, gateAllowed: true, observation: observation?.summary, timestamp: new Date().toISOString(), }); // ── 步骤 6: Evaluate(评估停止条件) ───────────────────── // 检查 CompletionPromise 是否满足 if (effectivePromise?.isFulfilled(state)) { state = loopReducer(state, { type: "loop.completed", payload: { message: `CompletionPromise "${effectivePromise.id}" fulfilled: ${effectivePromise.message()}`, }, }); break; } // 检查 StopHook(stuck、预算超限等) const stopDecision = this.stopHookDetector.check(state, loopBudget); if (stopDecision) { if (stopDecision.hook === "stuck") { state = loopReducer(state, { type: "loop.stuck", payload: { reason: stopDecision.reason, turnsAtSameState: this.countConsecutiveSimilarActions(state), }, }); } else if (stopDecision.hook === "budget_exhausted") { state = loopReducer(state, { type: "loop.budget_exhausted", payload: { maxIterations: loopBudget.maxIterations }, }); } else { // user_abort or error state = loopReducer(state, { type: "loop.completed", payload: { message: `StopHook: ${stopDecision.reason}` }, }); } break; } // ── Loop back to Reason ────────────────────────────────── } // ── Build result ───────────────────────────────────────────── return { status: state.status, stopHook: state.stopHook, evidencePaths: state.evidencePaths, gateDecisions: state.gateDecisions, trace, message: state.message, state, }; } // ── Private helpers ────────────────────────────────────────────────── /** * 检查 action kind 是否可通过 ToolPipeline 执行。 * * "stop" 是终止动作,不经过管道。 * "ask-user" 由 Reasoner 处理,不经过管道。 */ private isToolCallable(kind: ActionPlanKind): boolean { return kind !== "stop" && kind !== "ask-user"; } /** * 将 ActionPlanKind 映射为 ToolPipeline 执行的工具名称。 */ private actionKindToToolName(kind: ActionPlanKind): string { switch (kind) { case "write-code": return "kd_write"; case "read-context": return "kd_read"; case "verify": return "kd_verify"; case "delegate": return "kd_subagent"; default: return kind; } } /** * 统计连续相似动作的数量,用于检测 stuck 模式。 * 从最近的动作向前遍历,直到遇到不同的 intent。 */ private countConsecutiveSimilarActions(state: LoopState): number { if (state.actions.length < 2) return 0; const lastIntent = state.actions[state.actions.length - 1]?.intent; if (!lastIntent) return 0; let count = 0; for (let i = state.actions.length - 1; i >= 0; i--) { if (state.actions[i].intent === lastIntent) { count++; } else { break; } } return count; } } // ── ActionPlanKind (re-export for convenience) ─────────────────────────────── type ActionPlanKind = "write-code" | "ask-user" | "read-context" | "verify" | "delegate" | "stop"; // ── Factory ────────────────────────────────────────────────────────────────── /** * Create a LoopKernel with the given dependencies. * * @param gateRunner The gate runner for pre/post-action checks. * @param toolPipeline The tool pipeline for action execution. * @param stopHookDetector Detector for stuck/budget/abort conditions. * @param completionPromise Optional default completion promise. * @returns A new LoopKernel instance. */ export function createLoopKernel( gateRunner: GateRunner, toolPipeline: ToolPipeline, stopHookDetector: StopHookDetector, completionPromise?: CompletionPromise, ): LoopKernel { return new LoopKernel(gateRunner, toolPipeline, stopHookDetector, completionPromise); } // ── Replay ─────────────────────────────────────────────────────────────────── /** * Replay a sequence of LoopEvents to reconstruct the final LoopState. * * This is a convenience re-export of `replayLoopEvents` from reducers.ts. * It exists here for API discoverability — callers can import from either module. * * @param events Ordered sequence of loop events. * @param phase Initial phase for the loop (default: "execute"). * @returns The final LoopState after all events are applied. */ export function replay(events: ReadonlyArray, phase?: KdPhase): LoopState { return replayLoopEvents(events, phase); }