fix: archive caserun agent trace rows
This commit is contained in:
@@ -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。
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user