工具与执行
注册表定义契约,当前运行决定能力
aibuddy 将工具系统拆成能力面和执行面。能力面回答“模型在当前步骤可以看到哪些动作”,执行面回答“这些动作能够在哪里、以什么权限产生副作用”。两者只在一次具体调用上相交。
注册工具并不自动授予权限。一次运行的实际工具集由多个条件共同确定:配置中的 toolKeys、能力服务是否已注入、MCP 是否连接、宿主是否提供沙箱,以及当前任务是否满足团队或知识等能力的前置条件。缺少依赖时,工具配置可以返回空集合,而不是向模型暴露一个必然失败的动作。
每个 ServerToolConfig 同时拥有工具创建、运行说明和结果处理钩子。文件系统等工具组可以由一个配置生成 ls、read_file、write_file、edit_file、glob 和 grep 等多个具体工具;工具名称再映射回配置组,共用客户端投影与压缩策略。defineTool 还会在定义阶段检查输入 schema:JSON Schema 必须同步生成,顶层必须为 object。
启动时,assertConsistent 比较注册配置、TOOL_CONFIG_METAS 和名称映射。内建工具元数据遗漏、孤立声明或错误映射会直接阻止启动,避免界面和运行时在无提示的情况下使用不同工具集合。
能力目录按需收敛为当前工具集
内建核心工具保持直接可见。Skills 先暴露名称和用途,只有 Agent 确认适用后才通过 view_skill 读取完整流程与参考材料。MCP 工具则根据目录规模决定直接暴露还是延迟加载。
当前 MCP 使用两级数量门控:
| 条件 | 暴露策略 |
|---|---|
| 单个服务器不超过 10 个工具,且总量不超过 30 个 | schema 直接进入当前工具集 |
| 某个服务器超过 10 个工具 | 只延迟该服务器的工具 |
| 所有服务器合计超过 30 个工具 | 延迟全部 MCP 工具 |
阈值依据工具数量,而不是上下文窗口大小。扩大窗口可以容纳更多 schema,但不能消除大量相似工具带来的选择歧义。延迟项只在索引中保留名称和描述,Agent 通过 tool_search 找到候选后再展开完整 schema。
延迟策略会适配模型提供商:支持原生工具搜索的真实 Anthropic 路径,以及使用 Responses API 的 OpenAI BYOK 路径,可以保持工具数组稳定,并由提供商按需展开 schema;Haiku、平台 OpenAI 网关、第三方兼容端点和其他模型使用 aibuddy 的 activeTools 路径。两条路径共用同一组延迟判定,差别只在 schema 如何呈现。
activeTools 的发现状态按每次 Agent 运行重新建立。为保持协议有效,系统会扫描压缩后仍保留的历史消息;历史 tool-call 引用过的延迟工具会重新加入活动集合。只有引用已经被上下文压缩移除后,该工具才需要重新发现。
宿主边界先于工具执行
工具契约描述参数和结果,宿主决定工作区、路径、凭证和可用后端。同一个 read_file 语义可以运行在 Web 托管工作区或 Desktop 本地项目中,但它不能超出当前沙箱实现允许访问的范围。
ISandbox 为文件列举、读取、写入、编辑、搜索和命令执行提供统一接口。每个操作都接收 AbortSignal,Node 适配器将其继续传给文件系统或子进程;未来的远程适配器也必须将信号传入 HTTP 或 WebSocket 请求,才能在用户停止任务后释放悬挂操作。
当前 createSandbox 工厂只提供可运行的 Node 适配器。daytona 和 e2b 已进入 SandboxType 与配置结构,但适配器仍是占位实现;选择它们会得到明确的不可用错误。类型中出现一个后端,不代表当前部署已经支持它。
高影响动作还需要独立的人机确认。confirm 在副作用发生前记录目标、范围和潜在影响,并等待用户批准或拒绝。这个决定不能替代操作系统或沙箱权限,也不应从一次相似的历史确认中推断出来。
长耗时执行同时受心跳、取消和输出上限约束
长耗时工具不能只是等待 Promise 返回。以 execute 为例,aibuddy 同时维护三条控制路径:
- 沙箱超时限定命令本身的运行预算,JavaScript 侧再增加 30 秒网络宽限作为硬截止。
- 工具每 30 秒产生一次空心跳,重置流的 chunk 超时;心跳不会进入模型上下文,标准输出和错误输出继续通过瞬时事件实时发送到客户端。
- 用户停止或父 Agent 取消会沿
AbortSignal传入命令执行和后台压缩。
Shell 输出在进入模型上下文前限制为 30,000 字符,保留头部、尾部、退出码和执行时间,并标明被省略的中间长度。客户端仍可通过实时输出看到完整过程。这个边界在输出产生时生效,比等待上下文超限后再总结更可靠;Agent 需要完整细节时,应缩小命令范围或将结果写入文件后分段读取。
文件工具也在源头限定读取行数、匹配数或结果数量。执行层先控制单次结果规模,后续上下文压缩再处理跨步骤累积,两层分别解决“单次输出过大”和“历史结果持续累积”。
工具结果针对不同消费者生成不同投影
一次工具调用至少面向客户端、后续模型步骤和持久化恢复三个消费者,aibuddy 不要求三者共享同一份结果形态:
| 消费者 | 结果形态 |
|---|---|
| 客户端 | 适合展示的字段、瞬时进度和完整流式输出 |
| 后续模型步骤 | 由工具专属 compact 钩子生成的紧凑输入与结果 |
| 持久化与恢复 | 原始输入输出、稳定 toolCallId 和可选恢复指针 |
工具结果达到当前 2,500 token 预压阈值后,onToolExecutionEnd 会在后台启动工具专属压缩,不阻塞结果先返回客户端。压缩结果只有小于原结果时才进入缓存和持久化存储。工具可以选择结构化裁剪、模型摘要或 drop;前两种可将原始输入输出保存到 compacted_outputs/<taskId>/<toolCallId>.json,并通过 view_tool_call 按调用 ID 恢复。drop 只用于明确声明“用后即可丢弃”的结果,不生成恢复指针。
已完成的人机交互使用另一条路径。ask_user_question 和 confirm 的脚手架在上下文重建时会归一化为 role=user 文本,保留用户决定本身,而不是继续携带已经结束的工具调用结构。
工具返回成功之后仍需验证效果
工具层的成功只表明调用完成,不表明任务目标成立。文件写入后需要重新读取关键区域,页面操作后需要检查界面状态,构建命令结束后需要核对退出码和产物。验证失败应继续修正,不能把一次成功响应直接转换为任务完成。
这条生命周期也使故障可以按阶段定位:
| 阶段 | 典型故障 |
|---|---|
| 发现 | 能力存在,但描述、Skill 或 tool_search 未将其带入活动集合 |
| 参数 | 路径、标识符或结构化值在模型到 schema 的转换中失真 |
| 授权 | 当前用户、工作区或宿主不允许该副作用 |
| 执行 | 超时、取消、依赖缺失,或操作已产生部分副作用 |
| 投影 | 原始结果存在,但裁剪或压缩丢失了后续判断所需信息 |
| 验证 | 调用成功,实际文件、页面或服务状态仍不满足目标 |
分阶段归因比统一返回“工具失败”更利于重试和恢复:发现问题应改进索引,环境问题应调整宿主,部分副作用则必须先检查现场再决定是否重试。
实现锚点
| 设计职责 | 对应模块 |
|---|---|
| 工具配置、名称映射与启动一致性检查 | tool-registry / ServerToolConfig |
| 输入 schema 约束 | defineTool |
| MCP 数量门控与活动工具集合 | tool-discovery |
提供商原生延迟与 activeTools 分流 | tool-defer |
| 沙箱能力与取消契约 | ISandbox / createSandbox |
| 结果预压、持久化与按 ID 恢复 | tool-compactor / view_tool_call |
相关阅读
- 工具设计——工具粒度、参数保真和错误语义。
- Agent Skills——可复用流程如何按需进入上下文。
- MCP 集成——外部服务、命名空间与凭证边界。
- 上下文工程——工具 schema 与结果如何占用模型工作集。