From 233d33720562b874aaeb071e58ab1c585ddfb635 Mon Sep 17 00:00:00 2001 From: raychen <815315825@qq.com> Date: Fri, 4 Sep 2026 15:55:46 +0800 Subject: [PATCH] =?UTF-8?q?feature:=20=E5=B0=86=E5=9C=A8=20skill=20?= =?UTF-8?q?=E4=B8=AD=E5=AE=9A=E4=B9=89=20tool=20=E7=9A=84=E7=89=B9?= =?UTF-8?q?=E6=80=A7=E7=8B=AC=E7=AB=8B=E5=87=BA=E6=9D=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改skill_list_tools 的函数注释,避免模型理解歧义 - 对于在 skill 中指定 tool 的特性独立为单独的类 --- .gitignore | 1 + docs/mkdocs/en/skill.md | 67 +++++++++++++------ docs/mkdocs/zh/skill.md | 67 +++++++++++++------ examples/fastapi_server/_runner_manager.py | 2 +- examples/skills/agent/tools.py | 3 +- .../agent/tools/_skill_tools.py | 4 +- examples/team_with_skill/agent/prompts.py | 13 ++-- tests/skills/test_toolset.py | 34 ++++++++-- tests/skills/tools/test_skill_list_tool.py | 16 +++-- trpc_agent_sdk/skills/__init__.py | 2 + trpc_agent_sdk/skills/_dynamic_toolset.py | 27 ++++++++ trpc_agent_sdk/skills/_toolset.py | 29 ++++---- .../skills/tools/_skill_list_tool.py | 25 +++++-- 13 files changed, 209 insertions(+), 81 deletions(-) diff --git a/.gitignore b/.gitignore index 91426e394..df173cf46 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .idea .vscode .DS_Store +.cursor *.lock *.log examples/*.log diff --git a/docs/mkdocs/en/skill.md b/docs/mkdocs/en/skill.md index 9d09c8d54..dcdc2eaae 100644 --- a/docs/mkdocs/en/skill.md +++ b/docs/mkdocs/en/skill.md @@ -15,8 +15,8 @@ Background references: - 🔎 Overview injection (name + description) to guide selection - 📥 `skill_load` fetches `SKILL.md` body and selected documentation on demand, automatically loading tools defined in the skill - 📋 `skill_list` lists all available skill names -- 🔧 `skill_list_tools` lists tool names defined in a specified skill's `SKILL.md` -- ⚙️ `skill_select_tools` dynamically selects skill tools (add/replace/clear modes) for token optimization +- 🔧 `skill_list_tools` lists tool names defined in a specified skill's `SKILL.md` (dynamic tool loading only) +- ⚙️ `skill_select_tools` dynamically selects skill tools for token optimization (dynamic tool loading only) - 📚 `skill_select_docs` adds/replaces/clears documentation - 🧾 `skill_list_docs` lists available documentation - 🏃 `skill_run` executes commands and returns stdout/stderr and output files @@ -149,24 +149,18 @@ The `INSTRUCTION` should include complete skill usage workflow guidance: INSTRUCTION = """ You are an AI assistant with access to Agent Skills. -## Complete Skill Workflow +## Standard Skill Workflow When handling user requests: 1. **Discover** → Call skill_list() to see available skills -2. **Inspect** → Call skill_list_tools(skill_name="...") to preview tools -3. **Load** → Call skill_load(skill_name="...") to load the skill -4. **Optimize** → Call skill_select_tools(...) to select only needed tools (saves tokens) -5. **Document** → Call skill_list_docs(...) and skill_select_docs(...) if more info needed -6. **Execute** → Call skill_run(...) to execute commands or use skill's tools directly - -Example Complete Flow: -User: "What's the weather in Beijing?" -→ skill_list() → see "weather-tools" -→ skill_list_tools(skill_name="weather-tools") → see available tools -→ skill_load(skill_name="weather-tools") → load full content -→ skill_select_tools(skill_name="weather-tools", tools=["get_current_weather"]) → optimize -→ get_current_weather(city="Beijing") → execute +2. **Load** → Call skill_load(skill_name="...") to load the skill +3. **Document** → Call skill_list_docs(...) and skill_select_docs(...) if more info is needed +4. **Execute** → Call skill_run(...) to execute commands + +Use SkillToolSetWithDynamicTools together with DynamicSkillToolSet when the +agent must inspect and selectively expose a skill's business tools. That setup +also provides skill_list_tools and skill_select_tools. Always use environment variables in commands: - $WORKSPACE_DIR, $SKILLS_DIR, $WORK_DIR, $OUTPUT_DIR, $RUN_DIR, $SKILL_NAME @@ -176,14 +170,15 @@ Always use environment variables in commands: Key points: - **Automatic tool registration**: The following tools are automatically registered via `SkillToolSet`, requiring no manual wiring: - `skill_list`: Lists all available skills - - `skill_list_tools`: Lists tools of a skill - `skill_load`: Loads skill content - - `skill_select_tools`: Selects specific tools (token optimization) - `skill_list_docs`: Lists available documentation - `skill_select_docs`: Selects specific documentation - `skill_run`: Executes skill commands +- **Dynamic tool management**: `SkillToolSetWithDynamicTools` additionally registers + `skill_list_tools` and `skill_select_tools`. Use it with `DynamicSkillToolSet`; + the standard `SkillToolSet` intentionally does not expose these two tools. - **Intelligent prompt guidance**: Explicitly describe the workflow in the prompt to guide the LLM to call tools in the correct order -- **Token optimization**: Use `skill_select_tools` to load only the needed tools, significantly reducing context size +- **Token optimization**: In the dynamic setup, use `skill_select_tools` to load only the needed tools and reduce context size - **Code location**: - Package entry (aggregated exports): [trpc_agent_sdk/skills/tools/__init__.py](../../../trpc_agent_sdk/skills/tools/__init__.py) - `skill_run` implementation: [trpc_agent_sdk/skills/tools/_skill_run.py](../../../trpc_agent_sdk/skills/tools/_skill_run.py) (for other tools, see **Declaration location** in each section below) @@ -550,6 +545,28 @@ Assistant: Let me check what skills are available. **Declaration location**: [trpc_agent_sdk/skills/tools/_skill_list_tool.py](../../../trpc_agent_sdk/skills/tools/_skill_list_tool.py) +**Availability**: +- This tool is exposed by `SkillToolSetWithDynamicTools`, not by the standard `SkillToolSet`. +- Use it together with `DynamicSkillToolSet`, which resolves the selected business tools from the tool pool: + +```python +from trpc_agent_sdk.skills import DynamicSkillToolSet +from trpc_agent_sdk.skills import SkillToolSetWithDynamicTools + +skill_tool_set = SkillToolSetWithDynamicTools(repository=repository) +dynamic_tool_set = DynamicSkillToolSet( + skill_repository=repository, + available_tools=available_tools, + only_active_skills=True, +) + +agent = LlmAgent( + # ... + tools=[skill_tool_set, dynamic_tool_set], + skill_repository=repository, +) +``` + **Input parameters**: - `skill_name` (required): Skill name @@ -638,7 +655,7 @@ Overview **Behavior**: - Optimizes LLM context: activates only the tools needed for the current conversation - Updates the `temp:skill:tools:` session key -- When used with `DynamicSkillToolSet`, only selected tools are loaded into the LLM context +- `SkillToolSetWithDynamicTools` exposes this selection tool, while `DynamicSkillToolSet` loads only the selected tools into the LLM context **Prompt guidance**: @@ -1330,7 +1347,7 @@ By declaring which tools a skill needs through the **Tools section in SKILL.md** |------|------|------| | **Tool exposure method** | All tools are **fully injected** into the LLM context at agent creation | No business tools initially; tools are **injected on demand** based on the `Tools:` declaration in SKILL.md after `skill_load` | | **SKILL.md `Tools:` section** | Optional, used only for informational display | **Core mechanism** that determines which tools are loaded into the LLM context | -| **Required components** | Only `SkillToolSet` | `SkillToolSet` + `DynamicSkillToolSet` (used together) | +| **Required components** | Only `SkillToolSet` | `SkillToolSetWithDynamicTools` + `DynamicSkillToolSet` (used together) | | **Tool registration method** | Tools are attached directly to the agent's `tools` list | Tools are placed in the `available_tools` pool and declaratively filtered through SKILL.md | | **Token consumption** | Fixed consumption (all tool definitions always present in context) | On-demand consumption (only loads tools declared by active skills), **saves 85-95% with many tools** | | **Tool visibility control** | None, LLM always sees all tools | Fine-grained control via `skill_select_tools` for dynamic add/remove | @@ -1424,9 +1441,15 @@ Example 4: Ask someone name information #### 3. Configure the Agent -**File**: `agent/tools/_dynamic.py` and `agent/agent.py` +**File**: `agent/tools/_skill_tools.py`, `agent/tools/_dynamic.py`, and `agent/agent.py` ```python +# agent/tools/_skill_tools.py +from trpc_agent_sdk.skills import SkillToolSetWithDynamicTools + +def create_skill_tool_set(repository): + return SkillToolSetWithDynamicTools(repository=repository) + # agent/tools/_dynamic.py from trpc_agent_sdk.tools import FunctionTool from trpc_agent_sdk.skills import DynamicSkillToolSet, BaseSkillRepository diff --git a/docs/mkdocs/zh/skill.md b/docs/mkdocs/zh/skill.md index 5caa54a09..abc60e02a 100644 --- a/docs/mkdocs/zh/skill.md +++ b/docs/mkdocs/zh/skill.md @@ -15,8 +15,8 @@ Agent Skills 将可重用的工作流打包为包含 `SKILL.md` 规范文件以 - 🔎 概览注入(名称 + 描述)以指导选择 - 📥 `skill_load` 按需拉取 `SKILL.md` 主体和选定的文档,自动加载技能中定义的工具 - 📋 `skill_list` 列出所有可用的技能名称 -- 🔧 `skill_list_tools` 列出指定技能在 `SKILL.md` 中定义的工具名称 -- ⚙️ `skill_select_tools` 动态选择技能的工具(add/replace/clear 模式),实现 token 优化 +- 🔧 `skill_list_tools` 列出指定技能在 `SKILL.md` 中定义的工具名称(仅用于动态工具加载) +- ⚙️ `skill_select_tools` 动态选择技能的工具,实现 token 优化(仅用于动态工具加载) - 📚 `skill_select_docs` 添加/替换/清除文档 - 🧾 `skill_list_docs` 列出可用文档 - 🏃 `skill_run` 执行命令,返回 stdout/stderr 和输出文件 @@ -149,24 +149,18 @@ agent = LlmAgent( INSTRUCTION = """ You are an AI assistant with access to Agent Skills. -## Complete Skill Workflow +## Standard Skill Workflow When handling user requests: 1. **Discover** → Call skill_list() to see available skills -2. **Inspect** → Call skill_list_tools(skill_name="...") to preview tools -3. **Load** → Call skill_load(skill_name="...") to load the skill -4. **Optimize** → Call skill_select_tools(...) to select only needed tools (saves tokens) -5. **Document** → Call skill_list_docs(...) and skill_select_docs(...) if more info needed -6. **Execute** → Call skill_run(...) to execute commands or use skill's tools directly - -Example Complete Flow: -User: "What's the weather in Beijing?" -→ skill_list() → see "weather-tools" -→ skill_list_tools(skill_name="weather-tools") → see available tools -→ skill_load(skill_name="weather-tools") → load full content -→ skill_select_tools(skill_name="weather-tools", tools=["get_current_weather"]) → optimize -→ get_current_weather(city="Beijing") → execute +2. **Load** → Call skill_load(skill_name="...") to load the skill +3. **Document** → Call skill_list_docs(...) and skill_select_docs(...) if more info is needed +4. **Execute** → Call skill_run(...) to execute commands + +如果 Agent 需要查看并按需暴露 Skill 的业务工具,请组合使用 +SkillToolSetWithDynamicTools 和 DynamicSkillToolSet。该配置还会提供 +skill_list_tools 和 skill_select_tools。 Always use environment variables in commands: - $WORKSPACE_DIR, $SKILLS_DIR, $WORK_DIR, $OUTPUT_DIR, $RUN_DIR, $SKILL_NAME @@ -176,14 +170,15 @@ Always use environment variables in commands: 关键点: - **工具自动注册**:通过 `SkillToolSet` 自动注册以下工具,无需手动连接: - `skill_list`:列出所有可用技能 - - `skill_list_tools`:列出技能的工具 - `skill_load`:加载技能内容 - - `skill_select_tools`:选择特定工具(优化 token) - `skill_list_docs`:列出可用文档 - `skill_select_docs`:选择特定文档 - `skill_run`:执行技能命令 +- **动态工具管理**:`SkillToolSetWithDynamicTools` 会额外注册 + `skill_list_tools` 和 `skill_select_tools`。它需要与 `DynamicSkillToolSet` + 配合使用;普通 `SkillToolSet` 不会暴露这两个工具。 - **智能提示指导**:在提示词中明确说明工作流程,引导 LLM 按正确顺序调用工具 -- **Token 优化**:通过 `skill_select_tools` 仅加载需要的工具,显著减少上下文大小 +- **Token 优化**:在动态工具配置中,通过 `skill_select_tools` 仅加载需要的工具,显著减少上下文大小 - **代码位置**: - 工具包入口(聚合导出):[trpc_agent_sdk/skills/tools/__init__.py](../../../trpc_agent_sdk/skills/tools/__init__.py) - `skill_run` 实现:[trpc_agent_sdk/skills/tools/_skill_run.py](../../../trpc_agent_sdk/skills/tools/_skill_run.py)(其余工具见下文各节「声明位置」) @@ -549,6 +544,28 @@ Assistant: Let me check what skills are available. **声明位置**:[trpc_agent_sdk/skills/tools/_skill_list_tool.py](../../../trpc_agent_sdk/skills/tools/_skill_list_tool.py) +**可用范围**: +- 该工具由 `SkillToolSetWithDynamicTools` 暴露,普通 `SkillToolSet` 不包含它。 +- 它应与 `DynamicSkillToolSet` 配合使用,后者负责从工具池中解析并加载选中的业务工具: + +```python +from trpc_agent_sdk.skills import DynamicSkillToolSet +from trpc_agent_sdk.skills import SkillToolSetWithDynamicTools + +skill_tool_set = SkillToolSetWithDynamicTools(repository=repository) +dynamic_tool_set = DynamicSkillToolSet( + skill_repository=repository, + available_tools=available_tools, + only_active_skills=True, +) + +agent = LlmAgent( + # ... + tools=[skill_tool_set, dynamic_tool_set], + skill_repository=repository, +) +``` + **输入参数**: - `skill_name`(必需):技能名称 @@ -637,7 +654,7 @@ Overview **功能行为**: - 优化 LLM 上下文:仅激活当前对话需要的工具 - 更新 `temp:skill:tools:` 会话键 -- 与 `DynamicSkillToolSet` 配合使用时,只有选中的工具会被加载到 LLM 上下文 +- `SkillToolSetWithDynamicTools` 暴露该选择工具,`DynamicSkillToolSet` 仅将选中的工具加载到 LLM 上下文 **提示词指导**: @@ -1329,7 +1346,7 @@ LLM 调用对应的工具:get_current_weather(city="Beijing") |------|------|------| | **工具暴露方式** | 所有工具在 Agent 创建时**全部注入** LLM 上下文 | 初始无业务工具,`skill_load` 后才根据 SKILL.md 的 `Tools:` 声明**按需注入** | | **SKILL.md `Tools:` 部分** | 可选,仅用于信息展示 | **核心机制**,决定哪些工具会被加载到 LLM 上下文 | -| **所需组件** | 仅 `SkillToolSet` | `SkillToolSet` + `DynamicSkillToolSet`(两者配合) | +| **所需组件** | 仅 `SkillToolSet` | `SkillToolSetWithDynamicTools` + `DynamicSkillToolSet`(两者配合) | | **工具注册方式** | 工具直接挂在 Agent 的 `tools` 列表中 | 工具放入 `available_tools` 工具池,通过 SKILL.md 声明式过滤 | | **Token 消耗** | 固定消耗(所有工具定义常驻上下文) | 按需消耗(仅加载激活 skill 声明的工具),**工具多时节省 85-95%** | | **工具可见性控制** | 无,LLM 始终看到所有工具 | 精细控制,可通过 `skill_select_tools` 动态增减 | @@ -1423,9 +1440,15 @@ Example 4: Ask someone name information #### 3. 配置 Agent -**文件**: `agent/tools/_dynamic.py` 和 `agent/agent.py` +**文件**: `agent/tools/_skill_tools.py`、`agent/tools/_dynamic.py` 和 `agent/agent.py` ```python +# agent/tools/_skill_tools.py +from trpc_agent_sdk.skills import SkillToolSetWithDynamicTools + +def create_skill_tool_set(repository): + return SkillToolSetWithDynamicTools(repository=repository) + # agent/tools/_dynamic.py from trpc_agent_sdk.tools import FunctionTool from trpc_agent_sdk.skills import DynamicSkillToolSet, BaseSkillRepository diff --git a/examples/fastapi_server/_runner_manager.py b/examples/fastapi_server/_runner_manager.py index ebbe1be49..62d36a741 100644 --- a/examples/fastapi_server/_runner_manager.py +++ b/examples/fastapi_server/_runner_manager.py @@ -88,7 +88,7 @@ def new_session_id() -> str: async def close(self) -> None: """Gracefully close the runner and release resources.""" - self._runner.close() + await self._runner.close() logger.info("RunnerManager closed: app=%s", self.app_name) # ------------------------------------------------------------------ diff --git a/examples/skills/agent/tools.py b/examples/skills/agent/tools.py index a10e87249..17b842668 100644 --- a/examples/skills/agent/tools.py +++ b/examples/skills/agent/tools.py @@ -54,7 +54,8 @@ def create_skill_tool_set(is_link_stager: bool = True, use_cached_repository: bo workspace_runtime = _create_workspace_runtime(**workspace_runtime_args) skill_paths = _get_skill_paths() # use_cached_repository: Whether to use cached repository. - repository = create_default_skill_repository(skill_paths, workspace_runtime=workspace_runtime, + repository = create_default_skill_repository(skill_paths, + workspace_runtime=workspace_runtime, use_cached_repository=use_cached_repository) skill_stager = LinkSkillStager() if is_link_stager else CopySkillStager() # skill_stager: The stager to use for staging skills. diff --git a/examples/skills_with_dynamic_tools/agent/tools/_skill_tools.py b/examples/skills_with_dynamic_tools/agent/tools/_skill_tools.py index 414f5ecb3..bea6e1e73 100644 --- a/examples/skills_with_dynamic_tools/agent/tools/_skill_tools.py +++ b/examples/skills_with_dynamic_tools/agent/tools/_skill_tools.py @@ -14,6 +14,7 @@ from trpc_agent_sdk.skills import BaseSkillRepository from trpc_agent_sdk.skills import ENV_SKILLS_ROOT from trpc_agent_sdk.skills import SkillToolSet +from trpc_agent_sdk.skills import SkillToolSetWithDynamicTools from trpc_agent_sdk.skills import create_default_skill_repository @@ -62,4 +63,5 @@ def create_skill_tool_set(workspace_runtime_type: str = "local") -> tuple[SkillT **workspace_runtime_args) skill_paths = _get_skill_paths() repository = create_default_skill_repository(skill_paths, workspace_runtime=workspace_runtime) - return SkillToolSet(repository=repository, run_tool_kwargs=tool_kwargs), repository + + return SkillToolSetWithDynamicTools(repository=repository, run_tool_kwargs=tool_kwargs), 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/test_toolset.py b/tests/skills/test_toolset.py index 3f7f90bf9..d630d26b2 100644 --- a/tests/skills/test_toolset.py +++ b/tests/skills/test_toolset.py @@ -7,16 +7,16 @@ Covers: - SkillToolSet initialization -- SkillToolSet.get_tools: returns expected tool set +- SkillToolSet.get_tools: default set omits dynamic tool-selection helpers +- SkillToolSetWithDynamicTools.get_tools: opt-in skill_list_tools / skill_select_tools - repository property """ from __future__ import annotations -from unittest.mock import AsyncMock, MagicMock, patch - -import pytest +from unittest.mock import MagicMock +from trpc_agent_sdk.skills._dynamic_toolset import SkillToolSetWithDynamicTools from trpc_agent_sdk.skills._toolset import SkillToolSet @@ -63,12 +63,34 @@ async def test_get_tools_includes_function_tools(self, tmp_path): assert "skill_load" in tool_names assert "skill_list" in tool_names assert "skill_list_docs" in tool_names - assert "skill_list_tools" in tool_names assert "skill_select_docs" in tool_names - assert "skill_select_tools" in tool_names + assert "skill_list_tools" not in tool_names + assert "skill_select_tools" not in tool_names async def test_get_tools_sets_metadata(self, tmp_path): ts = SkillToolSet(paths=[str(tmp_path)]) ctx = _make_ctx() await ts.get_tools(ctx) ctx.agent_context.with_metadata.assert_called() + + +class TestSkillToolSetWithDynamicTools: + async def test_get_tools_includes_dynamic_selection_helpers(self, tmp_path): + ts = SkillToolSetWithDynamicTools(paths=[str(tmp_path)]) + ctx = _make_ctx() + tools = await ts.get_tools(ctx) + tool_names = [t.name for t in tools] + assert "skill_load" in tool_names + assert "skill_list" in tool_names + assert "skill_list_tools" in tool_names + assert "skill_select_tools" in tool_names + + async def test_get_tools_does_not_duplicate_helpers_on_second_call(self, tmp_path): + ts = SkillToolSetWithDynamicTools(paths=[str(tmp_path)]) + ctx = _make_ctx() + first = [t.name for t in await ts.get_tools(ctx)] + second = [t.name for t in await ts.get_tools(ctx)] + assert first.count("skill_list_tools") == 1 + assert first.count("skill_select_tools") == 1 + assert second.count("skill_list_tools") == 1 + assert second.count("skill_select_tools") == 1 diff --git a/tests/skills/tools/test_skill_list_tool.py b/tests/skills/tools/test_skill_list_tool.py index 220524a8a..10d0e21cf 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 "scope" not in result + assert "note" not in result 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 "scope" not in result + assert "note" not in result 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 "scope" not in result diff --git a/trpc_agent_sdk/skills/__init__.py b/trpc_agent_sdk/skills/__init__.py index dae59ac92..d8133332d 100644 --- a/trpc_agent_sdk/skills/__init__.py +++ b/trpc_agent_sdk/skills/__init__.py @@ -55,6 +55,7 @@ from ._constants import SkillToolsNames from . import hub from ._dynamic_toolset import DynamicSkillToolSet +from ._dynamic_toolset import SkillToolSetWithDynamicTools from ._registry import SkillRegistry from ._repository import BaseSkillRepository from ._repository import CachedFsSkillRepository @@ -140,6 +141,7 @@ "SkillProfileNames", "SkillToolsNames", "DynamicSkillToolSet", + "SkillToolSetWithDynamicTools", "SkillRegistry", "BaseSkillRepository", "CachedFsSkillRepository", diff --git a/trpc_agent_sdk/skills/_dynamic_toolset.py b/trpc_agent_sdk/skills/_dynamic_toolset.py index c2d541099..98d5a7952 100644 --- a/trpc_agent_sdk/skills/_dynamic_toolset.py +++ b/trpc_agent_sdk/skills/_dynamic_toolset.py @@ -17,6 +17,7 @@ from typing import Optional from typing_extensions import override +from trpc_agent_sdk.abc import ToolABC from trpc_agent_sdk.context import InvocationContext from trpc_agent_sdk.log import logger from trpc_agent_sdk.tools import BaseTool @@ -28,8 +29,11 @@ from ._common import loaded_scan_prefix from ._common import tool_scan_prefix from ._common import tool_state_key +from ._toolset import SkillToolSet from ._repository import BaseSkillRepository from ._utils import get_state_delta +from .tools import skill_list_tools +from .tools import skill_select_tools class DynamicSkillToolSet(BaseToolSet): @@ -387,3 +391,26 @@ def _get_skill_default_tools(self, skill_name: str) -> list[str]: except Exception as ex: # pylint: disable=broad-except logger.warning("Failed to get default tools for skill '%s': %s", skill_name, ex) return [] + + +class SkillToolSetWithDynamicTools(SkillToolSet): + """ToolSet that dynamically loads tools based on skill selections. + + This toolset monitors skill loading state and tool selection state, then dynamically + provides only the tools that are selected for loaded skills. This approach saves tokens + by only including relevant tool definitions in the LLM context. + """ + + @override + async def get_tools(self, invocation_context: Optional[InvocationContext] = None) -> List[ToolABC]: + """Get all tools from registered skills. + + Args: + invocation_context: Optional invocation context (not used currently) + + Returns: + List of tools from all registered skills + """ + if not self._default_tools: + self._function_tools.extend([skill_list_tools, skill_select_tools]) + return await super().get_tools(invocation_context) diff --git a/trpc_agent_sdk/skills/_toolset.py b/trpc_agent_sdk/skills/_toolset.py index 4499dc9fe..a975df057 100644 --- a/trpc_agent_sdk/skills/_toolset.py +++ b/trpc_agent_sdk/skills/_toolset.py @@ -37,10 +37,8 @@ from ._skill_config import set_skill_config from ._skill_config import is_exist_skill_config from .tools import skill_list_docs -from .tools import skill_list_tools from .tools import SkillLoadTool from .tools import skill_select_docs -from .tools import skill_select_tools from .tools import skill_list from .tools import SkillExecTool from .tools import SkillRunTool @@ -86,12 +84,17 @@ def __init__(self, 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. + **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" @@ -118,9 +121,7 @@ def __init__(self, self._function_tools: List[SkillToolFunction] = [ skill_list, skill_list_docs, - skill_list_tools, skill_select_docs, - skill_select_tools, ] if runtime_tools: self._runtime_tools = runtime_tools @@ -136,6 +137,7 @@ def __init__(self, WorkspaceWriteStdinTool(workspace_exec_tool), WorkspaceKillSessionTool(workspace_exec_tool), ] + self._default_tools: List[ToolABC] = [] @property def repository(self) -> BaseSkillRepository: @@ -152,9 +154,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 +166,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 +183,5 @@ 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 - + self._default_tools.extend(tools) return 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..9f4b9b33e 100644 --- a/trpc_agent_sdk/skills/tools/_skill_list_tool.py +++ b/trpc_agent_sdk/skills/tools/_skill_list_tool.py @@ -20,12 +20,22 @@ 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. + Notes: + This tool is used to list the tools declared by a specific skill. + It does not list the tools available to the agent. + It only lists the tools declared by the skill. + It scope is "skill_declared_tools_only". """ repository: Optional[BaseSkillRepository] = tool_context.agent_context.get_metadata(SKILL_REPOSITORY_KEY) if repository is None: @@ -33,5 +43,10 @@ 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, + }