扩展
本页面是 Pi 官方文档 的中文翻译。仅供学习参考。
扩展是给 Pi 添加可执行行为的 TypeScript 模块。当工作流需要工具、命令、事件处理器、模型 Provider、会话状态或终端 UI,而不只是指令时,使用扩展。
扩展在 Pi 进程内运行,拥有相同的操作系统权限。它可以检查 Prompt、tool call、文件、凭证和会话历史,所以只从你信任的来源加载扩展。
典型的扩展会添加 agent 工具、保护路径、确认危险命令、响应会话事件、修改上下文、暴露命令,或显示常驻状态。
创建并加载扩展
扩展导出一个默认工厂函数,它接收 ExtensionAPI。工厂函数为当前扩展 runtime 注册能力。
创建 ~/.pi/agent/extensions/hello.ts:
启动 Pi 并运行 /hello。开发期间可以直接加载文件:
Pi 使用 jiti,所以本地 TypeScript 扩展不需要单独的编译步骤。分发扩展和依赖请使用 Pi 包。
加入 Pi
把扩展放在你的用户或项目扩展目录中。Pi 加载直接的 TypeScript 或 JavaScript 文件,以及包含 index.ts 或 index.js 入口点的子目录。
小扩展用单个文件,多文件实现用目录。把 npm 依赖放在附近的 package.json 中。约定位置见配置,其他路径见设置。
重新加载会替换扩展 runtime,所以 await ctx.reload() 之后的代码不能复用旧 runtime 的状态。只有个人扩展和显式的命令行扩展可以参与 project_trust 事件,该事件在项目扩展加载之前运行。
遵循 runtime 生命周期
工厂函数可以是同步或异步的。Pi 会等待异步工厂函数完成后再继续启动,这使它能够获取配置或注册启动期间需要的 Provider。
不要在工厂函数中启动进程、socket、watcher 或定时器,因为有些调用会在不启动会话的情况下加载扩展。
请从 session_start 启动长生命周期资源,或从需要它们的命令或工具中启动。
从幂等的 session_shutdown 处理器中关闭会话级资源。
一次运行的流程是:从输入和 before_agent_start,经过模型、消息和工具事件,到 agent_end。
自动重试、恢复、压缩或排队工作可能在此后继续。
agent_before_settle 是最后一个可操作的边界:它可以追加条目并请求一次继续。
agent_settled 是最终的、仅通知的边界;当某个集成需要知道 Pi 不会自动继续时使用它。
选择集成点
确切的 event、context、tool 和 result 类型请参考 extensions/types.ts 中导出的声明。
遵循扩展契约
事件与并发
处理器按扩展加载和注册顺序运行。pi.on() 返回一个函数,用于取消该注册;改动不会影响已经在进行的分发。
有些事件只做通知;另一些会转换数据、替换结果或取消操作。
请使用每个事件声明的结果类型,不要假定任何返回值都有作用。
事件覆盖资源发现、会话、Agent 与消息生命周期、Provider、工具和原始输入。
before_agent_start 同时暴露当前 Prompt 和它的结构化 systemPromptOptions。请优先修改 Prompt 的分段、选中的工具或准则,让 Pi 能够追加转录增量。返回 systemPrompt 或设置 forceSystemPrompt 会为该次运行替换整个 Prompt,而转录会继续记录结构化分段。Provider 把强制的文本作为其开头的系统提示收到。
message_end 可以在保留角色不变的情况下替换已定稿的消息。tool_call 可以修改输入或阻止执行。tool_result 处理器会叠加,每个处理器都能看到之前的改动。
provider_stream_event 在 Pi 规范化之前,为每个已解析的 Provider 流事件触发。该事件标识 Provider、API 和模型;event.data 是 Pi 能拿到的最早结构化值,不一定是原始 HTTP 字节或 SSE 帧。把它当作只读,因为改动可能影响规范化。该事件仅通知,不会持久化。
处理器按流顺序 await,所以慢的处理器会延迟流消费。处理器错误会被报告,但不改变 Provider 响应。见 debug-provider.ts,它是一个可选的查看器,按 assistant 消息分组原始事件。
context 转换对话消息,但不包括 Prompt 和工具的系统消息;Pi 之后会恢复该状态。只有当某次请求本地的转换必须接管完整转录时,才使用 context_with_system,并让索引零处保留一条系统消息。
turn_end 和 agent_before_settle 是可操作的边界。它们的处理器可以串联提议的 custom、custom_message、context_edit 或 compaction 条目,并返回 continue: true 以进行一次后续模型请求。请为继续条件加防护,因为无条件继续会形成循环。完整的校验和排序契约见导出的 event 声明。
cache_warming_decision 可以用 { action: "warm" } 或 { action: "stop" } 覆盖空闲 Prompt 缓存刷新。最后一个返回动作的处理器胜出。
来自同一条 assistant 消息的 tool call 可以并行运行。
当另一个工具事件运行时,不要假定存在同批的调用或结果。
请用 ctx.signal 处理由活动 turn 拥有的嵌套工作;命令和空闲会话事件通常没有操作 signal。
返回 undefined 的 user_bash 处理器会把命令传给下一个处理器,如果没有处理器处理它,再交给本地执行。返回 operations 或 result 会停止传播。处理器失败会阻止该命令,而不会落到本地执行。
工具
自定义工具定义名称、面向模型的描述、TypeBox 参数 schema 和 execute() 函数。
它的结果需要面向模型的 content,以及用于渲染或状态重建的 details 字段。
没有结构化 details 时使用 details: undefined。如果该工具发起嵌套模型调用,请把它们的 usage 包含在结果中,以便会话总量保持准确。
从 execute() 中抛出异常会生成失败的工具结果。
返回对象不会把它标记为错误。
只有当该批次中每个已完成的工具都同意终止、agent 应当跳过自动 follow-up 时,才返回 terminate: true。
当工具共享可变的内存状态时,使用顺序执行。
修改文件的工具应当用 withFileMutationQueue() 包住完整的读-改-写操作。
请截断过大的面向模型的结果,并告诉模型去哪里读取完整输出。
当结果是数据时,声明 outputSchema 并返回匹配的 structuredContent。模型仍会收到 content;codemode 脚本等程序化调用方收到 structuredContent 而不是文本。没有 outputSchema 的工具以文本内容传给脚本。要报告仍带数据的失败,返回带 isError: true 的结果而不是抛出:模型看到错误,脚本仍收到 structuredContent。
工具可以用 ctx.executeTool(name, args, { signal, onUpdate }) 运行其他工具。嵌套调用会像模型发出的调用一样经过参数校验以及 tool_call 和 tool_result 处理器,并发出 tool_execution_start、tool_execution_update 和 tool_execution_end;这些事件都带 parentToolCallId,它们的 toolCallId 由 pi 赋为 <parent id>/<n>。这些 id 不会作为 tool call 或工具结果出现在转录中。嵌套调用不添加转录条目:结果只到达调用方工具,由它自己报告,例如通过 onUpdate 和 details。会话保留它们的有界记录(名称、参数、状态、时长、错误;从不包含结果)作为调用方工具结果消息上的 nestedCalls。它用于压缩文件列表,并显示在 HTML 导出中。每个调用超过 8 KiB 或每个工具结果超过 32 KiB 的参数会被省略,最多保留 256 次调用,complete: false 标记丢失了任何内容的记录。任意深度的嵌套结果 usage 会加到调用方工具的结果 usage 上,因此一个工具只报告自己的用量,不包括它调用的工具。ctx.tools 列出 ctx.executeTool() 能调用的工具。改写 content 的 tool_result 处理器也应替换 structuredContent;只替换 content 会丢掉它。
示例见 hello.ts、todo.ts、dynamic-tools.ts 和 truncated-tool.ts。
工具暴露
exposure 控制模型如何到达一个工具。"可调用"指可以通过 ctx.executeTool()(ctx.tools)从其他工具调用,codemode 工具的脚本就是这样做的:
direct(默认):激活时向模型声明,激活时可调用。model-only:激活时向模型声明,永不可调用。用于编排其他工具或询问用户的工具。codemode:只要已注册就可调用,并由codemode工具列出。除非显式激活,否则不向模型声明。deferred:类似codemode,但 codemode 工具不列出它;tool_search可以查找并激活它。hidden:已注册但不可达。用exposure: "hidden"重新注册一个工具来撤回它,因为工具无法注销。
namespace: { name, description, instructions } 把相关工具分组,MCP 服务器就是这样做的。codemode 工具把一个 namespace 列在同一个标题下,并带上它的 description。instructions 存放更长的用法说明;它不会列出,codemode 脚本用 describeNamespace(name) 读取。
注册 direct 或 model-only 工具会激活它;其他 exposure 在注册时不激活。活动集(pi.getActiveTools()、pi.setActiveTools())是向模型声明的工具集。pi.getAllTools() 报告每个工具的 exposure、namespace 和 annotations。
annotations 是关于工具做什么的提示,含义与 MCP 工具 annotations 相同:readOnlyHint、destructiveHint、idempotentHint 和 openWorldHint。MCP 工具携带服务器声明的提示。缺失的提示取 MCP 默认值:工具不是只读的,可能有破坏性并到达开放世界。这些提示未经验证,但权限扩展可以用它们决定确认哪些调用。下面会确认 Codex 要求批准的调用:
编排其他工具的工具可以用 prepareLoadout(loadout) 在自己激活时调整模型看到的内容。它在活动工具变更时运行,收到已声明的工具、可调用的工具,以及每个已注册工具及其 exposure 和 namespace。它返回已声明工具(包括自己)的替换 descriptions,以及 hiddenDeclarations:请求中省略声明、但仍保持活动且可调用的活动工具。codemode 只用这个 hook、exposure 和 ctx.executeTool(),因此另一个工具可以用不同名称实现相同行为。
动态激活工具
先注册所有工具,让可选工具保持不活动,再从某个加载器工具调用 pi.setActiveTools() 选择想要的活动工具。名称必须已经注册;未知名称会被忽略。
Pi 在转录的第一条系统消息中记录初始 Prompt 和工具集,然后在下次模型请求之前追加工具和 Prompt 的改动。无法表示这种转换的 Provider 会收到一份完整的转录检查点,这可能让缓存的 prefix 失效。
MCP 服务器
pi.registerMcpServer(name, config) 为当前会话添加一个 MCP 服务器。config 的形状与 mcp.json 中的 mcpServers 条目相同:stdio 服务器用 command、args、env 和 cwd,HTTP 服务器用 url、headers 和 oauth,另外还有 exposure、toolExposure、description、enabled 和 timeout。
扩展加载时注册的服务器会在会话启动时与 mcp.json 中的服务器一起连接;之后注册的服务器立即连接,pi.unregisterMcpServer() 关闭连接并使该服务器的工具不可达。注册不会保存:每次加载都要重新注册,例如根据扩展自己的设置。mcp.json 中的同名服务器优先,/mcp 会显示覆盖。再次注册同一名称会替换该扩展先前的注册;其他扩展已注册的名称、无效名称和无效配置会抛出。
内置 MCP 支持会连接已注册的服务器。当没有东西连接时(因为另一个扩展替换了它,见 MCP),每次注册都会报告为扩展错误。其他 MCP 扩展也可以连接已注册的服务器:在 session_start 上用 pi.getMcpServers() 读取它们,并处理 mcp_servers_change 事件以应对后续变更。
上下文与会话变更
ExtensionContext 提供工作目录、模式、UI、会话管理器、模型 runtime、中止 signal、上下文用量,以及压缩和关闭的控制方法。
用 ctx.modelRegistry.streamSimple() 发起与 Provider 无关的嵌套模型调用。
命令处理器收到 ExtensionCommandContext,它增加了等待空闲、重新加载、树导航和会话替换的操作。
这些操作仅限命令使用,因为从生命周期处理器中调用它们会让 runtime 死锁。
会话替换会让旧 context 失效。切换之前只捕获普通数据,然后使用 withSession 提供的新 context 处理与会话绑定的工作。
状态
根据状态如何参与对话来选择存储:
在 session_start 期间用 ctx.sessionManager.getBranch() 重建对分支敏感的状态。
不要从每个文件条目重建它,因为被放弃的分支代表的是另外的历史。
当自定义的已存内容应当出现在转录中时,注册条目或消息渲染器。
UI 与模式
ctx.ui 提供对话框、通知、状态文本、widget、标题、编辑器访问和自定义组件。
只有当交互需要自己的渲染和输入时,才使用 ctx.ui.custom()。
组件、焦点、overlay、主题和性能指引见终端 UI。
扩展会在交互、RPC、JSON 和 print 模式下加载。
交互模式提供完整的终端 UI。
RPC 可以通过 RPC 扩展 UI 协议转发支持的对话框和通知,但不支持自定义终端组件;JSON 和 print 模式没有 UI。
用 ctx.mode === "tui" 守住仅终端的行为,用 ctx.hasUI 判断交互式和 RPC 客户端支持的交互。
请让工具和事件行为独立于渲染,使非交互模式仍然可用。
错误与清理
Pi 会报告处理器错误,并尽可能继续。tool_call 处理器失败会作为故障保护阻止该工具;工具执行失败会成为给模型的错误结果。
即使正常操作尝试过清理,也要在 session_shutdown 中释放资源。
请让清理保持幂等,因为取消、重新加载、会话替换和进程退出可能汇聚到同一条路径。
用 ctx.shutdown() 请求有序关闭进程。
示例与参考
已检入的扩展示例覆盖工具、生命周期事件、命令、标志、快捷键、状态、渲染、Provider、OAuth、远程执行和终端组件。
请从与你的集成点匹配的最小示例开始。
模型服务集成见自定义 Provider,自定义组件见终端 UI,安装或与其他资源一起分发扩展见 Pi 包。
法律声明:本页面是 pi.dev 官方文档的中文翻译版本,仅供学习参考。本网站与 pi.dev 及 Earendil Inc. 无任何法律关系。

