From 3e1349576eecc949d73149c731c9cf0dadf2f2a9 Mon Sep 17 00:00:00 2001 From: kbsama <1747687484@qq.com> Date: Thu, 10 Sep 2026 14:51:42 +0800 Subject: [PATCH] fix(agents): filter assembly descriptor subagent policy by allowed_subagents (#5205) (#5262) * fix(agents): filter assembly descriptor subagent policy by allowed_subagents (#5205) * test(agents): ensure non-vacuous subagent catalog in assembly descriptor test --------- Co-authored-by: 1747687484-collab <229902011+1747687484-collab@users.noreply.github.com> --- .../deerflow/agents/lead_agent/agent.py | 5 +++- .../tests/test_agent_assembly_descriptor.py | 30 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/backend/packages/harness/deerflow/agents/lead_agent/agent.py b/backend/packages/harness/deerflow/agents/lead_agent/agent.py index 1db5801aa..013327dd4 100644 --- a/backend/packages/harness/deerflow/agents/lead_agent/agent.py +++ b/backend/packages/harness/deerflow/agents/lead_agent/agent.py @@ -128,6 +128,7 @@ def _subagent_release_policy( enabled: bool, max_concurrent: int, max_total: int, + allowed_subagents: list[str] | None = None, ) -> dict[str, object]: """Delegation limits as the run will actually enforce them. @@ -147,7 +148,7 @@ def _subagent_release_policy( from deerflow.subagents import get_available_subagent_names, get_subagent_config - type_allowlist = sorted(set(get_available_subagent_names(app_config=app_config))) + type_allowlist = sorted(set(get_available_subagent_names(app_config=app_config, allowed_subagents=allowed_subagents))) runtime_limits: dict[str, object] = {} for name in type_allowlist: subagent_config = get_subagent_config(name, app_config=app_config) @@ -1092,6 +1093,7 @@ def _assemble_lead_agent(config: RunnableConfig, *, app_config: AppConfig) -> Le enabled=subagent_enabled, max_concurrent=max_concurrent_subagents, max_total=max_total_subagents, + allowed_subagents=allowed_subagents, ), "deferred_tools": { "enabled": resolved_app_config.tool_search.enabled, @@ -1211,6 +1213,7 @@ def _assemble_lead_agent(config: RunnableConfig, *, app_config: AppConfig) -> Le enabled=subagent_enabled, max_concurrent=max_concurrent_subagents, max_total=max_total_subagents, + allowed_subagents=allowed_subagents, ), "deferred_tools": { "enabled": resolved_app_config.tool_search.enabled, diff --git a/backend/tests/test_agent_assembly_descriptor.py b/backend/tests/test_agent_assembly_descriptor.py index a71303e44..7f8aed24b 100644 --- a/backend/tests/test_agent_assembly_descriptor.py +++ b/backend/tests/test_agent_assembly_descriptor.py @@ -126,6 +126,7 @@ class TestLeadAgentAssembly: from deerflow.config.app_config import AppConfig from deerflow.config.model_config import ModelConfig from deerflow.config.sandbox_config import SandboxConfig + from deerflow.config.subagents_config import CustomSubagentConfig, SubagentsAppConfig app_config = AppConfig( models=[ @@ -139,6 +140,7 @@ class TestLeadAgentAssembly: supports_vision=False, ) ], + subagents=SubagentsAppConfig(custom_agents={"researcher": CustomSubagentConfig(description="research", system_prompt="research")}), sandbox=SandboxConfig(use="deerflow.sandbox.local:LocalSandboxProvider"), ) monkeypatch.setattr(lead_agent_module, "get_app_config", lambda: app_config) @@ -216,6 +218,34 @@ class TestLeadAgentAssembly: assert assembly.graph["system_prompt"] == "allowed_subagents=['general-purpose']" assert assembly.descriptor.base_prompt_hash == canonical_hash(assembly.graph["system_prompt"]) + def test_descriptor_subagent_policy_respects_custom_agent_allowed_subagents(self, monkeypatch): + """Fixes #5205: custom agent assembly descriptor must restrict its + subagents policy allowlist and runtime limits to allowed_subagents.""" + from deerflow.agents.lead_agent import agent as lead_agent_module + from deerflow.agents.lead_agent.agent import assemble_lead_agent + from deerflow.config.agents_config import AgentConfig + from deerflow.extensions import bind_agent_build_extensions + + self._isolate_from_the_ambient_config(monkeypatch) + agent_config = AgentConfig(name="custom", allowed_subagents=["general-purpose"]) + monkeypatch.setattr(lead_agent_module, "load_agent_config", lambda name, *, user_id=None: agent_config) + + with bind_agent_build_extensions(self._extensions_with_an_agent_assembly_observer()): + assembly = assemble_lead_agent( + { + "configurable": { + "thread_id": "t-scoped-subagents", + "agent_name": "custom", + "subagent_enabled": True, + } + } + ) + + subagent_policy = assembly.descriptor.effective_policies["subagents"] + assert subagent_policy["enabled"] is True + assert subagent_policy["type_allowlist"] == ["general-purpose"] + assert list(subagent_policy["runtime_limits"].keys()) == ["general-purpose"] + def test_observers_receive_the_descriptor(self, monkeypatch): from deerflow.agents.lead_agent.agent import assemble_lead_agent from deerflow.extensions import bind_agent_build_extensions