mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-07-09 16:08:31 +00:00
* feat: emit structured runtime metadata * fix: avoid subagent import cycle in replay gateway * fix: preserve legacy subtask result parsing * refactor: tighten runtime metadata contracts * fix(middleware): keep recovery hint on task exception wrapper content The structured-metadata stamp overwrote the wrapper text with the bare task-failure message, dropping the model-facing 'Continue with available context, or choose an alternative tool.' guidance that every other tool exception keeps. Append the shared hint after the formatted message. * fix(subagents): require lowercase hex for result_sha256 reader Length-only validation accepted any 64-char string; a faulty serializer or relaying wrapper could store a non-digest value in the delegation ledger. Enforce the producer's hexdigest shape with a fullmatch. --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
108 lines
3.4 KiB
Python
108 lines
3.4 KiB
Python
"""Tests for task exception metadata produced by ToolErrorHandlingMiddleware."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
|
|
from langchain_core.messages import ToolMessage
|
|
from langgraph.types import Command
|
|
|
|
from deerflow.agents.middlewares.tool_error_handling_middleware import (
|
|
ToolErrorHandlingMiddleware,
|
|
)
|
|
from deerflow.subagents.status_contract import (
|
|
SUBAGENT_ERROR_KEY,
|
|
SUBAGENT_STATUS_KEY,
|
|
)
|
|
|
|
|
|
class _FakeRequest:
|
|
"""Stand-in for ``ToolCallRequest`` used by the middleware."""
|
|
|
|
def __init__(self, tool_name: str, tool_call_id: str = "call-1") -> None:
|
|
self.tool_call = {"name": tool_name, "id": tool_call_id}
|
|
|
|
|
|
def test_task_tool_exception_returns_failed_metadata():
|
|
middleware = ToolErrorHandlingMiddleware()
|
|
request = _FakeRequest("task")
|
|
|
|
def handler(_req):
|
|
raise RuntimeError("blew up during execution")
|
|
|
|
result = middleware.wrap_tool_call(request, handler)
|
|
|
|
assert isinstance(result, ToolMessage)
|
|
assert result.additional_kwargs.get(SUBAGENT_STATUS_KEY) == "failed"
|
|
assert "RuntimeError" in result.additional_kwargs.get(SUBAGENT_ERROR_KEY, "")
|
|
|
|
|
|
def test_async_task_tool_exception_returns_failed_metadata():
|
|
middleware = ToolErrorHandlingMiddleware()
|
|
request = _FakeRequest("task")
|
|
|
|
async def handler(_req):
|
|
raise RuntimeError("async boom")
|
|
|
|
result = asyncio.run(middleware.awrap_tool_call(request, handler))
|
|
|
|
assert isinstance(result, ToolMessage)
|
|
assert result.additional_kwargs.get(SUBAGENT_STATUS_KEY) == "failed"
|
|
assert "RuntimeError" in result.additional_kwargs.get(SUBAGENT_ERROR_KEY, "")
|
|
|
|
|
|
def test_successful_plain_task_tool_message_is_not_stamped_from_content():
|
|
middleware = ToolErrorHandlingMiddleware()
|
|
request = _FakeRequest("task")
|
|
|
|
def handler(_req):
|
|
return ToolMessage(content="Task Succeeded. Result: ok", tool_call_id="call-1", name="task")
|
|
|
|
result = middleware.wrap_tool_call(request, handler)
|
|
|
|
assert isinstance(result, ToolMessage)
|
|
assert SUBAGENT_STATUS_KEY not in (result.additional_kwargs or {})
|
|
|
|
|
|
def test_does_not_stamp_non_task_tool_exception():
|
|
middleware = ToolErrorHandlingMiddleware()
|
|
request = _FakeRequest("bash")
|
|
|
|
def handler(_req):
|
|
raise RuntimeError("command failed")
|
|
|
|
result = middleware.wrap_tool_call(request, handler)
|
|
|
|
assert isinstance(result, ToolMessage)
|
|
assert SUBAGENT_STATUS_KEY not in (result.additional_kwargs or {})
|
|
|
|
|
|
def test_task_command_with_metadata_bypasses_middleware_stamp():
|
|
middleware = ToolErrorHandlingMiddleware()
|
|
request = _FakeRequest("task")
|
|
command = Command(
|
|
update={
|
|
"messages": [
|
|
ToolMessage(
|
|
"Task Succeeded. Result: ok",
|
|
tool_call_id="call-1",
|
|
name="task",
|
|
additional_kwargs={"subagent_status": "completed", "subagent_result_brief": "ok"},
|
|
)
|
|
]
|
|
}
|
|
)
|
|
|
|
assert middleware.wrap_tool_call(request, lambda _req: command) is command
|
|
|
|
|
|
def test_additional_kwargs_round_trip_via_json():
|
|
msg = ToolMessage(
|
|
content="Task Succeeded. Result: ok",
|
|
tool_call_id="call-1",
|
|
name="task",
|
|
additional_kwargs={SUBAGENT_STATUS_KEY: "completed", SUBAGENT_ERROR_KEY: ""},
|
|
)
|
|
serialised = msg.model_dump_json()
|
|
restored = ToolMessage.model_validate_json(serialised)
|
|
assert restored.additional_kwargs.get(SUBAGENT_STATUS_KEY) == "completed"
|