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:
@@ -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[] = [];
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user