fix: polish caserun aggregate trace markdown
This commit is contained in:
@@ -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 和最后 diff;Trace 中 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 和最后 diff;Trace 中 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。
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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, "&").replace(/</gu, "<").replace(/>/gu, ">");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user