refactor: thread app_config through middleware factories (#2652)

* refactor: thread app_config through middleware factories

Continues the incremental config-refactor sequence (#2611 root, #2612 lead
path) one layer deeper into the middleware factories. Two ambient lookups
inside _build_runtime_middlewares are eliminated and the LLMErrorHandling
band-aid removed:

- _build_runtime_middlewares / build_lead_runtime_middlewares /
  build_subagent_runtime_middlewares now require app_config: AppConfig.
- get_guardrails_config() inside the factory is replaced with
  app_config.guardrails (semantically identical — same default-factory
  GuardrailsConfig — verified by direct equality check).
- LLMErrorHandlingMiddleware.__init__ now requires app_config and reads
  circuit_breaker fields directly. The class-level
  circuit_failure_threshold / circuit_recovery_timeout_sec defaults are
  removed along with the try/except (FileNotFoundError, RuntimeError):
  pass band-aid — the let-it-crash invariant the rest of the refactor
  enforces.

Caller chain (already-resolved app_config sources):
- _build_middlewares in lead_agent/agent.py: reorder so
  resolved_app_config = app_config or get_app_config() is computed BEFORE
  build_lead_runtime_middlewares is called, then passed as kwarg.
- SubagentExecutor: optional app_config parameter (mirrors the lead-agent
  pattern); _create_agent does the same `or get_app_config()` fallback at
  agent-build time, so task_tool callers don't need to plumb app_config
  through yet (typed-context plumbing for tool runtimes is a separate
  refactor).

Tests:
- test_llm_error_handling_middleware: _make_app_config helper using
  AppConfig(sandbox=SandboxConfig(use="test")) — same minimal-config
  pattern conftest already uses. Three direct LLMErrorHandlingMiddleware()
  calls each followed by post-construction circuit_breaker mutation fold
  cleanly into _build_middleware(circuit_failure_threshold=...,
  circuit_recovery_timeout_sec=...).

Verification:
- tests/test_llm_error_handling_middleware.py — 14 passed
- tests/test_subagent_executor.py — 28 passed
- tests/test_tool_error_handling_middleware.py — 6 passed
- tests/test_task_tool_core_logic.py — 18 passed (verifies task_tool
  unchanged behavior)
- Full suite: 2697 passed, 3 skipped. The single intermittent failure in
  tests/test_client_e2e.py::test_tool_call_produces_events is pre-existing
  LLM flakiness (the test asserts the model decided to call a tool;
  reproduces 1/3 on unchanged main as well).

* fix: address middleware app config review comments

* fix: satisfy app config annotation lint

* test: cover explicit app config middleware wiring

---------

Co-authored-by: greatmengqi <chenmengqi.0376@bytedance.com>
This commit is contained in:
greatmengqi
2026-04-30 12:41:09 +08:00
committed by GitHub
parent 74081a85a6
commit 38714b6ceb
8 changed files with 236 additions and 34 deletions
+90
View File
@@ -17,6 +17,7 @@ import asyncio
import sys
import threading
from datetime import datetime
from types import ModuleType
from unittest.mock import MagicMock, patch
import pytest
@@ -153,6 +154,13 @@ def mock_agent():
return agent
def _module(name: str, **attrs):
module = ModuleType(name)
for key, value in attrs.items():
setattr(module, key, value)
return module
# Helper to create real message objects
class _MsgHelper:
"""Helper to create real message objects from fixture classes."""
@@ -176,6 +184,88 @@ def msg(classes):
return _MsgHelper(classes)
# -----------------------------------------------------------------------------
# Agent Construction Tests
# -----------------------------------------------------------------------------
class TestAgentConstruction:
"""Test _create_agent() wiring before execution starts."""
def test_create_agent_threads_explicit_app_config_to_model_and_middlewares(
self,
classes,
base_config,
monkeypatch: pytest.MonkeyPatch,
):
"""Explicit app_config must flow into both model and middleware factories."""
import deerflow.config as config_module
from deerflow.subagents import executor as executor_module
SubagentExecutor = classes["SubagentExecutor"]
app_config = object()
model = object()
middlewares = [object()]
agent = object()
captured: dict[str, dict] = {}
def fake_get_app_config():
raise AssertionError("ambient get_app_config() must not be used when app_config is explicit")
def fake_create_chat_model(**kwargs):
captured["model"] = kwargs
return model
def fake_build_subagent_runtime_middlewares(**kwargs):
captured["middlewares"] = kwargs
return middlewares
def fake_create_agent(**kwargs):
captured["agent"] = kwargs
return agent
monkeypatch.setattr(config_module, "get_app_config", fake_get_app_config)
monkeypatch.setattr(
executor_module,
"create_chat_model",
fake_create_chat_model,
)
monkeypatch.setattr(executor_module, "create_agent", fake_create_agent)
monkeypatch.setitem(
sys.modules,
"deerflow.agents.middlewares.tool_error_handling_middleware",
_module(
"deerflow.agents.middlewares.tool_error_handling_middleware",
build_subagent_runtime_middlewares=fake_build_subagent_runtime_middlewares,
),
)
executor = SubagentExecutor(
config=base_config,
tools=[],
app_config=app_config,
parent_model="parent-model",
)
result = executor._create_agent()
assert result is agent
assert captured["model"] == {
"name": "parent-model",
"thinking_enabled": False,
"app_config": app_config,
}
assert captured["middlewares"] == {
"app_config": app_config,
"lazy_init": True,
}
assert captured["agent"]["model"] is model
assert captured["agent"]["middleware"] is middlewares
assert captured["agent"]["tools"] == []
assert captured["agent"]["system_prompt"] == base_config.system_prompt
# -----------------------------------------------------------------------------
# Async Execution Path Tests
# -----------------------------------------------------------------------------