mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-08-01 19:06:01 +00:00
perf(messages): index tool call results (#4411)
This commit is contained in:
parent
adac3e185e
commit
b5d2a504a9
@ -85,6 +85,8 @@ Human input requests are a structured message protocol layered on normal chat hi
|
|||||||
|
|
||||||
Tool-calling AI messages can contain user-visible text as well as `tool_calls`. `core/messages/utils.ts` keeps these turns in an `assistant:processing` group, and `components/workspace/messages/message-group.tsx` must render the visible text as a processing step instead of treating the message as only tool metadata. This preserves provider text such as error explanations or "trying another approach" notes during tool-heavy runs.
|
Tool-calling AI messages can contain user-visible text as well as `tool_calls`. `core/messages/utils.ts` keeps these turns in an `assistant:processing` group, and `components/workspace/messages/message-group.tsx` must render the visible text as a processing step instead of treating the message as only tool metadata. This preserves provider text such as error explanations or "trying another approach" notes during tool-heavy runs.
|
||||||
|
|
||||||
|
`MessageGroup` builds its tool-result and browser-preview lookups once per processing group before converting messages to steps. The lookup preserves the first non-empty result and first screenshot-bearing browser view for each tool-call ID, matching the streamed-message display semantics without repeatedly scanning the full group for every tool call.
|
||||||
|
|
||||||
### Key Patterns
|
### Key Patterns
|
||||||
|
|
||||||
- **Server Components by default**, `"use client"` only for interactive components
|
- **Server Components by default**, `"use client"` only for interactive components
|
||||||
|
|||||||
@ -36,7 +36,7 @@ import type { TokenDebugStep } from "@/core/messages/usage-model";
|
|||||||
import {
|
import {
|
||||||
extractContentFromMessage,
|
extractContentFromMessage,
|
||||||
extractReasoningContentFromMessage,
|
extractReasoningContentFromMessage,
|
||||||
findToolCallResult,
|
extractTextFromMessage,
|
||||||
} from "@/core/messages/utils";
|
} from "@/core/messages/utils";
|
||||||
import { extractTitleFromMarkdown } from "@/core/utils/markdown";
|
import { extractTitleFromMarkdown } from "@/core/utils/markdown";
|
||||||
import { env } from "@/env";
|
import { env } from "@/env";
|
||||||
@ -895,27 +895,41 @@ interface BrowserViewMeta {
|
|||||||
title?: string;
|
title?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findBrowserViewMeta(
|
function indexToolCallData(messages: Message[]) {
|
||||||
toolCallId: string,
|
const toolCallResults = new Map<string, string>();
|
||||||
messages: Message[],
|
const browserViews = new Map<string, BrowserViewMeta>();
|
||||||
): BrowserViewMeta | undefined {
|
|
||||||
for (const message of messages) {
|
for (const message of messages) {
|
||||||
if (message.type === "tool" && message.tool_call_id === toolCallId) {
|
if (message.type !== "tool" || !message.tool_call_id) {
|
||||||
const meta = (
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolCallId = message.tool_call_id;
|
||||||
|
if (!toolCallResults.has(toolCallId)) {
|
||||||
|
const result = extractTextFromMessage(message);
|
||||||
|
if (result) {
|
||||||
|
toolCallResults.set(toolCallId, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!browserViews.has(toolCallId)) {
|
||||||
|
const browserView = (
|
||||||
message.additional_kwargs as
|
message.additional_kwargs as
|
||||||
| { browser_view?: BrowserViewMeta }
|
| { browser_view?: BrowserViewMeta }
|
||||||
| undefined
|
| undefined
|
||||||
)?.browser_view;
|
)?.browser_view;
|
||||||
if (meta && typeof meta.screenshot === "string") {
|
if (browserView && typeof browserView.screenshot === "string") {
|
||||||
return meta;
|
browserViews.set(toolCallId, browserView);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
|
||||||
|
return { browserViews, toolCallResults };
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertToSteps(messages: Message[]): CoTStep[] {
|
function convertToSteps(messages: Message[]): CoTStep[] {
|
||||||
const steps: CoTStep[] = [];
|
const steps: CoTStep[] = [];
|
||||||
|
const { browserViews, toolCallResults } = indexToolCallData(messages);
|
||||||
for (const [messageIndex, message] of messages.entries()) {
|
for (const [messageIndex, message] of messages.entries()) {
|
||||||
if (message.type === "ai") {
|
if (message.type === "ai") {
|
||||||
const content = extractContentFromMessage(message);
|
const content = extractContentFromMessage(message);
|
||||||
@ -950,7 +964,7 @@ function convertToSteps(messages: Message[]): CoTStep[] {
|
|||||||
};
|
};
|
||||||
const toolCallId = tool_call.id;
|
const toolCallId = tool_call.id;
|
||||||
if (toolCallId) {
|
if (toolCallId) {
|
||||||
const toolCallResult = findToolCallResult(toolCallId, messages);
|
const toolCallResult = toolCallResults.get(toolCallId);
|
||||||
if (toolCallResult) {
|
if (toolCallResult) {
|
||||||
try {
|
try {
|
||||||
const json = JSON.parse(toolCallResult);
|
const json = JSON.parse(toolCallResult);
|
||||||
@ -959,7 +973,7 @@ function convertToSteps(messages: Message[]): CoTStep[] {
|
|||||||
step.result = toolCallResult;
|
step.result = toolCallResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
step.browserView = findBrowserViewMeta(toolCallId, messages);
|
step.browserView = browserViews.get(toolCallId);
|
||||||
}
|
}
|
||||||
steps.push(step);
|
steps.push(step);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -204,6 +204,160 @@ describe("MessageGroup", () => {
|
|||||||
expect(visibleHtml).toContain('decoding="async"');
|
expect(visibleHtml).toContain('decoding="async"');
|
||||||
expect(deferredHtml).not.toContain("<img");
|
expect(deferredHtml).not.toContain("<img");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps the first non-empty result for a tool call and skips task calls", () => {
|
||||||
|
const html = renderGroup([
|
||||||
|
{
|
||||||
|
id: "ai-1",
|
||||||
|
type: "ai",
|
||||||
|
content: "",
|
||||||
|
tool_calls: [
|
||||||
|
{
|
||||||
|
id: "call-fetch",
|
||||||
|
name: "web_fetch",
|
||||||
|
args: { url: "https://example.com" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "call-task",
|
||||||
|
name: "task",
|
||||||
|
args: { description: "Do not render this subagent call" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as Message,
|
||||||
|
{
|
||||||
|
id: "tool-empty",
|
||||||
|
type: "tool",
|
||||||
|
name: "web_fetch",
|
||||||
|
tool_call_id: "call-fetch",
|
||||||
|
content: "",
|
||||||
|
} as Message,
|
||||||
|
{
|
||||||
|
id: "tool-first",
|
||||||
|
type: "tool",
|
||||||
|
name: "web_fetch",
|
||||||
|
tool_call_id: "call-fetch",
|
||||||
|
content: "# First fetched title\n\nFirst result.",
|
||||||
|
} as Message,
|
||||||
|
{
|
||||||
|
id: "tool-later",
|
||||||
|
type: "tool",
|
||||||
|
name: "web_fetch",
|
||||||
|
tool_call_id: "call-fetch",
|
||||||
|
content: "# Later fetched title\n\nLater result.",
|
||||||
|
} as Message,
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(html).toContain("First fetched title");
|
||||||
|
expect(html).not.toContain("Later fetched title");
|
||||||
|
expect(html).not.toContain("Do not render this subagent call");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps the first browser view that includes a screenshot", () => {
|
||||||
|
const html = renderGroup(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: "ai-1",
|
||||||
|
type: "ai",
|
||||||
|
content: "",
|
||||||
|
tool_calls: [
|
||||||
|
{
|
||||||
|
id: "call-browser",
|
||||||
|
name: "browser_navigate",
|
||||||
|
args: { url: "https://example.com" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as Message,
|
||||||
|
{
|
||||||
|
id: "tool-without-shot",
|
||||||
|
type: "tool",
|
||||||
|
name: "browser_navigate",
|
||||||
|
tool_call_id: "call-browser",
|
||||||
|
content: "Opened without a preview.",
|
||||||
|
additional_kwargs: {
|
||||||
|
browser_view: { url: "https://example.com" },
|
||||||
|
},
|
||||||
|
} as Message,
|
||||||
|
{
|
||||||
|
id: "tool-first-shot",
|
||||||
|
type: "tool",
|
||||||
|
name: "browser_navigate",
|
||||||
|
tool_call_id: "call-browser",
|
||||||
|
content: "Opened with the first preview.",
|
||||||
|
additional_kwargs: {
|
||||||
|
browser_view: {
|
||||||
|
screenshot: "/mnt/user-data/outputs/first-browser.png",
|
||||||
|
url: "https://example.com/first",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as Message,
|
||||||
|
{
|
||||||
|
id: "tool-later-shot",
|
||||||
|
type: "tool",
|
||||||
|
name: "browser_navigate",
|
||||||
|
tool_call_id: "call-browser",
|
||||||
|
content: "Opened with a later preview.",
|
||||||
|
additional_kwargs: {
|
||||||
|
browser_view: {
|
||||||
|
screenshot: "/mnt/user-data/outputs/later-browser.png",
|
||||||
|
url: "https://example.com/later",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as Message,
|
||||||
|
],
|
||||||
|
{ threadId: "thread-1" },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(html).toContain("first-browser.png");
|
||||||
|
expect(html).not.toContain("later-browser.png");
|
||||||
|
expect(html).toContain("https://example.com/first");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the earliest JSON tool result after an empty streamed update", () => {
|
||||||
|
const html = renderGroup([
|
||||||
|
{
|
||||||
|
id: "ai-1",
|
||||||
|
type: "ai",
|
||||||
|
content: "",
|
||||||
|
tool_calls: [
|
||||||
|
{
|
||||||
|
id: "call-search",
|
||||||
|
name: "web_search",
|
||||||
|
args: { query: "DeerFlow" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as Message,
|
||||||
|
{
|
||||||
|
id: "tool-empty",
|
||||||
|
type: "tool",
|
||||||
|
name: "web_search",
|
||||||
|
tool_call_id: "call-search",
|
||||||
|
content: "",
|
||||||
|
} as Message,
|
||||||
|
{
|
||||||
|
id: "tool-first",
|
||||||
|
type: "tool",
|
||||||
|
name: "web_search",
|
||||||
|
tool_call_id: "call-search",
|
||||||
|
content: JSON.stringify([
|
||||||
|
{ title: "First source", url: "https://first.example" },
|
||||||
|
]),
|
||||||
|
} as Message,
|
||||||
|
{
|
||||||
|
id: "tool-later",
|
||||||
|
type: "tool",
|
||||||
|
name: "web_search",
|
||||||
|
tool_call_id: "call-search",
|
||||||
|
content: JSON.stringify([
|
||||||
|
{ title: "Later source", url: "https://later.example" },
|
||||||
|
]),
|
||||||
|
} as Message,
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(html).toContain("First source");
|
||||||
|
expect(html).toContain("https://first.example");
|
||||||
|
expect(html).not.toContain("Later source");
|
||||||
|
expect(html).not.toContain("https://later.example");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function renderGroup(
|
function renderGroup(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user