diff --git a/examples/skills/agent/tools.py b/examples/skills/agent/tools.py index a10e87249..a51bd174e 100644 --- a/examples/skills/agent/tools.py +++ b/examples/skills/agent/tools.py @@ -58,5 +58,6 @@ def create_skill_tool_set(is_link_stager: bool = True, use_cached_repository: bo use_cached_repository=use_cached_repository) skill_stager = LinkSkillStager() if is_link_stager else CopySkillStager() # skill_stager: The stager to use for staging skills. - skill_toolset = SkillToolSet(repository=repository, run_tool_kwargs=tool_kwargs, skill_stager=skill_stager) + skill_toolset = SkillToolSet(repository=repository, run_tool_kwargs=tool_kwargs, + skill_stager=skill_stager, excluded_tools=["skill_list_tools"]) return skill_toolset, repository diff --git a/examples/team_with_skill/agent/prompts.py b/examples/team_with_skill/agent/prompts.py index 359e811bd..df8786575 100644 --- a/examples/team_with_skill/agent/prompts.py +++ b/examples/team_with_skill/agent/prompts.py @@ -13,17 +13,16 @@ Mandatory execution order for every user request: 1. Call `skill_list` and confirm `leader-research` exists. -2. Call `skill_list_tools` for `leader-research`. -3. Call `skill_load` for `leader-research`. -4. Call `skill_run` with command: +2. Call `skill_load` for `leader-research`. +3. Call `skill_run` with command: `bash scripts/gather_points.sh "" out/leader_notes.txt` and set `output_files` to include `out/leader_notes.txt`. -5. Then delegate to `researcher` exactly once. -6. Then delegate to `writer` exactly once. -7. Synthesize and return final answer. +4. Then delegate to `researcher` exactly once. +5. Then delegate to `writer` exactly once. +6. Synthesize and return final answer. Rules: -- Never call `delegate_to_member` before step 4 succeeds. +- Never call `delegate_to_member` before step 3 succeeds. - Use current-year context in final answer. - Keep the final answer concise and practical. """ diff --git a/tests/skills/tools/test_skill_list_tool.py b/tests/skills/tools/test_skill_list_tool.py index 220524a8a..679b29a70 100644 --- a/tests/skills/tools/test_skill_list_tool.py +++ b/tests/skills/tools/test_skill_list_tool.py @@ -13,14 +13,13 @@ from trpc_agent_sdk.skills._types import Skill, SkillSummary from trpc_agent_sdk.skills.tools._skill_list_tool import ( - skill_list_tools, -) - + skill_list_tools, ) # --------------------------------------------------------------------------- # skill_list_tools # --------------------------------------------------------------------------- + def _make_ctx(repository=None): ctx = MagicMock() ctx.agent_context.get_metadata = MagicMock(return_value=repository) @@ -28,6 +27,7 @@ def _make_ctx(repository=None): class TestSkillListTools: + def test_returns_tools(self): skill = Skill( summary=SkillSummary(name="test"), @@ -39,7 +39,10 @@ def test_returns_tools(self): ctx = _make_ctx(repository=repo) result = skill_list_tools(ctx, "test") + assert result["skill_name"] == "test" assert result["available_tools"] == ["get_weather", "get_data"] + assert result["scope"] == "skill_declared_tools_only" + assert "not represent all tools available to the agent" in result["note"] def test_skill_not_found(self): repo = MagicMock() @@ -47,7 +50,10 @@ def test_skill_not_found(self): ctx = _make_ctx(repository=repo) result = skill_list_tools(ctx, "nonexistent") - assert result == {"available_tools": []} + assert result["skill_name"] == "nonexistent" + assert result["available_tools"] == [] + assert result["scope"] == "skill_declared_tools_only" + assert "not represent all tools available to the agent" in result["note"] def test_no_repository_raises(self): ctx = _make_ctx(repository=None) @@ -61,4 +67,6 @@ def test_no_tools_or_examples(self): ctx = _make_ctx(repository=repo) result = skill_list_tools(ctx, "test") + assert result["skill_name"] == "test" assert result["available_tools"] == [] + assert result["scope"] == "skill_declared_tools_only" diff --git a/trpc_agent_sdk/skills/_toolset.py b/trpc_agent_sdk/skills/_toolset.py index 4499dc9fe..bebfb35a5 100644 --- a/trpc_agent_sdk/skills/_toolset.py +++ b/trpc_agent_sdk/skills/_toolset.py @@ -80,18 +80,24 @@ def __init__(self, runtime_tools: Optional[List[ToolABC]] = None, skill_stager: Optional[Stager] = None, skill_config: Optional[dict[str, Any]] = None, + excluded_tools: Optional[List[str]] = None, **run_tool_kwargs: dict[str, Any]): """Initialize the skill toolset. Args: paths: Optional list of skill paths. If None, will create a new one. repository: Skill repository. If None, will be retrieved from context metadata. - enable_hot_reload: Whether to enable skill hot reload checks for - auto-created repositories. + repo_resolver: Skill repository resolver. If None, will use the default repository resolver. + workspace_runtime_resolver: Workspace runtime resolver. If None, will use the default workspace runtime resolver. + enable_hot_reload: Whether to enable skill hot reload checks for auto-created repositories. tool_filter: Optional tool filter. If None, will include all tools. is_include_all_tools: Optional flag to include all tools. If True, will include all tools. - user_tools: Optional list of user tools. If None, will not include any user tools. - run_tool_kwargs: Optional keyword arguments for skill run tool. If None, will use default values. + create_ws_name_cb: Optional workspace name callback. If None, will use the default workspace name callback. + runtime_tools: Optional list of runtime tools. If None, will use the default runtime tools. + skill_stager: Optional skill stager. If None, will use the default skill stager. + skill_config: Optional skill config. If None, will use the default skill config. + excluded_tools: Optional list of tools to exclude. If None, will not exclude any tools. + **run_tool_kwargs: Optional keyword arguments for skill run tool. If None, will use default values. """ super().__init__(tool_filter=tool_filter, is_include_all_tools=is_include_all_tools) self.name = "skill_toolset" @@ -136,6 +142,8 @@ def __init__(self, WorkspaceWriteStdinTool(workspace_exec_tool), WorkspaceKillSessionTool(workspace_exec_tool), ] + self._excluded_tools: List[str] = excluded_tools or [] + self._default_tools: List[ToolABC] = [] @property def repository(self) -> BaseSkillRepository: @@ -152,9 +160,6 @@ async def get_tools(self, invocation_context: Optional[InvocationContext] = None Returns: List of tools from all registered skills """ - tools: List[ToolABC] = [] - skill_functions: List[SkillToolFunction] = SKILL_REGISTRY.get_all() - skill_functions.extend(self._function_tools) if self._repo_resolver is not None: repository = self._repo_resolver(invocation_context) else: @@ -167,10 +172,16 @@ async def get_tools(self, invocation_context: Optional[InvocationContext] = None agent_context.with_metadata(SKILL_REPOSITORY_KEY, repository) if not is_exist_skill_config(agent_context): set_skill_config(agent_context, self._skill_config) + if self._default_tools: + return self._default_tools.copy() + + tools: List[ToolABC] = [] tools.append(self._load_tool) tools.append(self._run_tool) tools.append(self._exec_tool) tools.extend(self._runtime_tools) + skill_functions: List[SkillToolFunction] = SKILL_REGISTRY.get_all() + skill_functions.extend(self._function_tools) for skill_function in skill_functions: try: tools.append(FunctionTool(func=skill_function)) @@ -178,5 +189,18 @@ async def get_tools(self, invocation_context: Optional[InvocationContext] = None # Log error but continue loading other tools logger.warning("Failed to get tools from skill '%s': %s", skill_function.__name__, ex) continue - + tools = self._exclude_tools(tools) + self._default_tools.extend(tools) return tools + + def _exclude_tools(self, tools: List[ToolABC]) -> List[ToolABC]: + """Exclude tools from the list.""" + if not self._excluded_tools: + return tools + available_tools: List[ToolABC] = [] + for tool in tools: + name = getattr(tool, "name", None) + if not name or name in self._excluded_tools: + continue + available_tools.append(tool) + return available_tools diff --git a/trpc_agent_sdk/skills/tools/_skill_list_tool.py b/trpc_agent_sdk/skills/tools/_skill_list_tool.py index 942d12760..c3f7d047b 100644 --- a/trpc_agent_sdk/skills/tools/_skill_list_tool.py +++ b/trpc_agent_sdk/skills/tools/_skill_list_tool.py @@ -20,12 +20,17 @@ def skill_list_tools(tool_context: InvocationContext, skill_name: str) -> dict[str, Any]: - """List callable tools declared for a skill. + """List tool names declared by a specific skill. + + This only reports tools referenced by the selected skill. It does not list + every tool available to the agent. An empty result means that this skill + declares no tools; it does not mean that the agent has no tools available. Args: - skill_name: The name of the skill to load. + skill_name: The name of the skill to inspect. + Returns: - Object containing available tools. + Object containing the tool names declared by this skill. """ repository: Optional[BaseSkillRepository] = tool_context.agent_context.get_metadata(SKILL_REPOSITORY_KEY) if repository is None: @@ -33,5 +38,17 @@ def skill_list_tools(tool_context: InvocationContext, skill_name: str) -> dict[s skill = repository.get(skill_name) if skill is None: logger.error("Skill %s not found", repr(skill_name)) - return {"available_tools": []} - return {"available_tools": list(skill.tools or [])} + available_tools = [] + else: + available_tools = list(skill.tools or []) + return { + "skill_name": + skill_name, + "available_tools": + available_tools, + "scope": + "skill_declared_tools_only", + "note": + "Only tools declared by this skill are listed. " + "This does not represent all tools available to the agent.", + }