fix: archive caserun agent trace rows

This commit is contained in:
Codex Agent
2026-06-06 22:49:12 +08:00
parent b73cf2d019
commit 841594437e
3 changed files with 137 additions and 32 deletions
+7 -9
View File
@@ -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。
+31 -13
View File
@@ -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 <jobId> --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 });
}
+99 -10
View File
@@ -181,6 +181,13 @@ type CaseReadableAgentArchive = {
finalResponse: Record<string, unknown>;
};
type ArchivedAgentTraceSnapshot = {
traceBody: any;
rows: TraceEventRow[];
events: Record<string, unknown>[];
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 <jobId> --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 <jobId> --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<CaseReadableAgentArchive> {
async function writeReadableAgentArtifacts(caseRepoRunDir: string, run: PreparedCaseRun, evidence: any, snapshot: ArchivedAgentTraceSnapshot): Promise<CaseReadableAgentArchive> {
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<ArchivedAgentTraceSnapshot> {
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<string, unknown>[]) {
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<string, unknown>): 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) {