跳转至

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 后二次验证是另一套合约。

flowchart LR P[prepareArguments] --> V[schema validate] V --> H[tool_call: 可改 input / 可 block] H --> B{block?} B -->|是| R[error result] B -->|否: 不自动重验| E[execute: 校验业务权限]

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 正是这样读取。

flowchart TD ROOT[共同 root] --> A[分支 A: checkpoint reviewed] ROOT --> B[分支 B: checkpoint pending] A --> TA[session_tree 切到 A] B --> TB[session_tree 切到 B] TA --> RA[重放 getBranch: reviewed] TB --> RB[重放 getBranch: pending]

只有 session_start 的缓存会在 tree 导航后过期。不能把全局 Map 沿用到另一条路径;custom entry 是分支状态数据,不是自动刷新的内存对象。

课程研究包故意无复杂状态,先让你掌握注册和拦截。后续练习可以增加 review checkpoints,用 custom entries 保存,不把它写成纯内存计数。

8.12 同名内建替换

CLI 的 MCP / Code Mode / tool_search 是可替换内建扩展。如果另一个扩展注册 /mcp 等冲突名称,默认内建可能被替换;这不是简单的「两个都运行」。升级或安装包后工具突然不见,先检查加载诊断和已安装扩展。

来源:工具实现、扩展文档、ResourceLoader、system prompt。