mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-23 16:35:59 +00:00
fix: inherit subagent skill allowlists (#2514)
This commit is contained in:
@@ -350,6 +350,7 @@ def make_lead_agent(config: RunnableConfig):
|
|||||||
"is_plan_mode": is_plan_mode,
|
"is_plan_mode": is_plan_mode,
|
||||||
"subagent_enabled": subagent_enabled,
|
"subagent_enabled": subagent_enabled,
|
||||||
"tool_groups": agent_config.tool_groups if agent_config else None,
|
"tool_groups": agent_config.tool_groups if agent_config else None,
|
||||||
|
"available_skills": ["bootstrap"] if is_bootstrap else (agent_config.skills if agent_config and agent_config.skills is not None else None),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,17 @@ from deerflow.subagents.executor import SubagentStatus, cleanup_background_task,
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_skill_allowlists(parent: list[str] | None, child: list[str] | None) -> list[str] | None:
|
||||||
|
"""Return the effective subagent skill allowlist under the parent policy."""
|
||||||
|
if parent is None:
|
||||||
|
return child
|
||||||
|
if child is None:
|
||||||
|
return list(parent)
|
||||||
|
|
||||||
|
parent_set = set(parent)
|
||||||
|
return [skill for skill in child if skill in parent_set]
|
||||||
|
|
||||||
|
|
||||||
@tool("task", parse_docstring=True)
|
@tool("task", parse_docstring=True)
|
||||||
async def task_tool(
|
async def task_tool(
|
||||||
runtime: ToolRuntime[ContextT, ThreadState],
|
runtime: ToolRuntime[ContextT, ThreadState],
|
||||||
@@ -83,9 +94,6 @@ async def task_tool(
|
|||||||
if max_turns is not None:
|
if max_turns is not None:
|
||||||
overrides["max_turns"] = max_turns
|
overrides["max_turns"] = max_turns
|
||||||
|
|
||||||
if overrides:
|
|
||||||
config = replace(config, **overrides)
|
|
||||||
|
|
||||||
# Extract parent context from runtime
|
# Extract parent context from runtime
|
||||||
sandbox_state = None
|
sandbox_state = None
|
||||||
thread_data = None
|
thread_data = None
|
||||||
@@ -108,6 +116,13 @@ async def task_tool(
|
|||||||
# Get or generate trace_id for distributed tracing
|
# Get or generate trace_id for distributed tracing
|
||||||
trace_id = metadata.get("trace_id") or str(uuid.uuid4())[:8]
|
trace_id = metadata.get("trace_id") or str(uuid.uuid4())[:8]
|
||||||
|
|
||||||
|
parent_available_skills = metadata.get("available_skills")
|
||||||
|
if parent_available_skills is not None:
|
||||||
|
overrides["skills"] = _merge_skill_allowlists(list(parent_available_skills), config.skills)
|
||||||
|
|
||||||
|
if overrides:
|
||||||
|
config = replace(config, **overrides)
|
||||||
|
|
||||||
# Get available tools (excluding task tool to prevent nesting)
|
# Get available tools (excluding task tool to prevent nesting)
|
||||||
# Lazy import to avoid circular dependency
|
# Lazy import to avoid circular dependency
|
||||||
from deerflow.tools import get_available_tools
|
from deerflow.tools import get_available_tools
|
||||||
|
|||||||
@@ -223,6 +223,90 @@ def test_task_tool_propagates_tool_groups_to_subagent(monkeypatch):
|
|||||||
get_available_tools.assert_called_once_with(model_name="ark-model", groups=parent_tool_groups, subagent_enabled=False)
|
get_available_tools.assert_called_once_with(model_name="ark-model", groups=parent_tool_groups, subagent_enabled=False)
|
||||||
|
|
||||||
|
|
||||||
|
def test_task_tool_inherits_parent_skill_allowlist_for_default_subagent(monkeypatch):
|
||||||
|
config = _make_subagent_config()
|
||||||
|
runtime = _make_runtime()
|
||||||
|
runtime.config["metadata"]["available_skills"] = ["safe-skill"]
|
||||||
|
events = []
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
class DummyExecutor:
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
captured["config"] = kwargs["config"]
|
||||||
|
|
||||||
|
def execute_async(self, prompt, task_id=None):
|
||||||
|
return task_id or "generated-task-id"
|
||||||
|
|
||||||
|
monkeypatch.setattr(task_tool_module, "SubagentStatus", FakeSubagentStatus)
|
||||||
|
monkeypatch.setattr(task_tool_module, "SubagentExecutor", DummyExecutor)
|
||||||
|
monkeypatch.setattr(task_tool_module, "get_subagent_config", lambda _: config)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
task_tool_module,
|
||||||
|
"get_background_task_result",
|
||||||
|
lambda _: _make_result(FakeSubagentStatus.COMPLETED, result="done"),
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(task_tool_module, "get_stream_writer", lambda: events.append)
|
||||||
|
monkeypatch.setattr(task_tool_module.asyncio, "sleep", _no_sleep)
|
||||||
|
monkeypatch.setattr("deerflow.tools.get_available_tools", MagicMock(return_value=[]))
|
||||||
|
|
||||||
|
output = _run_task_tool(
|
||||||
|
runtime=runtime,
|
||||||
|
description="执行任务",
|
||||||
|
prompt="use skills",
|
||||||
|
subagent_type="general-purpose",
|
||||||
|
tool_call_id="tc-skills",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert output == "Task Succeeded. Result: done"
|
||||||
|
assert captured["config"].skills == ["safe-skill"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_task_tool_intersects_parent_and_subagent_skill_allowlists(monkeypatch):
|
||||||
|
config = _make_subagent_config()
|
||||||
|
config = SubagentConfig(
|
||||||
|
name=config.name,
|
||||||
|
description=config.description,
|
||||||
|
system_prompt=config.system_prompt,
|
||||||
|
max_turns=config.max_turns,
|
||||||
|
timeout_seconds=config.timeout_seconds,
|
||||||
|
skills=["safe-skill", "other-skill"],
|
||||||
|
)
|
||||||
|
runtime = _make_runtime()
|
||||||
|
runtime.config["metadata"]["available_skills"] = ["safe-skill"]
|
||||||
|
events = []
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
class DummyExecutor:
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
captured["config"] = kwargs["config"]
|
||||||
|
|
||||||
|
def execute_async(self, prompt, task_id=None):
|
||||||
|
return task_id or "generated-task-id"
|
||||||
|
|
||||||
|
monkeypatch.setattr(task_tool_module, "SubagentStatus", FakeSubagentStatus)
|
||||||
|
monkeypatch.setattr(task_tool_module, "SubagentExecutor", DummyExecutor)
|
||||||
|
monkeypatch.setattr(task_tool_module, "get_subagent_config", lambda _: config)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
task_tool_module,
|
||||||
|
"get_background_task_result",
|
||||||
|
lambda _: _make_result(FakeSubagentStatus.COMPLETED, result="done"),
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(task_tool_module, "get_stream_writer", lambda: events.append)
|
||||||
|
monkeypatch.setattr(task_tool_module.asyncio, "sleep", _no_sleep)
|
||||||
|
monkeypatch.setattr("deerflow.tools.get_available_tools", MagicMock(return_value=[]))
|
||||||
|
|
||||||
|
output = _run_task_tool(
|
||||||
|
runtime=runtime,
|
||||||
|
description="执行任务",
|
||||||
|
prompt="use skills",
|
||||||
|
subagent_type="general-purpose",
|
||||||
|
tool_call_id="tc-skills-intersection",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert output == "Task Succeeded. Result: done"
|
||||||
|
assert captured["config"].skills == ["safe-skill"]
|
||||||
|
|
||||||
|
|
||||||
def test_task_tool_no_tool_groups_passes_none(monkeypatch):
|
def test_task_tool_no_tool_groups_passes_none(monkeypatch):
|
||||||
"""Verify that when metadata has no tool_groups, groups=None is passed (backward compat)."""
|
"""Verify that when metadata has no tool_groups, groups=None is passed (backward compat)."""
|
||||||
config = _make_subagent_config()
|
config = _make_subagent_config()
|
||||||
|
|||||||
Reference in New Issue
Block a user