工具的统一契约
src/tools/ 下五十多个目录,每个目录实现一个工具,读文件、跑命令、搜代码、派子代理,能力千差万别。能把这些东西装进同一个系统里运转,靠的是 src/Tool.ts 里定义的一份统一契约。Tool 是一个 TypeScript 接口,四十来个方法加属性,任何一个工具只要实现它,主循环就能一视同仁地调度。这一章把这个接口拆成四块讲,描述、执行、权限、渲染。
描述区回答模型怎么知道这个工具。inputSchema 是一个 zod schema,声明参数的形状和类型,模型给的入参先过它校验,校验不过连执行机会都没有。prompt 方法返回一份给模型看的使用说明,内容相当长,拿 BashTool 来说,安全规则、超时约定、输出处理全写在里面。模型对工具的全部认知来自这两样东西加一个 description 短句。发送请求时 toolToAPISchema 把三者转成 API 的工具定义,名字、说明、JSON Schema 参数表,模型每次对话都能看到全套清单。
执行区的核心是 call 方法,工具真正干活的地方,其余是给调度器看的元数据。isReadOnly 标记这次调用改不改世界。isConcurrencySafe 判断给定的入参能不能和其他工具并发跑,它接收输入做参数,因为同一个工具不同调用结论可能不同。isDestructive 默认关闭,只有删文件、覆盖、外发这类不可逆操作才标。maxResultSizeChars 约定结果的上限,超了就落盘只回传路径。这些元数据没人消费就是摆设,第 5 章会看到并发调度完全按它们行事。
权限区是工具自查的接口。checkPermissions 拿到解析过的入参,返回放行、拒绝或者转询问三种结论之一,返回的还可以携带改写过的入参。BashTool 在这里做命令级审查,Edit 工具在这里看目标路径。requiresUserInteraction 标记这个工具天生要和人交互,典型是提问工具。第 6 章的权限决策链会在固定位置调用这一区。
渲染区是渲染方法一族,renderToolUseMessage 之类,负责把这个工具的调用过程画成终端里的样子。工具自己最懂自己的输出长什么样,渲染跟着工具走,界面层就不用为每个工具写专门的展示组件。这块设计让逻辑和展示在工具内部就近结合,在整体分层上又没有破坏核心与 UI 的分离,渲染方法由 UI 层在消费侧调用,核心循环不碰它们。
两个细节值得记下。工具可以带别名,toolMatchesName 匹配主名或别名,给改名留了余地。工具还有 isEnabled 开关,构建期的特性开关可以整目录地关掉某个实验工具,这个机制第 12 章细说。接口里还有一些低频但考究的字段。outputSchema 声明结果的结构,给需要结构化输出的调用方用。inputsEquivalent 判断两次调用的入参是否等价,投机执行机制拿它判断缓存的结果还能不能复用。userFacingName 决定界面上怎么称呼这次调用,同一个工具在不同入参下可以显示成不同的动作名。
prompt 方法的参数列表也透露了工具系统的运行环境。它收到工具清单、代理定义列表和一个取当前权限上下文的回调,说明工具的说明书要按环境动态生成,某个工具在 plan 模式下的用法说明和在默认模式下可以不一样。工具的执行还能吃到上下文里的限额配置,读文件的长度上限、glob 的结果数上限都在 ToolUseContext 上按需携带,会话级可调。
顺着这份契约往下读,工具系统再大也只是一份接口的五十多份实现。