/** * 工具事件拦截器 —— 连接 Pi 的 tool_call / tool_result 生命周期到 Harness 门禁和写入事务。 * * V2 路径(KCODE_GATE_RUNNER_V2=1): * tool_call → runGateV2("pre-tool") 做门禁校验 → WritePipeline.beginWrite 管理写入事务 * tool_result → WritePipeline.completeWrite 完成写入校验 → recordToolResultContract 生成统一合同 * * 旧路径(V2 未启用): * tool_call → evaluateToolGateway 做门禁校验 → beginWriteTransaction 管理写入事务 * tool_result → completeWriteTransaction 完成写入校验 → recordToolResultContract 生成统一合同 * * recordGenericToolResult 对所有工具(包括 kd_* 系列)统一生成结果合同, * 不区分 V1/V2 路径,确保观察记录的完整性。 */ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; import { isSubagentChild, subagentRoleFromEnv } from "../src/harness/delegation.ts"; import { recordToolBlocked } from "../src/harness/observation.ts"; import { readActiveRun, recordToolResultContract, recordWriteTransaction } from "../src/harness/state.ts"; import { evaluateToolGateway, isWriteLikeToolCall } from "../src/harness/tool-gateway.ts"; import { beginWriteTransaction, completeWriteTransaction } from "../src/harness/write-transaction.ts"; import { isV2Enabled, runGateV2 } from "../src/harness/gates/v2-bridge.ts"; type ActiveRun = NonNullable>; export function registerHarnessToolEvents(pi: ExtensionAPI): void { pi.on("tool_call", async (event, ctx) => { const input = event.input as Record; const path = typeof input.path === "string" ? input.path : undefined; const subagentRole = isSubagentChild() ? subagentRoleFromEnv() : undefined; const run = readActiveRun(ctx.cwd); // ── V2 路径:GateRunner + WritePipeline 控制面 ────────────────────────── if (isV2Enabled()) { // pre-tool 门禁:提交给 GateRunner 做策略校验 const gateDecision = await runGateV2("pre-tool", ctx.cwd, run, { toolName: event.toolName, path, payload: { subagentRole }, }); if (gateDecision && !gateDecision.allowed) { const reason = gateDecision.findings.map((f) => f.message).join("; "); return blockToolCall(ctx, event.toolName, reason || `门禁拒绝工具 ${event.toolName}`, path, "warning"); } // V2 WritePipeline:写类工具通过 WritePipeline 管理事务生命周期 if (isWriteLikeToolCall(event.toolName) && path && run) { const { getV2WritePipeline } = await import("../src/harness/gates/v2-bridge.ts"); const wp = await getV2WritePipeline(); const wr = await wp.beginWrite({ cwd: ctx.cwd, run, path, toolName: event.toolName, }); if (wr.status === "blocked") { recordWriteTransaction(ctx.cwd, run, wr.transaction); return blockToolCall(ctx, event.toolName, wr.transaction.reason ?? `WritePipeline 拒绝写入 ${path}`, path, "warning"); } recordWriteTransaction(ctx.cwd, run, wr.transaction); } return undefined; } // ── 旧路径:evaluateToolGateway + beginWriteTransaction ───────────────── // 当 KCODE_GATE_RUNNER_V2 未启用时走此分支 const decision = evaluateToolGateway({ cwd: ctx.cwd, toolName: event.toolName, path, run, subagentRole, }); if (!decision.allowed) return blockToolCall(ctx, event.toolName, decision.reason, path, decision.level); if (isWriteLikeToolCall(event.toolName) && path && run) { const transaction = beginWriteTransaction({ cwd: ctx.cwd, run, path, toolName: event.toolName }, (run.writeTransactions?.length ?? 0) + 1); recordWriteTransaction(ctx.cwd, run, transaction); if (transaction.status === "blocked") { return blockToolCall(ctx, event.toolName, transaction.reason ?? `写入事务 ${transaction.id} 被阻断。`, path, "warning"); } } return undefined; }); pi.on("tool_result", async (event, ctx) => { const input = event.input as Record; const path = typeof input.path === "string" ? input.path : undefined; const run = readActiveRun(ctx.cwd); if (!run) return undefined; if (!path || !isWriteLikeToolCall(event.toolName)) { recordGenericToolResult(ctx.cwd, run, event.toolName, path, event.isError, event.content); return undefined; } // V2 路径:通过 WritePipeline.completeWrite 完成写入校验 if (isV2Enabled()) { const transaction = findPlannedWriteTransaction(run, path); if (!transaction) return undefined; const { getV2WritePipeline } = await import("../src/harness/gates/v2-bridge.ts"); const wp = await getV2WritePipeline(); if (event.isError) { transaction.status = "blocked" as const; transaction.checks = [...transaction.checks, "tool_result:error"]; transaction.reason = `工具执行失败:${summarizeToolResult(event.content)}`; transaction.updatedAt = new Date().toISOString(); recordWriteTransaction(ctx.cwd, run, transaction); } else { const wr = await wp.completeWrite({ cwd: ctx.cwd, run, path, transactionId: transaction.id, toolName: event.toolName, }); recordWriteTransaction(ctx.cwd, run, wr.transaction); } const latestRun = readActiveRun(ctx.cwd) ?? run; const finalTx = (latestRun.writeTransactions ?? []).find((t) => t.id === transaction.id) ?? transaction; recordToolResultContract(ctx.cwd, latestRun, { toolName: event.toolName, status: finalTx.status === "verified" ? "success" : "failed", kind: "write", summary: finalTx.reason ?? `WritePipeline ${finalTx.id} ${finalTx.status}:${path}`, modifiedFiles: [path], risk: finalTx.status === "verified" ? undefined : finalTx.reason, nextAction: finalTx.status === "verified" ? "继续执行 PLAN.md 批准范围内的下一步,并记录验证证据。" : "修复写入事务阻断原因;禁止把失败写入当作完成。", }); return undefined; } // 旧路径:通过 completeWriteTransaction 完成写入校验 const transaction = findPlannedWriteTransaction(run, path); if (!transaction) return undefined; const completed = event.isError ? { ...transaction, status: "blocked" as const, checks: [...transaction.checks, "tool_result:error"], reason: `工具执行失败:${summarizeToolResult(event.content)}`, updatedAt: new Date().toISOString(), } : completeWriteTransaction({ cwd: ctx.cwd, run, path, toolName: event.toolName, transaction }); recordWriteTransaction(ctx.cwd, run, completed); const latestRun = readActiveRun(ctx.cwd) ?? run; recordToolResultContract(ctx.cwd, latestRun, { toolName: event.toolName, status: completed.status === "verified" ? "success" : "failed", kind: "write", summary: completed.reason ?? `写入事务 ${completed.id} ${completed.status}:${path}`, modifiedFiles: [path], risk: completed.status === "verified" ? undefined : completed.reason, nextAction: completed.status === "verified" ? "继续执行 PLAN.md 批准范围内的下一步,并记录验证证据。" : "修复写入事务阻断原因;禁止把失败写入当作完成。", }); return undefined; }); } function blockToolCall( ctx: ExtensionContext, toolName: string, reason: string, path: string | undefined, level: "info" | "warning" = "warning", ): { block: true; reason: string } { recordToolBlocked(ctx.cwd, readActiveRun(ctx.cwd), { toolName, path, reason }); if (ctx.hasUI) ctx.ui.notify(reason, level); return { block: true, reason }; } /** * 通用工具结果记录 —— 对所有工具(包括 kd_* 系列)统一生成结果合同。 * 不区分 V1/V2 路径,确保每次工具调用都有观察记录。 * 通过最近 3 条记录的去重避免重复写入。 */ function recordGenericToolResult(cwd: string, run: ActiveRun, toolName: string, path: string | undefined, isError: boolean | undefined, content: unknown): void { const summary = summarizeToolResult(content); const status = isError ? "failed" : "success"; // Re-read to get latest state (tool may have self-recorded via recordToolSucceeded) const latestRun = readActiveRun(cwd) ?? run; const existing = latestRun.toolResults ?? []; const duplicate = [...existing] .reverse() .slice(0, 3) .some((result) => result.toolName === toolName && result.status === status && normalizeTransactionPath(result.paths?.[0] ?? "") === normalizeTransactionPath(path ?? "")); if (duplicate) return; recordToolResultContract(cwd, latestRun, { toolName, status, summary: summary === "无工具结果摘要。" ? `工具 ${toolName} ${status}。` : summary, paths: path ? [path] : undefined, risk: isError ? summary : undefined, nextAction: isError ? "根据工具失败原因修正调用;禁止把失败结果当作已完成。" : "复用该工具结果继续当前工作线程;禁止重新猜测路径。", }); } function findPlannedWriteTransaction(run: ActiveRun, path: string) { const normalizedPath = normalizeTransactionPath(path); return [...(run.writeTransactions ?? [])].reverse().find((transaction) => transaction.status === "planned" && normalizeTransactionPath(transaction.path) === normalizedPath); } function normalizeTransactionPath(path: string): string { return path.replace(/\\/g, "/").toLowerCase(); } function summarizeToolResult(content: unknown): string { const text = typeof content === "string" ? content : Array.isArray(content) ? content .map((item) => (typeof item === "string" ? item : typeof item?.text === "string" ? item.text : "")) .filter(Boolean) .join(" ") : ""; const normalized = text.trim().replace(/\s+/g, " "); return normalized ? (normalized.length <= 160 ? normalized : `${normalized.slice(0, 160)}...`) : "无工具结果摘要。"; }