fix: polish caserun aggregate trace markdown

This commit is contained in:
Codex Agent
2026-06-08 13:04:29 +08:00
parent 225269b5ad
commit a309f06867
3 changed files with 52 additions and 15 deletions
+2 -2
View File
@@ -301,7 +301,7 @@ CaseRun 的 trace 处理必须遵循 [spec-v02-code-agent-trace.md](spec-v02-cod
`hwlab-cli case aggregate <caseId> --run-id <runId> --case-repo /root/hwlab-case-registry` 是已完成 CaseRun 的无服务二次整理入口。它只读取 case registry repo 里已有的 `runs/<caseId>/<runId>/` 产物,不启动新的 CaseRun、不访问硬件、不触发 CI/CD、不追加自动评价或 pass/fail 判定;默认输出 `runs/<caseId>/<runId>/aggregate.md`,并只把这个 markdown 文件 `git add``git commit``git push` 回 case registry repo。若 registry 里存在其他并行 dirty 产物,`case aggregate` 不得顺带提交它们。
`aggregate.md` 是该 run 的主阅读入口,必须按固定顺序聚合运行环境信息、HWPOD 信息、Code Agent 信息、输入 Prompt、低噪声 Trace、final response 和最后 diff。Trace 正文优先使用 `agent-messages.json` 里的共享 Web/CLI renderer rows;普通 message row 直接展示正文,不做折叠,只有工具调用 row 使用 `<details>/<summary>`,且 `<summary>` 本身就是折叠按钮,文案写成 `已运行 <command>`。缺少 rows 时才嵌入已有 `agent-trace.md`。聚合文件可以列原始产物索引和 trace/result/inspect 命令作为审计线索,但不能把 `summary.md``result.json``final-response.md` 或原 trace 文件继续声明为对外主阅读路径。
`aggregate.md` 是该 run 的主阅读入口,必须按固定顺序聚合运行环境信息、HWPOD 信息、Code Agent 信息、输入 Prompt、低噪声 Trace、final response 和最后 diff。Trace 正文优先使用 `agent-messages.json` 里的共享 Web/CLI renderer rows;普通 message row 直接加粗展示正文,不做折叠,只有工具调用 row 使用 `- <details>/<summary>` 列表项折叠,且 `<summary>` 本身就是折叠按钮,文案写成单行截短的 `已运行 <command>`工具调用展开体必须保留或恢复 `stdout:``stderr:``exitCode=` 等结构换行,不能把详情挤成一行。缺少 rows 时才嵌入已有 `agent-trace.md`。聚合文件可以列原始产物索引和 trace/result/inspect 命令作为审计线索,但不能把 `summary.md``result.json``final-response.md` 或原 trace 文件继续声明为对外主阅读路径。
验收已完成 case04 时,使用现有 registry 产物回放,不重新启动 CaseRun:
@@ -328,6 +328,6 @@ hwlab-cli case aggregate d601-f103-v2-arm2d-integration \
7. 长任务验收必须覆盖 `hwlab-cli case run start <caseId>` 立即返回,以及 `status/result/logs <runId>` 在 worker 运行中能短查询到阶段、耗时、stdout/stderr byte count、trace/session/conversation/thread 信息和下一条轮询命令;不得把同步命令长时间无输出视为有效 CaseRun 操作体验。
8. `d601-f103-v2-compile` 的真实流程只覆盖当前编排链路和 compile-only smoke;操作员直接运行 `hwpod-cli build`、直接调用 Keil 或只检查 node 侧 job,不等同于 CaseRun 流程跑完。
9. agent-task CaseRun 的真实流程必须证明 `case run` 已创建或调用 Code Agent / AgentRun session,任务 prompt 已进入 agent 上下文,CaseRun 已采集隔离 subject worktree diff,并已继续执行编译、下载或 I/O smoke 等后续动作;当前版本只记录这些事实,不自动判断 agent 是否完成任务。
10. 已完成 run 的 registry 聚合验收必须通过 `hwlab-cli case aggregate <caseId> --run-id <runId>` 输出单个 `aggregate.md`,并证明该文件包含运行环境、HWPOD、Code Agent、Prompt、低噪声 Trace、final response 和最后 diffTrace 中 message 直接展示,工具调用用 `已运行 <command>` summary 折叠。该命令不得启动新 CaseRun,也不得提交当前聚合 markdown 以外的并行 registry dirty 文件。
10. 已完成 run 的 registry 聚合验收必须通过 `hwlab-cli case aggregate <caseId> --run-id <runId>` 输出单个 `aggregate.md`,并证明该文件包含运行环境、HWPOD、Code Agent、Prompt、低噪声 Trace、final response 和最后 diffTrace 中 message 加粗直接展示,工具调用用 `- <details>` 缩进和单行截短的 `已运行 <command>` summary 折叠,展开后保留详情换行。该命令不得启动新 CaseRun,也不得提交当前聚合 markdown 以外的并行 registry dirty 文件。
11. Windows subject worktree 文本编辑必须能在 CRLF 文件中通过 `workspace.apply-patch` 完成源码修改,返回 before/after SHA 与 diff 摘要,并保留原文件 CRLF;`workspace.apply-patch` 在 context 不匹配时必须返回可诊断 payload,而不是只给 `context not found`
12. Keil `debug.download` 自动装配必须输出结构化 `cmd.run` argv 步骤;带空格的 `probeName` 在 plan 中必须是单独 argv 元素。`io.uart.read` 必须通过节点本地 `serial-monitor` 读取正在监控的 COM/baud;未监控或工具失败时必须返回包含 `ioProbe`/COM 口/baud/monitor status/启动建议的结构化 blocker details。
+11 -5
View File
@@ -684,14 +684,16 @@ test("case aggregate writes one registry markdown entry without grading or broad
artifacts: [],
decisions: { runnerPostAgentCompileCheck: "recorded" }
});
const longTraceCommand = "hwpod workspace cat projects/01_baseline/Middlewares/Arm-2D/Library/include/arm_2d_types.h --spec .hwlab/hwpod-spec.yaml 2>&1 | grep -A 20 \"typedef struct arm_2d_tile_t\" 2>/dev/null || hwpod workspace cat projects/01_baseline/Middlewares/Arm-2D/Library/include/arm_2d_types.h --spec .hwlab/hwpod-spec.yaml 2>&1 | grep";
await writeJsonFile(path.join(registryRunDir, "agent-messages.json"), {
renderer: "tools/src/hwlab-cli/trace-renderer:traceDisplayRows",
sourceEventCount: 3,
renderedRowCount: 3,
sourceEventCount: 4,
renderedRowCount: 4,
rows: [
{ rowId: "row-1", seq: 1, header: "请求接受", body: "CaseRun started" },
{ rowId: "row-2", seq: 2, header: "ok commandExecution", body: "hwpod build --spec .hwlab/hwpod-spec.yaml\nexitCode=0" },
{ rowId: "row-3", seq: 3, header: "助手最终消息", terminal: true, bodyFormat: "markdown", body: "Build completed" }
{ rowId: "row-3", seq: 3, header: "ok commandExecution", body: `${longTraceCommand} stdout: first line\nsecond line exitCode=0` },
{ rowId: "row-4", seq: 4, header: "助手最终消息", terminal: true, bodyFormat: "markdown", body: "Build completed" }
]
});
await writeJsonFile(path.join(registryRunDir, "artifact-manifest.json"), {
@@ -743,8 +745,12 @@ test("case aggregate writes one registry markdown entry without grading or broad
assert.match(aggregate, /## Prompt/u);
assert.match(aggregate, /## Trace/u);
assert.doesNotMatch(aggregate, / 2\. ok commandExecution/u);
assert.match(aggregate, /<details>\n<summary> hwpod build --spec \.hwlab\/hwpod-spec\.yaml<\/summary>/u);
assert.match(aggregate, /<summary> hwpod build --spec \.hwlab\/hwpod-spec\.yaml<\/summary>[\s\S]*```text\nhwpod build --spec \.hwlab\/hwpod-spec\.yaml\nexitCode=0[\s\S]*<\/details>\n\nBuild completed\n\n## Final Response/u);
assert.match(aggregate, /\*\*CaseRun started\*\*/u);
assert.match(aggregate, /- <details>\n <summary> hwpod build --spec \.hwlab\/hwpod-spec\.yaml<\/summary>/u);
assert.match(aggregate, / ```text\n hwpod build --spec \.hwlab\/hwpod-spec\.yaml\n exitCode=0[\s\S]* <\/details>/u);
assert.match(aggregate, /<summary> hwpod workspace cat projects\/01_baseline\/Middlewares\/Arm-2D\/Library\/include\/arm_2d_type\.\.\.<\/summary>/u);
assert.match(aggregate, / ```text\n hwpod workspace cat [^\n]+\n stdout:\n first line\n second line\n exitCode=0[\s\S]* <\/details>\n\n\*\*Build completed\*\*\n\n## Final Response/u);
for (const [, summary] of aggregate.matchAll(/<summary>([\s\S]*?)<\/summary>/gu)) assert.doesNotMatch(summary, /\n|stdout:|stderr:|exitCode=|grep -A 20/u);
assert.doesNotMatch(aggregate, /<summary>.*/u);
assert.match(aggregate, /## Final Response/u);
assert.match(aggregate, /## Diff/u);
+39 -8
View File
@@ -86,21 +86,21 @@ export function renderTraceRowsMarkdown(rows: TraceEventRow[] = []): string {
export function renderTraceRowMarkdown(row: TraceEventRow, index = 1): string {
if (!isToolTraceRow(row)) return renderTraceMessageRowMarkdown(row);
const body = renderTraceRowDetailBody(row) || row.header || `trace row ${index}`;
const body = renderTraceToolDetailBody(row) || row.header || `trace row ${index}`;
return [
`<details>`,
`<summary>${escapeHtml(`已运行 ${traceToolSummary(row)}`)}</summary>`,
`- <details>`,
` <summary>${escapeHtml(`已运行 ${traceToolSummary(row)}`)}</summary>`,
``,
traceMarkdownFence(row.bodyFormat === "markdown" ? "markdown" : "text", body),
indentTraceMarkdownBlock(traceMarkdownFence(row.bodyFormat === "markdown" ? "markdown" : "text", body)),
``,
`</details>`
` </details>`
].join("\n");
}
function renderTraceMessageRowMarkdown(row: TraceEventRow): string {
const body = row.body?.trimEnd();
if (body) return body;
return row.header || `_No readable trace row body._`;
if (body) return traceBoldMarkdown(body);
return traceBoldMarkdown(row.header || `_No readable trace row body._`);
}
function isToolTraceRow(row: TraceEventRow): boolean {
@@ -109,7 +109,7 @@ function isToolTraceRow(row: TraceEventRow): boolean {
function traceToolSummary(row: TraceEventRow): string {
const firstLine = traceMarkdownPreviewLines(row.body).find((line) => !/^(stdout|stderr|exitCode|rowId|terminal):/iu.test(line.trim()));
return compactTraceOneLine(stripTraceToolOutputFromSummary(firstLine || row.header || "tool call"), 320);
return compactTraceOneLine(stripTraceToolOutputFromSummary(firstLine || row.header || "tool call"), 90);
}
function stripTraceToolOutputFromSummary(value: string): string {
@@ -483,6 +483,37 @@ function renderTraceRowDetailBody(row: TraceEventRow): string {
].filter(Boolean).join("\n");
}
function renderTraceToolDetailBody(row: TraceEventRow): string {
return normalizeTraceToolDetailBody(renderTraceRowDetailBody(row));
}
function normalizeTraceToolDetailBody(value: string): string {
return String(value ?? "")
.replace(/\r\n|\r/gu, "\n")
.replace(/[^\S\n]+(stdout:)[^\S\n]*/giu, "\n$1\n")
.replace(/[^\S\n]+(stderr:)[^\S\n]*/giu, "\n$1\n")
.replace(/[^\S\n]+(exitCode=)/giu, "\n$1")
.replace(/\n{3,}/gu, "\n\n")
.trimEnd();
}
function traceBoldMarkdown(value: string): string {
const body = String(value ?? "").trimEnd();
if (!body) return "";
return body.split("\n").map((line) => {
if (!line.trim()) return "";
return `**${escapeMarkdownStrongText(line)}**`;
}).join("\n");
}
function escapeMarkdownStrongText(value: string): string {
return escapeHtml(value).replace(/\\/gu, "\\\\").replace(/\*/gu, "\\*");
}
function indentTraceMarkdownBlock(value: string): string {
return String(value).split("\n").map((line) => ` ${line}`).join("\n");
}
function escapeHtml(value: string): string {
return String(value).replace(/&/gu, "&amp;").replace(/</gu, "&lt;").replace(/>/gu, "&gt;");
}