ac04f2704f
* feat(subagents): allow model override per subagent in config.yaml Wire the existing SubagentConfig.model field to config.yaml so users can assign different models to different subagent types. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(subagents): cover model override in SubagentsAppConfig + registry Addresses review feedback on #2064: - registry.py: update stale inline comment — the block now applies timeout, max_turns AND model overrides, not just timeout. - test_subagent_timeout_config.py: add coverage for model override resolution across SubagentOverrideConfig, SubagentsAppConfig (get_model_for + load), and registry.get_subagent_config: - per-agent model override is applied to registry-returned config - omitted `model` keeps the builtin value - explicit `model: null` in config.yaml is equivalent to omission - model override on one agent does not affect other agents - model override preserves all other fields (name, description, timeout_seconds, max_turns) - model override does not mutate BUILTIN_SUBAGENTS Copilot's suggestion (3) "setting model to 'inherit' forces inheritance" is skipped intentionally: there is no 'inherit' sentinel in the current implementation — model is `str | None`, and None already means "inherit from parent". Adding a sentinel would be a new feature, not test coverage for this PR. Tests run locally: 51 passed (37 existing + 14 new / expanded). * test(subagents): reject empty-string model at config load time Addresses WillemJiang's review comment on #2064 (empty-string edge case): - subagents_config.py: add `min_length=1` to the `model` field on SubagentOverrideConfig. `model: ""` in config.yaml would otherwise bypass the `is not None` check and reach create_chat_model(name="") as a confusing runtime error. This is symmetric with the existing `ge=1` guards on timeout_seconds / max_turns, so the validation style stays consistent across all three override fields. - test_subagent_timeout_config.py: add test_rejects_empty_model mirroring the existing test_rejects_zero / test_rejects_negative cases; update the docstring on test_model_accepts_any_string (now test_model_accepts_any_non_empty_string) to reflect the new guard. Not addressing the first comment (validating `model` against the `models:` section at load time) in this PR. `SubagentsAppConfig` is scoped to the `subagents:` block and cannot see the sibling `models:` section, so proper cross-section validation needs a second pass or a structural change that is out of scope here — and the current behavior is consistent with how timeout_seconds / max_turns work today. Happy to track this as a follow-up issue covering cross-section validation uniformly for all three fields. Tests run locally: 52 passed in this file; 1847 passed, 18 skipped across the full backend suite. Ruff check + format clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
100 lines
3.1 KiB
Python
100 lines
3.1 KiB
Python
"""Subagent registry for managing available subagents."""
|
|
|
|
import logging
|
|
from dataclasses import replace
|
|
|
|
from deerflow.sandbox.security import is_host_bash_allowed
|
|
from deerflow.subagents.builtins import BUILTIN_SUBAGENTS
|
|
from deerflow.subagents.config import SubagentConfig
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def get_subagent_config(name: str) -> SubagentConfig | None:
|
|
"""Get a subagent configuration by name, with config.yaml overrides applied.
|
|
|
|
Args:
|
|
name: The name of the subagent.
|
|
|
|
Returns:
|
|
SubagentConfig if found (with any config.yaml overrides applied), None otherwise.
|
|
"""
|
|
config = BUILTIN_SUBAGENTS.get(name)
|
|
if config is None:
|
|
return None
|
|
|
|
# Apply runtime overrides (timeout, max_turns, model) from config.yaml
|
|
# Lazy import to avoid circular deps.
|
|
from deerflow.config.subagents_config import get_subagents_app_config
|
|
|
|
app_config = get_subagents_app_config()
|
|
effective_timeout = app_config.get_timeout_for(name)
|
|
effective_max_turns = app_config.get_max_turns_for(name, config.max_turns)
|
|
|
|
overrides = {}
|
|
if effective_timeout != config.timeout_seconds:
|
|
logger.debug(
|
|
"Subagent '%s': timeout overridden by config.yaml (%ss -> %ss)",
|
|
name,
|
|
config.timeout_seconds,
|
|
effective_timeout,
|
|
)
|
|
overrides["timeout_seconds"] = effective_timeout
|
|
if effective_max_turns != config.max_turns:
|
|
logger.debug(
|
|
"Subagent '%s': max_turns overridden by config.yaml (%s -> %s)",
|
|
name,
|
|
config.max_turns,
|
|
effective_max_turns,
|
|
)
|
|
overrides["max_turns"] = effective_max_turns
|
|
effective_model = app_config.get_model_for(name)
|
|
if effective_model is not None and effective_model != config.model:
|
|
logger.debug(
|
|
"Subagent '%s': model overridden by config.yaml (%s -> %s)",
|
|
name,
|
|
config.model,
|
|
effective_model,
|
|
)
|
|
overrides["model"] = effective_model
|
|
if overrides:
|
|
config = replace(config, **overrides)
|
|
|
|
return config
|
|
|
|
|
|
def list_subagents() -> list[SubagentConfig]:
|
|
"""List all available subagent configurations (with config.yaml overrides applied).
|
|
|
|
Returns:
|
|
List of all registered SubagentConfig instances.
|
|
"""
|
|
return [get_subagent_config(name) for name in BUILTIN_SUBAGENTS]
|
|
|
|
|
|
def get_subagent_names() -> list[str]:
|
|
"""Get all available subagent names.
|
|
|
|
Returns:
|
|
List of subagent names.
|
|
"""
|
|
return list(BUILTIN_SUBAGENTS.keys())
|
|
|
|
|
|
def get_available_subagent_names() -> list[str]:
|
|
"""Get subagent names that should be exposed to the active runtime.
|
|
|
|
Returns:
|
|
List of subagent names visible to the current sandbox configuration.
|
|
"""
|
|
names = list(BUILTIN_SUBAGENTS.keys())
|
|
try:
|
|
host_bash_allowed = is_host_bash_allowed()
|
|
except Exception:
|
|
logger.debug("Could not determine host bash availability; exposing all built-in subagents")
|
|
return names
|
|
|
|
if not host_bash_allowed:
|
|
names = [name for name in names if name != "bash"]
|
|
return names
|