toolExecution.ts 解析
目标
这页回答:
单个
tool_use到底是怎样被执行、校验、过权限、运行工具实现、再包装成tool_result的?
文件:
src/services/tools/toolExecution.ts
一句话结论
runToolUse() 是“单工具执行协议”的核心入口。
它不是简单地 tool.call() 一下,而是会完整经过:
- 找工具定义
- 校验 schema
- 校验输入值
- 预跑 permission / hook 链
- 真正执行
tool.call() - 处理结果、hooks、error、telemetry
- 输出标准化
tool_result
也就是说:
tool.call()只是中间一步,runToolUse()才是完整工具运行时。
核心入口:runToolUse()
它的输入是:
toolUse: ToolUseBlockassistantMessagecanUseTooltoolUseContext
它先做什么?
1. 先找工具
先在当前可用工具池里找:
findToolByName(toolUseContext.options.tools, toolName)如果没找到,还会尝试:
- 到
getAllBaseTools()里看是不是老 alias / deprecated tool name
这说明系统考虑了:
- 老 transcript 重放
- 工具改名后的兼容性
第二步:进入 streamedCheckPermissionsAndCallTool()
这是一个很重要的设计。
它不是直接返回最终结果,而是包成一个 AsyncIterable,因为工具执行过程中可能会不断吐:
- progress message
- 最终 message
所以系统用 Stream<MessageUpdateLazy>() 把:
- progress 通道
- 结果通道
揉成同一条异步流。
这和 StreamingToolExecutor 的设计天然契合。
第三步:真正主逻辑在 checkPermissionsAndCallTool()
这个函数非常肥,但逻辑是清楚的。
1. 输入 schema 校验
先做:
tool.inputSchema.safeParse(input)如果失败:
- 返回错误型
tool_result - 记录 telemetry
- 对 deferred tool 还会补 schema-not-sent hint
这个点很值钱
说明模型吐出来的参数并不可信。 系统默认认为:
- 模型可能把数组写成字符串
- 可能字段类型不对
- 可能 tool schema 压根没进 prompt
所以运行时必须兜底。
2. 业务级输入校验
即使 schema 过了,还会跑:
tool.validateInput?.(...)这和 Zod schema 不同。
区别
- schema 校验:结构对不对
- validateInput:业务语义对不对
比如:
- 路径是否合法
- 参数组合是否冲突
- 当前上下文是否允许这样调用
3. Permission / Hook 链
在真正调工具前,会经过一长段权限与 hook 流程:
runPreToolUseHooks(...)resolveHookPermissionDecision(...)canUseTool(...)- 可能执行
executePermissionDeniedHooks(...)
关键意义
这说明工具执行前不只是“问一嘴用户允不允许”,而是有一套完整决策链:
- hook 可以改输入
- hook 可以阻止继续
- hook 可以给额外上下文
- permission system 可以 allow / deny / ask
- classifier 也可能参与
这就是一个成熟 agent runtime 的味道了: 工具执行前有独立治理层。
4. 真正执行 tool.call()
前面一堆都通过以后,才会到:
const result = await tool.call(...)调用时还会给工具一个扩展后的 context:
toolUseIduserModified- progress callback
这说明工具实现本身可以:
- 上报进度
- 依赖 toolUseId 做关联
- 感知用户是否在 permission 阶段修改了输入
5. 结果不是直接裸返回,而是再包装
执行完 tool.call() 后,不会直接把 result 交给主循环。
它会先:
A. 生成标准 tool_result block
const mappedToolResultBlock = tool.mapToolResultToToolResultBlockParam(...)B. 过 result storage / size budget 逻辑
processPreMappedToolResultBlock(...)
processToolResultBlock(...)C. 拼成 user message
最终用:
createUserMessage({ content: [tool_result, ...] })送回主循环。
这说明什么
工具自己的返回值和最终进入 transcript / 再喂给模型的 tool_result,不是一回事。
中间还有一个很重要的“serialization / storage / shaping”阶段。
6. PostToolUse hooks 也会参与收尾
在工具成功后还会跑:
runPostToolUseHooks(...)- 失败时跑
runPostToolUseFailureHooks(...)
这意味着 hook 不只存在于“执行前”,也存在于:
- 执行后结果修饰
- 错误后补充处理
有些 hook 还能:
- 更新 MCP output
- 追加 attachment
- 生成 stop summary
所以工具执行协议是三段式:
PreToolUse → tool.call → PostToolUse / PostToolUseFailure7. Error path 也是一等路径
如果工具抛错:
- 不会让整个系统原地爆炸
- 会进入统一 error formatting
- 记录 telemetry
- 生成错误型
tool_result - 跑 failure hooks
这个设计很重要。
因为在 agent loop 里,工具失败本身就是模型要消费的信息, 不是单纯进程级异常。
和 AgentTool 的关系
AgentTool 作为一种工具,也完全走这条协议。
也就是说,agent 调用在运行时会依次经过:
runToolUse()找到AgentTool- schema / validate / permission / hooks
- 执行
AgentTool.call() - 返回
completed / async_launched / remote_launched等结果 - 再统一包装成
tool_result
所以 AgentTool 再复杂,也没跳出统一工具运行时模型。
当前结论
toolExecution.ts 真正定义了:
一个工具在这个系统里,什么叫“被正确执行一次”。
它把:
- 参数校验
- 权限治理
- hook 插桩
- 执行
- 结果序列化
- 错误处理
- telemetry
全部捆成了一条统一协议。
这也是为什么 AgentTool 能优雅地作为普通工具接入,而不是特殊内核逻辑。