08 · 工具、资源加载与扩展¶
8.1 一个工具的契约¶
工具需要名称、说明、输入 schema 和 execute。返回值的几部分承担不同角色:
| 字段 | 消费者 | 用途 |
|---|---|---|
| content | 大模型 | 可读文字或图片结果 |
| details | UI / 宿主 | 展示、日志与应用元数据 |
| structuredContent | 程序调用者 | 对应 outputSchema 的机器数据 |
| isError | 运行时与模型 | 正确表达失败 |
| usage | 费用记录 | 工具内模型调用开销 |
| terminate | 运行时 | 当前批次终止提示 |
仅返回「操作失败」字符串而不 throw 或设置 isError,会把失败当成功。成功也必须给模型足够的真实信息,不能只说「done」却让它猜业务结果。
8.2 工具设计先决定权限,再决定描述¶
比如你的笔记助手只需要 search_notes(query) 和 read_note(id)。比给它 bash(command) 更容易理解和限制,因为参数空间和实际能力都窄。
描述写清:何时调用、输入从哪里来、是否修改外部状态、返回什么。不要把复杂授权依赖于模型正确阅读一长段说明。
课程工具不接受任意文件路径,只访问内存笔记列表;这是一种可验证的窄能力。若后来接数据库,必须把 tenant ID 从已认证宿主上下文注入,不能相信模型自己传的租户 ID。
8.3 内建 read:截断也是 API¶
read.ts 的 schema 是 path、可选 offset / limit。文本默认最多 2000 行或 50 KiB,取先到者;offset 从 1 开始。超出时返回下一次 offset 提示,而不是让模型以为已读完整文件。
图像按 MIME 检测和模型 resize 元数据处理。read 返回不止字符串:图片 content block 与文字说明一起存在。
这里的可插拔 ReadOperations 可以把读操作转到远程机器或隔离环境,但只替换 read 不会自动隔离所有其他工具和扩展。
8.4 edit:唯一匹配与批量不重叠替换¶
1.0 的现代输入:
{
"path": "src/config.ts",
"edits": [
{ "oldText": "const limit = 3;", "newText": "const limit = 5;" },
{ "oldText": "const mode = 'old';", "newText": "const mode = 'new';" }
]
}
每项 oldText 必须在原始文件里唯一且不重叠。不是先做第一项,再在修改后的文件匹配第二项。相邻或重叠变更应合并。
实现顺序:参数兼容准备 → 非空 edits 检查 → 解析路径 → 同文件队列 → 检查读写权限 → 读 Buffer → 去 BOM → 统一 LF → 计算全部替换 → 恢复行尾与 BOM → 写入 → 生成展示 diff 和标准 patch。
没有匹配或多处匹配应是明确错误。让模型重新读取更小更唯一的片段,优于随意替换第一处。
prepareArguments 仍会兼容旧 oldText/newText 和某些模型错误发送的 JSON 字符串,但课程应教现代 edits[],不把兼容层当正式首选 API。
8.5 同文件变更锁¶
file-mutation-queue.ts 为路径解析 realpath,按文件 key 串行读改写。不同文件仍可并行;registration queue 保证登记有序。
取消不会在一个尚未结束的 fs write 中立即释放锁。edit 在每个 await 后检查 signal,等实际文件操作 settled 再退出,避免下一个修改和前一个迟到写发生竞争。
这解决的是同进程文件动作的排序,不是跨进程锁,不是文件系统事务,也不是所有业务工具自动共享的资源锁。自己的数据库写入仍应使用事务。
8.6 bash:最强也最宽的默认工具¶
bash schema 接受 command 与可选秒级 timeout。实现使用进程操作接口、输出 accumulator 和节流 progress;模型直接看到尾部截断内容,完整日志可保存临时文件。
非零 exit code 会形成失败信息;Code Mode 的结构化 shell 结果仍需要检查 exit_code。不能因为 tools.bash() Promise resolve 就认为 shell 命令成功。
可插拔 BashOperations / spawnHook 可以修改执行环境。环境暴露、路径、进程组取消、timeout 都是你做远程 executor 时要研究的点。命令 prefix 拼接也不是安全沙箱。
8.7 Extension 是同进程代码¶
扩展默认导出 factory:
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default function (pi: ExtensionAPI) {
pi.on("tool_call", async (event) => {
if (event.toolName === "bash") return { block: true, reason: "本工作流禁止 shell" };
return undefined;
});
}
可以注册工具、命令、事件、UI、provider、MCP server。它不处在模型脚本 VM 中,而是在 Pi Node 进程里,拥有该进程权限。安装扩展等同运行代码。
8.8 Extension 事件放在哪里¶
| 事件 / API | 适合做什么 |
|---|---|
| input | 输入变换或消费 |
| before_agent_start | 当次 prompt / tool loadout / context 准备 |
| context | 请求前变换模型消息 |
| tool_call | 已校验参数的授权拦截 |
| tool_result | 结果脱敏、展示与修正 |
| turn_end | 有持久 entry ID 的轮次边界 |
| agent_before_settle | 完成前检查是否还需续跑 |
| agent_settled | 真正稳定后的通知 |
| session_start | 恢复扩展状态、连接 MCP |
| session_shutdown | 断开资源与清理 |
不要在一个纯 observer 内不加边界地改整个会话;先理解当前事件是否有 persisted ID、是否允许 continuation、是否可能重入。
改参边界:Core / Coding Agent 的 tool_call 位于 schema 校验之后;event.input 可原地修改,后续 handler 见到修改后的值,但之后不会自动再次 schema 校验。若扩展改参,扩展与业务 execute 应验证最终参数及授权。第 11 章 Durable 的 beforeTool 后二次验证是另一套合约。
8.9 资源发现与项目信任¶
DefaultResourceLoader.reload() 解析个人、项目和 package 资源,加载扩展、处理 conflicts 和 diagnostics,再加载 skills、prompts、themes、context files、system prompt。
CLI 先处理可控制 project_trust 的用户/命令行扩展,再决定受保护项目资源是否加载。项目自己的扩展不能先执行再批准自己。
context files 的发现不要求 project trust; .pi/settings.json 中 sessionDir 还会在信任前用于定位会话。项目信任不是完整启动沙箱,也不是每个 tool 的权限策略。
对自己的 SDK 产品,最容易重现的办法是自定义 ResourceLoader 或关闭自动资源发现,显式注入已审核内容。课程 SDK 例子也不加载项目 AGENTS 内容,避免目标仓库的指令变成你的产品规则。
8.10 Skills、Prompt、Extension 选择¶
| 类型 | 内容 | 加载成本 | 强制约束能力 |
|---|---|---|---|
| AGENTS / system 附加 | 常驻规则 | 每轮上下文 | 模型约定 |
| Prompt template | 参数化用户文本 | 使用时 | 模型约定 |
| Skill | 指令和参考文件 | 摘要常驻、全文按需 | 模型约定 |
| Extension | 可执行代码与 hooks | 运行时 | 可阻止工具路径,但不是 OS 隔离 |
工具拦截 hook 可以强制某条执行路径失败;Skill 的 allowed-tools 元数据不自动实现全局操作系统授权。
8.11 状态恢复¶
在 extension 中恢复分支状态:在 session_start 读取 ctx.sessionManager.getBranch() 的 custom entries 重放状态;同一会话的 /tree 导航还需要在 session_tree 重建,因为这条路径不触发 session_start。修改用 appendEntry。更简单的可靠策略是每次调用工具时从当前 branch 重算,内建 Code Mode store 正是这样读取。
只有 session_start 的缓存会在 tree 导航后过期。不能把全局 Map 沿用到另一条路径;custom entry 是分支状态数据,不是自动刷新的内存对象。
课程研究包故意无复杂状态,先让你掌握注册和拦截。后续练习可以增加 review checkpoints,用 custom entries 保存,不把它写成纯内存计数。
8.12 同名内建替换¶
CLI 的 MCP / Code Mode / tool_search 是可替换内建扩展。如果另一个扩展注册 /mcp 等冲突名称,默认内建可能被替换;这不是简单的「两个都运行」。升级或安装包后工具突然不见,先检查加载诊断和已安装扩展。