fix webui tool trace dedupe

This commit is contained in:
Xubin Ren
2026-05-19 13:12:19 +08:00
parent c4794b82a9
commit 0a5606b409
5 changed files with 165 additions and 25 deletions
+9 -3
View File
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { useClient } from "@/providers/ClientProvider";
import { toMediaAttachment } from "@/lib/media";
import { toolTraceLinesFromEvents } from "@/lib/tool-traces";
import { mergeUniqueToolTraceLines, toolTraceLinesFromEvents } from "@/lib/tool-traces";
import type { StreamError } from "@/lib/nanobot-client";
import type {
InboundEvent,
@@ -652,10 +652,16 @@ export function useNanobotStream(
: last.content
? [last.content]
: [];
const mergedLines = structuredLines.length > 0
? mergeUniqueToolTraceLines(previousTraces, structuredLines)
: null;
if (mergedLines && !mergedLines.added) return prev;
const merged: UIMessage = {
...last,
traces: [...previousTraces, ...lines],
content: lines[lines.length - 1],
traces: mergedLines ? mergedLines.traces : [...previousTraces, ...lines],
content: mergedLines
? mergedLines.traces[mergedLines.traces.length - 1]
: lines[lines.length - 1],
activitySegmentId: last.activitySegmentId ?? segmentId,
};
return [...prev.slice(0, -1), merged];
+31 -16
View File
@@ -44,20 +44,35 @@ const VALID_PHASES = new Set(["start", "end", "error"]);
export function toolTraceLinesFromEvents(events: unknown): string[] {
if (!Array.isArray(events)) return [];
const seen = new Set<string>();
return events
.filter((event) => {
if (!event || typeof event !== "object") return false;
const phase = (event as { phase?: unknown }).phase;
if (!(phase && typeof phase === "string" && VALID_PHASES.has(phase))) {
return false;
}
const callId = (event as { call_id?: unknown }).call_id;
if (callId && typeof callId === "string") {
if (seen.has(callId)) return false;
seen.add(callId);
}
return true;
})
.map(formatToolCallTrace)
.filter((trace): trace is string => !!trace);
const lines: string[] = [];
for (const event of events) {
if (!event || typeof event !== "object") continue;
const phase = (event as { phase?: unknown }).phase;
if (!(phase && typeof phase === "string" && VALID_PHASES.has(phase))) continue;
const callId = (event as { call_id?: unknown }).call_id;
if (callId && typeof callId === "string") {
if (seen.has(callId)) continue;
seen.add(callId);
}
const line = formatToolCallTrace(event);
if (!line) continue;
lines.push(line);
}
return lines;
}
export function mergeUniqueToolTraceLines(
previousTraces: string[],
lines: string[],
): { traces: string[]; added: boolean } {
const seen = new Set(previousTraces);
const traces = [...previousTraces];
let added = false;
for (const line of lines) {
if (seen.has(line)) continue;
seen.add(line);
traces.push(line);
added = true;
}
return { traces, added };
}
+50
View File
@@ -308,6 +308,56 @@ describe("useNanobotStream", () => {
);
});
it("dedupes finish-phase tool events after their start trace", () => {
const fake = fakeClient();
const { result } = renderHook(() => useNanobotStream("chat-tool-finish", EMPTY_MESSAGES), {
wrapper: wrap(fake.client),
});
act(() => {
fake.emit("chat-tool-finish", {
event: "message",
chat_id: "chat-tool-finish",
text: 'exec({"cmd":"ls"})',
kind: "tool_hint",
tool_events: [{
phase: "start",
call_id: "call-exec",
name: "exec",
arguments: { cmd: "ls" },
}],
});
fake.emit("chat-tool-finish", {
event: "message",
chat_id: "chat-tool-finish",
text: "",
kind: "progress",
tool_events: [
{
phase: "end",
call_id: "call-exec",
name: "exec",
arguments: { cmd: "ls" },
result: "ok",
},
{
phase: "error",
call_id: "call-read",
name: "read_file",
arguments: { path: "notes.md" },
error: "missing",
},
],
});
});
expect(result.current.messages).toHaveLength(1);
expect(result.current.messages[0].traces).toEqual([
'exec({"cmd":"ls"})',
'read_file({"path":"notes.md"})',
]);
});
it("renders live file_edit events as their own activity trace", () => {
const fake = fakeClient();
const { result } = renderHook(() => useNanobotStream("chat-file-edit", EMPTY_MESSAGES), {