mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-11 14:38:38 +00:00
* feat: manage and scope subagents * fix: address subagent review feedback * fix: address managed subagent review feedback * fix: harden subagent settings semantics * fix: harden managed subagent cache invalidation * fix: reuse assembled lead agent inputs * fix: migrate managed subagent definitions --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
188 lines
6.9 KiB
Python
188 lines
6.9 KiB
Python
"""Runtime precedence and caller filtering for managed subagents."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
|
|
from deerflow.config.subagents_config import CustomSubagentConfig, SubagentOverrideConfig, SubagentsAppConfig
|
|
from deerflow.persistence.managed_subagents import ManagedSubagentDefinition
|
|
from deerflow.subagents import registry
|
|
|
|
|
|
def _managed(name: str, *, enabled: bool = True) -> ManagedSubagentDefinition:
|
|
return ManagedSubagentDefinition(
|
|
name=name,
|
|
description=f"Managed {name}",
|
|
system_prompt=f"You are {name}.",
|
|
enabled=enabled,
|
|
)
|
|
|
|
|
|
def test_enabled_managed_definitions_join_runtime_catalog(monkeypatch):
|
|
monkeypatch.setattr(registry, "_managed_definitions", lambda **_: [_managed("planner"), _managed("disabled", enabled=False)])
|
|
config = SubagentsAppConfig()
|
|
|
|
assert "planner" in registry.get_subagent_names(app_config=config)
|
|
assert "disabled" not in registry.get_subagent_names(app_config=config)
|
|
assert registry.get_subagent_config("planner", app_config=config).system_prompt == "You are planner."
|
|
|
|
|
|
def test_default_lead_catalog_preserves_builtin_defaults(monkeypatch):
|
|
monkeypatch.setattr(registry, "_managed_definitions", lambda **_: [_managed("planner")])
|
|
config = SubagentsAppConfig()
|
|
|
|
assert registry.get_subagent_names(app_config=config) == ["general-purpose", "bash", "planner"]
|
|
|
|
general = registry.get_subagent_config("general-purpose", app_config=config)
|
|
assert general is not None
|
|
assert general.tools is None
|
|
assert set(general.disallowed_tools or []) == {"task", "ask_clarification", "present_files"}
|
|
assert general.model == "inherit"
|
|
assert general.max_turns == 150
|
|
assert general.timeout_seconds == 1800
|
|
|
|
bash = registry.get_subagent_config("bash", app_config=config)
|
|
assert bash is not None
|
|
assert bash.tools == ["bash", "ls", "read_file", "write_file", "str_replace"]
|
|
assert set(bash.disallowed_tools or []) == {"task", "ask_clarification", "present_files"}
|
|
assert bash.model == "inherit"
|
|
assert bash.max_turns == 60
|
|
assert bash.timeout_seconds == 1800
|
|
|
|
|
|
def test_builtin_and_config_definitions_win_name_conflicts(monkeypatch):
|
|
monkeypatch.setattr(registry, "_managed_definitions", lambda **_: [_managed("general-purpose"), _managed("reviewer")])
|
|
config = SubagentsAppConfig(
|
|
custom_agents={
|
|
"reviewer": CustomSubagentConfig(description="Config reviewer", system_prompt="Config wins."),
|
|
}
|
|
)
|
|
|
|
names = registry.get_subagent_names(app_config=config)
|
|
assert names.count("general-purpose") == 1
|
|
assert names.count("reviewer") == 1
|
|
assert registry.get_subagent_config("reviewer", app_config=config).system_prompt == "Config wins."
|
|
|
|
|
|
def test_allowed_subagents_is_a_hard_runtime_filter(monkeypatch):
|
|
monkeypatch.setattr(registry, "_managed_definitions", lambda **_: [_managed("planner"), _managed("writer")])
|
|
config = SubagentsAppConfig()
|
|
|
|
assert registry.get_subagent_names(app_config=config, allowed_subagents=[]) == []
|
|
assert registry.get_subagent_names(app_config=config, allowed_subagents=["planner"]) == ["planner"]
|
|
|
|
|
|
def test_config_yaml_overrides_remain_explicitly_higher_priority(monkeypatch):
|
|
monkeypatch.setattr(registry, "_managed_definitions", lambda **_: [_managed("planner")])
|
|
config = SubagentsAppConfig(
|
|
agents={"planner": SubagentOverrideConfig(model="configured-model", max_turns=12)},
|
|
)
|
|
|
|
resolved = registry.get_subagent_config("planner", app_config=config)
|
|
assert resolved.model == "configured-model"
|
|
assert resolved.max_turns == 12
|
|
|
|
|
|
def test_managed_definitions_cache_reuses_and_invalidates_store_snapshot(monkeypatch):
|
|
class FakeStore:
|
|
def __init__(self):
|
|
self.revision = 1
|
|
self.definitions = [_managed("planner")]
|
|
self.signature_calls = 0
|
|
self.list_calls = 0
|
|
|
|
def signature(self):
|
|
self.signature_calls += 1
|
|
return self.revision
|
|
|
|
def cache_identity(self):
|
|
return "fake-managed-subagent-store"
|
|
|
|
def list(self):
|
|
self.list_calls += 1
|
|
return self.definitions
|
|
|
|
store = FakeStore()
|
|
config = SubagentsAppConfig()
|
|
now = [100.0]
|
|
registry._clear_managed_definitions_cache()
|
|
monkeypatch.setattr(registry, "get_managed_subagent_store", lambda *_: store)
|
|
monkeypatch.setattr(time, "monotonic", lambda: now[0])
|
|
|
|
assert "planner" in registry.get_subagent_names(app_config=config)
|
|
assert registry.get_subagent_config("planner", app_config=config).description == "Managed planner"
|
|
assert store.signature_calls == 1
|
|
assert store.list_calls == 1
|
|
|
|
store.revision = 2
|
|
store.definitions = [_managed("writer")]
|
|
now[0] += registry._MANAGED_SIGNATURE_TTL_SECONDS
|
|
|
|
assert "writer" in registry.get_subagent_names(app_config=config)
|
|
assert "planner" not in registry.get_subagent_names(app_config=config)
|
|
assert store.signature_calls == 2
|
|
assert store.list_calls == 2
|
|
|
|
|
|
def test_list_subagents_checks_managed_signature_once_per_ttl_window(monkeypatch):
|
|
class FakeStore:
|
|
def __init__(self):
|
|
self.signature_calls = 0
|
|
self.list_calls = 0
|
|
self.definitions = [_managed(f"worker-{index}") for index in range(25)]
|
|
|
|
def signature(self):
|
|
self.signature_calls += 1
|
|
return 1
|
|
|
|
def cache_identity(self):
|
|
return "signature-ttl-managed-subagent-store"
|
|
|
|
def list(self):
|
|
self.list_calls += 1
|
|
return self.definitions
|
|
|
|
store = FakeStore()
|
|
config = SubagentsAppConfig()
|
|
registry._clear_managed_definitions_cache()
|
|
monkeypatch.setattr(registry, "get_managed_subagent_store", lambda *_: store)
|
|
monkeypatch.setattr(time, "monotonic", lambda: 100.0)
|
|
|
|
configs = registry.list_subagents(app_config=config)
|
|
|
|
assert len(configs) == 27
|
|
assert store.signature_calls == 1
|
|
assert store.list_calls == 1
|
|
|
|
|
|
def test_managed_definitions_cache_serializes_concurrent_first_load(monkeypatch):
|
|
class FakeStore:
|
|
def __init__(self):
|
|
self.signature_calls = 0
|
|
self.list_calls = 0
|
|
|
|
def signature(self):
|
|
self.signature_calls += 1
|
|
return 1
|
|
|
|
def cache_identity(self):
|
|
return "concurrent-managed-subagent-store"
|
|
|
|
def list(self):
|
|
self.list_calls += 1
|
|
return [_managed("planner")]
|
|
|
|
store = FakeStore()
|
|
config = SubagentsAppConfig()
|
|
registry._clear_managed_definitions_cache()
|
|
monkeypatch.setattr(registry, "get_managed_subagent_store", lambda *_: store)
|
|
monkeypatch.setattr(time, "monotonic", lambda: 100.0)
|
|
|
|
with ThreadPoolExecutor(max_workers=8) as pool:
|
|
results = list(pool.map(lambda _: registry.get_subagent_names(app_config=config), range(16)))
|
|
|
|
assert all("planner" in names for names in results)
|
|
assert store.signature_calls == 1
|
|
assert store.list_calls == 1
|