import type { ActiveRun, GateResult, KdQuestion } from "./types.ts"; import { inspectGate } from "./gates.ts"; import { phaseOrderForRun } from "./types.ts"; import { formatCurrentFacts } from "./question-memory.ts"; import { routeHarnessAction, type KdActionRoute } from "./action-router.ts"; import { checkHarnessConsistency, formatConsistencyReport, type KdConsistencyReport } from "./consistency.ts"; import { decideNextAction, formatNextAction as formatStructuredNextAction, type KdNextAction } from "./next-action.ts"; export type KdControlFocus = "consistency" | "blocking-question" | "resume-snapshot" | "gate" | "phase"; export type KdTurnInputIntent = "consistency-required" | "answer-required" | "resume-required" | "gate-required" | "phase-input"; export interface KdTurnInputPolicy { intent: KdTurnInputIntent; inputSummary: string; requiredFirstAction: string; allowedInterpretations: string[]; forbiddenInterpretations: string[]; actionRoute: KdActionRoute; } export interface KdControlFrame { runId: string; goal?: string; mode: ActiveRun["mode"]; phase: ActiveRun["phase"]; phaseOrder: string[]; focus: KdControlFocus; primaryDirective: string; nextAction: KdNextAction; consistency: KdConsistencyReport; gate: GateResult; openQuestion?: KdQuestion; resumeSnapshot?: ActiveRun["resumeSnapshot"]; facts: string; turnInputPolicy: KdTurnInputPolicy; contextPriority: string[]; } export function buildControlFrame(cwd: string, run: ActiveRun, userText = ""): KdControlFrame { const gate = effectiveControlGate(inspectGate(cwd, run), run, userText); const consistency = checkHarnessConsistency(cwd, run); const openQuestion = firstOpenBlockingQuestion(run); const focus = controlFocus(run, consistency, gate, openQuestion); const actionRoute = routeHarnessAction({ run, gate, openQuestion, userText }); const nextAction = decideNextAction({ cwd, run, gate, consistency, openQuestion, actionRoute }); return { runId: run.id, goal: run.goal, mode: run.mode, phase: run.phase, phaseOrder: phaseOrderForRun(run), focus, primaryDirective: nextAction.instruction, nextAction, consistency, gate, openQuestion, resumeSnapshot: activeResumeSnapshot(run), facts: formatCurrentFacts(run), turnInputPolicy: turnInputPolicyForFocus(focus, run, gate, openQuestion, userText, actionRoute, nextAction), contextPriority: [ "active run state", "harness consistency report", "open blocking question", "resumeSnapshot", "run.facts", "gate result", "phase artifacts", "user input event", ], }; } function effectiveControlGate(inspected: GateResult, run: ActiveRun, userText: string): GateResult { const snapshot = run.gate; if (inspected.passed && snapshot?.passed === false && shouldPreserveTargetGate(snapshot, userText)) { return snapshot; } return inspected; } function shouldPreserveTargetGate(gate: GateResult, userText: string): boolean { const reason = gate.reason ?? ""; if (!/不能进入|缺少必需证据|缺少 evidence|evidence\//i.test(reason)) return false; return /继续|推进|下一步|不能推进|门禁|阻塞|缺少|continue|advance|next/i.test(userText); } export function formatControlFrame(frame: KdControlFrame): string { return [ `Run:${frame.runId}`, frame.goal ? `目标:${frame.goal}` : undefined, `模式:${frame.mode}(${frame.phaseOrder.join(" -> ")})`, `阶段:${frame.phase}`, `焦点:${frame.focus}`, `最高优先级动作:${frame.primaryDirective}`, formatNextAction(frame.nextAction), "一致性检查:", formatConsistencyReport(frame.consistency), `门禁:${frame.gate.passed ? "通过" : "阻塞"}`, frame.gate.reason ? `门禁原因:${frame.gate.reason}` : undefined, frame.resumeSnapshot ? formatResumeSnapshot(frame.resumeSnapshot) : "恢复断点:无。", frame.openQuestion ? formatOpenQuestion(frame.openQuestion) : "未回答阻断问题:无。", "当前结构化事实:", frame.facts, "本轮输入策略:", formatTurnInputPolicy(frame.turnInputPolicy), "上下文优先级:", ...frame.contextPriority.map((item, index) => `${index + 1}. ${item}`), ].filter(Boolean).join("\n"); } function controlFocus(run: ActiveRun, consistency: KdConsistencyReport, gate: GateResult, openQuestion: KdQuestion | undefined): KdControlFocus { if (consistency.errors.length > 0) return "consistency"; if (openQuestion) return "blocking-question"; if (run.resumeSnapshot?.status === "open") return "resume-snapshot"; if (!gate.passed) return "gate"; return "phase"; } function turnInputPolicyForFocus( focus: KdControlFocus, run: ActiveRun, gate: GateResult, openQuestion: KdQuestion | undefined, userText: string, actionRoute: KdActionRoute, nextAction: KdNextAction, ): KdTurnInputPolicy { const inputSummary = summarizeUserText(userText); if (focus === "consistency") { return { intent: "consistency-required", inputSummary, requiredFirstAction: "只处理 Harness 状态一致性错误:读取 Consistency Report,修正 run state、ledger 或重新创建 run;禁止继续执行业务实现。", allowedInterpretations: ["作为状态修复指令。", "作为允许重新创建 run 的明确指令。", "作为要求输出诊断报告的指令。"], forbiddenInterpretations: ["作为业务事实答案直接写入 facts。", "作为继续提问、计划或编码的许可。", "作为忽略 consistency errors 的许可。"], actionRoute, }; } if (focus === "blocking-question" && openQuestion) { return { intent: "answer-required", inputSummary, requiredFirstAction: `先判断本轮输入是否回答 ${openQuestion.id};若是答案,第一动作必须记录 kd_answer_user action=answer id=${openQuestion.id} answer=<本轮输入>。`, allowedInterpretations: [ `作为 ${openQuestion.id} 的答案。`, "作为对已确认事实的明确更正;必须使用 kd_answer_user action=revise 记录。", "作为明确停止、废弃或重开当前 run 的指令;必须先说明会丢弃当前断点。", ], forbiddenInterpretations: [ "作为新需求直接进入计划或编码。", "作为继续提问或展开问题清单的许可。", "作为覆盖 resumeSnapshot、run.facts 或 open question 的新上下文。", ], actionRoute, }; } if (focus === "resume-snapshot" && run.resumeSnapshot) { return { intent: "resume-required", inputSummary, requiredFirstAction: run.resumeSnapshot.nextAction, allowedInterpretations: ["作为恢复断点的补充说明。", "作为明确事实更正;必须记录到结构化 facts。", "作为明确停止或改需求的指令。"], forbiddenInterpretations: ["绕过恢复断点重新分析。", "丢弃 sourceRefs 或提问前上下文。", "直接进入编码。"], actionRoute, }; } if (focus === "gate") { return { intent: "gate-required", inputSummary, requiredFirstAction: nextAction.instruction, allowedInterpretations: ["作为解除当前门禁的证据、事实或命令。", "作为明确事实更正。", "作为停止当前 run 的指令。"], forbiddenInterpretations: ["绕过门禁推进阶段。", "写入产品源码。", "把自由文本计划当作结构化事实或 evidence。"], actionRoute, }; } return { intent: "phase-input", inputSummary, requiredFirstAction: nextAction.instruction, allowedInterpretations: ["作为当前阶段的需求补充。", "作为结构化事实更正。", "作为阶段推进或验证请求。"], forbiddenInterpretations: ["忽略 Harness 模式和阶段边界。", "在事实不足时输出模板代码。", "重复询问 run.facts 中已有事实。"], actionRoute, }; } function firstOpenBlockingQuestion(run: ActiveRun): KdQuestion | undefined { return (Array.isArray(run.questions) ? run.questions : []).find((question) => question.status === "open" && question.blocking); } function activeResumeSnapshot(run: ActiveRun): ActiveRun["resumeSnapshot"] { return run.resumeSnapshot?.status === "open" ? run.resumeSnapshot : undefined; } function formatResumeSnapshot(snapshot: NonNullable): string { return [ "恢复断点:", `- 状态:${snapshot.status}`, `- 阶段:${snapshot.phase}`, `- 上下文:${snapshot.contextSummary}`, snapshot.gateReason ? `- 门禁原因:${snapshot.gateReason}` : undefined, snapshot.pendingQuestionId ? `- 关联问题:${snapshot.pendingQuestionId}` : undefined, snapshot.sourceRefs?.length ? `- 来源:${snapshot.sourceRefs.join(";")}` : undefined, `- 下一动作:${snapshot.nextAction}`, ].filter(Boolean).join("\n"); } function formatOpenQuestion(question: KdQuestion): string { return [ "未回答阻断问题:", `- ${question.id}:${question.question}`, question.reason ? `- 原因:${question.reason}` : undefined, question.factLabel ? `- 事实标签:${question.factLabel}` : undefined, question.contextSummary ? `- 提问前上下文:${question.contextSummary}` : undefined, question.sourceRefs?.length ? `- 来源:${question.sourceRefs.join(";")}` : undefined, ].filter(Boolean).join("\n"); } function formatTurnInputPolicy(policy: KdTurnInputPolicy): string { return [ `- 意图:${policy.intent}`, `- 输入摘要:${policy.inputSummary}`, `- 第一动作:${policy.requiredFirstAction}`, `- 结构化意图:${policy.actionRoute.intent}(${policy.actionRoute.allowed ? "允许" : "阻断"})`, `- 意图原因:${policy.actionRoute.reason}`, `- 意图动作:${policy.actionRoute.requiredAction}`, "- 允许解释:", ...policy.allowedInterpretations.map((item) => ` - ${item}`), "- 禁止解释:", ...policy.forbiddenInterpretations.map((item) => ` - ${item}`), ].join("\n"); } function formatNextAction(action: KdNextAction): string { return [ "结构化下一动作:", ...formatStructuredNextAction(action).split(/\r?\n/).map((line) => `- ${line}`), ].join("\n"); } function summarizeUserText(text: string): string { const trimmed = text.trim().replace(/\s+/g, " "); if (!trimmed) return "无。"; return trimmed.length > 180 ? `${trimmed.slice(0, 180)}...` : trimmed; }