From 841594437efeafce9354f16682c17d3d6671d76d Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Sat, 6 Jun 2026 22:49:12 +0800 Subject: [PATCH] fix: archive caserun agent trace rows --- docs/reference/spec-hwpod-harness.md | 16 ++-- tools/hwlab-cli/caserun.test.ts | 44 +++++++---- tools/src/hwlab-caserun-lib.ts | 109 ++++++++++++++++++++++++--- 3 files changed, 137 insertions(+), 32 deletions(-) diff --git a/docs/reference/spec-hwpod-harness.md b/docs/reference/spec-hwpod-harness.md index 0eb796ce..1012b47a 100644 --- a/docs/reference/spec-hwpod-harness.md +++ b/docs/reference/spec-hwpod-harness.md @@ -111,7 +111,9 @@ Keil MDK 装配规则:当 `spec.workspace.toolchain` 为 `keil-mdk`、`keil` ## hwpod-node-ops -`hwpod-node-ops` 不表达用户意图,只表达 node 可执行的基础动作。第一版最小集合: +`hwpod-node-ops` 不表达用户意图,只表达 node 可执行的基础动作。当前新增能力边界收敛为文件读写/patch 类 op 加 `cmd.run`:有 `cmd.run` 就能覆盖 host 上成熟 CLI、编译器、下载器、串口工具和维护命令,再加上受控文件读写即可支撑源码修改 case。后续新增 case 不应因为 build、download、job status、UART、reset 或某个调试工具需要而新增专用 node op;这些业务翻译必须优先落在 `hwpod-compiler-cli` / `hwpod-cli` 组合层。 + +基础 op 集合: | op | 用途 | | --- | --- | @@ -125,15 +127,11 @@ Keil MDK 装配规则:当 `spec.workspace.toolchain` 为 `keil-mdk`、`keil` | `workspace.write` | 带 `expectedSha`、换行策略和 diff 摘要的整文件写入 | | `workspace.replace` | 带 `expectedSha`、唯一匹配检查和 diff 摘要的精确文本替换 | | `workspace.insert-after` | 按精确 anchor 插入文本,避免 PowerShell/cmd quoting | -| `debug.build` | 编译工程 | -| `debug.download` | 下载或烧录目标设备 | -| `debug.reset` | 复位目标设备 | -| `io.uart.read` | 读取 UART | -| `io.uart.write` | 写入 UART | -| `io.uart.jsonrpc` | 通过 UART 做 JSON-RPC | -| `cmd.run` | 临时维护命令透传,仅用于 `hwpod-ctl` 或早期薄 node 调试 | +| `cmd.run` | 通用 host 命令透传,用于组合 Keil、serial-monitor、job status、下载、复位和临时维护动作 | -`hwpod-node` 是 HWPOD 的唯一受控节点执行器。第一阶段的业务扩展优先落在 `hwpod-compiler-cli` / `hwpod-cli`,通过已有文件读写 op 和 `cmd.run` 组合 Keil、serial-monitor 等成熟工具;不要因为某个 case 需要 build、job status、download、UART 或临时维护动作就新增专用 node op。`cmd.run` 的命令解析、PATH 稳定性和跨平台差异必须在 `hwpod-node` 本体内处理;当 Windows service、后台进程或 D601 host 的环境变量与交互 shell 不一致时,不得用 gateway shell、手工 PowerShell、预先手工创建 worktree 或其他旁路代替 `hwpod-node-ops -> hwpod-node`。D601/Windows 实地调试应使用 UniDesk SSH 透传 `D601:win` 观察进程、PATH、工具安装位置和日志,但修复必须回到 `hwpod-node` 源码、配置或启动入口,并用 `/v1/hwpod-node-ops` 原链路复测。 +若历史实现中仍存在 `debug.*` 或 `io.*` 这类专用 op,它们只能作为兼容存量观察对象,不再作为新 case 或新 harness 业务的扩展方向。标准路径是:compiler 根据 spec 生成 `cmd.run` 的 `command` + `argv`,node 只负责在目标 host 上忠实执行并返回 stdout/stderr/exit code。 + +`hwpod-node` 是 HWPOD 的唯一受控节点执行器。第一阶段的业务扩展优先落在 `hwpod-compiler-cli` / `hwpod-cli`,通过已有文件读写 op 和 `cmd.run` 组合 Keil、serial-monitor 等成熟工具;不要因为某个 case 需要 build、job status、download、UART 或临时维护动作就新增专用 node op。`cmd.run` 的命令解析、PATH 稳定性和跨平台差异必须在 `hwpod-node` 本体内处理;当 Windows service、后台进程或 D601 host 的环境变量与交互 shell 不一致时,不得用 gateway shell、手工 PowerShell、预先手工创建 worktree 或其他旁路代替 `hwpod-node-ops -> hwpod-node`。D601/Windows 实地调试应使用 UniDesk SSH 透传 `D601:win` 观察进程、PATH、工具安装位置和日志,但修复必须回到 `hwpod-node` 源码、配置或启动入口,并用 `/v1/hwpod-node-ops` 原链路复测。只有 `cmd.run` 自身的执行语义、环境继承或可见性不满足真实 host 执行时,才修改 `hwpod-node`;否则优先改 compiler/CLI。 Windows subject worktree 文本修改优先使用 `workspace.apply-patch`、`workspace.replace`、`workspace.insert-after` 或 `workspace.write`,不要把 `cmd.run` 的 PowerShell/cmd quoting 当成标准编辑路径。文本编辑结果必须返回编辑前后 SHA、文件字节数、换行类型、diff 摘要和 dry-run 状态;`apply-patch` 匹配失败时必须返回 normalized preview、候选行号、CRLF/LF 统计、文件 SHA/bytes 和 hwpod-node implementation version,便于判断是上下文不匹配、CRLF 问题还是远端实现版本问题。`workspace.apply-patch` 应保留原文件换行风格,尤其不能把 CRLF subject 文件静默改写成 LF。 diff --git a/tools/hwlab-cli/caserun.test.ts b/tools/hwlab-cli/caserun.test.ts index 9b43aa5f..c8f6bf54 100644 --- a/tools/hwlab-cli/caserun.test.ts +++ b/tools/hwlab-cli/caserun.test.ts @@ -319,6 +319,14 @@ test("case run orchestrates agent prompt, diff capture, and compile evidence wit runProcess: async (command) => { runProcessCalls.push(command); const gitArgs = command.slice(1); + if (command.includes("client") && command.includes("agent") && command.includes("trace") && command.includes("--full")) { + const traceId = command[command.indexOf("trace") + 1] ?? "trc_case_test"; + return { command, exitCode: 0, stdout: JSON.stringify({ ok: true, action: "client.agent.trace", status: "succeeded", body: { traceId, status: "succeeded", traceStatus: "completed", render: "web", source: "hwlab-cli.client.agent.trace", sourceEventCount: 3, renderedRowCount: 3, rows: [ + { rowId: "trace-request:1", seq: 1, tone: "source", header: "00:00:00 请求接受", body: null }, + { rowId: "tool:cmd-build", seq: 2, tone: "ok", header: "00:00:01 ok commandExecution", bodyFormat: "text", body: "hwpod build --spec .hwlab/hwpod-spec.yaml\nstdout:\n{ \"ok\": true, \"job_id\": \"20260605_203835_798515c0\" }\nexitCode=0" }, + { rowId: "trace-final-response:3", seq: 3, tone: "ok", header: "00:00:02 助手最终消息", terminal: true, bodyFormat: "markdown", body: "done" } + ], finalResponse: { present: true, status: "completed", text: "done" }, traceSummary: { terminalStatus: "completed" } } }), stderr: "" }; + } if (command[0] === "git" && gitArgs[0] === "status") return { command, exitCode: 0, stdout: "?? runs/d601-f103-v2-compile/run-agent-flow/\n", stderr: "" }; if (command[0] === "git" && gitArgs[0] === "add") return { command, exitCode: 0, stdout: "", stderr: "" }; if (command[0] === "git" && gitArgs[0] === "diff") return { command, exitCode: 1, stdout: "", stderr: "" }; @@ -341,19 +349,21 @@ test("case run orchestrates agent prompt, diff capture, and compile evidence wit assert.equal(result.payload.traceLookup.commands.trace, `hwlab-cli client agent trace ${result.payload.agent.traceId} --base-url http://web.test --render web`); assert.equal(result.payload.traceLookup.commands.result, `hwlab-cli client agent result ${result.payload.agent.traceId} --base-url http://web.test`); assert.equal(result.payload.traceLookup.commands.inspect, `hwlab-cli client agent inspect --trace-id ${result.payload.agent.traceId} --base-url http://web.test`); - assert.equal(result.payload.agentTrace.source, "caserun-identity"); - assert.equal(result.payload.agentTrace.lookupOnly, true); - assert.equal(result.payload.agentTrace.hwpodBuildCommandCount, 0); - assert.equal(result.payload.evidence.agentTrace.commandCount, 0); - assert.equal(result.payload.evidence.agentTrace.hwpodCommandCount, 0); - assert.equal(result.payload.evidence.agentTrace.keilJobCandidates.includes("20260605_203835_798515c0"), false); + assert.equal(result.payload.agentTrace.source, "hwlab-cli.client.agent.trace"); + assert.equal(result.payload.agentTrace.lookupOnly, undefined); + assert.equal(result.payload.agentTrace.commandCount, 1); + assert.equal(result.payload.agentTrace.hwpodCommandCount, 1); + assert.equal(result.payload.agentTrace.hwpodBuildCommandCount, 1); + assert.equal(result.payload.evidence.agentTrace.commandCount, 1); + assert.equal(result.payload.evidence.agentTrace.hwpodCommandCount, 1); + assert.equal(result.payload.evidence.agentTrace.keilJobCandidates.includes("20260605_203835_798515c0"), true); assert.equal(result.payload.evidence.agentTask.timeoutMs, 234000); assert.equal(result.payload.agent.polls <= 3, true); assert.equal(result.payload.evidence.hwpod.source, "case-run-runner-post-agent-compile-check"); assert.match(result.payload.agentDiff.statusShort, /projects\/01_baseline\/main\.c/u); assert.equal(result.payload.build.autoEvaluation, false); assert.equal(result.payload.build.traceLookup.commands.trace, result.payload.traceLookup.commands.trace); - assert.equal(result.payload.build.agentTraceHwpodCommandCount, 0); + assert.equal(result.payload.build.agentTraceHwpodCommandCount, 1); assert.equal(result.payload.build.hwpodExitCode, 0); assert.equal(result.payload.build.hwpodSource, "case-run-runner-post-agent-compile-check"); assert.equal(result.payload.build.keilStatus, "completed"); @@ -389,8 +399,8 @@ test("case run orchestrates agent prompt, diff capture, and compile evidence wit assert.equal(manifest.trace.traceId, result.payload.agent.traceId); assert.equal(manifest.trace.sessionId, "ses_case_run"); assert.equal(manifest.trace.conversationId, "cnv_case_d601-f103-v2-compile_run-agent-flow"); - assert.equal(manifest.trace.source, "caserun-identity"); - assert.equal(manifest.trace.lookupOnly, true); + assert.equal(manifest.trace.source, "hwlab-cli.client.agent.trace"); + assert.equal(manifest.trace.lookupOnly, undefined); assert.equal(manifest.trace.lookup.commands.trace, result.payload.traceLookup.commands.trace); assert.equal(manifest.traceLookup.commands.inspect, result.payload.traceLookup.commands.inspect); assert.equal(manifest.prompt.sha256, result.payload.evidence.agent.promptSha256); @@ -403,7 +413,7 @@ test("case run orchestrates agent prompt, diff capture, and compile evidence wit assert.equal(manifest.readableAgent.finalResponsePath, "final-response.md"); assert.equal(manifest.readableAgent.finalResponse.present, true); assert.equal(manifest.readableAgent.finalResponse.text, "done"); - assert.deepEqual(manifest.agentStage.kinds, []); + assert.deepEqual(manifest.agentStage.kinds, ["build"]); assert.equal(manifest.agentStage.autoEvaluation, false); assert.equal(manifest.decisions.autoEvaluation, false); assert.equal(manifest.decisions.downloadSkipped, undefined); @@ -415,10 +425,11 @@ test("case run orchestrates agent prompt, diff capture, and compile evidence wit assert.equal(manifest.files.some((item: any) => item.path === ".hwlab/hwpod-spec.yaml"), true); const messages = JSON.parse(await readFile(path.join(registryRunDir, "agent-messages.json"), "utf8")); assert.equal(messages.renderer, "tools/src/hwlab-cli/trace-renderer:traceDisplayRows"); - assert.equal(messages.lookupOnly, true); + assert.equal(messages.lookupOnly, undefined); assert.equal(messages.traceLookup.commands.trace, result.payload.traceLookup.commands.trace); assert.equal(messages.finalResponse.present, true); - assert.equal(messages.rows.length, 0); + assert.equal(messages.rows.length, 3); + assert.match(JSON.stringify(messages.rows), /hwpod build --spec \.hwlab\/hwpod-spec\.yaml/u); const traceMarkdown = await readFile(path.join(registryRunDir, "agent-trace.md"), "utf8"); assert.match(traceMarkdown, /renderer: tools\/src\/hwlab-cli\/trace-renderer:traceDisplayRows/u); assert.match(traceMarkdown, /traceLookupStrategy: id_plus_existing_cli/u); @@ -430,13 +441,15 @@ test("case run orchestrates agent prompt, diff capture, and compile evidence wit assert.match(finalResponse, /done/u); assert.equal(result.payload.collect.caseRepoRunDir, registryRunDir); assert.equal(result.payload.collect.artifactManifestPath, path.join(registryRunDir, "artifact-manifest.json")); - assert.deepEqual(result.payload.collect.agentStage.commands, []); + assert.deepEqual(result.payload.collect.agentStage.kinds, ["build"]); assert.equal(result.payload.collect.caseRepoFiles.some((item: string) => item.endsWith("artifact-manifest.json")), true); const registryResult = JSON.parse(await readFile(path.join(registryRunDir, "result.json"), "utf8")); assert.equal(registryResult.collect.artifactManifestPath, path.join(registryRunDir, "artifact-manifest.json")); const summaryMarkdown = await readFile(path.join(registryRunDir, "summary.md"), "utf8"); assert.match(summaryMarkdown, /traceLookupStrategy: id_plus_existing_cli/u); assert.match(summaryMarkdown, /traceCommand: hwlab-cli client agent trace trc_case_/u); + assert.match(summaryMarkdown, /agentTraceHwpodCommandCount: 1/u); + assert.match(summaryMarkdown, /hwpod build --spec \.hwlab\/hwpod-spec\.yaml/u); assert.doesNotMatch(summaryMarkdown, /downloadSkipped/u); const prompt = await readFile(path.join(cwd, ".state", "hwlab-cli", "caserun", "run-agent-flow", "agent-prompt.md"), "utf8"); @@ -451,6 +464,8 @@ test("case run orchestrates agent prompt, diff capture, and compile evidence wit assert.match(prompt, /CaseRun has already placed the run-local spec/u); assert.match(prompt, /hwpod-ctl spec validate --spec \.hwlab\/hwpod-spec\.yaml/u); assert.match(prompt, /standard hwpod\/hwpod-ctl commands/u); + assert.match(prompt, /hwpod job status --spec \.hwlab\/hwpod-spec\.yaml/u); + assert.match(prompt, /不要再用 shell timeout\/head\/pipe 包住/u); const diff = await readFile(path.join(cwd, ".state", "hwlab-cli", "caserun", "run-agent-flow", "agent-diff.patch"), "utf8"); assert.match(diff, /printf\("hello"\)/u); assert.equal(requests.some((item) => item.url === "http://web.test/v1/agent/sessions"), true); @@ -462,6 +477,9 @@ test("case run orchestrates agent prompt, diff capture, and compile evidence wit assert.doesNotMatch(agentChatRequest.body.message, /path: "F:\\Work\\HWLAB-CASE-F103\\\.worktree\\caserun-run-agent-flow"/u); assert.equal(requests.some((item) => item.url === "http://web.test/v1/agent/chat/result/" + result.payload.agent.traceId), true); assert.equal(requests.some((item) => item.url === "http://web.test/v1/agent/chat/trace/" + result.payload.agent.traceId), false); + const traceCommand = runProcessCalls.find((command) => command.includes("client") && command.includes("agent") && command.includes("trace") && command.includes("--render") && command.includes("web") && command.includes("--full")); + assert.equal(Boolean(traceCommand), true); + assert.equal(traceCommand?.includes("--base-url"), false); } finally { await rm(root, { recursive: true, force: true }); } diff --git a/tools/src/hwlab-caserun-lib.ts b/tools/src/hwlab-caserun-lib.ts index e092561f..12ead8b2 100644 --- a/tools/src/hwlab-caserun-lib.ts +++ b/tools/src/hwlab-caserun-lib.ts @@ -181,6 +181,13 @@ type CaseReadableAgentArchive = { finalResponse: Record; }; +type ArchivedAgentTraceSnapshot = { + traceBody: any; + rows: TraceEventRow[]; + events: Record[]; + summary: AgentTraceStage; +}; + export async function caseCommand(context: CaseContext) { const subcommand = context.rest[0] || "help"; if (["help", "--help", "-h"].includes(subcommand) || context.parsed.help === true) return caseHelp(); @@ -250,6 +257,7 @@ export async function runCaseRun(context: CaseContext) { finalCollect = collectSummaryFromArchive(completed.run, collected.evidence, finalArchive); await writeJson(resultPath, compactCaseRunResult({ ...collected, run: completed.run, summary: finalCollect })); } + const finalAgentTrace = collected.evidence.agentTrace ?? traceStage.trace; return ok("case.run", { caseId: collected.run.caseId, runId: collected.run.runId, @@ -257,10 +265,10 @@ export async function runCaseRun(context: CaseContext) { compileOnly: true, prepare: prepared.summary, agent: agentSummary(agentStage.agent), - agentTrace: traceSummary(traceStage.trace), + agentTrace: traceSummary(finalAgentTrace), traceLookup: agentTraceLookupForRun(traceStage.run), agentDiff: diffSummary(diffStage.diff), - build: built.summary, + build: buildSummary(collected.evidence), collect: finalCollect, evidence: collected.evidence }, "completed"); @@ -1010,7 +1018,8 @@ async function renderAgentTaskPrompt(run: PreparedCaseRun, agentTask: PreparedAg "CaseRun 已在 AgentRun workspace 预装 .hwlab/hwpod-spec.yaml;必须使用这个 run-local HWPOD spec,不要从 prompt 重建 spec。", "如果 .hwlab/hwpod-spec.yaml 缺失或内容明显不是本次 case,请报告 CaseRun workspace setup 错误,不要自行编造或迁移 spec。", "若 case prompt 要求源码修改,必须只改 subjectWorktreePath;若 prompt 未要求源码修改,则保持 subject 源码不变。", - "不要运行 CaseRun 答案执行器;你本人必须通过 hwpod/hwpod-ctl 标准入口触发编译验证。" + "不要运行 CaseRun 答案执行器;你本人必须通过 hwpod/hwpod-ctl 标准入口触发编译验证。", + "hwpod build/download 是长任务短连接入口;不要再用 shell timeout/head/pipe 包住它们。记录返回 JSON 里的 jobId/job_id,再用 hwpod job status --spec .hwlab/hwpod-spec.yaml 做有限轮询。" ].filter(Boolean); return [ `# HWPOD CaseRun Code Agent Task`, @@ -1037,6 +1046,7 @@ async function renderAgentTaskPrompt(run: PreparedCaseRun, agentTask: PreparedAg `- Run \`hwpod-ctl spec validate --spec .hwlab/hwpod-spec.yaml\`.`, `- Run \`hwpod inspect --spec .hwlab/hwpod-spec.yaml\`.`, `- Follow the case task using standard hwpod/hwpod-ctl commands. Run build/download/UART steps only when the case explicitly asks for them, and report returned JSON/job/artifact/serial summaries.`, + `- For hwpod build/download, keep the HWPOD command unwrapped so it can return async JSON; then poll the returned job id with \`hwpod job status --spec .hwlab/hwpod-spec.yaml\` a bounded number of times.`, `- CaseRun will inspect git diff under subjectWorktreePath after your turn completes and may run a runner post-check compile as separate evidence.`, `- CaseRun records trace/session/conversation, agent commandExecution, workspace diff and Keil build evidence without auto-grading them.` ].join("\n"); @@ -1063,7 +1073,7 @@ function summarizeAgentTrace(traceId: string, httpStatus: number, body: any): Ag lookup: compactObject(traceBody?.lookup ?? body?.lookup ?? null), sourceEventCount: numberOption(traceBody?.sourceEventCount ?? traceBody?.traceSummary?.sourceEventCount ?? body?.sourceEventCount), renderedRowCount: rows.length || numberOption(traceBody?.renderedRowCount), - commandCount: commands.length, + commandCount: allCommands.length, hwpodCommandCount, hwpodBuildCommandCount, keilJobCandidates: uniqueStrings(Array.from(searchable.matchAll(/20\d{6}_\d{6}_[0-9a-f]{8}/giu)).map((match) => match[0])), @@ -1578,6 +1588,9 @@ async function archiveCaseRunArtifacts(context: CaseContext, run: PreparedCaseRu const runRel = caseRegistryRunRel(run); const caseRepoRunDir = path.join(run.caseRepo, runRel); await mkdir(caseRepoRunDir, { recursive: true }); + const agentTraceSnapshot = await archivedAgentTraceSnapshot(context, run, evidence); + applyArchivedAgentTraceSummary(run, evidence, agentTraceSnapshot.summary); + await writeJson(path.join(run.runDir, "evidence.json"), evidence); await writeJson(path.join(caseRepoRunDir, "evidence.json"), evidence); await writeFile(path.join(caseRepoRunDir, "summary.md"), renderEvidenceSummary(evidence), "utf8"); @@ -1585,7 +1598,7 @@ async function archiveCaseRunArtifacts(context: CaseContext, run: PreparedCaseRu ["evidence.json", path.join(run.runDir, "evidence.json")], ["summary.md", undefined] ]); - const readableAgent = await writeReadableAgentArtifacts(caseRepoRunDir, run, evidence); + const readableAgent = await writeReadableAgentArtifacts(caseRepoRunDir, run, evidence, agentTraceSnapshot); for (const rel of readableAgent.generatedFiles) materialized.set(rel, undefined); const skippedFiles: string[] = []; for (const entry of caseRunArtifactEntries(run)) { @@ -1649,14 +1662,14 @@ function collectSummaryFromArchive(run: PreparedCaseRun, evidence: any, archive: }; } -async function writeReadableAgentArtifacts(caseRepoRunDir: string, run: PreparedCaseRun, evidence: any): Promise { +async function writeReadableAgentArtifacts(caseRepoRunDir: string, run: PreparedCaseRun, evidence: any, snapshot: ArchivedAgentTraceSnapshot): Promise { const messagesRel = "agent-messages.json"; const traceRel = "agent-trace.md"; const transcriptRel = "agent-transcript.md"; const finalResponseRel = "final-response.md"; - const traceBody = await archivedAgentTraceBody(run, evidence); - const events = arrayOfObjects(traceBody?.events ?? traceBody?.trace?.events ?? traceBody?.sourceEvents); - const rows = traceDisplayRows(traceBody ?? {}, events); + const traceBody = snapshot.traceBody; + const events = snapshot.events; + const rows = snapshot.rows; const finalResponse = agentFinalResponse(run, evidence, traceBody, rows); const traceLookup = traceBody?.lookup ?? evidence.traceLookup ?? agentTraceLookupForRun(run, evidence); const diffPatch = await readTextIfExists(evidence.agentDiff?.diffPatchPath ?? run.agentDiff?.diffPatchPath ?? path.join(run.runDir, "agent-diff.patch")); @@ -1694,9 +1707,85 @@ async function writeReadableAgentArtifacts(caseRepoRunDir: string, run: Prepared }; } +async function archivedAgentTraceSnapshot(context: CaseContext, run: PreparedCaseRun, evidence: any): Promise { + const initialTrace = await archivedAgentTraceBody(run, evidence); + const traceBody = await materializeFullAgentTraceWithExistingCli(context, run, evidence, initialTrace); + const bodyForSummary = traceBody?.body && typeof traceBody.body === "object" ? traceBody.body : traceBody; + const events = arrayOfObjects(bodyForSummary?.events ?? bodyForSummary?.trace?.events ?? bodyForSummary?.sourceEvents); + const rows = traceRowsFromBody(bodyForSummary, events); + const hasRenderableTrace = rows.length > 0 || events.length > 0; + const summary = hasRenderableTrace + ? summarizeAgentTrace(text(bodyForSummary?.traceId ?? traceBody?.traceId ?? evidence.agent?.traceId ?? run.agent?.traceId), numberOption(traceBody?.httpStatus) ?? 0, traceBody) + : existingAgentTraceSummary(run, evidence, bodyForSummary); + return { traceBody: bodyForSummary, rows, events, summary }; +} + +function existingAgentTraceSummary(run: PreparedCaseRun, evidence: any, traceBody: any): AgentTraceStage { + const existing = evidence.agentTrace ?? run.agentTrace; + if (existing?.traceId) return existing as AgentTraceStage; + return summarizeAgentTrace(text(traceBody?.traceId ?? evidence.agent?.traceId ?? run.agent?.traceId), 0, traceBody); +} + async function archivedAgentTraceBody(run: PreparedCaseRun, evidence: any) { const rawTrace = await readJsonIfExists(path.join(run.runDir, "agent-trace.json")); - return rawTrace ?? agentTraceBody(run, evidence); + if (rawTrace) return rawTrace; + const resultTrace = agentTraceBody(run, evidence); + if (resultTrace) return resultTrace; + return run.agent ? agentTraceIdentityArtifact(run, run.agent) : clean({ traceId: evidence.agentTrace?.traceId ?? run.agentTrace?.traceId, status: evidence.agentTrace?.status ?? run.agentTrace?.status ?? "recorded", lookupOnly: true }); +} + +async function materializeFullAgentTraceWithExistingCli(context: CaseContext, run: PreparedCaseRun, evidence: any, initialTrace: any) { + const body = initialTrace?.body && typeof initialTrace.body === "object" ? initialTrace.body : initialTrace; + const existingRows = arrayOfObjects(body?.rows ?? body?.renderedRows ?? body?.traceRows); + const existingEvents = arrayOfObjects(body?.events ?? body?.trace?.events ?? body?.sourceEvents); + if (existingRows.length > 0) return initialTrace; + if (existingEvents.length > 0) return initialTrace; + if (!run.agent) return initialTrace; + const traceId = text(body?.traceId ?? initialTrace?.traceId ?? evidence.agent?.traceId ?? run.agent?.traceId); + const lookup = agentTraceLookupForRun(run, evidence); + const apiKey = text(context.env.HWLAB_API_KEY ?? process.env.HWLAB_API_KEY); + if (!traceId || !lookup || !apiKey.startsWith(HWLAB_API_KEY_PREFIX)) return initialTrace; + const baseUrl = text((lookup as any).baseUrl) || webUrlFrom(context, run.definition); + const command = [process.execPath, path.join(context.cwd, "tools/hwlab-cli/bin/hwlab-cli.mjs"), "client", "agent", "trace", traceId, "--render", "web", "--full"]; + const result = await (context.runProcess ?? runProcess)(command, context.cwd, { ...process.env, ...context.env, HWLAB_RUNTIME_WEB_URL: baseUrl }); + if (result.exitCode !== 0) { + return clean({ ...body, traceFetch: { status: "failed", source: "existing-hwlab-cli", command: commandVisibility(command), exitCode: result.exitCode, stderr: clipText(result.stderr, 1200), stdout: clipText(result.stdout, 1200) } }); + } + const parsed = parseJsonMaybe(result.stdout); + if (!parsed) return clean({ ...body, traceFetch: { status: "failed", source: "existing-hwlab-cli", command: commandVisibility(command), exitCode: 0, error: "invalid_json_stdout", stdout: clipText(result.stdout, 1200) } }); + const parsedBody = parsed?.body && typeof parsed.body === "object" ? parsed.body : null; + const materialized = parsedBody ? { ...parsed, body: clean({ ...parsedBody, source: text(parsedBody.source) || "hwlab-cli.client.agent.trace", lookup: parsedBody.lookup ?? lookup }) } : clean({ ...parsed, source: text(parsed.source) || "hwlab-cli.client.agent.trace", lookup: parsed.lookup ?? lookup }); + await writeJson(path.join(run.runDir, "agent-trace.json"), materialized); + return materialized; +} + +function traceRowsFromBody(traceBody: any, events: Record[]) { + const existingRows = arrayOfObjects(traceBody?.rows ?? traceBody?.renderedRows ?? traceBody?.traceRows).map(traceEventRowFromObject).filter((row) => text(row.rowId || row.header || row.body)); + return existingRows.length > 0 ? existingRows : traceDisplayRows(traceBody ?? {}, events); +} + +function traceEventRowFromObject(row: Record): TraceEventRow { + return { + rowId: text(row.rowId ?? row.id), + seq: numberOption(row.seq) ?? null, + tone: traceRowTone(row.tone), + header: text(row.header ?? row.title ?? row.label), + body: row.body === null || row.body === undefined ? null : text(row.body ?? row.content ?? row.text ?? row.message), + terminal: row.terminal === true ? true : undefined, + bodyFormat: row.bodyFormat === "markdown" ? "markdown" : row.bodyFormat === "text" ? "text" : undefined + }; +} + +function traceRowTone(value: unknown): TraceEventRow["tone"] { + const raw = text(value); + return raw === "ok" || raw === "blocked" || raw === "warn" || raw === "source" ? raw : "source"; +} + +function applyArchivedAgentTraceSummary(run: PreparedCaseRun, evidence: any, summary: AgentTraceStage) { + if (!summary?.traceId) return; + run.agentTrace = summary; + evidence.agentTrace = summary; + evidence.traceLookup = evidence.traceLookup ?? agentTraceLookupForRun(run, evidence); } function agentTraceBody(run: PreparedCaseRun, evidence: any) {