Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.idea
.vscode
.DS_Store
.cursor
*.lock
*.log
examples/*.log
Expand Down
67 changes: 45 additions & 22 deletions docs/mkdocs/en/skill.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -638,7 +655,7 @@ Overview
**Behavior**:
- Optimizes LLM context: activates only the tools needed for the current conversation
- Updates the `temp:skill:tools:<name>` 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**:

Expand Down Expand Up @@ -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 |
Expand Down Expand Up @@ -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
Expand Down
67 changes: 45 additions & 22 deletions docs/mkdocs/zh/skill.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 和输出文件
Expand Down Expand Up @@ -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
Expand All @@ -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)(其余工具见下文各节「声明位置」)
Expand Down Expand Up @@ -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`(必需):技能名称

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

**提示词指导**:

Expand Down Expand Up @@ -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` 动态增减 |
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion examples/fastapi_server/_runner_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

问题: 本变更同时修复了 RunnerManager.close() 未 await 的问题(self._runner.close() → await self._runner.close()),经核对 Runner.close 为 async(runners.py:948),修复本身正确;但该修复与"支持关闭skill_list_tools"特性无关,混入同一提交使针对排除功能的回归定位/回滚范围扩大。

触发条件: 需要单独回滚或二分定位 excluded_tools 相关行为时。

实际影响: 无法独立回退关闭工具特性而不连带撤销该关闭相关的修补;提交范围混杂增大审查与维护成本。

修正方向: 将 FastAPI 示例的 close 修复拆分到独立提交(或独立 PR),保持特性提交的单一职责。

logger.info("RunnerManager closed: app=%s", self.app_name)

# ------------------------------------------------------------------
Expand Down
3 changes: 2 additions & 1 deletion examples/skills/agent/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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
13 changes: 6 additions & 7 deletions examples/team_with_skill/agent/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<user topic>" 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.
"""
Expand Down
34 changes: 28 additions & 6 deletions tests/skills/test_toolset.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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
Loading
Loading