mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-06-10 09:25:57 +00:00
fix(runtime): guide malformed write_file recovery (#3040)
* fix(runtime): guide malformed write_file recovery * fix(runtime): align write_file recovery guidance
This commit is contained in:
+23
-2
@@ -26,6 +26,11 @@ from langchain_core.messages import ToolMessage
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Workaround for issue #2894: malformed write_file calls can carry huge Markdown
|
||||||
|
# payloads in invalid tool-call args. Keep recovery error details short so the
|
||||||
|
# synthetic ToolMessage does not echo large or malformed content back to the model.
|
||||||
|
_MAX_RECOVERY_ERROR_DETAIL_LEN = 500
|
||||||
|
|
||||||
|
|
||||||
class DanglingToolCallMiddleware(AgentMiddleware[AgentState]):
|
class DanglingToolCallMiddleware(AgentMiddleware[AgentState]):
|
||||||
"""Inserts placeholder ToolMessages for dangling tool calls before model invocation.
|
"""Inserts placeholder ToolMessages for dangling tool calls before model invocation.
|
||||||
@@ -98,9 +103,25 @@ class DanglingToolCallMiddleware(AgentMiddleware[AgentState]):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _synthetic_tool_message_content(tool_call: dict) -> str:
|
def _synthetic_tool_message_content(tool_call: dict) -> str:
|
||||||
if tool_call.get("invalid"):
|
if tool_call.get("invalid"):
|
||||||
|
name = tool_call.get("name")
|
||||||
error = tool_call.get("error")
|
error = tool_call.get("error")
|
||||||
if isinstance(error, str) and error:
|
error_text = error[:_MAX_RECOVERY_ERROR_DETAIL_LEN] if isinstance(error, str) and error else ""
|
||||||
return f"[Tool call could not be executed because its arguments were invalid: {error}]"
|
# Workaround for issue #2894: malformed write_file calls can carry huge Markdown
|
||||||
|
# payloads in invalid tool-call args. Keep recovery guidance actionable without
|
||||||
|
# echoing large or malformed content back to the model.
|
||||||
|
if name == "write_file":
|
||||||
|
details = f" Parser error: {error_text}" if error_text else ""
|
||||||
|
return (
|
||||||
|
"[write_file failed before execution: the tool-call arguments were not valid JSON, "
|
||||||
|
"so no file was written. This often happens when the model tries to write a very "
|
||||||
|
"large Markdown file in a single tool call, especially when `content` contains "
|
||||||
|
"unescaped quotes, inline JSON, backslashes, or code fences. Do not retry the same "
|
||||||
|
"large `write_file` payload for this artifact; provide the report/content directly "
|
||||||
|
"as normal assistant text in your next response. If a file write is still needed "
|
||||||
|
f"later, split the file into smaller sections instead of one large payload.{details}]"
|
||||||
|
)
|
||||||
|
if error_text:
|
||||||
|
return f"[Tool call could not be executed because its arguments were invalid: {error_text}]"
|
||||||
return "[Tool call could not be executed because its arguments were invalid.]"
|
return "[Tool call could not be executed because its arguments were invalid.]"
|
||||||
return "[Tool call was interrupted and did not return a result.]"
|
return "[Tool call was interrupted and did not return a result.]"
|
||||||
|
|
||||||
|
|||||||
@@ -333,8 +333,27 @@ class TestBuildPatchedMessagesPatching:
|
|||||||
assert patched[1].tool_call_id == "write_file:36"
|
assert patched[1].tool_call_id == "write_file:36"
|
||||||
assert patched[1].name == "write_file"
|
assert patched[1].name == "write_file"
|
||||||
assert patched[1].status == "error"
|
assert patched[1].status == "error"
|
||||||
|
assert "write_file failed before execution" in patched[1].content
|
||||||
|
assert "no file was written" in patched[1].content
|
||||||
|
assert "very large Markdown file in a single tool call" in patched[1].content
|
||||||
|
assert "Do not retry the same large `write_file` payload" in patched[1].content
|
||||||
|
assert "split the file into smaller sections" in patched[1].content
|
||||||
|
assert "normal assistant text" in patched[1].content
|
||||||
|
assert "Failed to parse tool arguments" in patched[1].content
|
||||||
|
assert 'bad {"json"}' not in patched[1].content
|
||||||
|
|
||||||
|
def test_non_write_file_invalid_tool_call_uses_generic_recovery_message(self):
|
||||||
|
mw = DanglingToolCallMiddleware()
|
||||||
|
msgs = [_ai_with_invalid_tool_calls([_invalid_tc(name="search", tc_id="search:1")])]
|
||||||
|
|
||||||
|
patched = mw._build_patched_messages(msgs)
|
||||||
|
|
||||||
|
assert patched is not None
|
||||||
|
assert patched[1].tool_call_id == "search:1"
|
||||||
|
assert patched[1].name == "search"
|
||||||
assert "arguments were invalid" in patched[1].content
|
assert "arguments were invalid" in patched[1].content
|
||||||
assert "Failed to parse tool arguments" in patched[1].content
|
assert "Failed to parse tool arguments" in patched[1].content
|
||||||
|
assert "write_file failed before execution" not in patched[1].content
|
||||||
|
|
||||||
def test_valid_and_invalid_tool_calls_are_both_patched(self):
|
def test_valid_and_invalid_tool_calls_are_both_patched(self):
|
||||||
mw = DanglingToolCallMiddleware()
|
mw = DanglingToolCallMiddleware()
|
||||||
|
|||||||
Reference in New Issue
Block a user