import type { ActiveRun, KdQuestion } from "./types.ts"; import { appendLedgerEvent } from "./ledger.ts"; import { recordRunContext, recordToolResultContract } from "./state.ts"; import { classifyToolError } from "./tool-error.ts"; export interface InputObservation { text: string; source?: string; } export interface AutoAnswerObservation { question: Pick; answer: string; } export interface ToolBlockObservation { toolName: string; reason: string; path?: string; } export interface ToolSuccessObservation { toolName: string; summary: string; path?: string; sourceRefs?: string[]; paths?: string[]; modifiedFiles?: string[]; evidencePaths?: string[]; kind?: "read" | "search" | "list" | "shell" | "knowledge" | "evidence" | "other"; persistContext?: boolean; } export function recordInputReceived(cwd: string, run: ActiveRun | undefined, input: InputObservation): void { if (!run) return; const text = input.text.trim(); appendLedgerEvent(cwd, run, { type: "input.received", summary: text ? trimObservation(text) : "收到空输入。", data: { source: input.source, command: text.startsWith("/"), }, }); } export function recordAutoAnsweredInput(cwd: string, run: ActiveRun | undefined, input: AutoAnswerObservation): void { if (!run) return; appendLedgerEvent(cwd, run, { type: "input.auto_answered", summary: `${input.question.id}:${trimObservation(input.answer, 160)}`, data: { questionId: input.question.id, factLabel: input.question.factLabel, question: trimObservation(input.question.question, 160), }, }); } export function recordToolBlocked(cwd: string, run: ActiveRun | undefined, input: ToolBlockObservation): void { if (!run) return; const error = classifyToolError({ toolName: input.toolName, text: input.reason }); appendLedgerEvent(cwd, run, { type: "tool.blocked", summary: trimObservation(input.reason), data: { toolName: input.toolName, path: input.path, errorClass: error.errorClass, recoveryAction: error.recoveryAction, }, }); recordToolResultContract(cwd, run, { toolName: input.toolName, status: "blocked", kind: "other", summary: input.reason, paths: input.path ? [input.path] : undefined, risk: error.errorClass, nextAction: error.recoveryAction, }); } export function recordToolSucceeded(cwd: string, run: ActiveRun | undefined, input: ToolSuccessObservation): void { if (!run) return; const summary = trimObservation(input.summary); appendLedgerEvent(cwd, run, { type: "tool.succeeded", summary, data: { toolName: input.toolName, path: input.path, kind: input.kind, sourceRefs: input.sourceRefs, }, }); const paths = input.paths ?? input.sourceRefs ?? (input.path ? [input.path] : undefined); recordToolResultContract(cwd, run, { toolName: input.toolName, status: "success", kind: input.kind ?? "other", summary, paths, modifiedFiles: input.modifiedFiles, evidencePaths: input.evidencePaths, nextAction: input.kind === "read" || input.kind === "search" || input.kind === "list" ? "复用该工具结果继续当前工作线程;禁止重新猜测路径。" : undefined, }); if (input.persistContext === false) return; if (!shouldPersistToolContext(input)) return; recordRunContext(cwd, run, { kind: input.kind === "search" || input.kind === "list" ? "file-finding" : "constraint", text: `工具 ${input.toolName} 已完成:${summary}`, sourceRefs: input.sourceRefs ?? (input.path ? [input.path] : undefined), }); } function trimObservation(text: string, maxLength = 220): string { const normalized = text.trim().replace(/\s+/g, " "); return normalized.length <= maxLength ? normalized : `${normalized.slice(0, maxLength)}...`; } function shouldPersistToolContext(input: ToolSuccessObservation): boolean { return input.kind === "read" || input.kind === "search" || input.kind === "list" || input.kind === "shell"; }