统一管理:所有工具,共用一份合同

什么是"合同"? 假设你要雇一个快递员(工具),不管是顺丰还是菜鸟,你作为寄件人都有同样的期待:能告诉我它能不能送、送之前我要签什么授权、送的过程中实时更新进度、送完告诉我结果。这些期待就是"合同"——合同规定了双方的权利和义务,雇主(Agent Loop)不需要关心每家快递公司内部怎么实现,只要按合同调用就行。

src/Tool.ts 定义了整个工具系统的基础类型。每个工具,无论是内置的 BashTool 还是用户接入的 MCP 工具,都必须实现这个接口。

接口的核心是 buildTool() 函数:

// src/Tool.ts
export function buildTool<D extends AnyToolDef>(def: D): BuiltTool<D> {
  return {
    ...TOOL_DEFAULTS,
    userFacingName: () => def.name,
    ...def,
  } as BuiltTool<D>
}

buildTool() 的作用类似工厂函数:工具开发者只需要提供"有差异的部分",其余都由 TOOL_DEFAULTS 填充默认值。默认值遵循失败关闭原则:

// src/Tool.ts
const TOOL_DEFAULTS = {
  isEnabled: () => true,
  isConcurrencySafe: (_input?) => false,  // 默认不允许并发
  isReadOnly: (_input?) => false,          // 默认假设会写入
  isDestructive: (_input?) => false,       // 默认假设可撤销
  checkPermissions: (input, _ctx?) =>
    Promise.resolve({ behavior: 'allow', updatedInput: input }),  // 默认放行
  toAutoClassifierInput: (_input?) => '',  // 默认不参与安全分类
  userFacingName: (_input?) => '',
}

这个设计很谨慎:isConcurrencySafe 默认 false,意味着新工具默认不允许并发执行,必须显式声明自己是并发安全的,才会被 Agent Loop 并行调用。

什么叫"显式声明"? 就是工具开发者主动在代码里写出来 isConcurrencySafe: () => true,明确告诉系统"我支持并发"。与之相对的是隐式——什么都不写,系统按默认值处理。两者对比:

隐式(什么都不写)显式(主动声明)
代码形式省略 isConcurrencySafe 字段写 isConcurrencySafe: () => true
系统行为取 TOOL_DEFAULTS 的默认值 false,不允许并发系统知道这个工具可以和其他工具同时运行
典型例子新工具刚创建,开发者还不确定是否安全GlobTool、GrepTool(只读搜索,天然并发安全)

这是"失败关闭"(fail-closed)原则的体现:宁可保守地串行执行,也不冒并发带来数据竞争的风险。只有开发者主动站出来说"我没问题",系统才允许并发。