Skip to content

Commit 4f2b167

Browse files
raychen911weimch
authored andcommitted
feature: 将在 skill 中定义 tool 的特性独立出来
- 修改skill_list_tools 的函数注释,避免模型理解歧义 - 对于在 skill 中指定 tool 的特性独立为单独的类
1 parent 1aa44cf commit 4f2b167

13 files changed

Lines changed: 209 additions & 81 deletions

File tree

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
.idea
22
.vscode
33
.DS_Store
4+
.cursor
45
*.lock
56
*.log
67
examples/*.log

‎docs/mkdocs/en/skill.md‎

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ Background references:
1515
- 🔎 Overview injection (name + description) to guide selection
1616
- 📥 `skill_load` fetches `SKILL.md` body and selected documentation on demand, automatically loading tools defined in the skill
1717
- 📋 `skill_list` lists all available skill names
18-
- 🔧 `skill_list_tools` lists tool names defined in a specified skill's `SKILL.md`
19-
- ⚙️ `skill_select_tools` dynamically selects skill tools (add/replace/clear modes) for token optimization
18+
- 🔧 `skill_list_tools` lists tool names defined in a specified skill's `SKILL.md` (dynamic tool loading only)
19+
- ⚙️ `skill_select_tools` dynamically selects skill tools for token optimization (dynamic tool loading only)
2020
- 📚 `skill_select_docs` adds/replaces/clears documentation
2121
- 🧾 `skill_list_docs` lists available documentation
2222
- 🏃 `skill_run` executes commands and returns stdout/stderr and output files
@@ -149,24 +149,18 @@ The `INSTRUCTION` should include complete skill usage workflow guidance:
149149
INSTRUCTION = """
150150
You are an AI assistant with access to Agent Skills.
151151
152-
## Complete Skill Workflow
152+
## Standard Skill Workflow
153153
154154
When handling user requests:
155155
156156
1. **Discover** → Call skill_list() to see available skills
157-
2. **Inspect** → Call skill_list_tools(skill_name="...") to preview tools
158-
3. **Load** → Call skill_load(skill_name="...") to load the skill
159-
4. **Optimize** → Call skill_select_tools(...) to select only needed tools (saves tokens)
160-
5. **Document** → Call skill_list_docs(...) and skill_select_docs(...) if more info needed
161-
6. **Execute** → Call skill_run(...) to execute commands or use skill's tools directly
162-
163-
Example Complete Flow:
164-
User: "What's the weather in Beijing?"
165-
→ skill_list() → see "weather-tools"
166-
→ skill_list_tools(skill_name="weather-tools") → see available tools
167-
→ skill_load(skill_name="weather-tools") → load full content
168-
→ skill_select_tools(skill_name="weather-tools", tools=["get_current_weather"]) → optimize
169-
→ get_current_weather(city="Beijing") → execute
157+
2. **Load** → Call skill_load(skill_name="...") to load the skill
158+
3. **Document** → Call skill_list_docs(...) and skill_select_docs(...) if more info is needed
159+
4. **Execute** → Call skill_run(...) to execute commands
160+
161+
Use SkillToolSetWithDynamicTools together with DynamicSkillToolSet when the
162+
agent must inspect and selectively expose a skill's business tools. That setup
163+
also provides skill_list_tools and skill_select_tools.
170164
171165
Always use environment variables in commands:
172166
- $WORKSPACE_DIR, $SKILLS_DIR, $WORK_DIR, $OUTPUT_DIR, $RUN_DIR, $SKILL_NAME
@@ -176,14 +170,15 @@ Always use environment variables in commands:
176170
Key points:
177171
- **Automatic tool registration**: The following tools are automatically registered via `SkillToolSet`, requiring no manual wiring:
178172
- `skill_list`: Lists all available skills
179-
- `skill_list_tools`: Lists tools of a skill
180173
- `skill_load`: Loads skill content
181-
- `skill_select_tools`: Selects specific tools (token optimization)
182174
- `skill_list_docs`: Lists available documentation
183175
- `skill_select_docs`: Selects specific documentation
184176
- `skill_run`: Executes skill commands
177+
- **Dynamic tool management**: `SkillToolSetWithDynamicTools` additionally registers
178+
`skill_list_tools` and `skill_select_tools`. Use it with `DynamicSkillToolSet`;
179+
the standard `SkillToolSet` intentionally does not expose these two tools.
185180
- **Intelligent prompt guidance**: Explicitly describe the workflow in the prompt to guide the LLM to call tools in the correct order
186-
- **Token optimization**: Use `skill_select_tools` to load only the needed tools, significantly reducing context size
181+
- **Token optimization**: In the dynamic setup, use `skill_select_tools` to load only the needed tools and reduce context size
187182
- **Code location**:
188183
- Package entry (aggregated exports): [trpc_agent_sdk/skills/tools/__init__.py](../../../trpc_agent_sdk/skills/tools/__init__.py)
189184
- `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.
550545

551546
**Declaration location**: [trpc_agent_sdk/skills/tools/_skill_list_tool.py](../../../trpc_agent_sdk/skills/tools/_skill_list_tool.py)
552547

548+
**Availability**:
549+
- This tool is exposed by `SkillToolSetWithDynamicTools`, not by the standard `SkillToolSet`.
550+
- Use it together with `DynamicSkillToolSet`, which resolves the selected business tools from the tool pool:
551+
552+
```python
553+
from trpc_agent_sdk.skills import DynamicSkillToolSet
554+
from trpc_agent_sdk.skills import SkillToolSetWithDynamicTools
555+
556+
skill_tool_set = SkillToolSetWithDynamicTools(repository=repository)
557+
dynamic_tool_set = DynamicSkillToolSet(
558+
skill_repository=repository,
559+
available_tools=available_tools,
560+
only_active_skills=True,
561+
)
562+
563+
agent = LlmAgent(
564+
# ...
565+
tools=[skill_tool_set, dynamic_tool_set],
566+
skill_repository=repository,
567+
)
568+
```
569+
553570
**Input parameters**:
554571
- `skill_name` (required): Skill name
555572

@@ -638,7 +655,7 @@ Overview
638655
**Behavior**:
639656
- Optimizes LLM context: activates only the tools needed for the current conversation
640657
- Updates the `temp:skill:tools:<name>` session key
641-
- When used with `DynamicSkillToolSet`, only selected tools are loaded into the LLM context
658+
- `SkillToolSetWithDynamicTools` exposes this selection tool, while `DynamicSkillToolSet` loads only the selected tools into the LLM context
642659

643660
**Prompt guidance**:
644661

@@ -1330,7 +1347,7 @@ By declaring which tools a skill needs through the **Tools section in SKILL.md**
13301347
|------|------|------|
13311348
| **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` |
13321349
| **SKILL.md `Tools:` section** | Optional, used only for informational display | **Core mechanism** that determines which tools are loaded into the LLM context |
1333-
| **Required components** | Only `SkillToolSet` | `SkillToolSet` + `DynamicSkillToolSet` (used together) |
1350+
| **Required components** | Only `SkillToolSet` | `SkillToolSetWithDynamicTools` + `DynamicSkillToolSet` (used together) |
13341351
| **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 |
13351352
| **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** |
13361353
| **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
14241441

14251442
#### 3. Configure the Agent
14261443

1427-
**File**: `agent/tools/_dynamic.py` and `agent/agent.py`
1444+
**File**: `agent/tools/_skill_tools.py`, `agent/tools/_dynamic.py`, and `agent/agent.py`
14281445

14291446
```python
1447+
# agent/tools/_skill_tools.py
1448+
from trpc_agent_sdk.skills import SkillToolSetWithDynamicTools
1449+
1450+
def create_skill_tool_set(repository):
1451+
return SkillToolSetWithDynamicTools(repository=repository)
1452+
14301453
# agent/tools/_dynamic.py
14311454
from trpc_agent_sdk.tools import FunctionTool
14321455
from trpc_agent_sdk.skills import DynamicSkillToolSet, BaseSkillRepository

‎docs/mkdocs/zh/skill.md‎

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ Agent Skills 将可重用的工作流打包为包含 `SKILL.md` 规范文件以
1515
- 🔎 概览注入(名称 + 描述)以指导选择
1616
- 📥 `skill_load` 按需拉取 `SKILL.md` 主体和选定的文档,自动加载技能中定义的工具
1717
- 📋 `skill_list` 列出所有可用的技能名称
18-
- 🔧 `skill_list_tools` 列出指定技能在 `SKILL.md` 中定义的工具名称
19-
- ⚙️ `skill_select_tools` 动态选择技能的工具(add/replace/clear 模式),实现 token 优化
18+
- 🔧 `skill_list_tools` 列出指定技能在 `SKILL.md` 中定义的工具名称(仅用于动态工具加载)
19+
- ⚙️ `skill_select_tools` 动态选择技能的工具,实现 token 优化(仅用于动态工具加载)
2020
- 📚 `skill_select_docs` 添加/替换/清除文档
2121
- 🧾 `skill_list_docs` 列出可用文档
2222
- 🏃 `skill_run` 执行命令,返回 stdout/stderr 和输出文件
@@ -149,24 +149,18 @@ agent = LlmAgent(
149149
INSTRUCTION = """
150150
You are an AI assistant with access to Agent Skills.
151151
152-
## Complete Skill Workflow
152+
## Standard Skill Workflow
153153
154154
When handling user requests:
155155
156156
1. **Discover** → Call skill_list() to see available skills
157-
2. **Inspect** → Call skill_list_tools(skill_name="...") to preview tools
158-
3. **Load** → Call skill_load(skill_name="...") to load the skill
159-
4. **Optimize** → Call skill_select_tools(...) to select only needed tools (saves tokens)
160-
5. **Document** → Call skill_list_docs(...) and skill_select_docs(...) if more info needed
161-
6. **Execute** → Call skill_run(...) to execute commands or use skill's tools directly
162-
163-
Example Complete Flow:
164-
User: "What's the weather in Beijing?"
165-
→ skill_list() → see "weather-tools"
166-
→ skill_list_tools(skill_name="weather-tools") → see available tools
167-
→ skill_load(skill_name="weather-tools") → load full content
168-
→ skill_select_tools(skill_name="weather-tools", tools=["get_current_weather"]) → optimize
169-
→ get_current_weather(city="Beijing") → execute
157+
2. **Load** → Call skill_load(skill_name="...") to load the skill
158+
3. **Document** → Call skill_list_docs(...) and skill_select_docs(...) if more info is needed
159+
4. **Execute** → Call skill_run(...) to execute commands
160+
161+
如果 Agent 需要查看并按需暴露 Skill 的业务工具,请组合使用
162+
SkillToolSetWithDynamicTools 和 DynamicSkillToolSet。该配置还会提供
163+
skill_list_tools 和 skill_select_tools。
170164
171165
Always use environment variables in commands:
172166
- $WORKSPACE_DIR, $SKILLS_DIR, $WORK_DIR, $OUTPUT_DIR, $RUN_DIR, $SKILL_NAME
@@ -176,14 +170,15 @@ Always use environment variables in commands:
176170
关键点:
177171
- **工具自动注册**:通过 `SkillToolSet` 自动注册以下工具,无需手动连接:
178172
- `skill_list`:列出所有可用技能
179-
- `skill_list_tools`:列出技能的工具
180173
- `skill_load`:加载技能内容
181-
- `skill_select_tools`:选择特定工具(优化 token)
182174
- `skill_list_docs`:列出可用文档
183175
- `skill_select_docs`:选择特定文档
184176
- `skill_run`:执行技能命令
177+
- **动态工具管理**:`SkillToolSetWithDynamicTools` 会额外注册
178+
`skill_list_tools` 和 `skill_select_tools`。它需要与 `DynamicSkillToolSet`
179+
配合使用;普通 `SkillToolSet` 不会暴露这两个工具。
185180
- **智能提示指导**:在提示词中明确说明工作流程,引导 LLM 按正确顺序调用工具
186-
- **Token 优化**:通过 `skill_select_tools` 仅加载需要的工具,显著减少上下文大小
181+
- **Token 优化**:在动态工具配置中,通过 `skill_select_tools` 仅加载需要的工具,显著减少上下文大小
187182
- **代码位置**:
188183
- 工具包入口(聚合导出):[trpc_agent_sdk/skills/tools/__init__.py](../../../trpc_agent_sdk/skills/tools/__init__.py)
189184
- `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.
549544

550545
**声明位置**:[trpc_agent_sdk/skills/tools/_skill_list_tool.py](../../../trpc_agent_sdk/skills/tools/_skill_list_tool.py)
551546

547+
**可用范围**:
548+
- 该工具由 `SkillToolSetWithDynamicTools` 暴露,普通 `SkillToolSet` 不包含它。
549+
- 它应与 `DynamicSkillToolSet` 配合使用,后者负责从工具池中解析并加载选中的业务工具:
550+
551+
```python
552+
from trpc_agent_sdk.skills import DynamicSkillToolSet
553+
from trpc_agent_sdk.skills import SkillToolSetWithDynamicTools
554+
555+
skill_tool_set = SkillToolSetWithDynamicTools(repository=repository)
556+
dynamic_tool_set = DynamicSkillToolSet(
557+
skill_repository=repository,
558+
available_tools=available_tools,
559+
only_active_skills=True,
560+
)
561+
562+
agent = LlmAgent(
563+
# ...
564+
tools=[skill_tool_set, dynamic_tool_set],
565+
skill_repository=repository,
566+
)
567+
```
568+
552569
**输入参数**:
553570
- `skill_name`(必需):技能名称
554571

@@ -637,7 +654,7 @@ Overview
637654
**功能行为**:
638655
- 优化 LLM 上下文:仅激活当前对话需要的工具
639656
- 更新 `temp:skill:tools:<name>` 会话键
640-
- 与 `DynamicSkillToolSet` 配合使用时,只有选中的工具会被加载到 LLM 上下文
657+
- `SkillToolSetWithDynamicTools` 暴露该选择工具,`DynamicSkillToolSet` 仅将选中的工具加载到 LLM 上下文
641658

642659
**提示词指导**:
643660

@@ -1329,7 +1346,7 @@ LLM 调用对应的工具:get_current_weather(city="Beijing")
13291346
|------|------|------|
13301347
| **工具暴露方式** | 所有工具在 Agent 创建时**全部注入** LLM 上下文 | 初始无业务工具,`skill_load` 后才根据 SKILL.md 的 `Tools:` 声明**按需注入** |
13311348
| **SKILL.md `Tools:` 部分** | 可选,仅用于信息展示 | **核心机制**,决定哪些工具会被加载到 LLM 上下文 |
1332-
| **所需组件** | 仅 `SkillToolSet` | `SkillToolSet` + `DynamicSkillToolSet`(两者配合) |
1349+
| **所需组件** | 仅 `SkillToolSet` | `SkillToolSetWithDynamicTools` + `DynamicSkillToolSet`(两者配合) |
13331350
| **工具注册方式** | 工具直接挂在 Agent 的 `tools` 列表中 | 工具放入 `available_tools` 工具池,通过 SKILL.md 声明式过滤 |
13341351
| **Token 消耗** | 固定消耗(所有工具定义常驻上下文) | 按需消耗(仅加载激活 skill 声明的工具),**工具多时节省 85-95%** |
13351352
| **工具可见性控制** | 无,LLM 始终看到所有工具 | 精细控制,可通过 `skill_select_tools` 动态增减 |
@@ -1423,9 +1440,15 @@ Example 4: Ask someone name information
14231440

14241441
#### 3. 配置 Agent
14251442

1426-
**文件**: `agent/tools/_dynamic.py` 和 `agent/agent.py`
1443+
**文件**: `agent/tools/_skill_tools.py`、`agent/tools/_dynamic.py` 和 `agent/agent.py`
14271444

14281445
```python
1446+
# agent/tools/_skill_tools.py
1447+
from trpc_agent_sdk.skills import SkillToolSetWithDynamicTools
1448+
1449+
def create_skill_tool_set(repository):
1450+
return SkillToolSetWithDynamicTools(repository=repository)
1451+
14291452
# agent/tools/_dynamic.py
14301453
from trpc_agent_sdk.tools import FunctionTool
14311454
from trpc_agent_sdk.skills import DynamicSkillToolSet, BaseSkillRepository

‎examples/fastapi_server/_runner_manager.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def new_session_id() -> str:
8888

8989
async def close(self) -> None:
9090
"""Gracefully close the runner and release resources."""
91-
self._runner.close()
91+
await self._runner.close()
9292
logger.info("RunnerManager closed: app=%s", self.app_name)
9393

9494
# ------------------------------------------------------------------

‎examples/skills/agent/tools.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ def create_skill_tool_set(is_link_stager: bool = True, use_cached_repository: bo
5454
workspace_runtime = _create_workspace_runtime(**workspace_runtime_args)
5555
skill_paths = _get_skill_paths()
5656
# use_cached_repository: Whether to use cached repository.
57-
repository = create_default_skill_repository(skill_paths, workspace_runtime=workspace_runtime,
57+
repository = create_default_skill_repository(skill_paths,
58+
workspace_runtime=workspace_runtime,
5859
use_cached_repository=use_cached_repository)
5960
skill_stager = LinkSkillStager() if is_link_stager else CopySkillStager()
6061
# skill_stager: The stager to use for staging skills.

‎examples/skills_with_dynamic_tools/agent/tools/_skill_tools.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from trpc_agent_sdk.skills import BaseSkillRepository
1515
from trpc_agent_sdk.skills import ENV_SKILLS_ROOT
1616
from trpc_agent_sdk.skills import SkillToolSet
17+
from trpc_agent_sdk.skills import SkillToolSetWithDynamicTools
1718
from trpc_agent_sdk.skills import create_default_skill_repository
1819

1920

@@ -62,4 +63,5 @@ def create_skill_tool_set(workspace_runtime_type: str = "local") -> tuple[SkillT
6263
**workspace_runtime_args)
6364
skill_paths = _get_skill_paths()
6465
repository = create_default_skill_repository(skill_paths, workspace_runtime=workspace_runtime)
65-
return SkillToolSet(repository=repository, run_tool_kwargs=tool_kwargs), repository
66+
67+
return SkillToolSetWithDynamicTools(repository=repository, run_tool_kwargs=tool_kwargs), repository

‎examples/team_with_skill/agent/prompts.py‎

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,16 @@
1313
1414
Mandatory execution order for every user request:
1515
1. Call `skill_list` and confirm `leader-research` exists.
16-
2. Call `skill_list_tools` for `leader-research`.
17-
3. Call `skill_load` for `leader-research`.
18-
4. Call `skill_run` with command:
16+
2. Call `skill_load` for `leader-research`.
17+
3. Call `skill_run` with command:
1918
`bash scripts/gather_points.sh "<user topic>" out/leader_notes.txt`
2019
and set `output_files` to include `out/leader_notes.txt`.
21-
5. Then delegate to `researcher` exactly once.
22-
6. Then delegate to `writer` exactly once.
23-
7. Synthesize and return final answer.
20+
4. Then delegate to `researcher` exactly once.
21+
5. Then delegate to `writer` exactly once.
22+
6. Synthesize and return final answer.
2423
2524
Rules:
26-
- Never call `delegate_to_member` before step 4 succeeds.
25+
- Never call `delegate_to_member` before step 3 succeeds.
2726
- Use current-year context in final answer.
2827
- Keep the final answer concise and practical.
2928
"""

‎tests/skills/test_toolset.py‎

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,16 @@
77
88
Covers:
99
- SkillToolSet initialization
10-
- SkillToolSet.get_tools: returns expected tool set
10+
- SkillToolSet.get_tools: default set omits dynamic tool-selection helpers
11+
- SkillToolSetWithDynamicTools.get_tools: opt-in skill_list_tools / skill_select_tools
1112
- repository property
1213
"""
1314

1415
from __future__ import annotations
1516

16-
from unittest.mock import AsyncMock, MagicMock, patch
17-
18-
import pytest
17+
from unittest.mock import MagicMock
1918

19+
from trpc_agent_sdk.skills._dynamic_toolset import SkillToolSetWithDynamicTools
2020
from trpc_agent_sdk.skills._toolset import SkillToolSet
2121

2222

@@ -63,12 +63,34 @@ async def test_get_tools_includes_function_tools(self, tmp_path):
6363
assert "skill_load" in tool_names
6464
assert "skill_list" in tool_names
6565
assert "skill_list_docs" in tool_names
66-
assert "skill_list_tools" in tool_names
6766
assert "skill_select_docs" in tool_names
68-
assert "skill_select_tools" in tool_names
67+
assert "skill_list_tools" not in tool_names
68+
assert "skill_select_tools" not in tool_names
6969

7070
async def test_get_tools_sets_metadata(self, tmp_path):
7171
ts = SkillToolSet(paths=[str(tmp_path)])
7272
ctx = _make_ctx()
7373
await ts.get_tools(ctx)
7474
ctx.agent_context.with_metadata.assert_called()
75+
76+
77+
class TestSkillToolSetWithDynamicTools:
78+
async def test_get_tools_includes_dynamic_selection_helpers(self, tmp_path):
79+
ts = SkillToolSetWithDynamicTools(paths=[str(tmp_path)])
80+
ctx = _make_ctx()
81+
tools = await ts.get_tools(ctx)
82+
tool_names = [t.name for t in tools]
83+
assert "skill_load" in tool_names
84+
assert "skill_list" in tool_names
85+
assert "skill_list_tools" in tool_names
86+
assert "skill_select_tools" in tool_names
87+
88+
async def test_get_tools_does_not_duplicate_helpers_on_second_call(self, tmp_path):
89+
ts = SkillToolSetWithDynamicTools(paths=[str(tmp_path)])
90+
ctx = _make_ctx()
91+
first = [t.name for t in await ts.get_tools(ctx)]
92+
second = [t.name for t in await ts.get_tools(ctx)]
93+
assert first.count("skill_list_tools") == 1
94+
assert first.count("skill_select_tools") == 1
95+
assert second.count("skill_list_tools") == 1
96+
assert second.count("skill_select_tools") == 1

0 commit comments

Comments
 (0)