抽象基类 + ToolCard 元数据,元类织入生命周期
工具是一个类:继承 Tool,用 ToolCard 声明元数据,实现 invoke 与 stream 两个抽象方法。关键在 _ToolMeta 这个元类——它在实例化时把 invoke/stream 包进 _lifecycle_invoke / _lifecycle_stream,于是权限检查、日志、异常处理这些横切关注点由框架统一织入,工具作者写不到也绕不过。另有 exposure.py 控制工具是否向模型暴露。
- 适合
- 权限、日志、异常必须统一织入、工具作者绕不过去的企业场景。
- 代价
- 元类织入生命周期,调试时要先弄懂 _ToolMeta 做了什么。
此后的变化
- 2026-W38 →
Tool 契约新增 render_for_llm(output):工具可以自定义「模型读到的文本」与「流式展示给用户/开发者的结构化结果」分离,默认渲染取 data['content'],失败时渲染 error,不再是 AbilityManager 直接把 ToolOutput 的 pydantic repr 塞给模型;agent_teams 原先靠 MappedToolOutput.__str__ 打的补丁被这条通用契约取代。本格此前的两种建工具流派均未提及「输出如何渲染给模型」是契约的一部分。
- 2026-W37 →
本格强调 _ToolMeta 把异常处理等横切关注点织入框架,本周的 filesystem.py 修复给出了这条契约的边界用例:模型把整个 file_path/content 载荷塞进 file_path 时,Path.resolve() 抛出的 ENAMETOOLONG 此前会杀死整个回合;改为解包一层嵌套 JSON、拒绝超长路径分量,并把 OSError 转成 ValueError,使 invoke 返回 success=False 的 ToolOutput。PR 明确将其定性为公共文件系统工具契约问题。
class Tool(metaclass=_ToolMeta):
"""tool class that defined the data types and content for LLM modules"""
def __init__(self, card: ToolCard):
...
@abstractmethod
async def invoke(self, inputs: Input, **kwargs) -> Output:
"""Execute the tool with provided inputs and return final result."""
@abstractmethod
async def stream(self, inputs: Input, **kwargs) -> AsyncIterator[Output]:
...
# _ToolMeta.__call__ 在实例化时把 invoke/stream 包进 _lifecycle_invoke /
# _lifecycle_stream(同文件 76-158 行),生命周期由框架织入而非工具自理。