源码目录packages/agent/src/harness/tools/。〇、先看共同底座ExecutionToolContext/env四个工具没有一个直接碰 Node 的fs或child_process。它们全部通过注入的envExecutionEnv操作文件系统、执行命令// tools 看到的操作面env.readTextFile(path,signal)env.readBinaryFile(path,signal)env.writeFile(path,content,signal)env.fileInfo(path,signal)env.absolutePath(path,signal)env.canonicalPath(path)env.runCommand(...)// bash 工具走的命令执行这个抽象有三个直接收益可测试单测注入内存版 env不碰真实文件系统可隔离Gondolin 模式专项四把read/write/edit/bash全部路由进微 VM靠的就是工具只认 env 接口不认 OS——换一个远端 env 实现工具代码一行不改可审计所有文件操作都过一个口可以在 env 层统一打点。这是工具层与操作系统解耦的标准姿势——你的 Java 工具也应该定义FilePort/CommandPort接口而不是直接在工具里new FileOutputStream()。一、readoffset/limit 分页 图片 输出有界read的 schematool-read.tsconstreadSchemaType.Object({path:Type.String({description:Path to the file to read (relative or absolute)}),offset:Type.Optional(Type.Number({description:Line number to start reading from (1-indexed)})),limit:Type.Optional(Type.Number({description:Maximum number of lines to read})),});三个细节值得抄① 输出有界且明确告诉模型怎么读完。工具描述原话For text files, output is truncated to 2000 lines or 50KB (whichever is hit first). Use offset/limit for large files. When you need the full file, continue with offset until complete.“输出截断 指引模型用 offset 继续读完”——Pi 把怎么处理大文件写进工具描述模型按指引自动分页。② 图片是一等输入。read支持 jpg/png/gif/webp/bmp读图走imageProcessor转成{type:image, data, mimeType}内容块直接作为附件发给模型。返回的提示里甚至带hints图片处理器的建议比如这张图可能被裁剪了。③ 路径解析做了 Unicode 容错path-utils.tsconstvariants[resolved,resolved.replace(/(AM|PM)\./gi,${NARROW_NO_BREAK_SPACE}$1.),// 全角空格resolved.normalize(NFD),// 音标字符分解resolved.replace(//g,’),// 直引号→弯引号];模型可能把文件路径里的全角空格、直引号、Unicode 组合字符搞混read会逐个变体尝试。模型输出文件路径是不可靠的工具端要做模糊匹配 容错。二、write创建父目录 字节数回执writetool-write.ts很薄但有一个行为值得注意description:Write content to a file. Creates the file if it doesnt exist, overwrites if it does. Automatically creates parent directories.自动创建父目录返回Successfully wrote ${content.length} bytes to ${path}。content.length是字符数不是字节数但这不耽误模型理解写入了多少。write 是整个工具集里最直白的它真正的复杂度在锁。三、edit精确文本替换附带变更快照edittool-edit.ts是四个工具里工程含量最高的。schemaconsteditSchemaType.Object({path:Type.String(...),edits:Type.Array(Type.Object({oldText:Type.String({description:Exact text for one targeted replacement. It must be unique in the original file...}),newText:Type.String({description:Replacement text for this targeted edit.}),}),{description:One or more targeted replacements. Each edit is matched against the original file, not incrementally...}),});执行流水线去掉样板后const{bom,text:content}stripBom(readResult.value);// ① 去 BOMconstoriginalEndingdetectLineEnding(content);// ② 检测换行符constnormalizedContentnormalizeToLF(content);// ③ 统一成 LFconst{baseContent,newContent}applyEditsToNormalizedContent(normalizedContent,edits,path);constfinalContentbomrestoreLineEndings(newContent,originalEnding);// ④ 还原换行符awaitenv.writeFile(absolutePath,finalContent,signal);constdiffResultgenerateDiffString(baseContent,newContent);// ⑤ 生成 diff 快照return{content:...,details:{diff,patch:generateUnifiedPatch(...),firstChangedLine}};五个要点先规范化再编辑编辑完还原BOM、CRLF/LF 先剥掉/统一替换完再还原。否则模型用 LF 的 oldText 匹配 CRLF 文件会失败。每个 oldText 必须唯一、且互不重叠“It must be unique in the original file and must not overlap with any other edits[].oldText”——schema 层就挡住模糊替换。edits 全部针对原文件匹配不增量匹配“Each edit is matched against the original file, not incrementally”——避免前一个编辑改变行号影响后一个。返回 diff/patch/firstChangedLine这就是文件变更快照。模型、UI、审计都能看到这次编辑到底改了什么。generateUnifiedPatch给出统一 difffirstChangedLine定位首行变化。兼容层prepareArguments老格式的{oldText, newText}被折叠成新的edits数组——工具 schema 演进不破坏历史调用。对比很多 Agent 用整文件重写改文件Pi 的 edit 是精确补丁 快照——副作用最小、可 diff、可回滚。四、文件锁按规范路径串行化变更file-mutation-queue.ts实现的是同一文件上的变更串行化——这是文件锁的 Promise 版本constkeyawaitgetMutationQueueKey(env,path);// 规范路径作为锁 keyconstcurrentQueuestate.queues.get(key)??Promise.resolve();// 排队当前队列完成后才轮到下一个constchainedQueuecurrentQueue.then(()nextQueue);state.queues.set(key,chainedQueue);awaitcurrentQueue;try{returnawaitfn();}finally{releaseNext();...}细节锁粒度 规范路径canonical path不是传进来的字符串。../a.md和a.md、符号链接和真实路径会归到同一个锁——按规范路径加锁才挡得住同文件不同路径的并发编辑。锁作用域 单个envWeakMapExecutionEnv, ...。不同 env比如 Gondolin 的远端 env各归各的。为什么需要它一个 turn 里可能有多个工具调用batch两个 edit 同时改同一个文件读-改-写会互相覆盖。文件锁把读原文件 → 应用编辑 → 写回变成同一路径上的原子操作。这是Agent 改代码场景最容易翻车的地方并发写同一文件。你在 Java 里做工具层时务必有同款按规范路径串行化文件变更的机制。五、bash隔离、捕获、会话环境注入bash 工具tools/bash.tsshell-output.ts是权限最大、最需要防护的工具。它的设计① 输出捕获 双限截断。命令输出边跑边攒超限即截断constmaxOutputBytesDEFAULT_MAX_BYTES*2;// 100KB 上限// 截断到尾部 N 行 / N KB谁先到算谁截断结果带完整元信息truncate.ts的TruncationResulttruncated / truncatedBy / totalLines / totalBytes / outputLines / lastLinePartial并且完整输出落临时文件fullOutputPath——模型需要时再读全文主上下文只留尾部。② 输出净化sanitizeBinaryOutput过滤掉二进制控制字符只保留 tab/换行/回车防止二进制输出污染上下文。③ 超时显性化timeout 可选、无默认但受MAX_TIMEOUT_SECONDS硬上限约束超时以错误返回“Command timed out after N seconds”——模型能看到并决定怎么办。④ 会话环境注入environment-variables.mdbash 工具运行的命令会拿到当前会话状态PI_SESSION_ID 当前会话 ID PI_SESSION_FILE 会话 JSONL 路径临时会话为空 PI_PROVIDER 当前 provider PI_MODEL 当前模型 PI_REASONING_LEVEL 当前推理级别命令可以据此自查“我在哪个会话、用什么模型跑的”——这是 agent 自我感知能力的底座。spawnHook可以再改 env比如注入 CI1exposeSessionEnvironment: false关闭注入。⑤ 命令执行本身走 env 抽象runCommand所以 bash 同样可以被 Gondolin 路由进 VM——这就是工具隔离的实现路径bash 不是在进程里开 shell而是向 env 端口提交一个命令。六、对照你的工程四工具的移植清单Pi 的机制你要不要做说明env 抽象FilePort/CommandPort必须可测试 可隔离 可审计的根源read 分页 输出有界必须防止大文件灌爆上下文edit 精确替换 diff 快照强烈建议副作用最小、可回滚、可审计文件锁规范路径串行化必须并发改同一文件会互相覆盖bash 输出双限截断 临时文件必须命令输出是上下文杀手会话环境注入建议命令能感知自己在哪、用什么模型路径 Unicode 容错建议模型给的文件路径不可靠知识卡片本节体系归档┌──────────────────────────────────────────────────────────┐ │ 知识节点内置四工具env 抽象 文件锁 bash 隔离 │ │ │ │ What env 抽象FilePort/CommandPortread 分页/图片 │ │ edit 精确替换/BOM/diff 快照规范路径文件锁 │ │ bash 输出双限截断 会话 env 注入 │ │ │ │ Why 一般原理工具层与 OS 解耦。不变量 │ │ ① 工具只认 env 接口可测试/可隔离/可审计 │ │ ② 同文件变更按规范路径串行化 │ │ ③ 输出必须有界完整内容落临时文件 │ │ │ │ How 校验动作 │ 画四工具结构 PaiFlow 文件操作换 FilePort 接口 │ │ │ │ Pits 坑点 │ 并发写文件 / 输出灌爆 / 模型给的路径不可靠 │ │ │ │ Transfer 到 PaiFlowFilePort 规范路径锁 输出截断 │ └──────────────────────────────────────────────────────────┘源码与文档出处packages/agent/src/harness/tools/{read,write,edit,bash,file-mutation-queue,path-utils,tool-context}.ts、packages/agent/src/harness/utils/{truncate,shell-output}.ts、packages/coding-agent/docs/environment-variables.md。