diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/agents/lead_agent/agent.py b/offline-backend-20260512/backend/packages/harness/deerflow/agents/lead_agent/agent.py index ae1398fe..20bebfcc 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/agents/lead_agent/agent.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/agents/lead_agent/agent.py @@ -321,6 +321,13 @@ def _build_middlewares( max_concurrent_subagents = cfg.get("max_concurrent_subagents", 3) middlewares.append(SubagentLimitMiddleware(max_concurrent=max_concurrent_subagents)) + # SkillReviewMiddleware — trigger background skill review after each turn. + # Gated by skill_evolution.enabled + skill_evolution.review_worker.enabled. + if resolved_app_config.skill_evolution.enabled and resolved_app_config.skill_evolution.review_worker.enabled: + from deerflow.agents.middlewares.skill_review_middleware import SkillReviewMiddleware + + middlewares.append(SkillReviewMiddleware()) + # LoopDetectionMiddleware — detect and break repetitive tool call loops middlewares.append(LoopDetectionMiddleware()) diff --git a/offline-backend-20260512/backend/tests/test_skill_review_middleware.py b/offline-backend-20260512/backend/tests/test_skill_review_middleware.py index a6281889..9b6f0c88 100644 --- a/offline-backend-20260512/backend/tests/test_skill_review_middleware.py +++ b/offline-backend-20260512/backend/tests/test_skill_review_middleware.py @@ -68,3 +68,45 @@ async def test_after_agent_exception_is_swallowed(): with patch("deerflow.agents.middlewares.skill_review_middleware.get_app_config", side_effect=RuntimeError("boom")): # Must not raise await middleware.after_agent(state, runtime) + + +def test_skill_review_middleware_in_chain_when_enabled(): + """SkillReviewMiddleware appears in middleware chain before LoopDetectionMiddleware when enabled.""" + from unittest.mock import patch, MagicMock + from deerflow.agents.lead_agent.agent import _build_middlewares + from deerflow.agents.middlewares.skill_review_middleware import SkillReviewMiddleware + + config = { + "configurable": { + "thread_id": "test", + "is_plan_mode": False, + "is_bootstrap": False, + "is_scheduled_run": False, + } + } + + app_cfg = MagicMock() + app_cfg.skill_evolution.enabled = True + app_cfg.skill_evolution.review_worker.enabled = True + app_cfg.token_usage.enabled = False + app_cfg.tool_search.enabled = False + app_cfg.skill_stop.enabled = False + app_cfg.summarization.enabled = False + app_cfg.memory = MagicMock() + app_cfg.guardrails.enabled = False + app_cfg.title = MagicMock() + app_cfg.title.model_name = None + + with patch("deerflow.agents.lead_agent.agent.get_app_config", return_value=app_cfg): + with patch("deerflow.agents.lead_agent.agent.build_lead_runtime_middlewares", return_value=[]): + with patch("deerflow.agents.lead_agent.agent._create_summarization_middleware", return_value=None): + with patch("deerflow.agents.lead_agent.agent._create_todo_list_middleware", return_value=None): + with patch("deerflow.models.vision.vision_enabled", return_value=False): + middlewares = _build_middlewares(config, model_name=None, app_config=app_cfg) + + type_names = [type(m).__name__ for m in middlewares] + assert "SkillReviewMiddleware" in type_names, f"SkillReviewMiddleware not in chain: {type_names}" + + review_idx = type_names.index("SkillReviewMiddleware") + loop_idx = type_names.index("LoopDetectionMiddleware") + assert review_idx < loop_idx, f"SkillReviewMiddleware ({review_idx}) must be before LoopDetectionMiddleware ({loop_idx})"