fix(skills): preserve read_file for lead skill loading (#3862) (#3863)

This commit is contained in:
kongxiangxin 2026-07-03 21:44:02 +08:00 committed by GitHub
parent 8a26b5c9a4
commit 9d7e131340
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 27 additions and 7 deletions

View file

@ -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),

View file

@ -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]

View file

@ -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):