import type { ThinkingLevel } from "@earendil-works/pi-agent-core"; import type { ClusterTaskInput, ReviewResult, WorkerResult } from "./types.ts"; import { isSuccessfulWorker, runPiJson, type PiRunEvent, type ProcessPauseControl } from "./worker.ts"; function extractJson(text: string): unknown { const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i); const candidate = fenced?.[1] ?? text.slice(text.indexOf("{"), text.lastIndexOf("}") + 1); if (!candidate || !candidate.trim()) return undefined; try { return JSON.parse(candidate); } catch { return undefined; } } function asString(value: unknown): string { return typeof value === "string" ? value : ""; } function asStringArray(value: unknown): string[] { return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : []; } export interface ReviewerOptions { cwd: string; model: string; thinkingLevel?: ThinkingLevel; tools?: string[]; timeoutMs: number; signal?: AbortSignal; pauseControl?: ProcessPauseControl; onEvent?: (event: PiRunEvent) => void; apiRetryDelaysMs?: readonly number[]; } export interface ReviewerExecutionResult { review: ReviewResult; reviewer: WorkerResult; } export async function reviewWorkerResult( task: ClusterTaskInput, worker: WorkerResult, options: ReviewerOptions, ): Promise { const reviewPrompt = [ "请审核下面的 worker 执行结果。只根据任务目标和验收标准判断,不要修改任何文件。", "必须只输出一个 JSON 对象,不要输出 Markdown,不要输出解释文字。", "decision 只能是 pass、retry、escalate、ask_user 之一。", "pass 表示所有验收标准都满足;retry 表示当前等级有明确可修正的问题;escalate 表示问题需要更强模型;ask_user 表示信息不足、目标冲突或无法安全自动处理。", "JSON 格式:{\"decision\":\"pass|retry|escalate|ask_user\",\"reason\":\"...\",\"missingCriteria\":[\"...\"],\"nextInstruction\":\"...\"}", "任务:", JSON.stringify( { id: task.id, title: task.title, taskType: task.taskType, task: task.task, acceptanceCriteria: task.acceptanceCriteria, }, null, 2, ), "worker 结果:", JSON.stringify( { exitCode: worker.exitCode, output: worker.output, stderr: worker.stderr.slice(-10_000), stopReason: worker.stopReason, errorMessage: worker.errorMessage, usage: worker.usage, }, null, 2, ), ].join("\n\n"); const systemPrompt = [ "你是一个严格的代码任务审核器。", "你没有写文件权限,只能检查输入中的执行结果。", "不要因为 worker 声称完成就直接通过;必须逐条核对验收标准。", "输出必须是可解析的 JSON 对象。", ].join("\n"); const reviewer = await runPiJson({ cwd: options.cwd, model: options.model, thinkingLevel: options.thinkingLevel, tools: options.tools, systemPrompt, prompt: reviewPrompt, timeoutMs: options.timeoutMs, signal: options.signal, pauseControl: options.pauseControl, onEvent: options.onEvent, apiRetryDelaysMs: options.apiRetryDelaysMs, }); let review: ReviewResult; if (reviewer.exitCode === 124) { review = { decision: "timeout", reason: reviewer.errorMessage || `审核器超过 ${options.timeoutMs}ms 未完成。`, missingCriteria: [], nextInstruction: "请检查审核器配置或稍后重新运行任务。", rawOutput: reviewer.output, }; } else if (!isSuccessfulWorker(reviewer)) { review = { decision: "error", reason: reviewer.errorMessage || reviewer.stderr.slice(-4_000) || `审核器退出码为 ${reviewer.exitCode}`, missingCriteria: [], nextInstruction: "请检查审核器进程错误后重新运行任务。", rawOutput: reviewer.output, }; } else { const parsed = extractJson(reviewer.output); if (!isRecord(parsed)) { review = { decision: "error", reason: `审核器返回了无效 JSON。${reviewer.errorMessage ? ` ${reviewer.errorMessage}` : ""}`.trim(), missingCriteria: [], nextInstruction: "请检查审核器原始输出后重新运行任务。", rawOutput: reviewer.output, }; } else { const decision = parsed.decision; if (decision !== "pass" && decision !== "retry" && decision !== "escalate" && decision !== "ask_user") { review = { decision: "error", reason: "审核器返回了未知 decision。", missingCriteria: [], nextInstruction: "请检查审核器原始输出后重新运行任务。", rawOutput: reviewer.output, }; } else { review = { decision, reason: asString(parsed.reason) || "审核器未提供原因。", missingCriteria: asStringArray(parsed.missingCriteria), nextInstruction: asString(parsed.nextInstruction), rawOutput: reviewer.output, }; } } } return { review, reviewer }; } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); }