mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-11 14:38:38 +00:00
* feat(observability): persist deferred tool promotions Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com> * fix(ci): trim agent guidance chain Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com> --------- Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com> Co-authored-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>
190 lines
6.8 KiB
Python
190 lines
6.8 KiB
Python
"""Tests for deferred-tool promotion audit events."""
|
|
|
|
import asyncio
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
from langchain_core.messages import ToolMessage
|
|
from langgraph.types import Command
|
|
|
|
from deerflow.agents.middlewares.tool_promotion_audit_middleware import DeferredToolPromotionAuditMiddleware
|
|
from deerflow.runtime.events.store.memory import MemoryRunEventStore
|
|
from deerflow.runtime.journal import RunJournal
|
|
|
|
|
|
class _Recorder:
|
|
def __init__(self):
|
|
self.calls: list[dict] = []
|
|
self.claimed: set[str] = set()
|
|
|
|
def claim_tool_promotions(self, tool_names):
|
|
names = sorted(set(tool_names) - self.claimed)
|
|
self.claimed.update(names)
|
|
return names
|
|
|
|
def record_middleware(self, **kwargs):
|
|
self.calls.append(kwargs)
|
|
|
|
|
|
class _ToolRequest:
|
|
def __init__(self, *, name="tool_search", state=None, context=None, query="private query"):
|
|
self.tool_call = {"name": name, "id": "tc1", "args": {"query": query}}
|
|
self.state = state or {}
|
|
self.runtime = SimpleNamespace(context=context or {})
|
|
|
|
|
|
def _middleware():
|
|
return DeferredToolPromotionAuditMiddleware(frozenset({"mcp_a", "mcp_b"}), "h1")
|
|
|
|
|
|
def test_records_only_new_names_from_the_final_current_catalog_command():
|
|
recorder = _Recorder()
|
|
request = _ToolRequest(
|
|
state={"promoted": {"catalog_hash": "h1", "names": ["mcp_b"]}},
|
|
context={"__run_journal": recorder},
|
|
query="credential-adjacent query",
|
|
)
|
|
result = Command(
|
|
update={
|
|
"promoted": {"catalog_hash": "h1", "names": ["not_deferred", "mcp_b", "mcp_a", "mcp_a"]},
|
|
"messages": [ToolMessage(content="private schema", tool_call_id="tc1", name="tool_search")],
|
|
}
|
|
)
|
|
|
|
assert _middleware().wrap_tool_call(request, lambda _: result) is result
|
|
|
|
assert recorder.calls == [
|
|
{
|
|
"tag": "tool_promotion",
|
|
"name": "DeferredToolPromotionAuditMiddleware",
|
|
"hook": "wrap_tool_call",
|
|
"action": "promote",
|
|
"changes": {
|
|
"source": "tool_search",
|
|
"tool_names": ["mcp_a"],
|
|
"count": 1,
|
|
"is_subagent": False,
|
|
"agent_id": None,
|
|
},
|
|
}
|
|
]
|
|
persisted = repr(recorder.calls)
|
|
assert "credential-adjacent query" not in persisted
|
|
assert "private schema" not in persisted
|
|
assert "h1" not in persisted
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_records_promotion_but_repeated_stale_and_non_search_results_do_not():
|
|
recorder = _Recorder()
|
|
middleware = _middleware()
|
|
request = _ToolRequest(context={"__run_journal": recorder})
|
|
promoted = Command(update={"promoted": {"catalog_hash": "h1", "names": ["mcp_a"]}})
|
|
|
|
async def handle_promoted(_):
|
|
return promoted
|
|
|
|
assert await middleware.awrap_tool_call(request, handle_promoted) is promoted
|
|
assert recorder.calls[0]["changes"]["tool_names"] == ["mcp_a"]
|
|
|
|
request.state = {"promoted": {"catalog_hash": "h1", "names": ["mcp_a"]}}
|
|
|
|
async def handle_stale(_):
|
|
return Command(update={"promoted": {"catalog_hash": "stale", "names": ["mcp_b"]}})
|
|
|
|
assert await middleware.awrap_tool_call(request, handle_promoted) is promoted
|
|
assert await middleware.awrap_tool_call(request, handle_stale)
|
|
request.tool_call["name"] = "another_tool"
|
|
assert await middleware.awrap_tool_call(request, handle_promoted) is promoted
|
|
assert len(recorder.calls) == 1
|
|
|
|
|
|
def test_final_handler_result_is_observed_without_rebuilding_it():
|
|
"""An outer audit wrapper must see names after inner policy filtering."""
|
|
recorder = _Recorder()
|
|
request = _ToolRequest(context={"__run_journal": recorder})
|
|
policy_filtered = Command(update={"promoted": {"catalog_hash": "h1", "names": ["mcp_a"]}})
|
|
|
|
observed = _middleware().wrap_tool_call(request, lambda _: policy_filtered)
|
|
|
|
assert observed is policy_filtered
|
|
assert recorder.calls[0]["changes"]["tool_names"] == ["mcp_a"]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"result",
|
|
[
|
|
ToolMessage(content="no match", tool_call_id="tc1", name="tool_search"),
|
|
Command(update={}),
|
|
Command(update={"promoted": {"catalog_hash": "h1", "names": []}}),
|
|
Command(update={"promoted": {"catalog_hash": "h1", "names": ["mcp_a", 7]}}),
|
|
],
|
|
)
|
|
def test_non_promotion_and_malformed_results_emit_nothing(result):
|
|
recorder = _Recorder()
|
|
request = _ToolRequest(context={"__run_journal": recorder})
|
|
|
|
assert _middleware().wrap_tool_call(request, lambda _: result) is result
|
|
assert recorder.calls == []
|
|
|
|
|
|
def test_recorder_failure_does_not_replace_the_tool_result(caplog):
|
|
class BrokenRecorder:
|
|
def record_middleware(self, **kwargs):
|
|
raise RuntimeError("event store unavailable")
|
|
|
|
request = _ToolRequest(context={"__run_journal": BrokenRecorder()})
|
|
result = Command(update={"promoted": {"catalog_hash": "h1", "names": ["mcp_a"]}})
|
|
|
|
with caplog.at_level("WARNING"):
|
|
assert _middleware().wrap_tool_call(request, lambda _: result) is result
|
|
|
|
assert "Failed to record middleware:tool_promotion event" in caplog.text
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_event_round_trips_through_run_journal_and_store():
|
|
store = MemoryRunEventStore()
|
|
journal = RunJournal("run-1", "thread-1", store, flush_threshold=100)
|
|
request = _ToolRequest(context={"__run_journal": journal})
|
|
result = Command(update={"promoted": {"catalog_hash": "h1", "names": ["mcp_a"]}})
|
|
|
|
assert _middleware().wrap_tool_call(request, lambda _: result) is result
|
|
await journal.flush()
|
|
|
|
events = await store.list_events("thread-1", "run-1")
|
|
assert len(events) == 1
|
|
assert events[0]["event_type"] == "middleware:tool_promotion"
|
|
assert events[0]["category"] == "middleware"
|
|
assert events[0]["content"]["changes"] == {
|
|
"source": "tool_search",
|
|
"tool_names": ["mcp_a"],
|
|
"count": 1,
|
|
"is_subagent": False,
|
|
"agent_id": None,
|
|
}
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_parallel_searches_claim_the_same_new_name_only_once():
|
|
"""Parallel tool Sends share pre-step state but must not duplicate events."""
|
|
store = MemoryRunEventStore()
|
|
journal = RunJournal("run-parallel", "thread-1", store, flush_threshold=100)
|
|
middleware = _middleware()
|
|
result = Command(update={"promoted": {"catalog_hash": "h1", "names": ["mcp_a"]}})
|
|
requests = [
|
|
_ToolRequest(context={"__run_journal": journal}),
|
|
_ToolRequest(context={"__run_journal": journal}),
|
|
]
|
|
|
|
async def handler(_):
|
|
await asyncio.sleep(0)
|
|
return result
|
|
|
|
observed = await asyncio.gather(*(middleware.awrap_tool_call(request, handler) for request in requests))
|
|
await journal.flush()
|
|
|
|
assert observed == [result, result]
|
|
events = await store.list_events("thread-1", "run-parallel")
|
|
assert [event["event_type"] for event in events] == ["middleware:tool_promotion"]
|