mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-08-01 19:06:01 +00:00
Subagents build their runtime chain via build_subagent_runtime_middlewares, which attached none of the lead's runaway guards — no LoopDetectionMiddleware. A degenerate subagent tool loop therefore ran unchecked until max_turns, re-sending a growing context each turn (the #3875 4.4M-token burn). Append LoopDetectionMiddleware.from_config(app_config.loop_detection) to the subagent chain, gated by the existing loop_detection.enabled config (default on) — no new config field. Registered before SafetyFinishReasonMiddleware to match the lead's after-model ordering (the safety middleware requires placement after LoopDetection). Subagents disallow task, so only the tool-loop heuristic can fire here — no recursive-delegation false positives to worry about. Phase 1 of #3875 (smallest blast radius). Phase 2 adds a deterministic turn/token budget with a lead-visible stop reason; Phase 3 defers summarization. Tests: enabled/disabled wiring + before-SafetyFinish ordering, mirroring the lead chain's coverage. Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
e9161ff148
commit
80e031dcc6
@ -272,6 +272,26 @@ def build_subagent_runtime_middlewares(
|
|||||||
|
|
||||||
middlewares.append(DeferredToolFilterMiddleware(deferred_setup.deferred_names, deferred_setup.catalog_hash))
|
middlewares.append(DeferredToolFilterMiddleware(deferred_setup.deferred_names, deferred_setup.catalog_hash))
|
||||||
|
|
||||||
|
# LoopDetectionMiddleware — subagents inherit none of the lead's runaway
|
||||||
|
# guards today (see #3875): with no loop detection a degenerate subagent tool
|
||||||
|
# loop runs unchecked until ``max_turns``, re-sending a growing context each
|
||||||
|
# turn (the reported 4.4M-token burn). Mirror the lead chain so the loop is
|
||||||
|
# detected and broken. Subagents disallow ``task``, so only the tool-loop
|
||||||
|
# heuristic can fire here — no recursive-delegation path to false-positive on.
|
||||||
|
# Registered before SafetyFinishReasonMiddleware (earlier in the list).
|
||||||
|
# LangChain dispatches after_model hooks in REVERSE registration order, so
|
||||||
|
# SafetyFinishReasonMiddleware (appended below) executes first and strips
|
||||||
|
# safety-terminated tool_calls; LoopDetectionMiddleware then accounts on the
|
||||||
|
# cleaned message. This is the placement SafetyFinishReasonMiddleware's
|
||||||
|
# docstring requires ("register after LoopDetection") and mirrors the lead
|
||||||
|
# chain (``lead_agent/agent.py``). Phase 1 of #3875; a deterministic
|
||||||
|
# turn/token budget with lead-visible stop reason is Phase 2.
|
||||||
|
loop_detection_config = app_config.loop_detection
|
||||||
|
if loop_detection_config.enabled:
|
||||||
|
from deerflow.agents.middlewares.loop_detection_middleware import LoopDetectionMiddleware
|
||||||
|
|
||||||
|
middlewares.append(LoopDetectionMiddleware.from_config(loop_detection_config))
|
||||||
|
|
||||||
# Same provider safety-termination guard the lead agent uses — subagents
|
# Same provider safety-termination guard the lead agent uses — subagents
|
||||||
# are equally exposed to truncated tool_calls returned with
|
# are equally exposed to truncated tool_calls returned with
|
||||||
# finish_reason=content_filter (and friends), and the bad call would then
|
# finish_reason=content_filter (and friends), and the bad call would then
|
||||||
|
|||||||
@ -144,12 +144,12 @@ def test_build_subagent_runtime_middlewares_threads_app_config_to_llm_middleware
|
|||||||
assert captured["app_config"] is app_config
|
assert captured["app_config"] is app_config
|
||||||
# 8 baseline (InputSanitization, ToolOutputBudget, ThreadData, Sandbox,
|
# 8 baseline (InputSanitization, ToolOutputBudget, ThreadData, Sandbox,
|
||||||
# DanglingToolCall, LLMErrorHandling, SandboxAudit, ToolErrorHandling)
|
# DanglingToolCall, LLMErrorHandling, SandboxAudit, ToolErrorHandling)
|
||||||
# + 1 ReadBeforeWriteMiddleware + 1 SafetyFinishReasonMiddleware (both
|
# + 1 ReadBeforeWriteMiddleware + 1 LoopDetectionMiddleware
|
||||||
# enabled by default).
|
# + 1 SafetyFinishReasonMiddleware (all enabled by default).
|
||||||
from deerflow.agents.middlewares.safety_finish_reason_middleware import SafetyFinishReasonMiddleware
|
from deerflow.agents.middlewares.safety_finish_reason_middleware import SafetyFinishReasonMiddleware
|
||||||
from deerflow.agents.middlewares.tool_output_budget_middleware import ToolOutputBudgetMiddleware
|
from deerflow.agents.middlewares.tool_output_budget_middleware import ToolOutputBudgetMiddleware
|
||||||
|
|
||||||
assert len(middlewares) == 10
|
assert len(middlewares) == 11
|
||||||
assert isinstance(middlewares[0], FakeMiddleware) # InputSanitizationMiddleware stub
|
assert isinstance(middlewares[0], FakeMiddleware) # InputSanitizationMiddleware stub
|
||||||
assert isinstance(middlewares[1], ToolOutputBudgetMiddleware)
|
assert isinstance(middlewares[1], ToolOutputBudgetMiddleware)
|
||||||
assert any(isinstance(m, ToolErrorHandlingMiddleware) for m in middlewares)
|
assert any(isinstance(m, ToolErrorHandlingMiddleware) for m in middlewares)
|
||||||
@ -472,6 +472,56 @@ def test_subagent_runtime_middlewares_skip_deferred_filter_without_names(monkeyp
|
|||||||
assert not any(isinstance(m, DeferredToolFilterMiddleware) for m in middlewares)
|
assert not any(isinstance(m, DeferredToolFilterMiddleware) for m in middlewares)
|
||||||
|
|
||||||
|
|
||||||
|
def test_subagent_runtime_middlewares_attach_loop_detection_when_enabled(monkeypatch):
|
||||||
|
"""Subagents must inherit the lead's LoopDetectionMiddleware so a degenerate
|
||||||
|
tool loop is broken instead of burning tokens until ``max_turns`` (#3875).
|
||||||
|
``loop_detection.enabled`` defaults to True, so the default subagent chain
|
||||||
|
carries the guard. Phase 1 of #3875."""
|
||||||
|
from deerflow.agents.middlewares.loop_detection_middleware import LoopDetectionMiddleware
|
||||||
|
|
||||||
|
app_config = _make_app_config()
|
||||||
|
_stub_runtime_middleware_imports(monkeypatch)
|
||||||
|
|
||||||
|
middlewares = build_subagent_runtime_middlewares(app_config=app_config, model_name="test-model")
|
||||||
|
|
||||||
|
loop = [m for m in middlewares if isinstance(m, LoopDetectionMiddleware)]
|
||||||
|
assert len(loop) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_subagent_runtime_middlewares_omit_loop_detection_when_disabled(monkeypatch):
|
||||||
|
"""``loop_detection.enabled=False`` must drop the guard from the subagent
|
||||||
|
chain, mirroring the lead's gate (``lead_agent/agent.py``)."""
|
||||||
|
from deerflow.agents.middlewares.loop_detection_middleware import LoopDetectionMiddleware
|
||||||
|
from deerflow.config.loop_detection_config import LoopDetectionConfig
|
||||||
|
|
||||||
|
app_config = _make_app_config().model_copy(update={"loop_detection": LoopDetectionConfig(enabled=False)})
|
||||||
|
_stub_runtime_middleware_imports(monkeypatch)
|
||||||
|
|
||||||
|
middlewares = build_subagent_runtime_middlewares(app_config=app_config, model_name="test-model")
|
||||||
|
|
||||||
|
assert not any(isinstance(m, LoopDetectionMiddleware) for m in middlewares)
|
||||||
|
|
||||||
|
|
||||||
|
def test_subagent_runtime_middlewares_place_loop_detection_before_safety_finish(monkeypatch):
|
||||||
|
"""LoopDetectionMiddleware must be registered before SafetyFinishReasonMiddleware
|
||||||
|
(earlier in the middleware list). LangChain dispatches after_model hooks in
|
||||||
|
reverse registration order, so SafetyFinishReasonMiddleware (registered
|
||||||
|
later) executes first — the placement its docstring requires and the lead
|
||||||
|
chain (``lead_agent/agent.py``) uses. The assertion pins registration order,
|
||||||
|
not execution order."""
|
||||||
|
from deerflow.agents.middlewares.loop_detection_middleware import LoopDetectionMiddleware
|
||||||
|
from deerflow.agents.middlewares.safety_finish_reason_middleware import SafetyFinishReasonMiddleware
|
||||||
|
|
||||||
|
app_config = _make_app_config()
|
||||||
|
_stub_runtime_middleware_imports(monkeypatch)
|
||||||
|
|
||||||
|
middlewares = build_subagent_runtime_middlewares(app_config=app_config, model_name="test-model")
|
||||||
|
|
||||||
|
loop_idx = next(i for i, m in enumerate(middlewares) if isinstance(m, LoopDetectionMiddleware))
|
||||||
|
safety_idx = next(i for i, m in enumerate(middlewares) if isinstance(m, SafetyFinishReasonMiddleware))
|
||||||
|
assert loop_idx < safety_idx
|
||||||
|
|
||||||
|
|
||||||
def test_lead_runtime_chain_finds_historical_uploads_under_lazy_init_false(tmp_path, monkeypatch):
|
def test_lead_runtime_chain_finds_historical_uploads_under_lazy_init_false(tmp_path, monkeypatch):
|
||||||
"""Integration anchor for the ThreadData → Uploads ordering.
|
"""Integration anchor for the ThreadData → Uploads ordering.
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user