feature: 支持关闭skill_list_tools tool - #323
Conversation
raychen911
commented
Sep 4, 2026
- 修改skill_list_tools 的函数注释,避免模型理解歧义
- 支持关闭skill_list_tools
AI Code Review审查结论通过 审查范围:f05797d..ccb4f8f 单个提交(feature: 支持关闭 skill_list_tools tool),5 个文件 +74/-25: 计划符合性:核心需求已实现—— 主要风险:1) 测试充分性: 门禁结论:未发现阻止合入的正确性/安全/数据损坏级缺陷,状态为 PASSED,附 2 条 MODERATE 与 2 条 LOW 评论。 发现的问题中等
问题: 触发条件: 任意一次 实际影响: 后续所有请求持续返回旧列表:新注册的技能函数永远不会暴露给 LLM,已注销或被 修正方向: 为缓存增加失效条件(例如在 中等
问题: 新增的 触发条件: 用户按参数定义排除任何被引导文案点名的内置工具(例如 实际影响: 修正方向: 将排除信息同步进技能配置/profile 机制,例如构造 较低
问题: 触发条件: LLM 或调用方传入拼写错误/不存在的 实际影响: 模型收到 修正方向: 在返回结构中区分两种情况,例如增加 较低
问题: 本次提交的核心功能—— 触发条件: 运行现有测试套件即可确认: 实际影响: 排除名称拼写错误、过滤逻辑回归(例如误删具有合法名称的工具)或缓存行为破坏都不会被测试发现;"支持关闭 skill_list_tools" 这一提交主目标本身处于未验证状态。 修正方向: 在 |
| 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) |
There was a problem hiding this comment.
问题: 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 或加锁,消除并发重复写入。
| excluded_tools: Optional[List[str]] = None, | ||
| **run_tool_kwargs: dict[str, Any]): |
There was a problem hiding this comment.
问题: 新增的 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 仅适用于引导文案未点名的工具,保证引导与实际可用工具集一致。
| else: | ||
| available_tools = list(skill.tools or []) | ||
| return { | ||
| "skill_name": | ||
| skill_name, | ||
| "available_tools": | ||
| available_tools, |
There was a problem hiding this comment.
问题: 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 的区分逻辑。
| 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 |
There was a problem hiding this comment.
问题: 本次提交的核心功能——excluded_tools 参数、_exclude_tools 过滤逻辑和 _default_tools 缓存——没有任何测试覆盖。
触发条件: 运行现有测试套件即可确认:tests/skills/test_toolset.py 未新增用例且只断言默认工具存在,tests/skills/tools/test_skill_list_tool.py 仅断言返回结构,没有任何用例构造带 excluded_tools 的 SkillToolSet。
实际影响: 排除名称拼写错误、过滤逻辑回归(例如误删具有合法名称的工具)或缓存行为破坏都不会被测试发现;"支持关闭 skill_list_tools" 这一提交主目标本身处于未验证状态。
修正方向: 在 tests/skills/test_toolset.py 补充用例:默认包含 skill_list_tools;传入 excluded_tools=["skill_list_tools"] 后该工具被移除且其余工具保留;连续两次调用 get_tools 返回一致结果。
ccb4f8f to
9158446
Compare
AI Code Review审查结论通过 审查范围: 发现的问题中等
问题: 触发条件: 任一宿主在首次 实际影响: 新注册的技能工具在本进程生命周期内对模型不可见、已注销工具仍可见;同时 修正方向: 删除 中等
问题: 触发条件: 任何按旧契约消费结果的调用方(例如把 实际影响: 相等性/类型断言调用方得到意外结果(新增键导致相等断言失败、LLM 收到非数组负载),提示词与文档会诱导模型按数组格式解析而实际拿到 dict,属于 source-breaking 的兼容性变化;本次仓库内无消费方,但 SDK 属对外发布库,外部影响真实存在。 修正方向: 同步更新两处 较低
问题: 本次提交为 触发条件: 后续迭代修改 实际影响: 排除工具的过滤正确性、缓存行为无测试保障;例如排除名单写错工具名、 修正方向: 在 |
| 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 |
There was a problem hiding this comment.
问题: 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 局部进行,不要触碰已延展的实例成员。
| 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.", | ||
| } |
There was a problem hiding this comment.
问题: 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.md 中 skill_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, |
There was a problem hiding this comment.
问题: 本次提交为 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 两次调用返回等长度列表且不含被排除工具;补充 name 为 None 的工具被跳过的用例。
- 修改skill_list_tools 的函数注释,避免模型理解歧义 - 支持关闭skill_list_tools
9158446 to
503e884
Compare
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)与多上下文场景共享同一批工具而元数据按上下文重写、首次调用返回内部活列表存在别名风险。 发现的问题严重
问题: 新增的 触发条件: 任何在首个 实际影响: 新注册技能的工具对模型静默缺失,注销也不再生效,工具集内容在整个进程生命周期内陈旧,且无任何告警;这是本次变更引入的行为回归(变更前每次调用都重新求值)。 修正方向: 移除缓存,改为在每次调用时执行排除过滤后直接返回;若确需缓存,则为缓存增加显式失效机制(如监听 严重
问题: 首次调用路径 触发条件: 任何对 实际影响: 未来调用方(或对首调用结果做过滤/裁剪的集成代码)一旦就地修改,缓存即被污染,LLM 请求中出现重复或伪造的工具声明(重复函数声明可能导致模型拒绝请求或歧义工具调用),问题难以排查。 修正方向: 首次调用同样返回 严重
问题: 工具列表缓存( 触发条件: 一个 实际影响: 子代理/后续上下文的技能工具执行在错误仓库或错误工作区目录上, 修正方向: 缓存按上下文或按仓库一致性键控(如仅当 中等
问题: 触发条件: 用户传入非空 实际影响: 与排除意图无关的工具从 LLM 可见工具集中消失,模型能力静默降级;同名函数被全部排除时用户无法感知排除只应作用于目标工具。 修正方向: 仅跳过 中等
问题: 特性落地不一致:本变更在此处通过 触发条件: 运行 实际影响: 修正方向: 统一示例与文档:要么在所有示例中一致启用排除并同步修改提示词,要么保留工具并更新提示词/文档为新的字典返回结构;同时更新 较低
问题: 本次变更的核心新特性 触发条件: 后续任何人回归 实际影响: 特性本身("支持关闭skill_list_tools")的保证不被任何自动化测试守护,示例中唯一的使用点( 修正方向: 在 较低
问题: 本变更同时修复了 触发条件: 需要单独回滚或二分定位 实际影响: 无法独立回退关闭工具特性而不连带撤销该关闭相关的修补;提交范围混杂增大审查与维护成本。 修正方向: 将 FastAPI 示例的 close 修复拆分到独立提交(或独立 PR),保持特性提交的单一职责。 |
| 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 |
There was a problem hiding this comment.
问题: 新增的 _default_tools 缓存把 SKILL_REGISTRY.get_all() 的快照冻结在首次 get_tools() 调用(第 184 行),此后所有调用都命中 return self._default_tools.copy()(第 176-177 行)跳过重新读取。变更前每次调用都重新读取进程级单例 SKILL_REGISTRY(_registry.py:25 的 SkillRegistry(SingletonBase)),运行期 register()/unregister() 会即时反映;现在注册晚于首次调用的技能函数永远不会出现在工具列表中,也没有任何失效/刷新路径。
触发条件: 任何在首个 get_tools() 调用之后调用 SKILL_REGISTRY.register(...)/unregister(...)(公开导出 API,skills/__init__.py:143)的场景,例如动态插件加载、运行期装载技能,或使用同一 SkillToolSet 实例的团队/多子代理流程。
实际影响: 新注册技能的工具对模型静默缺失,注销也不再生效,工具集内容在整个进程生命周期内陈旧,且无任何告警;这是本次变更引入的行为回归(变更前每次调用都重新求值)。
修正方向: 移除缓存,改为在每次调用时执行排除过滤后直接返回;若确需缓存,则为缓存增加显式失效机制(如监听 SKILL_REGISTRY 变更或提供 invalidate() API),并保证排除逻辑不依赖缓存即可生效。
| 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 |
There was a problem hiding this comment.
问题: 首次调用路径 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:501、GovernedSkillToolSet 列表推导)均只读,但缓存命中路径返回副本、未命中路径返回原列表的不对称性构成未文档化的契约陷阱,且无任何测试断言两次调用的结果一致性。
实际影响: 未来调用方(或对首调用结果做过滤/裁剪的集成代码)一旦就地修改,缓存即被污染,LLM 请求中出现重复或伪造的工具声明(重复函数声明可能导致模型拒绝请求或歧义工具调用),问题难以排查。
修正方向: 首次调用同样返回 self._default_tools.copy()(或构建到局部列表后 self._default_tools = tools 再返回 tools.copy()),保证所有路径返回的都是缓存副本。
| 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 |
There was a problem hiding this comment.
问题: _exclude_tools 中 if 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 缺失的工具保留而非丢弃(或改为按工具对象/身份精确排除);排除应只移除具名目标,不改变其余工具集。
| skill_toolset = SkillToolSet(repository=repository, run_tool_kwargs=tool_kwargs, | ||
| skill_stager=skill_stager, excluded_tools=["skill_list_tools"]) |
There was a problem hiding this comment.
问题: 特性落地不一致:本变更在此处通过 excluded_tools=["skill_list_tools"] 关闭了该示例工具集中的 skill_list_tools,但仅此一个示例生效——examples/skills_with_dynamic_tools 的提示词仍把 skill_list_tools 作为强制步骤(第 20、69、71、93 行,含 MUST call skill_list_tools、NEVER skip skill_list() and skill_list_tools()),而该示例构建 SkillToolSet(tools/_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/mkdocs 中 skill_list_tools 的返回值与工作流说明。
| self._excluded_tools: List[str] = excluded_tools or [] | ||
| self._default_tools: List[ToolABC] = [] |
There was a problem hiding this comment.
问题: 本次变更的核心新特性 excluded_tools 与 _default_tools 缓存完全无测试覆盖:全仓库 tests 中 excluded_tools 零引用,tests/skills/test_toolset.py 的 test_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() |
There was a problem hiding this comment.
问题: 本变更同时修复了 RunnerManager.close() 未 await 的问题(self._runner.close() → await self._runner.close()),经核对 Runner.close 为 async(runners.py:948),修复本身正确;但该修复与"支持关闭skill_list_tools"特性无关,混入同一提交使针对排除功能的回归定位/回滚范围扩大。
触发条件: 需要单独回滚或二分定位 excluded_tools 相关行为时。
实际影响: 无法独立回退关闭工具特性而不连带撤销该关闭相关的修补;提交范围混杂增大审查与维护成本。
修正方向: 将 FastAPI 示例的 close 修复拆分到独立提交(或独立 PR),保持特性提交的单一职责。