diff --git a/backend/packages/harness/deerflow/agents/lead_agent/agent.py b/backend/packages/harness/deerflow/agents/lead_agent/agent.py index f3cba9bc3..ce1d50e24 100644 --- a/backend/packages/harness/deerflow/agents/lead_agent/agent.py +++ b/backend/packages/harness/deerflow/agents/lead_agent/agent.py @@ -43,7 +43,7 @@ from deerflow.agents.thread_state import ThreadState from deerflow.config.agents_config import load_agent_config, validate_agent_name from deerflow.config.app_config import AppConfig, get_app_config from deerflow.models import create_chat_model -from deerflow.skills.tool_policy import filter_tools_by_skill_allowed_tools +from deerflow.skills.tool_policy import SKILL_LOADING_TOOL_NAMES, filter_tools_by_skill_allowed_tools from deerflow.skills.types import Skill from deerflow.tracing import build_tracing_callbacks @@ -504,7 +504,7 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig): # Keep the bootstrap skill set intentionally narrow so agent creation # remains deterministic before the custom agent's own config exists. raw_tools = get_available_tools(model_name=model_name, subagent_enabled=subagent_enabled, app_config=resolved_app_config) + [setup_agent] - filtered = filter_tools_by_skill_allowed_tools(raw_tools, skills_for_tool_policy) + filtered = filter_tools_by_skill_allowed_tools(raw_tools, skills_for_tool_policy, always_allowed_tool_names=SKILL_LOADING_TOOL_NAMES) final_tools, setup = assemble_deferred_tools(filtered, enabled=resolved_app_config.tool_search.enabled) return create_agent( model=create_chat_model(name=model_name, thinking_enabled=thinking_enabled, app_config=resolved_app_config, attach_tracing=False), @@ -531,7 +531,7 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig): extra_tools = [update_agent] if agent_name else [] # Default lead agent (unchanged behavior) raw_tools = get_available_tools(model_name=model_name, groups=agent_config.tool_groups if agent_config else None, subagent_enabled=subagent_enabled, app_config=resolved_app_config) - filtered = filter_tools_by_skill_allowed_tools(raw_tools + extra_tools, skills_for_tool_policy) + filtered = filter_tools_by_skill_allowed_tools(raw_tools + extra_tools, skills_for_tool_policy, always_allowed_tool_names=SKILL_LOADING_TOOL_NAMES) final_tools, setup = assemble_deferred_tools(filtered, enabled=resolved_app_config.tool_search.enabled) return create_agent( model=create_chat_model(name=model_name, thinking_enabled=thinking_enabled, reasoning_effort=reasoning_effort, app_config=resolved_app_config, attach_tracing=False), diff --git a/backend/packages/harness/deerflow/skills/tool_policy.py b/backend/packages/harness/deerflow/skills/tool_policy.py index 058848379..d85302ddd 100644 --- a/backend/packages/harness/deerflow/skills/tool_policy.py +++ b/backend/packages/harness/deerflow/skills/tool_policy.py @@ -10,6 +10,9 @@ class NamedTool(Protocol): name: str +SKILL_LOADING_TOOL_NAMES = frozenset({"read_file"}) + + def allowed_tool_names_for_skills(skills: list[Skill]) -> set[str] | None: """Return the union of explicit skill allowed-tools declarations. @@ -36,9 +39,15 @@ def allowed_tool_names_for_skills(skills: list[Skill]) -> set[str] | None: return allowed -def filter_tools_by_skill_allowed_tools[ToolT: NamedTool](tools: list[ToolT], skills: list[Skill]) -> list[ToolT]: +def filter_tools_by_skill_allowed_tools[ToolT: NamedTool]( + tools: list[ToolT], + skills: list[Skill], + *, + always_allowed_tool_names: set[str] | frozenset[str] = frozenset(), +) -> list[ToolT]: allowed = allowed_tool_names_for_skills(skills) if allowed is None: return tools - return [tool for tool in tools if tool.name in allowed] + allowed_with_framework_tools = allowed | set(always_allowed_tool_names) + return [tool for tool in tools if tool.name in allowed_with_framework_tools] diff --git a/backend/tests/test_lead_agent_skills.py b/backend/tests/test_lead_agent_skills.py index f10aa6fce..7611d0c73 100644 --- a/backend/tests/test_lead_agent_skills.py +++ b/backend/tests/test_lead_agent_skills.py @@ -195,7 +195,7 @@ def test_make_lead_agent_filters_tools_from_available_skills(monkeypatch): monkeypatch.setattr(lead_agent_module, "apply_prompt_template", lambda **kwargs: "mock_prompt") monkeypatch.setattr(lead_agent_module, "create_agent", lambda **kwargs: kwargs) monkeypatch.setattr(lead_agent_module, "load_agent_config", lambda x: AgentConfig(name="test", skills=["restricted", "legacy"])) - monkeypatch.setattr(lead_agent_module, "_load_enabled_skills_for_tool_policy", lambda available_skills, *, app_config: [_make_skill("restricted", ["read_file"]), _make_skill("legacy", None)]) + monkeypatch.setattr(lead_agent_module, "_load_enabled_skills_for_tool_policy", lambda available_skills, *, app_config: [_make_skill("restricted", ["web_search"]), _make_skill("legacy", None)]) monkeypatch.setattr("deerflow.tools.get_available_tools", lambda **kwargs: [NamedTool("bash"), NamedTool("read_file"), NamedTool("web_search")]) mock_app_config = MagicMock() @@ -204,7 +204,18 @@ def test_make_lead_agent_filters_tools_from_available_skills(monkeypatch): agent_kwargs = lead_agent_module.make_lead_agent({"configurable": {"agent_name": "test"}}) - assert [tool.name for tool in agent_kwargs["tools"]] == ["read_file"] + assert [tool.name for tool in agent_kwargs["tools"]] == ["read_file", "web_search"] + + +def test_skill_allowed_tools_default_does_not_preserve_read_file_for_subagents(): + from deerflow.skills.tool_policy import filter_tools_by_skill_allowed_tools + + tools = [NamedTool("read_file"), NamedTool("dataagent_query"), NamedTool("bash")] + skills = [_make_skill("data-query", ["dataagent_query"])] + + filtered = filter_tools_by_skill_allowed_tools(tools, skills) + + assert [tool.name for tool in filtered] == ["dataagent_query"] def test_make_lead_agent_all_legacy_skills_preserve_all_tools(monkeypatch):