工具实现精读

pi 的七个内置工具住在 packages/coding-agent/src/core/tools/。mini-pi 的四个工具是它们的"教学简化版",本篇讲真实版本多出来的那些心思——每一处都对应一个踩过的坑。

公共基础设施

truncate.ts——输出的保险丝。两个常量定调:DEFAULT_MAX_LINES = 2000DEFAULT_MAX_BYTES = 50 * 1024,谁先到谁截。三个函数三种策略:

  • truncateHead(read 用):留头部,告诉模型用 offset 翻页;
  • truncateTail(bash 用):留尾部——命令的关键输出(错误、结果)通常在最后;
  • truncateLine:单行过长时截(比如 grep 匹配行,GREP_MAX_LINE_LENGTH = 500)。

共同约束:绝不返回半行(truncateHead 在字节截断后回退到最后一个换行符),返回 TruncationResult 元数据(截断方式、总行数/字节)供 UI 展示。

file-mutation-queue.ts——写操作的交通灯(61 行,值得全文读)。并行工具执行是默认,但两个并发 edit 写同一文件会互相覆盖。解法:按文件(realpath 解析后的路径)维护 promise 链,同一文件的 write/edit 串行,不同文件照常并行。这是"默认并行 + 局部串行"思想在工具层的落地(loop 层的对应物是 executionMode,见设计思想:agent loop)。

path-utils.ts / tool-definition-wrapper.ts:所有路径相对 cwd 解析;wrapToolDefinition 把带 UI 渲染能力的 ToolDefinition 包成 agent-core 的 AgentTool——同一个工具,一份执行逻辑,一层可选的渲染皮肤。

output-accumulator.ts——bash 的缓冲策略:输出增量累积,超限时走 truncateTail,完整输出同时写入临时文件(pi-bash-*.log),给模型的截断说明里带上临时文件路径——上下文省了,信息一点没丢,模型想看全文自己 read 那个文件。

read:文本之外还有图片

  • schema:path / offset?(1 起)/ limit?;描述里直接写明截断上限和"用 offset 翻页直到读完"——描述即使用说明
  • 支持图片(jpg/png/gif/webp/bmp):检测 MIME、自动缩放到 2000×2000 内,以附件形式进 content;模型不支持视觉时,附一句降级说明。
  • 尾部空行裁剪、tab 展开、行号范围展示等 UI 细节与 LLM 内容分离。

write:最薄,但有两个细节

  • 自动 mkdir -p 父目录——少一类无谓失败;
  • 描述里明确"Creates the file if it doesn't exist, overwrites if it does",再配合系统提示词的 "Use write only for new files or complete rewrites"——覆盖语义对模型完全透明,避免误用。

edit:当前版本是 edits[] 数组

这是与博客时代相比最大的演进(edit.ts):

  • schema 是 path + edits: [{oldText, newText}, …]:一次调用完成多处互不重曡的替换;描述里叮嘱"改同一区块就合并成一处,别为了串连远端改动捎带大段未改文本"。旧的单 oldText/newText 形式仍被兼容(LegacyEditToolInput,内部归一化为 edits[])。
  • edit-diff.ts 处理真实世界的文本:BOM(stripBom)、CRLF/LF(normalizeToLF / restoreLineEndings——匹配在规范化后进行,写回时恢复原行尾),并产出两种 diff:给 UI 渲染的 diff 字符串和标准 unified patch,连同 firstChangedLine(编辑器跳转用)一起放进 details——给 LLM 的 content 与给 UI 的 details 分离的样板。
  • 多匹配/零匹配报错文案与 mini-pi 同款:"must be unique in the original file"。
  • 写入走 withFileMutationQueue

bash:同步执行,认真对待输出

  • 通过 spawn 起 shell(utils/shell.tsgetShellConfig 处理各平台 shell 探测,支持 stdin 方式传命令);
  • BashOperations 是可插拔接口——默认本地执行(createLocalBashOperations),可以换成 SSH 远程执行、容器内执行(官方 gondolin/sandbox 扩展示例就是这么做的),工具的定义与执行环境解耦;
  • BashSpawnHook 允许扩展改写 spawn 上下文(比如统一加环境变量、命令前缀);
  • 输出走 OutputAccumulator(上文),超时由模型给的 timeout 秒数控制。

grep / find / ls:默认禁用的只读三件套

  • grep 底层是 ripgrep(ensureTool("rg") 找不到会自动下载);find 底层是 fd(可注入自定义 glob 实现);ls 是纯内部实现。
  • 默认不给模型(有 bash 就够,见设计思想:极简核心);它们的主战场是 pi --tools read,grep,find,ls只读模式——一组没有写权限的温和工具。

一句话总结

pi 的工具实现里没有"聪明",只有"周到":截断不丢信息(temp 文件)、并行不乱写(mutation queue)、文本兼容 BOM/CRLF、错误文案写给模型看、描述即文档。这些正是 mini-pi 简化掉、而你的产品级 agent 迟早要补回来的部分。

下一篇:会话与运行时 →