import { createHash } from "node:crypto"; import { readFile } from "node:fs/promises"; import path from "node:path"; import { isDeepStrictEqual } from "node:util"; import { truncateHead, type AgentToolResult, type ExtensionAPI, type ExtensionContext, type RpcSessionState, type SessionEntry } from "@earendil-works/pi-coding-agent"; import type { installApprovals } from "./approvals.ts"; import { current, nativeEntries, snapshot, type SessionBinding } from "./parent-writer.ts"; import { CHILD_EXIT, DELEGATION_ENTRY, createChildDialogs, delegateReadOnly, parseReadOnlySession, persistedAssistantFacts, readyChild, startChild, type ChildRpc, type ChildTask } from "./subagents.ts"; import { ABNORMAL_STATUS, COMPLETED_STATUS, createTaskProgress, summarizeToolErrors, TOOL_ERROR_GUIDANCE, type ProgressUpdate } from "./progress.ts"; import { captureCandidate, type CandidateSnapshot, type CandidateScope } from "./candidate.ts"; import { prepareReview } from "./review.ts"; import { getWriterStateRoot, parseWriterLeaseReference, resolveWorkspaceIdentity, WriterLeaseManager, type WorkspaceIdentity, type WriterLeaseOwner, type WriterLeaseRecord, type WriterLeaseReference } from "./workspace.ts"; import { appendExecutionPath, type ExecutionPathKind } from "./execution-path.ts"; export const DEVELOPMENT_TOOL = "delivery_develop"; export const REVIEW_TOOL = "delivery_review"; export const CHILD_ARM = "delivery-child-arm"; export const DEVELOPMENT_ENTRY = "delivery-development"; interface ChildGrant { lease: WriterLeaseReference; owner: WriterLeaseOwner; parent: WriterLeaseOwner; paths: string[]; protectedPaths: string[]; inputs: string[]; } const digest = (value: unknown) => createHash("sha256").update(JSON.stringify(value)).digest("hex"); export function createChildDevelopment() { let prepared: { workspace: WorkspaceIdentity; gitDir: string; leases: WriterLeaseManager; owner: WriterLeaseOwner } | undefined; let armed = false; let stopped = false; const lifetime = new AbortController(); return { async ready(runId: string, ctx: ExtensionContext) { if (prepared || !runId || stopped) throw new Error("子 writer 已准备、已关闭或执行 ID 缺失"); const workspace = await resolveWorkspaceIdentity(ctx.cwd); const stateRoot = await getWriterStateRoot(workspace); const leases = new WriterLeaseManager(stateRoot); const owner: WriterLeaseOwner = { kind: "child", sessionId: ctx.sessionManager.getSessionId(), pid: process.pid, processToken: leases.processToken, runId }; prepared = { workspace, gitDir: path.dirname(stateRoot), leases, owner }; return { ...owner }; }, async arm(value: unknown, ctx: ExtensionContext) { if (!prepared || armed || stopped || ctx.mode !== "rpc") throw new Error("子 writer 尚未准备、已接收交接或模式不符"); const grant = snapshot(value) as ChildGrant; const lease = parseWriterLeaseReference(grant?.lease); if (!lease || !isDeepStrictEqual(grant.owner, prepared.owner) || !Array.isArray(grant.paths) || !grant.paths.length || !Array.isArray(grant.protectedPaths) || !grant.protectedPaths.length || [...grant.paths, ...grant.protectedPaths].some((item) => typeof item !== "string" || !path.isAbsolute(item))) throw new Error("子 writer 的交接范围或身份无效"); const record = await prepared.leases.read(prepared.workspace.key); if (!record || record.leaseId !== lease.leaseId || lease.workspaceKey !== prepared.workspace.key || !isDeepStrictEqual(record.owner, prepared.owner) || !isDeepStrictEqual(record.coordinator, grant.parent)) throw new Error("父 writer 尚未真实交接给本子会话"); if (!Array.isArray(grant.inputs) || grant.inputs.some((item) => typeof item !== "string" || !path.isAbsolute(item))) throw new Error("审查输入范围无效"); if (!ctx.sessionManager.getSessionFile()) throw new Error("子 Session 不持久化,未开放文件写入"); armed = true; }, async finish(ctx: ExtensionContext) { stopped = true; lifetime.abort(new Error("子会话正在关闭")); if (!prepared) throw new Error("子 writer 尚未准备"); const file = ctx.sessionManager.getSessionFile(); if (!file) throw new Error("子 Session 文件缺失"); let content: string; try { content = await readFile(file, "utf8"); } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT" && !ctx.sessionManager.getEntries().some((entry) => entry.type === "message")) return { owner: { ...prepared.owner }, clean: true }; throw error; } if (!content.endsWith("\n")) throw new Error("子 Session 记录不完整"); const rows = content.trimEnd().split("\n").map((line) => JSON.parse(line)); if (rows[0]?.id !== prepared.owner.sessionId || !isDeepStrictEqual(rows.slice(1), snapshot(ctx.sessionManager.getEntries()))) throw new Error("子执行记录尚未完整落盘"); return { owner: { ...prepared.owner }, clean: true, historyDigest: digest(rows) }; }, }; } interface DevelopmentRun extends SessionBinding { workspace: WorkspaceIdentity; progress?: ReturnType; id: string; name: string; finished: boolean; attemptedLease: boolean; call?: SessionEntry; lease?: WriterLeaseReference; owner?: WriterLeaseOwner; parent?: WriterLeaseOwner; leases?: WriterLeaseManager; child?: RpcSessionState; rpc?: ChildRpc; childTerminal?: string; childTerminalFacts?: string; taskSent?: boolean; approvalId?: string; designApprovalId?: string; readonlyReference?: Record; readonlyStarted?: boolean; readonlyTerminal?: string; executionPath?: { path: ExecutionPathKind; node: string; reason: string; approvalId: string; sessionId: string; workspaceKey: string }; stage?: string; interruption?: string; toolErrors?: boolean; pathEnded?: boolean; review?: { result: Awaited>; artifact: Awaited>; initialCandidate: CandidateSnapshot; candidate: CandidateSnapshot; changed: boolean }; problem?: unknown; terminalVerified?: boolean; fault?: string; run?: Promise>; result?: { content: AgentToolResult["content"]; details?: unknown; isError: boolean }; } async function childTerminal(state: DevelopmentRun) { const { rpc, child, owner } = state; if (!rpc || rpc.exit?.code !== 0 || rpc.exit.signal !== null || rpc.failure || rpc.openTools.size) throw new Error("子 Pi 或工具终态未知,保留 writer"); if (!state.taskSent && owner?.kind === "parent") return { digest: digest({ pid: rpc.process.pid, exit: rpc.exit, taskSent: false }), last: undefined }; if (!child?.sessionFile || !owner) throw new Error("子 Pi 或工具终态未知,保留 writer"); let content: string; try { content = await readFile(child.sessionFile, "utf8"); } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT" && !state.taskSent) return { digest: digest({ owner, exit: rpc.exit, taskSent: false }), last: undefined }; throw error; } if (!content.endsWith("\n")) throw new Error("子执行记录不完整"); const rows = content.trimEnd().split("\n").map((line) => JSON.parse(line)); const exits = rows.filter((row) => row.type === "custom" && row.customType === CHILD_EXIT); const exit = exits[0]; if (rows[0]?.id !== child.sessionId || exits.length !== 1 || exit.data?.pid !== rpc.process.pid || exit.data?.sessionId !== child.sessionId || exit.data?.development?.clean !== true || !isDeepStrictEqual(exit.data.development.owner, owner) || exit.data.development.historyDigest !== digest(rows.slice(0, rows.indexOf(exit))) || rows.slice(rows.indexOf(exit) + 1).some((row) => row.type === "message")) throw new Error("子 writer 的持久收尾记录不符"); const last = rows.findLast((row) => row.type === "message" && row.message?.role === "assistant")?.message; return { digest: digest(rows), last, toolNotes: summarizeToolErrors(rows) }; } async function readonlyTerminal(state: DevelopmentRun) { const reference = state.readonlyReference!; const exit = reference.exit as { code: number | null; signal: string | null } | undefined; if (reference.phase !== "ended" || reference.status === "unknown" || exit?.code !== 0 || exit.signal !== null) throw new Error("审查子 Pi 的实际终态未知,保留 writer"); if (!state.readonlyStarted) return digest(reference); if (typeof reference.sessionFile !== "string" || typeof reference.sessionId !== "string") throw new Error("审查子 Session 引用缺失,保留 writer"); return digest(parseReadOnlySession(await readFile(reference.sessionFile, "utf8"), reference.sessionId, reference.pid as number)); } function executionFacts(state: DevelopmentRun) { return { stage: state.stage, modelTerminal: state.childTerminalFacts ?? state.readonlyReference?.modelTerminal, childSessionFile: state.child?.sessionFile, childSessionPersisted: Boolean(state.childTerminal), childTerminalDigest: state.childTerminal, childProcessExit: state.rpc?.exit ? { code: state.rpc.exit.code, signal: state.rpc.exit.signal } : state.readonlyReference?.exit, childProcessFailure: state.rpc?.failure ? String(state.rpc.failure) : state.readonlyReference?.processFailure, inFlightTools: state.rpc ? [...state.rpc.openTools] : state.readonlyReference?.inFlightTools, toolErrors: state.rpc?.toolError ?? state.readonlyReference?.toolErrors ?? state.toolErrors, interruption: state.interruption, readonlySessionFile: state.readonlyReference?.sessionFile, readonlySessionPersisted: Boolean(state.readonlyTerminal), readonlyTerminalDigest: state.readonlyTerminal, reviewDirectory: state.readonlyReference?.reviewDirectory, reviewStatus: state.review ? state.review.changed ? "invalidated" : "valid" : undefined, }; } export async function verifyRecordedResult(state: DevelopmentRun, ctx: ExtensionContext) { if ((state.rpc || state.owner?.kind === "child") && (!state.childTerminal || (await childTerminal(state)).digest !== state.childTerminal)) throw new Error("子执行终态已变化"); if (state.readonlyReference?.pid && (!state.readonlyTerminal || await readonlyTerminal(state) !== state.readonlyTerminal)) throw new Error("审查子执行终态已变化或未知"); const records = await nativeEntries(state, ctx); records.requireEntry(state.call!); const matches = records.entries.filter((entry) => entry.type === "message" && entry.message.role === "toolResult" && entry.message.toolCallId === state.id); const result = matches[0]; if (matches.length !== 1 || !result || result.type !== "message" || result.message.role !== "toolResult" || result.message.toolName !== state.name || !isDeepStrictEqual(snapshot({ content: result.message.content, details: result.message.details, isError: result.message.isError }), state.result)) throw new Error("父交付工具终态未唯一落盘或与实际结果不符"); records.requireEntry(result); if (records.branch.findIndex((row) => row.id === result.id) <= records.branch.findIndex((row) => row.id === state.call!.id)) throw new Error("父交付工具终态不在本次调用之后"); } export function createDevelopmentDelegator(pi: ExtensionAPI, approvals: ReturnType, recoverResidual?: (ctx: ExtensionContext, signal?: AbortSignal) => Promise) { let starting = false; let active: DevelopmentRun | undefined; let stopped = false; let finishing: Promise | undefined; let shutdownSettled: (() => void) | undefined; pi.on("tool_result", (event) => { const state = active; if (!state?.finished || !state.result?.isError || event.toolCallId !== state.id || event.toolName !== state.name) return; if (!isDeepStrictEqual({ content: event.content, isError: event.isError }, { content: state.result.content, isError: state.result.isError })) return; const progress = state.progress?.snapshot(); if (!progress?.endedAt) return; const details = { ...(event.details as Record | undefined), ...(state.result.details as Record | undefined), progress }; state.result = snapshot({ ...state.result, details }); if (!state.attemptedLease && active === state) active = undefined; return { details }; }); async function execute(name: string, input: ChildTask, signal: AbortSignal | undefined, ctx: ExtensionContext, update: ProgressUpdate): Promise> { if (stopped || starting || (active && (!active.fault || !recoverResidual))) throw new Error("交付 writer 尚未完成交接或已关闭,未启动新任务"); starting = true; try { return await executeStarted(name, input, signal, ctx, update); } finally { starting = false; } } async function executeStarted(name: string, input: ChildTask, signal: AbortSignal | undefined, ctx: ExtensionContext, update: ProgressUpdate): Promise> { signal?.throwIfAborted(); const sessionFile = ctx.sessionManager.getSessionFile(); if (!sessionFile) throw new Error("没有持久父 Session,未启动交付任务"); const grant = await approvals.readApproval(ctx, signal); const state: DevelopmentRun = { id: input.id, name, cwd: ctx.cwd, sessionId: ctx.sessionManager.getSessionId(), sessionFile, lifetime: new AbortController(), workspace: grant.workspace, finished: false, attemptedLease: false }; const progress = state.progress = createTaskProgress(input.id, name === REVIEW_TOOL ? "审查" : "开发", input.task, update); progress.phase("准备中"); const setStage = (value: string, detail?: string) => { state.stage = value; progress.stage(value, detail); }; setStage("调用前范围校验"); let call!: SessionEntry; let paths!: string[], inputs!: string[]; try { const rawPaths = input.paths ?? [], rawInputs = input.inputs ?? []; if (rawPaths.some((value) => !value.trim()) || rawInputs.some((value) => !value.trim())) throw new Error("调用前范围校验失败:开发或审查路径不能是空白字符串。"); paths = rawPaths.map((value) => path.resolve(ctx.cwd, value)); inputs = rawInputs.map((value) => path.resolve(ctx.cwd, value)); if (!paths.length) throw new Error("调用前范围校验失败:本次开发或审查必须提供非空 paths;实施计划由 AI 内部维护,不再通过第二次确认声明范围。"); const outside = (value: string) => { const relative = path.relative(grant.workspace.workspacePath, value); return relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative); }; if ([...paths, ...inputs].some(outside)) throw new Error("调用前范围校验失败:开发或审查路径必须在当前 worktree 内。"); const overlaps = (left: string, right: string) => { const relative = path.relative(left, right); return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`)); }; const conflicts = [ ...paths.map((value) => ({ field: "paths", value })), ...inputs.map((value) => ({ field: "inputs", value })), ].filter(({ value }) => grant.planningPaths.some((planning) => overlaps(planning, value) || overlaps(value, planning))); if (conflicts.length) { const ranges = [...new Set(conflicts.map(({ field, value }) => `${field}=${path.relative(grant.workspace.workspacePath, value) || "."}`))]; const shown = ranges.slice(0, 8); const suffix = ranges.length > shown.length ? `;另有 ${ranges.length - shown.length} 项` : ""; throw new Error(`调用前范围校验失败:开发或审查路径不能包含父维护规划文档。\n冲突范围:${shown.join("、")}${suffix}\n请只把需要核对的源码、测试或配置放入 paths;额外只读文件放入 inputs。父维护规划文档由父会话维护,不放入本次范围。\n子 Session 尚未启动。`); } const records = await nativeEntries(state, ctx); const candidate = records.branch.findLast((entry) => entry.type === "message" && entry.message.role === "assistant"); const toolCalls = candidate?.type === "message" && candidate.message.role === "assistant" ? candidate.message.content.filter((part) => part.type === "toolCall" && part.id === input.id && part.name === name && isDeepStrictEqual(snapshot(part.arguments), snapshot(input.toolInput))) : []; if (!candidate || toolCalls.length !== 1) throw new Error("本次交付工具调用未核实"); records.requireEntry(candidate); if (records.entries.some((entry) => entry.type === "message" && entry.message.role === "toolResult" && entry.message.toolCallId === input.id)) throw new Error("交付工具调用已有终态,不能重放"); call = candidate; await recoverResidual?.(ctx, AbortSignal.any([grant.signal, ...(signal ? [signal] : [])])); current(state, ctx); grant.signal.throwIfAborted(); signal?.throwIfAborted(); if (stopped || active) throw new Error("交付 writer 尚未完成交接或已关闭,未启动新任务"); } catch (error) { const message = error instanceof Error ? error.message : String(error); state.result = { content: [{ type: "text", text: message }], details: {}, isError: true }; state.finished = true; progress.end(ABNORMAL_STATUS, message); return { content: state.result.content, details: { progress: progress.snapshot(), executionFacts: { stage: state.stage } }, isError: true }; } active = state; const operation = signal ? AbortSignal.any([signal, state.lifetime.signal]) : state.lifetime.signal; state.run = Promise.resolve().then(async () => { let executionSignal = operation; let dialogs: ReturnType | undefined; try { setStage("批准与候选准备"); state.approvalId = grant.approvalId; state.designApprovalId = grant.approvalId; const running = executionSignal = AbortSignal.any([operation, grant.signal]); setStage("writer 与候选准备"); state.call = call; state.executionPath = { path: name === REVIEW_TOOL ? "delivery_review" : "delivery_develop", node: input.task, reason: name === REVIEW_TOOL ? "独立核对当前候选和实际检查结果" : "父 Pi 委派本节点给独立开发子 Agent", approvalId: grant.approvalId, sessionId: state.sessionId, workspaceKey: grant.workspace.key }; appendExecutionPath(pi, { id: input.id, ...state.executionPath, phase: "started" }); const stateRoot = await getWriterStateRoot(grant.workspace); state.leases = new WriterLeaseManager(stateRoot); current(state, ctx); running.throwIfAborted(); state.attemptedLease = true; const acquired = await state.leases.acquire(grant.workspace, { kind: "parent", sessionId: state.sessionId, pid: process.pid, runId: input.id }); if (!acquired.ok) { state.attemptedLease = false; throw new Error(acquired.reason); } state.lease = { ...acquired.reference }; state.owner = { ...acquired.record.owner }; const parent = state.parent = { ...state.owner }; if (name === REVIEW_TOOL) { setStage("候选与审查制品准备"); const scope: CandidateScope = { workspace: grant.workspace, readPaths: inputs, writePaths: paths, protectedPaths: [...grant.planningPaths, sessionFile, path.dirname(stateRoot)] }; const candidate = await captureCandidate(scope, running); const artifact = await prepareReview(scope, candidate); const reviewTask = [ "独立验收和代码审查。不要只表态是否完成:先列出方案与实现所依赖的关键事实(调用的接口、数据结构、假定的行为),逐条到实际代码中核对并给出文件与行号;再列出你认为最可能失败但现有检查未覆盖的路径。只有经实际读取或实际运行证实的结论才写“成立”,其余明确标注为未验证。", "主动识别项目已有的测试、编译、lint 或其他适合本任务的检查并实际运行,记录命令和真实结果;没有运行不能声称通过。", "沿用父 Pi 的全部普通工具和权限;职责是独立检查、运行检查并报告问题,默认不修改源码。发现问题交回父 Pi,由父 Pi 决定直接修复或重新委派开发;不批准、不继续委派。", `已批准方案:${grant.designBody}`, `内部实施计划由父会话维护;本次节点:${input.task}`, `候选:${candidate.digest}`, `代码路径:${JSON.stringify(paths)}`, `父维护的规划文档:${JSON.stringify(grant.planningPaths)}(本次设计确认声明的父维护文档,不在你的修改范围,出现在工作区不算越界改动;但文档描述与实际代码是否一致仍要核对)`, `额外审查输入:${JSON.stringify(inputs)}`, `审查证据:${JSON.stringify({ reviewDirectory: artifact.directory, diffFile: artifact.diffFile })}`, `差异基线:${artifact.baseHead ?? "无 HEAD,空基线"};before/after 为原始 Git blob/当前文件副本。`, `审查重点:${input.task}`, ].join("\n\n"); setStage("子 Session 启动"); const result = await delegateReadOnly({ ...input, readPaths: [...(input.readPaths ?? []), artifact.directory], task: reviewTask }, running, (data) => { if (data.phase === "started") { state.readonlyStarted = true; setStage("子 Agent 工具执行"); } if (data.phase === "ended") setStage("子 Session 收尾"); state.readonlyReference = snapshot({ ...data, approvalId: grant.approvalId, designApprovalId: grant.approvalId, reviewDirectory: artifact.directory }); pi.appendEntry(DELEGATION_ENTRY, state.readonlyReference); }, update, ctx, progress, "review"); setStage("审查候选核对"); const finalCandidate = await captureCandidate(scope, running); state.toolErrors = result.toolErrors; const changed = finalCandidate.digest !== candidate.digest; const finalArtifact = changed ? await prepareReview(scope, finalCandidate) : artifact; state.review = { result, artifact: finalArtifact, initialCandidate: candidate, candidate: finalCandidate, changed }; } else { setStage("子 Session 启动"); const rpc = state.rpc = await startChild(input, "development"); const interrupt = new AbortController(); const childSignal = AbortSignal.any([running, interrupt.signal]); dialogs = createChildDialogs(rpc, ctx, childSignal, interrupt); rpc.onEvent = (event) => { progress.event(event); dialogs!.handle(event); }; const { state: child, data } = await readyChild(rpc, input, childSignal, (value) => { state.child = value; }); progress.agent({ provider: child.model!.provider, id: child.model!.id, thinking: child.thinkingLevel, reason: input.selectionReason }); if (data.owner?.pid !== rpc.process.pid || data.owner?.sessionId !== child.sessionId || data.owner?.runId !== input.id) throw new Error("子 writer 身份未核实"); setStage("子 Agent 工具执行"); const childOwner = snapshot(data.owner) as WriterLeaseOwner; await state.leases.handoff(state.lease, parent, childOwner, async () => { const latest = await approvals.readApproval(ctx, childSignal); if (latest.approvalId !== grant.approvalId) throw new Error("方案授权已变化"); current(state, ctx); }, childSignal); state.owner = childOwner; await rpc.control(CHILD_ARM, input.entryPath, childSignal, JSON.stringify({ lease: state.lease, owner: state.owner!, parent, paths, inputs, protectedPaths: [...grant.planningPaths, sessionFile] } satisfies ChildGrant)); pi.appendEntry(DEVELOPMENT_ENTRY, { id: input.id, phase: "started", lease: state.lease, childSessionFile: child.sessionFile, approvalId: grant.approvalId, designApprovalId: grant.approvalId, agent: progress.snapshot().agent }); state.taskSent = true; progress.phase("运行中", "开发子任务已接收 writer", child.sessionFile); const developmentTask = [ "开发子任务。沿用父 Pi 原有工具和权限,完成批准范围内的实现;必要时主动运行项目测试或编译检查,但不要把自检当成独立审查。", "不要修改父规划文档,不继续委派,不执行未授权的外部写入。", `本机工作目录:${input.cwd}`, `本次开发路径:${JSON.stringify(paths)}`, `父维护的规划文档(不要修改):${JSON.stringify(grant.planningPaths)}`, `额外审查输入:${JSON.stringify(inputs)}`, `已批准方案:${grant.designBody}`, `内部实施计划由父会话维护;本次节点:${input.task}`, `本次任务:${input.task}`, ].join("\n\n"); await Promise.all([rpc.waitSettled(childSignal), rpc.request({ type: "prompt", message: developmentTask }, childSignal)]); setStage("子 Session 收尾"); } } catch (error) { state.problem = error; } if (state.problem && (state.rpc || state.readonlyStarted || state.stage === "子 Session 启动" || state.stage === "子 Agent 工具执行")) setStage("子 Session 收尾"); progress.phase(executionSignal.aborted ? "正在取消" : "核对收尾中"); try { await dialogs?.close(); } catch (error) { state.problem ??= error; } try { if (state.rpc) await state.rpc.stop(input.entryPath); if (state.rpc) { const terminal = await childTerminal(state); state.childTerminal = terminal.digest; state.childTerminalFacts = persistedAssistantFacts(terminal.last); if (terminal.last?.stopReason !== "stop") { const message = `子模型没有正常完成的持久模型终态\n${state.childTerminalFacts}`; state.problem = state.problem ? new Error(`${state.problem instanceof Error ? state.problem.message : String(state.problem)}\n${message}`, { cause: state.problem }) : new Error(message); } } if (state.readonlyReference?.pid) state.readonlyTerminal = await readonlyTerminal(state); } catch (error) { const original = state.problem ? `${state.problem instanceof Error ? state.problem.message : String(state.problem)}\n` : ""; state.problem = new Error(`${original}交付任务收尾失败:${String(error)}`, { cause: state.problem ?? error }); } if (executionSignal.aborted) { state.interruption = String(executionSignal.reason ?? "父回合中断"); state.problem ??= executionSignal.reason; } try { if (state.problem) throw state.problem; if (state.review) { const review = state.review; const reviewHasToolErrors = Boolean(review.result.toolErrors); progress.end(COMPLETED_STATUS, review.changed ? "审查期间候选发生变化,原审查结论失效" : reviewHasToolErrors ? "子 Agent 存在工具失败,审查结论不能直接视为通过" : undefined); const text = review.changed ? [ "审查执行已结束,但未形成有效结论:审查期间候选发生变化。", `审查开始候选:${review.initialCandidate.digest}`, `审查结束候选:${review.candidate.digest}`, "审查正文只对应审查开始时的候选,不能用于判断当前候选。工作区未自动回滚,请父 Pi 核对当前变更后重新调用 delivery_review。", `审查原始记录:${review.result.sessionFile}`, `当前候选制品:${review.artifact.directory}`, `当前差异:${review.artifact.diffFile}`, ].join("\n") : [`审查执行已结束;独立验收和审查结论见下文,最终由父 Pi 对照实际命令结果和当前候选核对,不自动等于交付通过(修复方式:单文件且不改对外行为契约的由父 Pi 直接改并复跑检查;多文件或金额、并发、权限等边界问题重新委派 delivery_develop):`, review.result.text, `候选:${review.candidate.digest}`, `审查原始记录:${review.result.sessionFile}`, `审查制品:${review.artifact.directory}`, `实际差异:${review.artifact.diffFile}`].join("\n"); if (review.changed) throw new Error(text); setStage("父结果与 writer 交接"); const details = { reviewStatus: "valid", candidate: review.candidate, reviewSessionFile: review.result.sessionFile, diffFile: review.artifact.diffFile, pid: review.result.pid, toolErrors: review.result.toolErrors, readonlySessionPersisted: Boolean(state.readonlyTerminal), executionFacts: executionFacts(state), progress: progress.snapshot() }; const result = { content: [{ type: "text" as const, text }], details }; state.result = snapshot({ ...result, isError: false }); appendExecutionPath(pi, { id: input.id, ...state.executionPath!, phase: "ended", status: "completed", toolErrors: review.result.toolErrors, sessionFile: review.result.sessionFile }); state.pathEnded = true; return result; } const terminal = await childTerminal(state); const text = ((terminal.last as any)?.content ?? []).filter((part: any) => part.type === "text").map((part: any) => part.text).join(""); setStage("父结果与 writer 交接"); const hasToolErrors = Boolean(terminal.toolNotes); progress.end(COMPLETED_STATUS, hasToolErrors ? "子 Agent 存在工具失败,检查结论不能直接视为通过" : undefined); const result = { content: [{ type: "text" as const, text: `开发执行已结束;检查结论由父 Pi 根据真实命令结果核对,仍需核对实际变更和审查结果:\n${truncateHead(text).content}\n子会话:${state.child!.sessionFile}${state.rpc!.toolError ? `\n\n${TOOL_ERROR_GUIDANCE}\n${terminal.toolNotes?.text ?? "请查看原始子 Session 的工具返回。"}` : ""}` }], details: { childSessionFile: state.child!.sessionFile, childSessionId: state.child!.sessionId, pid: state.rpc!.process.pid, toolErrors: terminal.toolNotes, childSessionPersisted: true, childProcessExit: { code: state.rpc!.exit?.code, signal: state.rpc!.exit?.signal }, executionFacts: executionFacts(state), progress: progress.snapshot() } }; state.result = snapshot({ ...result, isError: false }); appendExecutionPath(pi, { id: input.id, ...state.executionPath!, phase: "ended", status: "completed", toolErrors: state.rpc!.toolError, sessionFile: state.child!.sessionFile ?? undefined }); state.pathEnded = true; return result; } catch (error) { progress.end(ABNORMAL_STATUS, `失败阶段:${state.stage ?? "未取得"}`); const childSession = state.child?.sessionFile ?? state.readonlyReference?.sessionFile; const toolFailure = state.rpc?.toolError ?? state.toolErrors ? "\n工具失败:子 Agent 存在工具失败,不能把正常进程退出当作检查通过。" : ""; const text = `失败阶段:${state.stage ?? "未取得"}\n` + (error instanceof Error ? error.message : String(error)) + toolFailure + (typeof childSession === "string" ? `\n原始子 Session:${childSession}` : "\n子 Session 引用尚未取得。") + (typeof state.readonlyReference?.reviewDirectory === "string" ? `\n审查制品:${state.readonlyReference.reviewDirectory}` : "") + (state.child && !state.taskSent ? "\n子任务尚未发送,Session 文件可能尚未生成。" : "") + (state.rpc || state.readonlyReference?.pid ? `\n子收尾核验:${state.childTerminal || state.readonlyTerminal ? "已取得证明,交接时仍须复核。" : "未取得证明,保持关闭。"}` : "") + `\n父 Session:${state.sessionFile}\n本次工具调用:${state.id}` + (state.lease ? "\n父 writer 尚待本次工具结果落盘后核验交接;此失败结果不证明已交回,可用 /delivery-status 核对现场。" : ""); const failure = new Error(text, { cause: error }); if (state.executionPath && !state.pathEnded) { appendExecutionPath(pi, { id: input.id, ...state.executionPath, phase: "ended", status: state.childTerminal || state.readonlyTerminal ? "failed" : "unknown", toolErrors: state.rpc?.toolError ?? state.toolErrors, error: text, ...(typeof childSession === "string" ? { sessionFile: childSession } : {}) }); state.pathEnded = true; } state.result = snapshot({ content: [{ type: "text", text }], details: { executionFacts: executionFacts(state) }, isError: true }); throw failure; } finally { state.finished = true; } }); return state.run; } async function finish(ctx: ExtensionContext) { const state = active; if (!state?.finished || state.fault) return; try { if (!state.lease || !state.owner || !state.parent || !state.leases || !state.result || !state.call) throw new Error("交付 writer 获取或执行状态未知"); const verify = async () => { await verifyRecordedResult(state, ctx); state.terminalVerified = true; }; if (state.owner.kind === "child") await state.leases.releaseChild(state.lease, state.owner, state.parent, verify, state.lifetime.signal); else await state.leases.releaseParent(state.lease, state.owner, verify, state.lifetime.signal); active = undefined; } catch (error) { state.stage = "writer 交接"; state.progress?.stage("writer 交接", "交付 writer 未交回,保持关闭"); state.fault = String(error); ctx.ui.notify(`交付 writer 未交回,保持关闭:${state.fault}`, "error"); } } const settle = async (_event: unknown, ctx: ExtensionContext) => { if (finishing) return finishing; finishing = finish(ctx); try { await finishing; } finally { finishing = undefined; } }; pi.on("turn_end", settle); pi.on("agent_settled", async (event, ctx) => { try { await settle(event, ctx); } finally { shutdownSettled?.(); } }); const preventSwitch = () => active ? { cancel: true } : undefined; pi.on("session_before_switch", preventSwitch); pi.on("session_before_fork", preventSwitch); pi.on("session_before_tree", preventSwitch); pi.on("session_shutdown", async (_event, ctx) => { stopped = true; const settled = active && !ctx.isIdle() ? new Promise((resolve) => { shutdownSettled = resolve; }) : undefined; if (settled) ctx.abort(); await Promise.allSettled([active?.run, finishing, settled]); shutdownSettled = undefined; active?.lifetime.abort(new Error("父会话正在关闭或重载")); if (active) ctx.ui.notify("交付操作已停止;交接未完成,保持关闭,不自动恢复。", "warning"); }); return { execute: (input: ChildTask, signal: AbortSignal | undefined, ctx: ExtensionContext, update: ProgressUpdate) => execute(DEVELOPMENT_TOOL, input, signal, ctx, update), review: (input: ChildTask, signal: AbortSignal | undefined, ctx: ExtensionContext, update: ProgressUpdate) => execute(REVIEW_TOOL, input, signal, ctx, update), get progress() { return active?.progress?.snapshot(); }, get pending() { return active !== undefined; }, // 恢复沿用原终态核验;没有 lease 时只能由调用方请求明确确认后清理。 async verifyRecovery(ctx: ExtensionContext, workspace: WorkspaceIdentity, record?: WriterLeaseRecord) { const state = active; if (stopped || !state?.finished || !state.fault || !state.terminalVerified || !state.lease || !isDeepStrictEqual(state.workspace, workspace) || state.lease.workspaceKey !== workspace.key || (record && (state.lease.leaseId !== record.leaseId || !isDeepStrictEqual(record.workspace, workspace) || !isDeepStrictEqual(record.owner, state.owner) || !isDeepStrictEqual(record.coordinator, state.owner?.kind === "child" ? state.parent : undefined)))) { throw new Error("上次交付的结束状态与当前占用记录不一致,暂未清理;请查看详情后再继续。"); } await verifyRecordedResult(state, ctx); current(state, ctx); if (active !== state) throw new Error("恢复期间交付运行态已变化,未清理。"); return { runId: state.id, leaseId: state.lease.leaseId }; }, reconcileAfterUnlock(workspaceKey: string, leaseId: string): void { if (!active?.finished || !active.fault || !active.terminalVerified || active.lease?.workspaceKey !== workspaceKey || active.lease.leaseId !== leaseId) throw new Error("交付运行态已变化,未复位。"); active = undefined; }, // 已结束且未自动收尾的失败是终态记录,不是仍在途的执行。 get fault() { return active?.fault; }, }; }