fix(web/cli): do not truncate the terminal final-response body in trace rows

- HWLAB v0.2 Final Response was being truncated to 5000 characters in
  traceAssistantSummaryRow and then again to 1200 characters in the CLI
  compactTraceRenderRow, even when the response was the assistant’s
  terminal (last) message. The same Final Response is what
  /v1/agent/chat/result returns as reply.content, so the trace view
  should show the full text and stay consistent with the CLI
  assistantText path.
- Mark each rendered row with a terminal flag (true only for the
  terminal Final Response) and lift the body cap to
  Number.POSITIVE_INFINITY for terminal rows. The CLI Web-render path
  now passes the full body through when row.terminal is true and keeps
  the 1200 char preview for non-terminal streaming rows.
- Add unit tests in web/hwlab-cloud-web/app-trace.test.ts and
  tools/hwlab-cli/client.test.ts to lock the behavior in for the
  terminal/no-truncate path and the streaming/2200 cap path.

Refs: pikasTech/HWLAB#722
This commit is contained in:
Codex
2026-06-03 13:26:19 +08:00
parent 2168ad2ad7
commit d39e322bf0
4 changed files with 72 additions and 2 deletions
+20
View File
@@ -1266,6 +1266,26 @@ test("hwlab-cli Web trace render keeps AgentRun middle assistant message", async
assert.match(text, //u);
});
test("hwlab-cli Web trace render keeps the full final-response body without truncating long agent summaries", async () => {
const longSummary = "下面是详细总结。".repeat(2000);
const result = await runHwlabCli(["client", "agent", "trace", "trc_final_long", "--base-url", "http://web.test", "--cookie", "hwlab_session=session-a", "--render", "web"], {
fetchImpl: async () => new Response(JSON.stringify({
status: "completed",
traceId: "trc_final_long",
events: [
{ traceId: "trc_final_long", seq: 1, label: "request:accepted", status: "accepted", createdAt: "2026-06-01T13:00:00.000Z", promptSummary: "请给出详细总结" },
{ traceId: "trc_final_long", seq: 2, label: "assistant:completed", type: "assistant_message", terminal: true, status: "completed", itemId: "assistant-final", createdAt: "2026-06-01T13:00:02.000Z", message: longSummary }
]
}), { status: 200 })
});
assert.equal(result.exitCode, 0);
const terminalRow = (result.payload.body.rows as any[]).find((row) => row.terminal === true);
assert.ok(terminalRow, "expected a terminal row to be rendered for the final response");
assert.equal(terminalRow.body, longSummary);
assert.equal(terminalRow.body?.includes("..."), false);
});
test("hwlab-cli client agent trace auto logs in when protected trace has no local session", async () => {
const cwd = await mkdtemp(path.join(os.tmpdir(), "hwlab-cli-client-auth-"));
const calls: any[] = [];
+4 -1
View File
@@ -2325,7 +2325,10 @@ function compactTraceRenderRow(row: any) {
tone: row?.tone ?? null,
header: row?.header ?? null,
bodyFormat: row?.bodyFormat ?? null,
body: row?.body ? preview(String(row.body), 1200) : undefined
terminal: row?.terminal === true ? true : undefined,
body: row?.body
? (row?.terminal === true ? String(row.body) : preview(String(row.body), 1200))
: undefined
});
}
+46
View File
@@ -407,3 +407,49 @@ function matchCount(text, pattern) {
const flags = pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`;
return [...String(text ?? "").matchAll(new RegExp(pattern.source, flags))].length;
}
test("trace display rows keep full final-response text without truncating long agent summaries", () => {
const longSummary = "下面是详细总结。".repeat(2000);
const events = [
event(1, "request:accepted", { promptSummary: "请给出详细总结" }),
event(2, "assistant:completed", {
type: "assistant_message",
terminal: true,
status: "completed",
itemId: "assistant-final",
message: longSummary
})
];
const rows = traceDisplayRows({ traceId: "trc_final_long", events }, events);
const terminalRow = rows.find((row) => row.terminal === true);
assert.ok(terminalRow, "expected a terminal row to be rendered for the final response");
assert.equal(terminalRow.tone, "ok");
assert.equal(terminalRow.body?.length, longSummary.length);
assert.equal(terminalRow.body?.includes("内容已截断"), false);
});
test("trace display rows still cap non-terminal streaming assistant text at the 2200 char limit", () => {
const streamingText = "streaming ".repeat(2000);
const events = [
event(1, "request:accepted", { promptSummary: "stream" }),
event(2, "assistant:item_completed", {
type: "assistant_message",
status: "completed",
itemId: "msg_streaming",
message: streamingText
}),
event(3, "assistant:completed", {
type: "assistant_message",
terminal: true,
status: "completed",
itemId: "assistant-final",
message: "done"
})
];
const rows = traceDisplayRows({ traceId: "trc_streaming_long", events }, events);
const streamingRow = rows.find((row) => row.rowId === "event:2");
assert.ok(streamingRow, "expected a non-terminal streaming row to be rendered");
assert.equal(streamingRow.terminal, false);
assert.ok((streamingRow.body?.length ?? 0) <= 2200);
assert.match(streamingRow.body ?? "", //u);
});
+2 -1
View File
@@ -743,7 +743,8 @@ function traceAssistantSummaryRow(trace, event, index = 0, textOverride = undefi
header: terminal
? `${traceClock(event.createdAt)} 🤖 助手最后一条消息,轮次完成(总耗时 ${elapsed}`
: `${traceClock(event.createdAt)} 🤖 助手消息${index === 0 ? "(第一条)" : ""}`,
body: text ? compactTraceAssistantMarkdown(text, terminal ? 5000 : 2200) : null,
terminal: terminal === true,
body: text ? compactTraceAssistantMarkdown(text, terminal ? Number.POSITIVE_INFINITY : 2200) : null,
bodyFormat: "markdown"
};
}