Skip to content

feature: 支持关闭skill_list_tools tool - #323

Open
raychen911 wants to merge 1 commit into
mainfrom
feature/close_skill_list_tools
Open

feature: 支持关闭skill_list_tools tool#323
raychen911 wants to merge 1 commit into
mainfrom
feature/close_skill_list_tools

Conversation

@raychen911

Copy link
Copy Markdown
Contributor
  • 修改skill_list_tools 的函数注释,避免模型理解歧义
  • 支持关闭skill_list_tools

@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

审查结论

通过

审查范围:f05797d..ccb4f8f 单个提交(feature: 支持关闭 skill_list_tools tool),5 个文件 +74/-25:SkillToolSet 新增 excluded_tools 参数、_exclude_tools 过滤方法与 get_tools 结果缓存 self._default_toolsskill_list_tools 返回结构增加 skill_name/scope/note 字段;examples/skills 示例启用排除;team_with_skill 提示词同步移除 skill_list_tools 步骤并重新编号;测试更新返回结构断言。

计划符合性:核心需求已实现——excluded_tools=["skill_list_tools"]FunctionTool 包装 skill_list_tools 函数得到的工具名一致,过滤生效;不传参数时默认行为与基线一致;skill_list_tools 返回结构变更为向后兼容的增量字段;示例与提示词改动内部自洽(步骤重编号正确,examples/skills 提示词不再引用该工具)。

主要风险:1) get_tools 新增缓存无失效、无同步,运行期通过 SkillRegistry 单例 register/unregister/clear 的技能函数在首次请求后被冻结(新注册不出现、已注销仍暴露),多线程并发首次调用可能产生重复工具列表;2) excluded_tools 仅在工具集层过滤,与 SkillsRequestProcessor 注入的系统提示词引导脱节,排除引导中点名的工具(skill_runskill_select_tools 等)会导致 LLM 调用不存在的工具并收到 tool_not_found 错误;3) skill_list_tools 将"技能不存在"与"技能未声明工具"混为同一响应,新增 note 的断言在 not-found 路径上不成立,可能误导模型;4) 提交主功能(排除与缓存)零测试覆盖。

测试充分性:test_skill_list_tool.py 覆盖了新返回结构的成功、未找到、无仓库、无工具路径,与实现一致;但 excluded_tools_exclude_tools 和缓存行为无任何用例。

门禁结论:未发现阻止合入的正确性/安全/数据损坏级缺陷,状态为 PASSED,附 2 条 MODERATE 与 2 条 LOW 评论。

发现的问题

中等

trpc_agent_sdk/skills/_toolset.py:175-193

问题: get_tools 新增的 self._default_tools 结果缓存没有任何失效机制,也未做线程同步;首次调用构建后工具列表即被永久冻结,与该方法文档字符串 "Get all tools from registered skills" 的契约不符。

触发条件: 任意一次 get_tools 完成缓存填充(首次 LLM 请求的 process_llm_request 即触发)之后,运行期通过 SkillRegistry 单例的 register/unregister/clear 变更技能函数(SkillRegistry() 与模块级 SKILL_REGISTRY 是同一单例),或调用方追加传入的 runtime_tools 列表;此外多线程并发执行首次 get_tools 时,"检查为空—构建—extend" 序列会交错执行。

实际影响: 后续所有请求持续返回旧列表:新注册的技能函数永远不会暴露给 LLM,已注销或被 clear 的技能函数继续暴露;并发首次调用还会把构建结果重复 extend 进缓存,使后续请求携带同名重复工具,LLM 请求出现重复 function declaration 并可能被模型接口拒绝。变更前 get_tools 每次重新执行 SKILL_REGISTRY.get_all(),不存在上述问题。

修正方向: 为缓存增加失效条件(例如在 SkillRegistry 变更时递增版本号并在 get_tools 中比对,或只缓存静态内置工具、每次调用重新解析注册表函数),并将 self._default_tools.extend(tools) 改为原子赋值 self._default_tools = tools 或加锁,消除并发重复写入。

中等

trpc_agent_sdk/skills/_toolset.py:83-84

问题: 新增的 excluded_tools 只在 SkillToolSet.get_tools 一层过滤工具,与 SkillsRequestProcessor/SkillProfileFlags 生成系统提示词引导的既有机制完全脱节,二者之间没有任何信息传递。

触发条件: 用户按参数定义排除任何被引导文案点名的内置工具(例如 excluded_tools=["skill_run"]["skill_select_tools"]["skill_list_skills"]),同时 Agent 按示例标准接法设置了 skill_repository,使 SkillsRequestProcessor 以默认 full profile 注入引导。

实际影响: trpc_agent_sdk/agents/core/_skill_processor.py_tooling_guidance_text_default_full_tooling_and_workspace_guidance 仍会指示 LLM 使用已被排除的工具(如 "Use the skill_select_tools tool..." 以及大量 skill_run/skill_exec 指引),LLM 随后调用不存在的工具,触发 tool_not_found 错误事件,浪费对话轮次甚至导致任务失败。变更前工具无法从工具集中移除,引导不会指向不存在的工具。

修正方向: 将排除信息同步进技能配置/profile 机制,例如构造 SkillsRequestProcessor 时从 SkillToolSet 读取 excluded_tools 并并入 forbidden_tools/SkillProfileFlags 解析,或在参数文档中明确 excluded_tools 仅适用于引导文案未点名的工具,保证引导与实际可用工具集一致。

较低

trpc_agent_sdk/skills/tools/_skill_list_tool.py:42-48

问题: skill_list_tools 对"技能不存在"与"技能未声明工具"两种情况返回完全相同的载荷(available_tools 为空且携带同一段 note),而本次新增的文档字符串和 note 断言 "An empty result means that this skill declares no tools",该断言在技能不存在的路径上不成立。

触发条件: LLM 或调用方传入拼写错误/不存在的 skill_name(如 "leader-researchx"),repository.get 返回 None,代码仅记录 logger.error 后以空列表落入共享返回结构。

实际影响: 模型收到 skill_name 回显、空 available_tools 和 "Only tools declared by this skill are listed..." 的说明,会把"技能不存在"误读为"该技能存在但未声明工具",在后续推理中得出错误结论(例如向用户报告技能没有工具而不是技能不存在),与本次变更想澄清返回语义的目标相悖。

修正方向: 在返回结构中区分两种情况,例如增加 found/status 字段,技能不存在时返回明确的 "skill not found" 提示;或保留命中路径才附加 note 的区分逻辑。

较低

trpc_agent_sdk/skills/_toolset.py:196-206

问题: 本次提交的核心功能——excluded_tools 参数、_exclude_tools 过滤逻辑和 _default_tools 缓存——没有任何测试覆盖。

触发条件: 运行现有测试套件即可确认:tests/skills/test_toolset.py 未新增用例且只断言默认工具存在,tests/skills/tools/test_skill_list_tool.py 仅断言返回结构,没有任何用例构造带 excluded_toolsSkillToolSet

实际影响: 排除名称拼写错误、过滤逻辑回归(例如误删具有合法名称的工具)或缓存行为破坏都不会被测试发现;"支持关闭 skill_list_tools" 这一提交主目标本身处于未验证状态。

修正方向:tests/skills/test_toolset.py 补充用例:默认包含 skill_list_tools;传入 excluded_tools=["skill_list_tools"] 后该工具被移除且其余工具保留;连续两次调用 get_tools 返回一致结果。

Comment on lines +175 to +193
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))
except Exception as ex: # pylint: disable=broad-except
# 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)

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.

问题: get_tools 新增的 self._default_tools 结果缓存没有任何失效机制,也未做线程同步;首次调用构建后工具列表即被永久冻结,与该方法文档字符串 "Get all tools from registered skills" 的契约不符。

触发条件: 任意一次 get_tools 完成缓存填充(首次 LLM 请求的 process_llm_request 即触发)之后,运行期通过 SkillRegistry 单例的 register/unregister/clear 变更技能函数(SkillRegistry() 与模块级 SKILL_REGISTRY 是同一单例),或调用方追加传入的 runtime_tools 列表;此外多线程并发执行首次 get_tools 时,"检查为空—构建—extend" 序列会交错执行。

实际影响: 后续所有请求持续返回旧列表:新注册的技能函数永远不会暴露给 LLM,已注销或被 clear 的技能函数继续暴露;并发首次调用还会把构建结果重复 extend 进缓存,使后续请求携带同名重复工具,LLM 请求出现重复 function declaration 并可能被模型接口拒绝。变更前 get_tools 每次重新执行 SKILL_REGISTRY.get_all(),不存在上述问题。

修正方向: 为缓存增加失效条件(例如在 SkillRegistry 变更时递增版本号并在 get_tools 中比对,或只缓存静态内置工具、每次调用重新解析注册表函数),并将 self._default_tools.extend(tools) 改为原子赋值 self._default_tools = tools 或加锁,消除并发重复写入。

Comment on lines +83 to 84
excluded_tools: Optional[List[str]] = None,
**run_tool_kwargs: dict[str, Any]):

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.

问题: 新增的 excluded_tools 只在 SkillToolSet.get_tools 一层过滤工具,与 SkillsRequestProcessor/SkillProfileFlags 生成系统提示词引导的既有机制完全脱节,二者之间没有任何信息传递。

触发条件: 用户按参数定义排除任何被引导文案点名的内置工具(例如 excluded_tools=["skill_run"]["skill_select_tools"]["skill_list_skills"]),同时 Agent 按示例标准接法设置了 skill_repository,使 SkillsRequestProcessor 以默认 full profile 注入引导。

实际影响: trpc_agent_sdk/agents/core/_skill_processor.py_tooling_guidance_text_default_full_tooling_and_workspace_guidance 仍会指示 LLM 使用已被排除的工具(如 "Use the skill_select_tools tool..." 以及大量 skill_run/skill_exec 指引),LLM 随后调用不存在的工具,触发 tool_not_found 错误事件,浪费对话轮次甚至导致任务失败。变更前工具无法从工具集中移除,引导不会指向不存在的工具。

修正方向: 将排除信息同步进技能配置/profile 机制,例如构造 SkillsRequestProcessor 时从 SkillToolSet 读取 excluded_tools 并并入 forbidden_tools/SkillProfileFlags 解析,或在参数文档中明确 excluded_tools 仅适用于引导文案未点名的工具,保证引导与实际可用工具集一致。

Comment on lines +42 to +48
else:
available_tools = list(skill.tools or [])
return {
"skill_name":
skill_name,
"available_tools":
available_tools,

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.

问题: skill_list_tools 对"技能不存在"与"技能未声明工具"两种情况返回完全相同的载荷(available_tools 为空且携带同一段 note),而本次新增的文档字符串和 note 断言 "An empty result means that this skill declares no tools",该断言在技能不存在的路径上不成立。

触发条件: LLM 或调用方传入拼写错误/不存在的 skill_name(如 "leader-researchx"),repository.get 返回 None,代码仅记录 logger.error 后以空列表落入共享返回结构。

实际影响: 模型收到 skill_name 回显、空 available_tools 和 "Only tools declared by this skill are listed..." 的说明,会把"技能不存在"误读为"该技能存在但未声明工具",在后续推理中得出错误结论(例如向用户报告技能没有工具而不是技能不存在),与本次变更想澄清返回语义的目标相悖。

修正方向: 在返回结构中区分两种情况,例如增加 found/status 字段,技能不存在时返回明确的 "skill not found" 提示;或保留命中路径才附加 note 的区分逻辑。

Comment on lines +196 to +206
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

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.

问题: 本次提交的核心功能——excluded_tools 参数、_exclude_tools 过滤逻辑和 _default_tools 缓存——没有任何测试覆盖。

触发条件: 运行现有测试套件即可确认:tests/skills/test_toolset.py 未新增用例且只断言默认工具存在,tests/skills/tools/test_skill_list_tool.py 仅断言返回结构,没有任何用例构造带 excluded_toolsSkillToolSet

实际影响: 排除名称拼写错误、过滤逻辑回归(例如误删具有合法名称的工具)或缓存行为破坏都不会被测试发现;"支持关闭 skill_list_tools" 这一提交主目标本身处于未验证状态。

修正方向:tests/skills/test_toolset.py 补充用例:默认包含 skill_list_tools;传入 excluded_tools=["skill_list_tools"] 后该工具被移除且其余工具保留;连续两次调用 get_tools 返回一致结果。

@raychen911
raychen911 force-pushed the feature/close_skill_list_tools branch from ccb4f8f to 9158446 Compare September 9, 2026 07:29
@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

审查结论

通过

审查范围:c98a61d..9158446 共 6 个变更文件(SDK 工具集 _toolset.pyskill_list_tools 实现、2 个示例、1 个测试文件、减号修复)。计划符合性:核心需求“支持关闭 skill_list_tools tool”已达成——新增 SkillToolSet(excluded_tools=[...]) 参数,get_tools/工具执行路径(_tools_processorfind_toolexecute_tools_async 每次经 get_tools 解析)均走统一过滤,examples/skills/agent/tools.py:62 已实际启用;examples/fastapi_server/_runner_manager.py 补上缺失的 await self._runner.close(),修复真实异步泄漏。主要风险:(1) get_tools 首次调用后把结果快照到 _default_tools 并永久缓存,SKILL_REGISTRY/热加载的后续变化不可见;(2) skill_list_tools 返回结构从 {"available_tools": [...]} 扩为带 skill_name/scope/note 的对象,与仓库内 docs/mkdocs/en|zh/skill.md 多处描述(“返回工具名数组”)不一致,存在对结果做相等断言的调用方破坏兼容;(3) 新增的 excluded_tools/_exclude_tools 无任何测试覆盖。测试充分性:仅 test_skill_list_tool.py 更新了返回结构断言,test_toolset.py 未覆盖排除路径与缓存语义,新参数测试缺口明显。门禁结论:无高危正确性/安全问题,功能达成且被示例验证,判定 PASSED;上述三项影响真实但有界,按 MODERATE/LOW 提交。

发现的问题

中等

trpc_agent_sdk/skills/_toolset.py:176-195

问题: get_tools 新增的一次性快照缓存:首次调用把 _load_tool/_run_tool/_exec_tool/_runtime_tools/SKILL_REGISTRY.get_all()(含 _function_tools)全部收起为列表并 self._default_tools.extend(tools),此后所有调用(包括每次 tool-call 执行经 find_tool/execute_tools_async 再次解析 get_tools)都返回 copy(),SKILL_REGISTRY 与热加载感知不到运行时(首个 get_tools 之后)的注册变化,且没有任何按 turn/会话刷新的机制。

触发条件: 任一宿主在首次 get_tools() 之后向 SKILL_REGISTRY 注册/注销技能函数(注册表 API 公开且 SKILL_REGISTRY 被注入到 agent_context metadata,注册入口可被宿主复用),或工具集实例生命周期内发生热加载变更。

实际影响: 新注册的技能工具在本进程生命周期内对模型不可见、已注销工具仍可见;同时 _default_tools 被延展引用,调用方篡改返回列表会污染后续快照(返回的是 copy 真列表但 append 相同元素),与旧实现每次重新扫描注册表的行为不一致。

修正方向: 删除 _default_tools 缓存,恢复每次 get_tools 全量重建(排除逻辑本身无状态);若确需缓存,应在注册表变更或热加载事件上使其失效,并保证增量只在 tools 局部进行,不要触碰已延展的实例成员。

中等

trpc_agent_sdk/skills/tools/_skill_list_tool.py:44-54

问题: skill_list_tools 的返回结构由 {"available_tools": [...]} 扩展为带 skill_name/scope/note 的固定四字段对象,改变了该工具的对外契约;仓库内 docs/mkdocs/en/skill.md(1612、1985、2028 行等)与 docs/mkdocs/zh/skill.md(1611、1983、2026 行等)仍以“返回工具名数组 ['get_current_weather', ...]”描述该工具,没有随变更更新。

触发条件: 任何按旧契约消费结果的调用方(例如把 result == {"available_tools": [...]}result["available_tools"] 下标或取值数组直接喂给模型的调用)运行包含该变更的版本。

实际影响: 相等性/类型断言调用方得到意外结果(新增键导致相等断言失败、LLM 收到非数组负载),提示词与文档会诱导模型按数组格式解析而实际拿到 dict,属于 source-breaking 的兼容性变化;本次仓库内无消费方,但 SDK 属对外发布库,外部影响真实存在。

修正方向: 同步更新两处 docs/mkdocs/*/skill.mdskill_list_tools 的返回示例/描述;若无法更新全部文档,可保留 available_tools 键并显式声明为兼容层,并在变更说明中标注破坏性行为。

较低

trpc_agent_sdk/skills/_toolset.py:83

问题: 本次提交为 SkillToolSet 新增 excluded_tools 参数与 _exclude_tools 过滤逻辑,但对应测试完全缺失:tests/skills/test_toolset.py 未对 excluded_tools 构造、过滤结果、name 缺失工具的跳过行为做任何断言,新增的缓存快照语义(_default_tools)与 _exclude_tools 直接unes 测。

触发条件: 后续迭代修改 _exclude_tools 的过滤条件或 get_tools 快照逻辑时,回归不会被任何用例捕获。

实际影响: 排除工具的过滤正确性、缓存行为无测试保障;例如排除名单写错工具名、name 缺失工具被丢弃等回归难以被发现。

修正方向:tests/skills/test_toolset.py 增加用例:构造 excluded_tools=["skill_list_tools"] 断言名字过滤生效;断言 get_tools 两次调用返回等长度列表且不含被排除工具;补充 nameNone 的工具被跳过的用例。

Comment on lines +176 to 195
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))
except Exception as ex: # pylint: disable=broad-except
# 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

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.

问题: get_tools 新增的一次性快照缓存:首次调用把 _load_tool/_run_tool/_exec_tool/_runtime_tools/SKILL_REGISTRY.get_all()(含 _function_tools)全部收起为列表并 self._default_tools.extend(tools),此后所有调用(包括每次 tool-call 执行经 find_tool/execute_tools_async 再次解析 get_tools)都返回 copy(),SKILL_REGISTRY 与热加载感知不到运行时(首个 get_tools 之后)的注册变化,且没有任何按 turn/会话刷新的机制。

触发条件: 任一宿主在首次 get_tools() 之后向 SKILL_REGISTRY 注册/注销技能函数(注册表 API 公开且 SKILL_REGISTRY 被注入到 agent_context metadata,注册入口可被宿主复用),或工具集实例生命周期内发生热加载变更。

实际影响: 新注册的技能工具在本进程生命周期内对模型不可见、已注销工具仍可见;同时 _default_tools 被延展引用,调用方篡改返回列表会污染后续快照(返回的是 copy 真列表但 append 相同元素),与旧实现每次重新扫描注册表的行为不一致。

修正方向: 删除 _default_tools 缓存,恢复每次 get_tools 全量重建(排除逻辑本身无状态);若确需缓存,应在注册表变更或热加载事件上使其失效,并保证增量只在 tools 局部进行,不要触碰已延展的实例成员。

Comment on lines +44 to +54
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.",
}

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.

问题: skill_list_tools 的返回结构由 {"available_tools": [...]} 扩展为带 skill_name/scope/note 的固定四字段对象,改变了该工具的对外契约;仓库内 docs/mkdocs/en/skill.md(1612、1985、2028 行等)与 docs/mkdocs/zh/skill.md(1611、1983、2026 行等)仍以“返回工具名数组 ['get_current_weather', ...]”描述该工具,没有随变更更新。

触发条件: 任何按旧契约消费结果的调用方(例如把 result == {"available_tools": [...]}result["available_tools"] 下标或取值数组直接喂给模型的调用)运行包含该变更的版本。

实际影响: 相等性/类型断言调用方得到意外结果(新增键导致相等断言失败、LLM 收到非数组负载),提示词与文档会诱导模型按数组格式解析而实际拿到 dict,属于 source-breaking 的兼容性变化;本次仓库内无消费方,但 SDK 属对外发布库,外部影响真实存在。

修正方向: 同步更新两处 docs/mkdocs/*/skill.mdskill_list_tools 的返回示例/描述;若无法更新全部文档,可保留 available_tools 键并显式声明为兼容层,并在变更说明中标注破坏性行为。

runtime_tools: Optional[List[ToolABC]] = None,
skill_stager: Optional[Stager] = None,
skill_config: Optional[dict[str, Any]] = None,
excluded_tools: Optional[List[str]] = None,

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.

问题: 本次提交为 SkillToolSet 新增 excluded_tools 参数与 _exclude_tools 过滤逻辑,但对应测试完全缺失:tests/skills/test_toolset.py 未对 excluded_tools 构造、过滤结果、name 缺失工具的跳过行为做任何断言,新增的缓存快照语义(_default_tools)与 _exclude_tools 直接unes 测。

触发条件: 后续迭代修改 _exclude_tools 的过滤条件或 get_tools 快照逻辑时,回归不会被任何用例捕获。

实际影响: 排除工具的过滤正确性、缓存行为无测试保障;例如排除名单写错工具名、name 缺失工具被丢弃等回归难以被发现。

修正方向:tests/skills/test_toolset.py 增加用例:构造 excluded_tools=["skill_list_tools"] 断言名字过滤生效;断言 get_tools 两次调用返回等长度列表且不含被排除工具;补充 nameNone 的工具被跳过的用例。

- 修改skill_list_tools 的函数注释,避免模型理解歧义
- 支持关闭skill_list_tools
@raychen911
raychen911 force-pushed the feature/close_skill_list_tools branch from 9158446 to 503e884 Compare September 10, 2026 02:21
@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

审查结论

不通过

审查范围:c98a61d..503e884(feature: 支持关闭skill_list_tools tool),6 个文件 76+/26-,含 SDK 核心(SkillToolSet、skill_list_tools)、示例与测试。计划符合性:核心特性(skill_list_tools 可通过 excluded_tools 参数关闭、返回结构增加 skill_name/scope/note 澄清信息、team_with_skill 提示词移除该步骤、FastAPI 示例修复未 await 的 close)均已实现且测试更新通过。主要风险:为支持排除功能在 SkillToolSet.get_tools 引入的实例级缓存(_default_tools)改变了既有语义——SKILL_REGISTRY 快照被首次调用冻结(运行期注册/注销不再生效)、返回列表被冻结导致子代理(BorrowedToolSet)与多上下文场景共享同一批工具而元数据按上下文重写、首次调用返回内部活列表存在别名风险。_exclude_tools 会把 name 为空/None 的工具静默丢弃(配置了排除时),且按名匹配会误删同名已注册技能函数。特性落地不一致:排除仅在本示例(examples/skills/agent/tools.py)生效,而 skills_with_dynamic_tools 提示词仍强制要求调用该工具且未排除、team_with_skill 提示词删除步骤但工具集仍暴露、docs/mkdocs 中英文文档仍描述旧的"数组返回"与"必须调用"工作流。核心新特性(excluded_tools 与缓存路径)完全没有测试。fastapi_server/_runner_manager.py 的 await 修复经核对为正确修复(Runner.close 为 async)。7 条评论全部锚定本次变更文件、与变更后行范围相交;前 3 条为 SEVERE(缓存语义回归、返回活列表别名、子代理上下文元数据错配),第 4-5 条 MODERATE,第 6-7 条 LOW,存在 SEVERE 故 state 为 FAILED。

发现的问题

严重

trpc_agent_sdk/skills/_toolset.py:184-195

问题: 新增的 _default_tools 缓存把 SKILL_REGISTRY.get_all() 的快照冻结在首次 get_tools() 调用(第 184 行),此后所有调用都命中 return self._default_tools.copy()(第 176-177 行)跳过重新读取。变更前每次调用都重新读取进程级单例 SKILL_REGISTRY_registry.py:25SkillRegistry(SingletonBase)),运行期 register()/unregister() 会即时反映;现在注册晚于首次调用的技能函数永远不会出现在工具列表中,也没有任何失效/刷新路径。

触发条件: 任何在首个 get_tools() 调用之后调用 SKILL_REGISTRY.register(...)/unregister(...)(公开导出 API,skills/__init__.py:143)的场景,例如动态插件加载、运行期装载技能,或使用同一 SkillToolSet 实例的团队/多子代理流程。

实际影响: 新注册技能的工具对模型静默缺失,注销也不再生效,工具集内容在整个进程生命周期内陈旧,且无任何告警;这是本次变更引入的行为回归(变更前每次调用都重新求值)。

修正方向: 移除缓存,改为在每次调用时执行排除过滤后直接返回;若确需缓存,则为缓存增加显式失效机制(如监听 SKILL_REGISTRY 变更或提供 invalidate() API),并保证排除逻辑不依赖缓存即可生效。

严重

trpc_agent_sdk/skills/_toolset.py:176-195

问题: 首次调用路径 return tools(第 195 行)返回的正是同时存入 _default_tools 的同一活列表(self._default_tools.extend(tools) 第 194 行),而后续调用返回 self._default_tools.copy()(第 177 行),返回对象身份语义不一致:任何调用方对首次调用的返回值做就地修改(append/remove/extend/sort)都会永久污染缓存,后续所有调用返回受损工具列表。

触发条件: 任何对 get_tools() 首次返回值执行就地变更的调用方。现有内置调用方(_tool_adapter.py:104_claude_agent.py:501GovernedSkillToolSet 列表推导)均只读,但缓存命中路径返回副本、未命中路径返回原列表的不对称性构成未文档化的契约陷阱,且无任何测试断言两次调用的结果一致性。

实际影响: 未来调用方(或对首调用结果做过滤/裁剪的集成代码)一旦就地修改,缓存即被污染,LLM 请求中出现重复或伪造的工具声明(重复函数声明可能导致模型拒绝请求或歧义工具调用),问题难以排查。

修正方向: 首次调用同样返回 self._default_tools.copy()(或构建到局部列表后 self._default_tools = tools 再返回 tools.copy()),保证所有路径返回的都是缓存副本。

严重

trpc_agent_sdk/skills/_toolset.py:168-194

问题: 工具列表缓存(_default_tools)是实例级、与上下文无关的,但仓库/元数据按上下文重写(第 168-175 行每次调用都执行):agents/sub_agent/_runner.py:59_BorrowedToolSet.get_tools() 把父代理共享的 SkillToolSet 转发给子代理,子代理用不同的 invocation_context 调用同一个实例,缓存命中后返回的是首次调用(父代理上下文)构建的同一批 FunctionToolSkillLoadTool/SkillRunTool;而 skill_list_tools_skill_list_tool.py:35-37)随后从元数据缺失 SKILL_REPOSITORY_KEY 的上下文读取仓库时抛 ValueError("repository not found")。变更前每次调用都重建工具,不会出现跨上下文复用。

触发条件: 一个 SkillToolSet 被多个上下文/子代理复用:团队代理并行子代理(TeamAgent 并行委派)、AG-UI 多会话共享代理、或 repo_resolver 按请求返回不同仓库的服务端部署。

实际影响: 子代理/后续上下文的技能工具执行在错误仓库或错误工作区目录上,skill_list_tools/skill_load/skill_run 可能整体失败或操作错误技能,跨会话/跨子代理数据隔离被破坏。

修正方向: 缓存按上下文或按仓库一致性键控(如仅当 repository 与首次构建时一致才命中),或在每次调用时重建工具列表而只缓存构建成本较高的部分;至少应把元数据写入移到缓存判断之前并校验仓库一致性。

中等

trpc_agent_sdk/skills/_toolset.py:197-207

问题: _exclude_toolsif not name or name in self._excluded_tools: continue(第 204 行)会把 name 为空/None 的工具静默丢弃——该行为仅在配置了 excluded_tools 时启用(空列表走第 199 行提前返回)。任何不带 name 属性的自定义 ToolABC 或运行时工具,只要用户开启了排除功能就会被无提示地移除;同时按名称匹配会误删与排除名同名的其他已注册技能函数(如用户注册了恰好名为 skill_list_tools 的官方 FunctionTool 以外的同名函数)。

触发条件: 用户传入非空 excluded_tools,且工具集包含名称缺失的工具或同名工具;默认内置工具均带名称,但框架公开的 ToolABC 扩展点允许任意自定义工具。

实际影响: 与排除意图无关的工具从 LLM 可见工具集中消失,模型能力静默降级;同名函数被全部排除时用户无法感知排除只应作用于目标工具。

修正方向: 仅跳过 name in self._excluded_tools 的匹配项,对 name 缺失的工具保留而非丢弃(或改为按工具对象/身份精确排除);排除应只移除具名目标,不改变其余工具集。

中等

examples/skills/agent/tools.py:61-62

问题: 特性落地不一致:本变更在此处通过 excluded_tools=["skill_list_tools"] 关闭了该示例工具集中的 skill_list_tools,但仅此一个示例生效——examples/skills_with_dynamic_tools 的提示词仍把 skill_list_tools 作为强制步骤(第 20、69、71、93 行,含 MUST call skill_list_toolsNEVER skip skill_list() and skill_list_tools()),而该示例构建 SkillToolSettools/_skill_tools.py:65)时并未排除该工具;examples/team_with_skill 本次删除了提示词中的调用步骤(prompts.py),但其工具集(tools.py:92)同样未排除,工具仍暴露给模型;docs/mkdocs/{en,zh}/skill.md 也仍把该工具描述为"必须调用"且返回"数组"。

触发条件: 运行 examples/skills_with_dynamic_tools 示例(或其提示词模式),模型按提示词强制调用 skill_list_tools 预览工具;或运行 examples/team_with_skill,模型仍可通过未排除的工具调用它。

实际影响: skill_list_tools 的返回结构已在本变更中从数组改为含 skill_name/scope/note 的字典(_skill_list_tool.py:44-53),提示词与文档中 → ["get_weather", "get_data"] 的示例和强制调用步骤与真实行为矛盾,模型解析结果出错或误解该工具失效,示例行为退化;同一特性在不同示例中的开关状态互相矛盾,用户无从判断该工具是否可用。

修正方向: 统一示例与文档:要么在所有示例中一致启用排除并同步修改提示词,要么保留工具并更新提示词/文档为新的字典返回结构;同时更新 docs/mkdocsskill_list_tools 的返回值与工作流说明。

较低

trpc_agent_sdk/skills/_toolset.py:146-147

问题: 本次变更的核心新特性 excluded_tools_default_tools 缓存完全无测试覆盖:全仓库 tests 中 excluded_tools 零引用,tests/skills/test_toolset.pytest_get_tools* 每个实例只调用一次 get_tools(),缓存命中路径、排除效果、以及排除与 SkillToolSet 集成的行为均未验证。

触发条件: 后续任何人回归 _exclude_tools(如按错误属性匹配、参数被忽略)或缓存路径(如返回原列表而非副本、重复累加),现有测试套件无法发现。

实际影响: 特性本身("支持关闭skill_list_tools")的保证不被任何自动化测试守护,示例中唯一的使用点(examples/skills/agent/tools.py:62)也无端到端验证;与 test_skill_list_tool.py 三个用例仅验证新返回字段形成反差。

修正方向:tests/skills/test_toolset.py 增加:构造 SkillToolSet(excluded_tools=["skill_list_tools"]) 后断言 get_tools() 结果不含该工具但含其余 5 个功能工具;连续两次 get_tools() 断言长度相等且返回不同列表对象(验证缓存与副本语义)。

较低

examples/fastapi_server/_runner_manager.py:91

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

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

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

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

Comment on lines +184 to 195
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))
except Exception as ex: # pylint: disable=broad-except
# 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

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.

问题: 新增的 _default_tools 缓存把 SKILL_REGISTRY.get_all() 的快照冻结在首次 get_tools() 调用(第 184 行),此后所有调用都命中 return self._default_tools.copy()(第 176-177 行)跳过重新读取。变更前每次调用都重新读取进程级单例 SKILL_REGISTRY_registry.py:25SkillRegistry(SingletonBase)),运行期 register()/unregister() 会即时反映;现在注册晚于首次调用的技能函数永远不会出现在工具列表中,也没有任何失效/刷新路径。

触发条件: 任何在首个 get_tools() 调用之后调用 SKILL_REGISTRY.register(...)/unregister(...)(公开导出 API,skills/__init__.py:143)的场景,例如动态插件加载、运行期装载技能,或使用同一 SkillToolSet 实例的团队/多子代理流程。

实际影响: 新注册技能的工具对模型静默缺失,注销也不再生效,工具集内容在整个进程生命周期内陈旧,且无任何告警;这是本次变更引入的行为回归(变更前每次调用都重新求值)。

修正方向: 移除缓存,改为在每次调用时执行排除过滤后直接返回;若确需缓存,则为缓存增加显式失效机制(如监听 SKILL_REGISTRY 变更或提供 invalidate() API),并保证排除逻辑不依赖缓存即可生效。

Comment on lines +176 to 195
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))
except Exception as ex: # pylint: disable=broad-except
# 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

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.

问题: 首次调用路径 return tools(第 195 行)返回的正是同时存入 _default_tools 的同一活列表(self._default_tools.extend(tools) 第 194 行),而后续调用返回 self._default_tools.copy()(第 177 行),返回对象身份语义不一致:任何调用方对首次调用的返回值做就地修改(append/remove/extend/sort)都会永久污染缓存,后续所有调用返回受损工具列表。

触发条件: 任何对 get_tools() 首次返回值执行就地变更的调用方。现有内置调用方(_tool_adapter.py:104_claude_agent.py:501GovernedSkillToolSet 列表推导)均只读,但缓存命中路径返回副本、未命中路径返回原列表的不对称性构成未文档化的契约陷阱,且无任何测试断言两次调用的结果一致性。

实际影响: 未来调用方(或对首调用结果做过滤/裁剪的集成代码)一旦就地修改,缓存即被污染,LLM 请求中出现重复或伪造的工具声明(重复函数声明可能导致模型拒绝请求或歧义工具调用),问题难以排查。

修正方向: 首次调用同样返回 self._default_tools.copy()(或构建到局部列表后 self._default_tools = tools 再返回 tools.copy()),保证所有路径返回的都是缓存副本。

Comment on lines +197 to +207
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

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.

问题: _exclude_toolsif not name or name in self._excluded_tools: continue(第 204 行)会把 name 为空/None 的工具静默丢弃——该行为仅在配置了 excluded_tools 时启用(空列表走第 199 行提前返回)。任何不带 name 属性的自定义 ToolABC 或运行时工具,只要用户开启了排除功能就会被无提示地移除;同时按名称匹配会误删与排除名同名的其他已注册技能函数(如用户注册了恰好名为 skill_list_tools 的官方 FunctionTool 以外的同名函数)。

触发条件: 用户传入非空 excluded_tools,且工具集包含名称缺失的工具或同名工具;默认内置工具均带名称,但框架公开的 ToolABC 扩展点允许任意自定义工具。

实际影响: 与排除意图无关的工具从 LLM 可见工具集中消失,模型能力静默降级;同名函数被全部排除时用户无法感知排除只应作用于目标工具。

修正方向: 仅跳过 name in self._excluded_tools 的匹配项,对 name 缺失的工具保留而非丢弃(或改为按工具对象/身份精确排除);排除应只移除具名目标,不改变其余工具集。

Comment on lines +61 to +62
skill_toolset = SkillToolSet(repository=repository, run_tool_kwargs=tool_kwargs,
skill_stager=skill_stager, excluded_tools=["skill_list_tools"])

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.

问题: 特性落地不一致:本变更在此处通过 excluded_tools=["skill_list_tools"] 关闭了该示例工具集中的 skill_list_tools,但仅此一个示例生效——examples/skills_with_dynamic_tools 的提示词仍把 skill_list_tools 作为强制步骤(第 20、69、71、93 行,含 MUST call skill_list_toolsNEVER skip skill_list() and skill_list_tools()),而该示例构建 SkillToolSettools/_skill_tools.py:65)时并未排除该工具;examples/team_with_skill 本次删除了提示词中的调用步骤(prompts.py),但其工具集(tools.py:92)同样未排除,工具仍暴露给模型;docs/mkdocs/{en,zh}/skill.md 也仍把该工具描述为"必须调用"且返回"数组"。

触发条件: 运行 examples/skills_with_dynamic_tools 示例(或其提示词模式),模型按提示词强制调用 skill_list_tools 预览工具;或运行 examples/team_with_skill,模型仍可通过未排除的工具调用它。

实际影响: skill_list_tools 的返回结构已在本变更中从数组改为含 skill_name/scope/note 的字典(_skill_list_tool.py:44-53),提示词与文档中 → ["get_weather", "get_data"] 的示例和强制调用步骤与真实行为矛盾,模型解析结果出错或误解该工具失效,示例行为退化;同一特性在不同示例中的开关状态互相矛盾,用户无从判断该工具是否可用。

修正方向: 统一示例与文档:要么在所有示例中一致启用排除并同步修改提示词,要么保留工具并更新提示词/文档为新的字典返回结构;同时更新 docs/mkdocsskill_list_tools 的返回值与工作流说明。

Comment on lines +146 to +147
self._excluded_tools: List[str] = excluded_tools or []
self._default_tools: List[ToolABC] = []

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.

问题: 本次变更的核心新特性 excluded_tools_default_tools 缓存完全无测试覆盖:全仓库 tests 中 excluded_tools 零引用,tests/skills/test_toolset.pytest_get_tools* 每个实例只调用一次 get_tools(),缓存命中路径、排除效果、以及排除与 SkillToolSet 集成的行为均未验证。

触发条件: 后续任何人回归 _exclude_tools(如按错误属性匹配、参数被忽略)或缓存路径(如返回原列表而非副本、重复累加),现有测试套件无法发现。

实际影响: 特性本身("支持关闭skill_list_tools")的保证不被任何自动化测试守护,示例中唯一的使用点(examples/skills/agent/tools.py:62)也无端到端验证;与 test_skill_list_tool.py 三个用例仅验证新返回字段形成反差。

修正方向:tests/skills/test_toolset.py 增加:构造 SkillToolSet(excluded_tools=["skill_list_tools"]) 后断言 get_tools() 结果不含该工具但含其余 5 个功能工具;连续两次 get_tools() 断言长度相等且返回不同列表对象(验证缓存与副本语义)。

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.closeasyncrunners.py:948),修复本身正确;但该修复与"支持关闭skill_list_tools"特性无关,混入同一提交使针对排除功能的回归定位/回滚范围扩大。

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

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants