mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-23 16:35:59 +00:00
refactor: thread app_config through lead and subagent task path (#2666)
* refactor: thread app config through lead prompt * fix: honor explicit app config across runtime paths * style: format subagent executor tests * fix: thread resolved app config and guard subagents-only fallback Address two PR review findings: 1. _create_summarization_middleware passed the original (possibly None) app_config into create_chat_model, forcing the model factory back to ambient get_app_config() and risking config drift between the middleware's resolved view and the model's view. Pass the resolved AppConfig instance through end-to-end. 2. get_available_subagent_names accepted Any-typed config and forwarded it to is_host_bash_allowed, which reads ``.sandbox``. A SubagentsAppConfig (also accepted upstream as a sum-type input) has no ``.sandbox`` attribute and would be silently treated as "no sandbox configured", incorrectly disabling the bash subagent. Guard on hasattr and fall back to ambient lookup otherwise. Adds regression tests for both paths. * chore: simplify hasattr guard and tighten regression tests - Collapse if/else into ternary in get_available_subagent_names; hasattr(None, ...) is False so the explicit None check was redundant. - Drop comments that narrate the change rather than explain non-obvious WHY (test names already convey intent). - Replace stringly-typed sentinel "no-arg" in regression test with direct args tuple comparison. --------- Co-authored-by: greatmengqi <chenmengqi.0376@bytedance.com>
This commit is contained in:
@@ -168,6 +168,8 @@ def _get_isolated_subagent_loop() -> asyncio.AbstractEventLoop:
|
||||
_isolated_subagent_loop_thread = thread
|
||||
_isolated_subagent_loop_started = started_event
|
||||
|
||||
if _isolated_subagent_loop is None:
|
||||
raise RuntimeError("Isolated subagent event loop is not initialized")
|
||||
return _isolated_subagent_loop
|
||||
|
||||
|
||||
@@ -308,8 +310,10 @@ class SubagentExecutor:
|
||||
try:
|
||||
from deerflow.skills.storage import get_or_new_skill_storage
|
||||
|
||||
storage_kwargs = {"app_config": self.app_config} if self.app_config is not None else {}
|
||||
storage = await asyncio.to_thread(get_or_new_skill_storage, **storage_kwargs)
|
||||
# Use asyncio.to_thread to avoid blocking the event loop (LangGraph ASGI requirement)
|
||||
all_skills = await asyncio.to_thread(get_or_new_skill_storage().load_skills, enabled_only=True)
|
||||
all_skills = await asyncio.to_thread(storage.load_skills, enabled_only=True)
|
||||
logger.info(f"[trace={self.trace_id}] Subagent {self.config.name} loaded {len(all_skills)} enabled skills from disk")
|
||||
except Exception:
|
||||
logger.warning(f"[trace={self.trace_id}] Failed to load skills for subagent {self.config.name}", exc_info=True)
|
||||
@@ -395,6 +399,10 @@ class SubagentExecutor:
|
||||
status=SubagentStatus.RUNNING,
|
||||
started_at=datetime.now(),
|
||||
)
|
||||
ai_messages = result.ai_messages
|
||||
if ai_messages is None:
|
||||
ai_messages = []
|
||||
result.ai_messages = ai_messages
|
||||
|
||||
try:
|
||||
agent = self._create_agent()
|
||||
@@ -404,10 +412,12 @@ class SubagentExecutor:
|
||||
run_config: RunnableConfig = {
|
||||
"recursion_limit": self.config.max_turns,
|
||||
}
|
||||
context = {}
|
||||
context: dict[str, Any] = {}
|
||||
if self.thread_id:
|
||||
run_config["configurable"] = {"thread_id": self.thread_id}
|
||||
context["thread_id"] = self.thread_id
|
||||
if self.app_config is not None:
|
||||
context["app_config"] = self.app_config
|
||||
|
||||
logger.info(f"[trace={self.trace_id}] Subagent {self.config.name} starting async execution with max_turns={self.config.max_turns}")
|
||||
|
||||
@@ -454,13 +464,13 @@ class SubagentExecutor:
|
||||
message_id = message_dict.get("id")
|
||||
is_duplicate = False
|
||||
if message_id:
|
||||
is_duplicate = any(msg.get("id") == message_id for msg in result.ai_messages)
|
||||
is_duplicate = any(msg.get("id") == message_id for msg in ai_messages)
|
||||
else:
|
||||
is_duplicate = message_dict in result.ai_messages
|
||||
is_duplicate = message_dict in ai_messages
|
||||
|
||||
if not is_duplicate:
|
||||
result.ai_messages.append(message_dict)
|
||||
logger.info(f"[trace={self.trace_id}] Subagent {self.config.name} captured AI message #{len(result.ai_messages)}")
|
||||
ai_messages.append(message_dict)
|
||||
logger.info(f"[trace={self.trace_id}] Subagent {self.config.name} captured AI message #{len(ai_messages)}")
|
||||
|
||||
logger.info(f"[trace={self.trace_id}] Subagent {self.config.name} completed async execution")
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import logging
|
||||
from dataclasses import replace
|
||||
from typing import Any
|
||||
|
||||
from deerflow.sandbox.security import is_host_bash_allowed
|
||||
from deerflow.subagents.builtins import BUILTIN_SUBAGENTS
|
||||
@@ -10,19 +11,26 @@ from deerflow.subagents.config import SubagentConfig
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _build_custom_subagent_config(name: str) -> SubagentConfig | None:
|
||||
def _resolve_subagents_app_config(app_config: Any | None = None):
|
||||
if app_config is None:
|
||||
from deerflow.config.subagents_config import get_subagents_app_config
|
||||
|
||||
return get_subagents_app_config()
|
||||
return getattr(app_config, "subagents", app_config)
|
||||
|
||||
|
||||
def _build_custom_subagent_config(name: str, *, app_config: Any | None = None) -> SubagentConfig | None:
|
||||
"""Build a SubagentConfig from config.yaml custom_agents section.
|
||||
|
||||
Args:
|
||||
name: The name of the custom subagent.
|
||||
app_config: Optional AppConfig or SubagentsAppConfig to resolve from.
|
||||
|
||||
Returns:
|
||||
SubagentConfig if found in custom_agents, None otherwise.
|
||||
"""
|
||||
from deerflow.config.subagents_config import get_subagents_app_config
|
||||
|
||||
app_config = get_subagents_app_config()
|
||||
custom = app_config.custom_agents.get(name)
|
||||
subagents_config = _resolve_subagents_app_config(app_config)
|
||||
custom = subagents_config.custom_agents.get(name)
|
||||
if custom is None:
|
||||
return None
|
||||
|
||||
@@ -39,7 +47,7 @@ def _build_custom_subagent_config(name: str) -> SubagentConfig | None:
|
||||
)
|
||||
|
||||
|
||||
def get_subagent_config(name: str) -> SubagentConfig | None:
|
||||
def get_subagent_config(name: str, *, app_config: Any | None = None) -> SubagentConfig | None:
|
||||
"""Get a subagent configuration by name, with config.yaml overrides applied.
|
||||
|
||||
Resolution order (mirrors Codex's config layering):
|
||||
@@ -49,6 +57,7 @@ def get_subagent_config(name: str) -> SubagentConfig | None:
|
||||
|
||||
Args:
|
||||
name: The name of the subagent.
|
||||
app_config: Optional AppConfig or SubagentsAppConfig to resolve overrides from.
|
||||
|
||||
Returns:
|
||||
SubagentConfig if found (with any config.yaml overrides applied), None otherwise.
|
||||
@@ -56,7 +65,7 @@ def get_subagent_config(name: str) -> SubagentConfig | None:
|
||||
# Step 1: Look up built-in, then fall back to custom_agents
|
||||
config = BUILTIN_SUBAGENTS.get(name)
|
||||
if config is None:
|
||||
config = _build_custom_subagent_config(name)
|
||||
config = _build_custom_subagent_config(name, app_config=app_config)
|
||||
if config is None:
|
||||
return None
|
||||
|
||||
@@ -65,12 +74,9 @@ def get_subagent_config(name: str) -> SubagentConfig | None:
|
||||
# (timeout_seconds, max_turns at the top level) apply to built-in agents
|
||||
# but must NOT override custom agents' own values — custom agents define
|
||||
# their own defaults in the custom_agents section.
|
||||
# Lazy import to avoid circular deps.
|
||||
from deerflow.config.subagents_config import get_subagents_app_config
|
||||
|
||||
app_config = get_subagents_app_config()
|
||||
subagents_config = _resolve_subagents_app_config(app_config)
|
||||
is_builtin = name in BUILTIN_SUBAGENTS
|
||||
agent_override = app_config.agents.get(name)
|
||||
agent_override = subagents_config.agents.get(name)
|
||||
|
||||
overrides = {}
|
||||
|
||||
@@ -79,27 +85,27 @@ def get_subagent_config(name: str) -> SubagentConfig | None:
|
||||
if agent_override.timeout_seconds != config.timeout_seconds:
|
||||
logger.debug("Subagent '%s': timeout overridden (%ss -> %ss)", name, config.timeout_seconds, agent_override.timeout_seconds)
|
||||
overrides["timeout_seconds"] = agent_override.timeout_seconds
|
||||
elif is_builtin and app_config.timeout_seconds != config.timeout_seconds:
|
||||
logger.debug("Subagent '%s': timeout from global default (%ss -> %ss)", name, config.timeout_seconds, app_config.timeout_seconds)
|
||||
overrides["timeout_seconds"] = app_config.timeout_seconds
|
||||
elif is_builtin and subagents_config.timeout_seconds != config.timeout_seconds:
|
||||
logger.debug("Subagent '%s': timeout from global default (%ss -> %ss)", name, config.timeout_seconds, subagents_config.timeout_seconds)
|
||||
overrides["timeout_seconds"] = subagents_config.timeout_seconds
|
||||
|
||||
# Max turns: per-agent override > global default (builtins only) > config's own value
|
||||
if agent_override is not None and agent_override.max_turns is not None:
|
||||
if agent_override.max_turns != config.max_turns:
|
||||
logger.debug("Subagent '%s': max_turns overridden (%s -> %s)", name, config.max_turns, agent_override.max_turns)
|
||||
overrides["max_turns"] = agent_override.max_turns
|
||||
elif is_builtin and app_config.max_turns is not None and app_config.max_turns != config.max_turns:
|
||||
logger.debug("Subagent '%s': max_turns from global default (%s -> %s)", name, config.max_turns, app_config.max_turns)
|
||||
overrides["max_turns"] = app_config.max_turns
|
||||
elif is_builtin and subagents_config.max_turns is not None and subagents_config.max_turns != config.max_turns:
|
||||
logger.debug("Subagent '%s': max_turns from global default (%s -> %s)", name, config.max_turns, subagents_config.max_turns)
|
||||
overrides["max_turns"] = subagents_config.max_turns
|
||||
|
||||
# Model: per-agent override only (no global default for model)
|
||||
effective_model = app_config.get_model_for(name)
|
||||
effective_model = subagents_config.get_model_for(name)
|
||||
if effective_model is not None and effective_model != config.model:
|
||||
logger.debug("Subagent '%s': model overridden (%s -> %s)", name, config.model, effective_model)
|
||||
overrides["model"] = effective_model
|
||||
|
||||
# Skills: per-agent override only (no global default for skills)
|
||||
effective_skills = app_config.get_skills_for(name)
|
||||
effective_skills = subagents_config.get_skills_for(name)
|
||||
if effective_skills is not None and effective_skills != config.skills:
|
||||
logger.debug("Subagent '%s': skills overridden (%s -> %s)", name, config.skills, effective_skills)
|
||||
overrides["skills"] = effective_skills
|
||||
@@ -110,21 +116,21 @@ def get_subagent_config(name: str) -> SubagentConfig | None:
|
||||
return config
|
||||
|
||||
|
||||
def list_subagents() -> list[SubagentConfig]:
|
||||
def list_subagents(*, app_config: Any | None = None) -> list[SubagentConfig]:
|
||||
"""List all available subagent configurations (with config.yaml overrides applied).
|
||||
|
||||
Returns:
|
||||
List of all registered SubagentConfig instances (built-in + custom).
|
||||
"""
|
||||
configs = []
|
||||
for name in get_subagent_names():
|
||||
config = get_subagent_config(name)
|
||||
for name in get_subagent_names(app_config=app_config):
|
||||
config = get_subagent_config(name, app_config=app_config)
|
||||
if config is not None:
|
||||
configs.append(config)
|
||||
return configs
|
||||
|
||||
|
||||
def get_subagent_names() -> list[str]:
|
||||
def get_subagent_names(*, app_config: Any | None = None) -> list[str]:
|
||||
"""Get all available subagent names (built-in + custom).
|
||||
|
||||
Returns:
|
||||
@@ -133,25 +139,23 @@ def get_subagent_names() -> list[str]:
|
||||
names = list(BUILTIN_SUBAGENTS.keys())
|
||||
|
||||
# Merge custom_agents from config.yaml
|
||||
from deerflow.config.subagents_config import get_subagents_app_config
|
||||
|
||||
app_config = get_subagents_app_config()
|
||||
for custom_name in app_config.custom_agents:
|
||||
subagents_config = _resolve_subagents_app_config(app_config)
|
||||
for custom_name in subagents_config.custom_agents:
|
||||
if custom_name not in names:
|
||||
names.append(custom_name)
|
||||
|
||||
return names
|
||||
|
||||
|
||||
def get_available_subagent_names() -> list[str]:
|
||||
def get_available_subagent_names(*, app_config: Any | None = None) -> 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 = get_subagent_names()
|
||||
names = get_subagent_names(app_config=app_config)
|
||||
try:
|
||||
host_bash_allowed = is_host_bash_allowed()
|
||||
host_bash_allowed = is_host_bash_allowed(app_config) if hasattr(app_config, "sandbox") else is_host_bash_allowed()
|
||||
except Exception:
|
||||
logger.debug("Could not determine host bash availability; exposing all subagents")
|
||||
return names
|
||||
|
||||
Reference in New Issue
Block a user