mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-19 11:06:18 +00:00
fix: keep streamed answers out of thinking and improve local bash probes (#5001)
* fix: improve streaming reasoning and local bash guidance * test: cover reasoning-only processing group * fix(frontend): preserve streaming reasoning order * test(frontend): cover reasoning tool-call regrouping * fix(frontend): keep thinking-only blocks in processing --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
parent
db6130861d
commit
972020cf85
@ -1247,6 +1247,8 @@ In the Web UI, completed assistant turns can be branched into a new main convers
|
||||
|
||||
The Web UI reports completed task time once per run. This is total wall-clock time—including model reasoning, tool calls, and waiting—not a per-step or model-only thinking duration. Reasoning content remains available through its own separate disclosure.
|
||||
|
||||
While a response streams, reasoning-only messages stay in the processing panel, including Anthropic thinking blocks. Once answer content arrives alongside reasoning, it appears in an assistant bubble.
|
||||
|
||||
In the Web UI, the latest completed user turn can also be edited and rerun from the message toolbar. DeerFlow restores the conversation checkpoint before that user message, submits the edited text as a new user message, and hides the superseded turn once the replay is in progress or succeeds. This is a conversation-state replay only: files, memory updates, and external tool side effects are not undone.
|
||||
|
||||
Web UI chat links percent-encode custom thread identifiers before placing them in route segments, so reserved URL characters such as `#` and `?` do not change which conversation is opened.
|
||||
|
||||
@ -203,3 +203,16 @@ def test_bash_tool_description_guides_backgrounding_long_lived_processes():
|
||||
description = bash_tool.description.lower()
|
||||
assert "background" in description
|
||||
assert "server" in description
|
||||
|
||||
|
||||
def test_bash_tool_description_guides_safe_cross_platform_local_environment_probes():
|
||||
"""The model-visible bash contract must recover from local path-guard failures (#4999)."""
|
||||
from deerflow.sandbox.tools import bash_tool
|
||||
|
||||
description = " ".join(bash_tool.description.lower().split())
|
||||
assert "local host" in description
|
||||
assert "uname -s" in description
|
||||
assert "sw_vers" in description
|
||||
assert "only when the active sandbox policy permits it" in description
|
||||
assert "do not repeat the rejected command" in description
|
||||
assert "command-only probes" in description
|
||||
|
||||
@ -81,6 +81,8 @@
|
||||
as removable "missing" entries instead of silently widening the allowlist.
|
||||
6. Components subscribe to thread state and render updates
|
||||
|
||||
AI message grouping uses `extractContentFromMessage()` to identify visible answer content. A non-empty content array may contain only Anthropic thinking blocks; keep it in `assistant:processing` until answer content arrives. Cover both streamed snapshots in `tests/unit/core/messages/utils.test.ts`.
|
||||
|
||||
Project moves in `core/threads/hooks.ts` cancel all per-thread metadata query
|
||||
variants after the write succeeds, merge only `deerflow_project_id`, then
|
||||
invalidate/refetch that metadata prefix. This fences delayed pre-move reads and
|
||||
|
||||
@ -140,13 +140,24 @@ export function getMessageGroups(
|
||||
// same message later. Keep that unresolved message in the processing
|
||||
// group so its visible text does not jump from an assistant bubble into
|
||||
// the steps panel when the tool call arrives (#4304).
|
||||
// A reasoning-bearing answer is treated as terminal until tool calls
|
||||
// actually arrive. If they do arrive on that same message, it is
|
||||
// deliberately reclassified as processing so its tool activity remains
|
||||
// visible with the text that introduced it.
|
||||
// Non-empty content arrays can contain only Anthropic thinking blocks.
|
||||
// Require content the answer renderer can actually display.
|
||||
const hasAnswerContent = extractContentFromMessage(message).length > 0;
|
||||
const isUnresolvedAssistantText =
|
||||
currentTurnStartIndex >= 0 &&
|
||||
messageIndex > currentTurnStartIndex &&
|
||||
hasContent(message) &&
|
||||
!hasToolCalls(message);
|
||||
hasAnswerContent &&
|
||||
!hasToolCalls(message) &&
|
||||
// A provider that has already supplied reasoning with answer text is
|
||||
// completing an answer, not merely streaming a pre-tool narration.
|
||||
// Keep it out of the processing disclosure while the turn is active.
|
||||
!hasReasoning(message);
|
||||
const becomesAssistantBubble =
|
||||
hasContent(message) &&
|
||||
hasAnswerContent &&
|
||||
!hasToolCalls(message) &&
|
||||
!isUnresolvedAssistantText;
|
||||
|
||||
@ -733,7 +744,7 @@ export function hasReasoning(message: Message) {
|
||||
return false;
|
||||
}
|
||||
if (typeof message.additional_kwargs?.reasoning_content === "string") {
|
||||
return true;
|
||||
return message.additional_kwargs.reasoning_content.trim().length > 0;
|
||||
}
|
||||
if (Array.isArray(message.content)) {
|
||||
const part = message.content[0];
|
||||
|
||||
@ -145,10 +145,10 @@ test("renders reasoning above the answer text while the turn is streaming", asyn
|
||||
await textarea.fill("Summarize that briefly");
|
||||
await textarea.press("Enter");
|
||||
|
||||
// The streaming turn renders inside the chain-of-thought panel, whose
|
||||
// reasoning disclosure is labelled "Thinking".
|
||||
// The streaming turn renders inside the assistant bubble, whose
|
||||
// reasoning disclosure is labelled "Reasoning".
|
||||
await expectRenderedAbove(
|
||||
page.getByText("Thinking", { exact: true }),
|
||||
page.getByText("Reasoning", { exact: true }),
|
||||
page.getByText(ANSWER_TEXT),
|
||||
);
|
||||
} finally {
|
||||
|
||||
@ -287,6 +287,146 @@ test("keeps unresolved streaming text in the processing group when tool calls ar
|
||||
);
|
||||
});
|
||||
|
||||
test("keeps streaming reasoning and answer text out of the processing group", () => {
|
||||
const messages = [
|
||||
{ id: "human-1", type: "human", content: "Explain the result" },
|
||||
{
|
||||
id: "ai-1",
|
||||
type: "ai",
|
||||
content: "The final answer is ready.",
|
||||
additional_kwargs: {
|
||||
reasoning_content: "I checked the available evidence.",
|
||||
},
|
||||
},
|
||||
] as Message[];
|
||||
|
||||
const groups = getMessageGroups(messages, { isCurrentTurnLoading: true });
|
||||
|
||||
expect(groups.map((group) => group.type)).toEqual(["human", "assistant"]);
|
||||
});
|
||||
|
||||
test("moves a reasoning-bearing message into processing when it gains tool calls", () => {
|
||||
const messages = [
|
||||
{ id: "human-1", type: "human", content: "Explain the result" },
|
||||
{
|
||||
id: "ai-1",
|
||||
type: "ai",
|
||||
content: "I will verify that with a source.",
|
||||
additional_kwargs: {
|
||||
reasoning_content: "I should verify the answer before replying.",
|
||||
},
|
||||
},
|
||||
] as Message[];
|
||||
|
||||
expect(
|
||||
getMessageGroups(messages, { isCurrentTurnLoading: true }).map(
|
||||
(group) => group.type,
|
||||
),
|
||||
).toEqual(["human", "assistant"]);
|
||||
|
||||
messages[1] = {
|
||||
...messages[1],
|
||||
tool_calls: [{ id: "call-1", name: "web_search", args: {} }],
|
||||
} as Message;
|
||||
|
||||
const groups = getMessageGroups(messages, { isCurrentTurnLoading: true });
|
||||
|
||||
expect(groups.map((group) => group.type)).toEqual([
|
||||
"human",
|
||||
"assistant:processing",
|
||||
]);
|
||||
expect(groups[1]?.messages.map((message) => message.id)).toEqual(["ai-1"]);
|
||||
});
|
||||
|
||||
test("keeps content with empty reasoning metadata in the processing group while streaming", () => {
|
||||
const messages = [
|
||||
{ id: "human-1", type: "human", content: "Explain the result" },
|
||||
{
|
||||
id: "ai-1",
|
||||
type: "ai",
|
||||
content: "I will check the result first.",
|
||||
additional_kwargs: { reasoning_content: "" },
|
||||
},
|
||||
] as Message[];
|
||||
|
||||
const groups = getMessageGroups(messages, { isCurrentTurnLoading: true });
|
||||
|
||||
expect(groups.map((group) => group.type)).toEqual([
|
||||
"human",
|
||||
"assistant:processing",
|
||||
]);
|
||||
});
|
||||
|
||||
test("keeps streaming reasoning-only messages in the processing group", () => {
|
||||
const messages = [
|
||||
{ id: "human-1", type: "human", content: "Explain the result" },
|
||||
{
|
||||
id: "ai-1",
|
||||
type: "ai",
|
||||
content: "",
|
||||
additional_kwargs: {
|
||||
reasoning_content: "I am still checking the available evidence.",
|
||||
},
|
||||
},
|
||||
] as Message[];
|
||||
|
||||
const groups = getMessageGroups(messages, { isCurrentTurnLoading: true });
|
||||
|
||||
expect(groups.map((group) => group.type)).toEqual([
|
||||
"human",
|
||||
"assistant:processing",
|
||||
]);
|
||||
expect(groups[1]?.messages.map((message) => message.id)).toEqual(["ai-1"]);
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ answerBlocks: [] },
|
||||
{ answerBlocks: [{ type: "text", text: " " }] },
|
||||
])(
|
||||
"keeps Anthropic thinking blocks in processing until answer text arrives: %j",
|
||||
({ answerBlocks }) => {
|
||||
const messages = [
|
||||
{ id: "human-1", type: "human", content: "Explain the result" },
|
||||
{
|
||||
id: "ai-1",
|
||||
type: "ai",
|
||||
content: [
|
||||
{ type: "thinking", thinking: "Still checking." },
|
||||
...answerBlocks,
|
||||
],
|
||||
},
|
||||
] as Message[];
|
||||
|
||||
const groups = getMessageGroups(messages, { isCurrentTurnLoading: true });
|
||||
|
||||
expect(groups.map((group) => group.type)).toEqual([
|
||||
"human",
|
||||
"assistant:processing",
|
||||
]);
|
||||
expect(groups[1]?.messages.map((message) => message.id)).toEqual(["ai-1"]);
|
||||
|
||||
messages[1] = {
|
||||
id: "ai-1",
|
||||
type: "ai",
|
||||
content: [
|
||||
{ type: "thinking", thinking: "Still checking." },
|
||||
{ type: "text", text: "The answer is ready." },
|
||||
],
|
||||
} as Message;
|
||||
|
||||
const answeredGroups = getMessageGroups(messages, {
|
||||
isCurrentTurnLoading: true,
|
||||
});
|
||||
expect(answeredGroups.map((group) => group.type)).toEqual([
|
||||
"human",
|
||||
"assistant",
|
||||
]);
|
||||
expect(answeredGroups[1]?.messages.map((message) => message.id)).toEqual([
|
||||
"ai-1",
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
test("keeps post-tool streaming text in the processing group until the turn settles", () => {
|
||||
const messages = [
|
||||
{ id: "human-1", type: "human", content: "Inspect and summarize" },
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user