Merge pull request #970 from pikasTech/fix/issue955-trace-replay-final

fix: stabilize AgentRun trace replay and final rows
This commit is contained in:
Lyon
2026-06-06 10:35:06 +08:00
committed by GitHub
5 changed files with 303 additions and 17 deletions
+61 -14
View File
@@ -435,11 +435,8 @@ export async function syncAgentRunChatResult({ traceId, currentResult = null, op
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
const managerUrl = resolveAgentRunManagerUrl(env, mapped.agentRun.managerUrl);
const timeoutMs = parsePositiveInteger(env.HWLAB_CODE_AGENT_AGENTRUN_HTTP_TIMEOUT_MS, 20_000);
const eventsResponse = await agentRunJson(fetchImpl, managerUrl, `/api/v1/runs/${encodeURIComponent(mapped.agentRun.runId)}/events?afterSeq=${encodeURIComponent(String(mapped.agentRun.lastSeq ?? 0))}&limit=500`, {
method: "GET",
timeoutMs
});
const events = Array.isArray(eventsResponse?.items) ? eventsResponse.items : [];
const eventsResponse = await fetchAgentRunEventsForTrace({ fetchImpl, managerUrl, timeoutMs, mapping: { ...mapped.agentRun, traceSummary: mapped.traceSummary } });
const events = eventsResponse.events;
appendAgentRunEventsToTrace(traceStore, traceId, events, mapped.agentRun);
const result = await agentRunJson(fetchImpl, managerUrl, `/api/v1/runs/${encodeURIComponent(mapped.agentRun.runId)}/commands/${encodeURIComponent(mapped.agentRun.commandId)}/result`, {
method: "GET",
@@ -448,7 +445,7 @@ export async function syncAgentRunChatResult({ traceId, currentResult = null, op
const nextMapping = {
...mapped.agentRun,
...agentRunResultRefs(result),
lastSeq: Math.max(Number(mapped.agentRun.lastSeq ?? 0), Number(result?.lastSeq ?? 0), ...events.map((event) => Number(event?.seq ?? 0))),
lastSeq: agentRunTraceCursorSeq(eventsResponse, mapped.agentRun.lastSeq),
status: result?.status ?? mapped.agentRun.status ?? "running",
runStatus: result?.runStatus ?? mapped.agentRun.runStatus ?? null,
commandState: result?.commandState ?? mapped.agentRun.commandState ?? null,
@@ -469,13 +466,10 @@ export async function refreshAgentRunTrace({ traceId, result = null, options = {
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
const managerUrl = resolveAgentRunManagerUrl(env, mapped.agentRun.managerUrl);
const timeoutMs = parsePositiveInteger(env.HWLAB_CODE_AGENT_AGENTRUN_HTTP_TIMEOUT_MS, 20_000);
const eventsResponse = await agentRunJson(fetchImpl, managerUrl, `/api/v1/runs/${encodeURIComponent(mapped.agentRun.runId)}/events?afterSeq=${encodeURIComponent(String(mapped.agentRun.lastSeq ?? 0))}&limit=500`, {
method: "GET",
timeoutMs
});
const events = Array.isArray(eventsResponse?.items) ? eventsResponse.items : [];
const eventsResponse = await fetchAgentRunEventsForTrace({ fetchImpl, managerUrl, timeoutMs, mapping: { ...mapped.agentRun, traceSummary: mapped.traceSummary } });
const events = eventsResponse.events;
appendAgentRunEventsToTrace(traceStore, traceId, events, mapped.agentRun);
const lastSeq = Math.max(Number(mapped.agentRun.lastSeq ?? 0), ...events.map((event) => Number(event?.seq ?? 0)));
const lastSeq = agentRunTraceCursorSeq(eventsResponse, mapped.agentRun.lastSeq);
if (lastSeq !== Number(mapped.agentRun.lastSeq ?? 0)) {
options.codeAgentChatResults?.set?.(traceId, { ...mapped, agentRun: { ...mapped.agentRun, lastSeq, updatedAt: nowIso(options.now) } });
}
@@ -1293,11 +1287,64 @@ function appendAgentRunEventsToTrace(traceStore, traceId, events, mapping = {})
}
}
async function fetchAgentRunEventsForTrace({ fetchImpl, managerUrl, timeoutMs, mapping = {} }) {
const runId = requiredString(mapping.runId, "runId");
const currentCommandId = typeof mapping.commandId === "string" ? mapping.commandId : "";
const { afterSeq, endSeq } = agentRunTraceReplayWindow(mapping);
const path = `/api/v1/runs/${encodeURIComponent(runId)}/events?afterSeq=${encodeURIComponent(String(afterSeq))}&limit=500`;
const response = await agentRunJson(fetchImpl, managerUrl, path, { method: "GET", timeoutMs });
const rawEvents = Array.isArray(response?.items) ? response.items : [];
const events = rawEvents.filter((event) => agentRunEventBelongsToTrace(event, { currentCommandId, afterSeq, endSeq }));
return {
events,
afterSeq,
endSeq,
commandFiltered: Boolean(currentCommandId),
maxSeq: Math.max(afterSeq, ...rawEvents.map((event) => Number(event?.seq ?? 0))),
traceLastSeq: Math.max(afterSeq, ...events.map((event) => Number(event?.seq ?? 0)).filter(Number.isFinite))
};
}
function agentRunTraceCursorSeq(eventsResponse = {}, previousLastSeq = 0) {
const afterSeq = Number(eventsResponse.afterSeq ?? 0);
const endSeq = Number(eventsResponse.endSeq ?? 0);
const traceLastSeq = Number(eventsResponse.traceLastSeq ?? 0);
if (Number.isFinite(traceLastSeq) && traceLastSeq > afterSeq) return Math.floor(traceLastSeq);
if (Number.isFinite(endSeq) && endSeq > 0) return Math.floor(endSeq);
if (eventsResponse.commandFiltered === true) return Math.max(Number(previousLastSeq ?? 0), afterSeq);
return Math.max(Number(previousLastSeq ?? 0), Number(eventsResponse.maxSeq ?? 0));
}
function agentRunTraceReplayWindow(mapping = {}) {
const eventStartSeq = Number(mapping.eventStartSeq ?? mapping.commandStartSeq ?? mapping.startSeq ?? 0);
const summary = mapping.traceSummary && typeof mapping.traceSummary === "object" ? mapping.traceSummary : null;
const summaryAgentRun = summary?.agentRun && typeof summary.agentRun === "object" ? summary.agentRun : null;
const summaryLastSeq = Number(summaryAgentRun?.lastSeq ?? summary?.lastSeq ?? 0);
const currentLastSeq = Number(mapping.lastSeq ?? 0);
const endSeq = Number.isFinite(summaryLastSeq) && summaryLastSeq > 0 ? Math.floor(summaryLastSeq) : 0;
if (Number.isFinite(eventStartSeq) && eventStartSeq > 0) return { afterSeq: Math.max(0, Math.floor(eventStartSeq) - 1), endSeq };
if (endSeq > 0) return { afterSeq: Math.max(0, endSeq - 500), endSeq };
if (Number.isFinite(currentLastSeq) && currentLastSeq > 0) return { afterSeq: Math.floor(currentLastSeq), endSeq: 0 };
return { afterSeq: 0, endSeq: 0 };
}
function agentRunEventBelongsToTrace(event, { currentCommandId = "", afterSeq = 0, endSeq = 0 } = {}) {
const eventCommandId = agentRunEventCommandId(event);
const seq = Number(event?.seq ?? 0);
if (currentCommandId && eventCommandId && eventCommandId !== currentCommandId) return false;
if (endSeq > 0 && Number.isFinite(seq)) return seq > afterSeq && seq <= endSeq;
return true;
}
function agentRunEventCommandId(event) {
const payload = event?.payload && typeof event.payload === "object" ? event.payload : {};
return typeof payload.commandId === "string" ? payload.commandId : "";
}
function isForeignAgentRunCommandEvent(event, mapping = {}) {
const currentCommandId = typeof mapping.commandId === "string" ? mapping.commandId : "";
if (!currentCommandId) return false;
const payload = event?.payload && typeof event.payload === "object" ? event.payload : {};
const eventCommandId = typeof payload.commandId === "string" ? payload.commandId : "";
const eventCommandId = agentRunEventCommandId(event);
return Boolean(eventCommandId && eventCommandId !== currentCommandId);
}
+96
View File
@@ -1662,6 +1662,102 @@ test("cloud api trace returns persisted summary when live trace store has expire
}
});
test("cloud api trace replays an earlier AgentRun command after same-run lastSeq advances (#955)", async () => {
const calls = [];
const firstTraceId = "trc_issue955_first_trace";
const codeAgentChatResults = new Map();
const runId = "run_issue955_replay";
const firstCommandId = "cmd_issue955_first";
const secondCommandId = "cmd_issue955_second";
const firstFinalText = "目前只有一个 HWPOD 可用:\n\n- d601-f103-v2 (board: D601-F103-V2 / STM32F103)\n\nAPI 返回 count=1, availableCount=1。";
const firstEvents = [
{ id: "evt_issue955_first_setup", runId, seq: 1, type: "backend_status", payload: { phase: "runner-job-created", commandId: firstCommandId, jobName: "agentrun-v01-runner-issue955-first", namespace: "agentrun-v01" }, createdAt: "2026-06-06T01:43:25.000Z" },
{ id: "evt_issue955_first_tool", runId, seq: 18, type: "tool_call", payload: { method: "item/completed", type: "commandExecution", toolName: "commandExecution", itemId: "call_issue955_hwpod_list", command: "/bin/sh -lc 'hwpod list --available'", status: "completed", exitCode: 0, outputSummary: '{"count":1,"availableCount":1,"ids":["d601-f103-v2"]}', commandId: firstCommandId }, createdAt: "2026-06-06T01:43:42.000Z" },
{ id: "evt_issue955_first_assistant", runId, seq: 27, type: "assistant_message", payload: { commandId: firstCommandId, itemId: "msg_item_0", text: firstFinalText }, createdAt: "2026-06-06T01:43:55.000Z" },
{ id: "evt_issue955_first_terminal", runId, seq: 35, type: "terminal_status", payload: { commandId: firstCommandId, terminalStatus: "completed" }, createdAt: "2026-06-06T01:43:56.000Z" }
];
const secondEvents = [
{ id: "evt_issue955_second_setup", runId, seq: 47, type: "backend_status", payload: { phase: "runner-claim-waiting-for-stale-lease" }, createdAt: "2026-06-06T01:45:56.000Z" },
{ id: "evt_issue955_second_assistant", runId, seq: 58, type: "assistant_message", payload: { commandId: secondCommandId, itemId: "msg_item_0", text: "编译成功!来总结一下结果。" }, createdAt: "2026-06-06T01:46:24.000Z" },
{ id: "evt_issue955_second_terminal", runId, seq: 66, type: "terminal_status", payload: { commandId: secondCommandId, terminalStatus: "completed" }, createdAt: "2026-06-06T01:46:28.000Z" }
];
const agentRunServer = createHttpServer(async (request, response) => {
const url = new URL(request.url || "/", "http://127.0.0.1");
calls.push({ method: request.method, path: url.pathname, afterSeq: url.searchParams.get("afterSeq") });
if (request.method === "GET" && url.pathname === `/api/v1/runs/${runId}/events`) {
response.writeHead(200, { "content-type": "application/json" });
response.end(`${JSON.stringify({ ok: true, data: { items: [...firstEvents, ...secondEvents] }, traceId: "trc_fake_issue955_replay" })}\n`);
return;
}
response.writeHead(404, { "content-type": "application/json" });
response.end(`${JSON.stringify({ ok: false, message: `unexpected ${request.method} ${url.pathname}` })}\n`);
});
await new Promise((resolve) => agentRunServer.listen(0, "127.0.0.1", resolve));
const agentRunPort = agentRunServer.address().port;
codeAgentChatResults.set(firstTraceId, {
accepted: true,
status: "completed",
shortConnection: true,
traceId: firstTraceId,
conversationId: "cnv_issue955_replay",
sessionId: "ses_issue955_replay",
threadId: "thread-issue955-replay",
ownerUserId: TEST_AGENT_ACTOR.id,
ownerRole: TEST_AGENT_ACTOR.role,
reply: { role: "assistant", content: firstFinalText },
finalResponse: { text: firstFinalText, textChars: firstFinalText.length, role: "assistant", status: "completed", traceId: firstTraceId, valuesPrinted: false },
traceSummary: { traceId: firstTraceId, source: "agent-session-snapshot", sourceEventCount: 41, terminalStatus: "completed", agentRun: { runId, commandId: firstCommandId, lastSeq: 35, valuesPrinted: false }, valuesPrinted: false },
agentRun: { adapter: "agentrun-v01", managerUrl: `http://127.0.0.1:${agentRunPort}`, runId, commandId: firstCommandId, lastSeq: 66, terminalStatus: "completed", valuesPrinted: false },
valuesPrinted: false
});
const traceStore = createCodeAgentTraceStore();
const server = createCloudApiServer({
traceStore,
codeAgentChatResults,
env: {
HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
AGENTRUN_MGR_URL: `http://127.0.0.1:${agentRunPort}`,
HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1",
HWLAB_ENVIRONMENT: "v02",
HWLAB_GITOPS_PROFILE: "v02"
},
accessController: {
required: false,
async authenticate() {
return { ok: true, actor: TEST_AGENT_ACTOR, session: TEST_AUTH_SESSION };
}
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat/trace/${firstTraceId}`, {
headers: { cookie: "hwlab_session=test-stub-session" }
});
assert.equal(response.status, 200);
const body = await response.json();
const text = JSON.stringify(body);
assert.equal(body.traceId, firstTraceId);
assert.equal(body.status, "completed");
assert.ok(calls.some((call) => call.path === `/api/v1/runs/${runId}/events` && call.afterSeq === "0"));
assert.ok(body.events.some((event) => event.commandId === firstCommandId && event.label === "item/commandExecution:completed"));
assert.ok(body.events.some((event) => event.commandId === firstCommandId && event.label === "agentrun:assistant:message"));
assert.equal(body.events.some((event) => event.commandId === secondCommandId), false);
assert.equal(body.finalResponse.text, firstFinalText);
assert.match(text, /d601-f103-v2/u);
assert.doesNotMatch(text, //u);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
await new Promise((resolve, reject) => {
agentRunServer.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api result polling compacts large runnerTrace while preserving providerTrace", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-result-compact-"));
const codexHome = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-result-compact-codex-home-"));
+26 -1
View File
@@ -1835,8 +1835,8 @@ export async function handleCodeAgentTraceHttp(request, response, url, options)
}
function traceSnapshotWithPersistentFallback(snapshot, persistedResult, traceId, refreshError = null) {
if (snapshot?.status !== "missing") return snapshot;
const fallback = persistentTraceFallback(persistedResult, traceId);
if (snapshot?.status !== "missing") return traceSnapshotWithPersistentEvidence(snapshot, fallback, refreshError);
if (!fallback) return {
...snapshot,
ok: false,
@@ -1878,6 +1878,31 @@ function traceSnapshotWithPersistentFallback(snapshot, persistedResult, traceId,
};
}
function traceSnapshotWithPersistentEvidence(snapshot, fallback, refreshError = null) {
if (!fallback) return snapshot;
return {
...snapshot,
conversationId: snapshot.conversationId ?? fallback.conversationId,
sessionId: snapshot.sessionId ?? fallback.sessionId,
threadId: snapshot.threadId ?? fallback.threadId,
agentRun: snapshot.agentRun ?? fallback.agentRun,
finalResponse: snapshot.finalResponse ?? fallback.finalResponse,
traceSummary: snapshot.traceSummary ?? fallback.traceSummary,
fallback: snapshot.fallback ?? (refreshError ? {
available: true,
source: fallback.source,
refresh: {
attempted: true,
ok: false,
code: refreshError?.code ?? "agentrun_trace_refresh_failed",
message: refreshError?.message ?? "AgentRun trace refresh failed; live events may be partial.",
valuesPrinted: false
},
valuesPrinted: false
} : undefined)
};
}
function persistentTraceFallback(result, traceId) {
if (!result || typeof result !== "object") return null;
const storedSummary = result.traceSummary && typeof result.traceSummary === "object" ? result.traceSummary : null;
+95 -2
View File
@@ -68,6 +68,10 @@ export function traceDisplayRows(trace: Record<string, unknown> = {}, events: Tr
rows.push(traceDisplayRow(trace, event));
}
if (completionEvent) {
const finalResponseText = traceFinalResponseText(trace);
if (finalResponseText) {
lastAssistantRowIndex = upsertAuthoritativeFinalResponseRow(rows, assistantRows, completionEvent, finalResponseText);
}
const lastAssistantRow = lastAssistantRowIndex >= 0 ? rows[lastAssistantRowIndex] : undefined;
if (lastAssistantRow) rows[lastAssistantRowIndex] = markAssistantRowTerminal(trace, lastAssistantRow, completionEvent);
else rows.push(traceCompletionSummaryRow(trace, completionEvent));
@@ -78,6 +82,13 @@ export function traceDisplayRows(trace: Record<string, unknown> = {}, events: Tr
interface AssistantRowState {
rowIndex: number;
comparableText: string;
sourceEvent: TraceEvent;
derivedSnapshotSuffix?: boolean;
}
interface AssistantSnapshotDecision {
action: "replace" | "append-suffix" | "keep-existing";
suffixText?: string;
}
function traceToolCallRow(trace: Record<string, unknown>, event: TraceEvent): TraceEventRow {
@@ -157,7 +168,7 @@ function upsertAssistantMessageRow(rows: TraceEventRow[], assistantRows: Assista
const comparableText = comparableAssistantText(row.body);
if (!comparableText) {
rows.push(row);
assistantRows.push({ rowIndex: rows.length - 1, comparableText });
assistantRows.push({ rowIndex: rows.length - 1, comparableText, sourceEvent: event });
return rows.length - 1;
}
@@ -168,11 +179,31 @@ function upsertAssistantMessageRow(rows: TraceEventRow[], assistantRows: Assista
if (!existingRow) continue;
if (state.comparableText === comparableText) {
rows[state.rowIndex] = mergeAssistantRows(existingRow, row);
state.sourceEvent = event;
state.derivedSnapshotSuffix = false;
return state.rowIndex;
}
if (comparableText.includes(state.comparableText)) {
const previousIndex = comparableText.indexOf(state.comparableText);
if (state.derivedSnapshotSuffix === true && previousIndex === 0) {
rows[state.rowIndex] = mergeAssistantRows(row, existingRow);
state.comparableText = comparableText;
state.sourceEvent = event;
state.derivedSnapshotSuffix = false;
return state.rowIndex;
}
const snapshotDecision = assistantSnapshotDecision(comparableText, state.comparableText, event, state.sourceEvent);
if (snapshotDecision.action === "append-suffix" && snapshotDecision.suffixText) {
const suffixRow = { ...row, body: snapshotDecision.suffixText };
rows.push(suffixRow);
assistantRows.push({ rowIndex: rows.length - 1, comparableText: snapshotDecision.suffixText, sourceEvent: event, derivedSnapshotSuffix: true });
return rows.length - 1;
}
if (snapshotDecision.action === "keep-existing") return state.rowIndex;
rows[state.rowIndex] = mergeAssistantRows(row, existingRow);
state.comparableText = comparableText;
state.sourceEvent = event;
state.derivedSnapshotSuffix = false;
return state.rowIndex;
}
if (state.comparableText.includes(comparableText)) {
@@ -182,10 +213,67 @@ function upsertAssistantMessageRow(rows: TraceEventRow[], assistantRows: Assista
}
rows.push(row);
assistantRows.push({ rowIndex: rows.length - 1, comparableText });
assistantRows.push({ rowIndex: rows.length - 1, comparableText, sourceEvent: event });
return rows.length - 1;
}
function upsertAuthoritativeFinalResponseRow(rows: TraceEventRow[], assistantRows: AssistantRowState[], completionEvent: TraceEvent, finalText: string): number {
const comparableText = comparableAssistantText(finalText);
let matchedState: AssistantRowState | null = null;
for (const state of assistantRows) {
const row = rows[state.rowIndex];
if (!row || row.bodyFormat !== "markdown") continue;
row.terminal = undefined;
if (!matchedState && state.comparableText === comparableText) matchedState = state;
}
if (matchedState) {
const row = rows[matchedState.rowIndex];
rows[matchedState.rowIndex] = {
...row,
body: finalText,
terminal: true,
tone: "ok",
header: `${traceClock(completionEvent.createdAt)} 助手最终消息`
};
matchedState.comparableText = comparableText;
matchedState.derivedSnapshotSuffix = false;
return matchedState.rowIndex;
}
rows.push({
rowId: `trace-final-response:${completionEvent.seq ?? "completed"}`,
seq: numberOrNull(completionEvent.seq),
tone: "ok",
header: `${traceClock(completionEvent.createdAt)} 助手最终消息`,
terminal: true,
body: finalText,
bodyFormat: "markdown"
});
assistantRows.push({ rowIndex: rows.length - 1, comparableText, sourceEvent: completionEvent });
return rows.length - 1;
}
function assistantSnapshotDecision(nextText: string, previousText: string, nextEvent: TraceEvent, previousEvent: TraceEvent): AssistantSnapshotDecision {
if (!sameAssistantSnapshotIdentity(nextEvent, previousEvent)) return { action: "replace" };
if (isAuthoritativeAssistantEvent(nextEvent) || isAuthoritativeAssistantEvent(previousEvent)) return { action: "replace" };
const previousIndex = nextText.indexOf(previousText);
if (previousIndex < 0) return { action: "replace" };
const suffixText = nextText.slice(previousIndex + previousText.length).trim();
if (suffixText && suffixText.length >= Math.max(80, previousText.length / 3)) return { action: "append-suffix", suffixText };
if (previousIndex > 0 || !suffixText) return { action: "keep-existing" };
return { action: "replace" };
}
function sameAssistantSnapshotIdentity(left: TraceEvent, right: TraceEvent): boolean {
const leftItem = nonEmptyString(left.itemId ?? left.messageId ?? left.id);
const rightItem = nonEmptyString(right.itemId ?? right.messageId ?? right.id);
if (leftItem && rightItem) return leftItem === rightItem;
return false;
}
function isAuthoritativeAssistantEvent(event: TraceEvent): boolean {
return event.replyAuthority === true || event.final === true || event.terminal === true || String(event.status ?? "") === "completed";
}
function mergeAssistantRows(preferred: TraceEventRow, metadata: TraceEventRow): TraceEventRow {
return {
...preferred,
@@ -199,6 +287,11 @@ function comparableAssistantText(value: unknown): string {
return cleanTraceText(value).replace(/\s+/gu, " ");
}
function traceFinalResponseText(trace: Record<string, unknown>): string | null {
const response = trace.finalResponse && typeof trace.finalResponse === "object" ? trace.finalResponse as Record<string, unknown> : null;
return nonEmptyString(response?.text ?? response?.content ?? response?.message);
}
function markAssistantRowTerminal(trace: Record<string, unknown>, row: TraceEventRow, completionEvent: TraceEvent): TraceEventRow {
return {
...row,
@@ -66,6 +66,31 @@ test("web trace rows do not fold progress assistant text into the final response
assert.doesNotMatch(terminalRows[0]?.body ?? "", /Let me check|/u);
});
test("web trace rows keep clean AgentRun final response when same itemId snapshots stay running (#955)", () => {
const progressOne = "好,我来走 HWPOD 编译路径。先看看 hwpod 命令是否可用,并读取 hwpod-cli 技能了解完整的编译流程。";
const progressTwo = "好,现在用 hwpod build 走编译。根据 skill,标准路径是 hwpod build 触发 hwpod-compiler-cli,再通过 API 发给 hwpod-node。";
const finalText = "编译成功!来总结一下结果:\n\n hwpod build ✅ completed\n\n- 设备: D601-F103-V2 (STM32F103)\n- 工具链: Keil MDK,通过 keil-cli.py build 异步发起\n- 构建命令: py -3 keil-cli.py build -p atk_f103.uvprojx -t USART\n- exit code: 0,任务已入队列\n- 预期产物: F:\\Work\\D601-HWLAB\\projects\\01_baseline\\Output\\atk_f103.hex\n- 无 blocker";
const events: Record<string, unknown>[] = [
{ seq: 1, label: "agentrun:request:accepted", status: "accepted", createdAt: "2026-06-06T01:46:11.000Z" },
{ seq: 17, source: "agentrun", sourceSeq: 48, label: "agentrun:assistant:message", type: "assistant", status: "running", itemId: "msg_item_0", message: progressOne, createdAt: "2026-06-06T01:46:15.000Z" },
{ seq: 22, source: "agentrun", sourceSeq: 53, label: "agentrun:assistant:message", type: "assistant", status: "running", itemId: "msg_item_0", message: progressTwo, createdAt: "2026-06-06T01:46:18.000Z" },
{ seq: 26, source: "agentrun", sourceSeq: 57, label: "agentrun:assistant:message", type: "assistant", status: "running", itemId: "msg_item_0", message: `${progressOne}${progressTwo}${finalText.slice(0, 170)}`, createdAt: "2026-06-06T01:46:24.000Z" },
{ seq: 27, source: "agentrun", sourceSeq: 58, label: "agentrun:assistant:message", type: "assistant", status: "running", itemId: "msg_item_0", message: finalText, createdAt: "2026-06-06T01:46:25.000Z" },
{ seq: 30, source: "agentrun", sourceSeq: 61, label: "agentrun:assistant:message", type: "assistant", status: "running", itemId: "msg_item_0", message: `${progressOne}${progressTwo}${finalText}`, createdAt: "2026-06-06T01:46:26.000Z" },
{ seq: 36, label: "agentrun:result:completed", type: "result", status: "completed", terminal: true, message: "AgentRun result is ready for HWLAB short-connection polling.", createdAt: "2026-06-06T01:46:28.000Z" }
];
const rows = traceDisplayRows({ startedAt: "2026-06-06T01:46:11.000Z", finalResponse: { text: finalText } }, events);
const assistantRows = rows.filter((row) => row.bodyFormat === "markdown");
const terminalRows = assistantRows.filter((row) => row.terminal === true);
assert.equal(terminalRows.length, 1);
assert.equal(terminalRows[0]?.body, finalText);
assert.doesNotMatch(terminalRows[0]?.body ?? "", / HWPOD | hwpod build/u);
assert.equal(assistantRows.filter((row) => / HWPOD /u.test(row.body ?? "")).length, 1);
assert.equal(assistantRows.filter((row) => / hwpod build/u.test(row.body ?? "")).length, 1);
});
test("web trace rows collapse tool start/completed and clean shell command escaping", () => {
const events: Record<string, unknown>[] = [
{ seq: 1, label: "agentrun:request:accepted", status: "accepted", createdAt: "2026-06-04T12:13:58.212Z" },