青雲的博客

第六部:工具与扩展怎样进入运行时

从七个 built-in definition 与四个默认 active tool 出发,进入同文件 mutation queue、Bash 有界输出、三类 Markdown 资源,再沿项目 trust、ExtensionRunner 与各 registry 的冲突规则走完能力装配链。

内置工具、资源和可执行扩展经过信任与冲突规则装配进 AgentSession。
展开阅读路线与实验入口

第五部把 AgentSession 定位成 Coding Agent 的控制面。它还没有回答能力从哪里来:read 为什么存在但 grep 默认不可见,两个 edit 为什么没有互相覆盖,几兆 Bash 输出为什么只回到模型几十 KB,项目里的 Skill 与 extension 又怎样进入同一轮。

这一部不把它们统称为“插件”。Pi v0.83.0 至少有四种不同对象:built-in tool definitions、Markdown 资源、可执行 extension、SDK 调用方直接传入的 custom tool。它们在不同阶段进入,也由不同 owner 保管。先分清对象,后面的 trust 和 precedence 才有意义。

七章共享的状态地图

第 31 章先拆开 definition、registry、active set 与 system prompt。Pi 定义七个内置工具,默认 active names 只有 read/bash/edit/write。这不是遗漏,也不是权限表。grep/find/ls 仍在 registry 候选中,调用方可以显式启用;操作系统权限则仍由 executor 所在进程承担。

第 32、33 章进入两个 executor 的局部工程约束。editwrite 对同一 canonical path 排队,避免当前进程内的写写重叠;不同文件不共用全局锁。Bash 的 OutputAccumulator 只在内存保留有界尾部,越过 2000 行或 50KB 后才把完整原始字节 spill 到 temp file。前者约束并发,后者约束输出规模,都没有把 tool 变成安全沙箱。

第 34 章从执行回到资源。AGENTS.md/CLAUDE.md 全文进入 project context,Skill 启动时只把 name/description/path 索引放进 prompt,Prompt template 则等 slash name 命中才展开。三类文件都可能是 Markdown,运行合同却没有共享。

第 35 至 37 章再处理可执行 extension。ResourceLoader 先在 untrusted settings 下预载用户、CLI 与 inline extension,让可信的全局策略可以参与 project_trust;决定后再补入获准的项目设置、包和 extension。Runner 把 AgentSession action 与 handler 接起来,并按事件类型决定广播、链式 transform 或短路。最后分别读取 tool、provider、renderer 的同名规则,不用一句含混的“后者覆盖前者”收尾。

flowchart TD
  accTitle: 第六部的能力装配地图
  accDescr: built-in definitions、文本资源、extension factories 与 SDK custom tools 从不同入口进入;AgentSession 组装 tool registry 和 active set,ResourceLoader 构造 prompt 资源,ExtensionRunner 接入事件、provider 与 renderer。
  BUILTIN["7 built-in definitions"] --> TOOLREG["AgentSession tool registry"]
  SDK["SDK customTools"] --> TOOLREG
  SETTINGS["user / project / package resources"] --> LOADER["ResourceLoader"]
  LOADER --> TEXT["context / skills / prompts"]
  LOADER --> EXT["extension factories"]
  EXT --> RUNNER["ExtensionRunner"]
  RUNNER --> TOOLREG
  RUNNER --> PROVIDER["ModelRuntime providers"]
  RUNNER --> RENDER["TUI renderers"]
  TOOLREG --> ACTIVE["active tools for next turn"]
  TEXT --> PROMPT["system/user prompt pipeline"]
  ACTIVE --> AGENT["Agent request"]
  PROMPT --> AGENT

图中的箭头表示 ownership 与装配关系,不是一条单线程启动时序。extension factory 可以在 load-time 注册 provider,实际 provider mutation 要等 Runner bind 时 flush;resources_discover handler 又能在 session_start 后补充 Skill/Prompt/Theme 路径。active tools 改变后会重建 system prompt,但正在执行的 tool call 不会被倒带。

两组边界需要一直保留

第一组是“模型可见”和“进程可执行”的区别。active tool set 控制下一轮传给模型的 schema,项目 trust 控制项目级代码与资源是否进入进程。它们都不是 OS sandbox。一个受信 extension 本身可以访问 Node 能访问的环境;移除 bash schema 也不能证明宿主程序没有其他命令执行路径。

第二组是“完整事实”和“当前投影”的区别。Bash tool result 只显示尾部,完整输出可能在 temp file;Skill prompt 只显示目录卡片,正文仍在磁盘;冲突 diagnostic 告诉你两个 extension 注册重名 tool,却不表示后一个 factory 没执行。看到 UI、日志或 registry 的一份投影时,要继续追它背后的 owner。

建议怎样读

如果你准备改工具执行,按 31、32、33、37 的顺序读:先知道 definition 与 active set 的差别,再看文件和进程 executor,最后确认 custom override 会连执行函数、prompt metadata 与 tool renderer 一起替换。

如果你准备写 extension,按 35、36、37 读,再回到 34。先弄清 factory 在什么权限下执行、load-time 能调用哪些 API;然后按具体 event 看 handler 结果和错误语义;最后核对同名注册 precedence。等 runner 边界稳定后,再用 resources_discover 添加 Skill、Prompt 或 Theme 路径。

如果你只在项目里放 AGENTS.md、Skills 和 Prompt templates,第 34 章可以独立阅读,但不要跳过第 35 章的 trust 边界:项目级资源是否被发现,仍取决于 cwd、SettingsManager 的 projectTrusted 状态和 package manager resolution。Markdown 不执行,不代表它一定会被未信任项目加载。

每章都提供只读 git show/git grep 实验。涉及真实 extension 的实验应放进一次性 HOME、agentDir 与 cwd;否则一次“列出 extension”的测试就可能执行你日常配置中的 factory,观察动作本身已经改变了安全范围。

本部交给下一部什么

到第 37 章结束,可以从源码回答四个问题:模型下一轮看见哪些工具;内置 executor 怎样限制并发与输出;文本资源和 extension 代码分别怎样进入;同名注册由哪份 registry 裁决。

下一部改变的是承载方式。SDK 可以直接拿到 AgentSession,print mode 等一轮结束后输出文本,JSON mode 投影事件,RPC mode 保持一个可控制进程。它们复用本部建立的工具和扩展 runtime,却不会自动复用 interactive TUI 的 renderer、选择器与输入上下文。入口相同不等于表面合同相同。

深入浅出 Pi 第六部:工具与扩展怎样进入运行时 第 31 章

七个内置工具为什么默认只开四个

沿 createAllToolDefinitions、AgentSession 工具 registry 与 active tool 集合,拆清 Pi 的七个内置工具为何只默认启用 read、bash、edit、write。

源码版本
v0.83.0
验证日期
Commit
845d6ff1f6643aba440341cce877ce1c43ebbc39

第五部停在 AgentSession 的控制面。现在把镜头往下移一层:AgentSession 说“当前有四个工具”时,究竟是仓库只实现了四个,registry 里只有四个,还是模型这一轮只看见四个?这三个说法在 Pi 里对应三份不同状态。

源码列出的 ToolName 一共有七个:readbasheditwritegrepfindls。同一文件还提供三种组装函数:coding tools 是前四个,read-only tools 是 read 加三种检索工具,all tools 才把七个全部建出来。它们不是权限等级,只是便于调用方选取 factory 的分组。

Registry 里有七个,Agent state 未必有七个

AgentSession._buildRuntime() 在没有 SDK override 时调用 createAllToolDefinitions(),把七份 ToolDefinition 放进 _baseToolDefinitions。随后 _refreshToolRegistry() 才把 built-in、extension tool 和 SDK customTools 合成 _toolRegistry。这份 map 回答“这个名字现在能解析成哪个 executor”,仍未回答模型会拿到哪些 schema。

真正传给 agent-core 的是 agent.state.toolssetActiveToolsByName() 遍历调用方给出的名字,只接纳 registry 中存在的项;未知名字被忽略。完成选择后,它还用有效工具名重建 system prompt,使 prompt 里的工具说明与模型请求中的 tools 尽量同步。

可以把这几份状态画成一条窄链,而不是一个含混的“工具列表”:

flowchart LR
  accTitle: Pi 内置工具从定义到模型可见
  accDescr: 七个内置 definition 先进入 base definitions,与扩展和 SDK 工具合并成 registry;active names 再筛出 agent state tools,并驱动 system prompt 重建。
  DEF["7 built-in definitions"] --> MERGE["merge extension + SDK definitions"]
  MERGE --> REG["_toolRegistry by name"]
  NAMES["active tool names"] --> FILTER["registry lookup"]
  REG --> FILTER
  FILTER --> STATE["agent.state.tools"]
  FILTER --> PROMPT["rebuild system prompt"]

这张图也解释了一个常见现象:调用 getAllTools() 能看到 grep,不代表当前模型请求带了 grep schema。前者读取 _toolDefinitions,后者取决于 agent.state.tools。相反,某个扩展工具刚注册进 registry 后,如果刷新逻辑把它加入 active names,它可以立即出现在下一轮,而不用成为 built-in。

默认四个是在哪一刻选出来的

SDK 入口先计算 initialActiveToolNames。没有显式 tools、也没有 noTools 时,值就是 read/bash/edit/writeexcludeTools 在这组名字上再做过滤。noTools: "all" 让初始集合为空,noTools: "builtin" 也从空 built-in 集合开始,但它的接口注释明确保留 extension/custom tool 的启用空间。CLI 的 --no-tools--no-builtin-tools 最终要落到这两个不同语义上,不能只看名称猜行为。

显式 tools 是 allowlist,excludeTools 是其后的 denylist。_refreshToolRegistry() 对 built-in、extension 和 SDK custom 定义统一调用 isAllowedTool,所以 allowlist 不是只筛 built-in。若同时写 tools: ["read", "my_tool"]excludeTools: ["my_tool"],后者胜出。

这里可以做一个不加载扩展、也不触碰用户配置的静态实验:

repo="${PI_SOURCE_DIR:-/tmp/pi-handbook-qbQTcA}"

git -C "$repo" show v0.83.0:packages/coding-agent/src/core/tools/index.ts \
  | sed -n '83,85p'
git -C "$repo" show v0.83.0:packages/coding-agent/src/core/sdk.ts \
  | sed -n '245,251p'

第一段应显示七个 ToolName,第二段显示四个 defaultActiveToolNames。两段同时成立,才能得出“七个已定义、四个默认 active”;只运行其中一段,都不足以解释最终模型看到什么。

工具名不是权限

registry 的 allowlist 能限制模型可调用的 schema,却不是操作系统权限层。read 是否能读某个路径、bash 继承哪些环境变量,由各自 executor 和进程权限决定。把 bash 从 active set 移除,只是让当前模型没有这条标准调用路径;扩展仍可能注册另一个能执行进程的工具,宿主程序也可以在外部执行命令。

另一个边界是变更时点。setActiveToolsByName() 直接改 agent.state.tools,注释把合同写成“下一次 agent turn 生效”。正在执行的 tool call 已经持有选中的 executor 与参数,不会因为 UI 切换工具列表而被回收。动态工具章节若只展示开关变化,很容易误写成对正在运行任务的强制撤权。

所以第 31 章的准确表述是:Pi v0.83.0 内置七份工具定义,产品默认把四个名字放进 active set。registry 决定名字解析,active set 决定下一轮模型可见面,文件和进程能力仍由 executor 与宿主环境负责。

下一章沿 editwrite 的 executor 继续。Agent loop 允许一批互不依赖的 tool call 并行,但两个写操作若指向同一个文件,Pi 还需要一层比 tool batch 更细的并发约束。