Pi Agent 101|09|Extension Runtime

Pi 的扩展系统把加载期注册、运行期 action、工具 hook、UI context 和资源发现分开。

Pi Agent 101|09|Extension Runtime

先说人话:如果每加一个能力都要改 Pi 的核心代码,Pi 很快就会变成一坨大泥球。

Extension Runtime 的作用,是给外部能力留门:可以加工具、加命令、加 UI、接 provider、拦截工具调用,但不让这些能力随便污染 core。

很多 agent 项目后期都会遇到同一个问题:用户想加工具、加命令、改 UI、接自定义 provider、注入上下文、拦截工具调用。如果每个需求都改 core,系统会越来越重。Pi 的答案是 extension runtime。

Pi 的扩展不是只挂几个 slash command,而是贯穿 input、context、provider request、tool call、tool result、message lifecycle、session lifecycle 和 UI 的 hook 系统。

Extension Runtime

这张图的重点是 lifecycle。扩展不是随便把一段代码塞进 agent,而是在明确时机注册能力、监听事件、拦截输入或参与工具执行。加载期解决“有什么能力”,运行期解决“这次任务什么时候用”。

这套边界让 extension 既能增强系统,又不至于把 agent loop 改成不可预测的全局副作用集合。

给基础读者的慢速版地图

扩展让 Agent 不只依赖内置能力。它可以增加命令、工具、快捷键、模型 provider、UI 交互,也可以在关键事件发生前后插入策略。但扩展越强,越需要边界:什么时候能改输入,什么时候能阻止工具,什么时候只能观察,什么时候必须失效。

Extension Runtime|扩展能力

读图说明:这张图把“扩展能力”拆成五个连续位置。先不要记术语,先看每一格负责什么,以及上一格的结果怎样交给下一格。

Extension Runtime|事件位置

读图说明:这张图把“事件位置”拆成五个连续位置。先不要记术语,先看每一格负责什么,以及上一格的结果怎样交给下一格。

Extension Runtime|合并策略

读图说明:这张图把“合并策略”拆成五个连续位置。先不要记术语,先看每一格负责什么,以及上一格的结果怎样交给下一格。

Extension Runtime|上下文失效

读图说明:这张图把“上下文失效”拆成五个连续位置。先不要记术语,先看每一格负责什么,以及上一格的结果怎样交给下一格。

Extension Runtime|冲突处理

读图说明:这张图把“冲突处理”拆成五个连续位置。先不要记术语,先看每一格负责什么,以及上一格的结果怎样交给下一格。

这组图的目的不是替代正文,而是给读者一个低门槛入口:先形成整体画面,再回到正文理解为什么这些边界必须存在。

读完这一篇,你应该能看懂什么

  • 理解 extension 为什么要区分加载期注册和运行期 action
  • 看清 tool_call / tool_result / session_before_* 等 hook 的边界
  • 学会把 UI 能力抽象成 context,而不是直接绑定 TUI 实现

和主流产品怎么对应

Claude Code 的 MCP、Cursor 的扩展生态、OpenClaw 的 plugin,本质上都在回答同一个问题:怎么给 agent 加能力,又不把 core 改成一团泥。

区别在边界。MCP 更像外部工具协议,extension/plugin 更像产品运行时扩展。比如一个插件想在工具执行前做审批,在工具结果后做审计,或者给 TUI 加一个面板,这些就不是单纯“多接一个工具”能解决的事。

加载期注册,运行期绑定

Extension 是 TS/JS module,默认导出 factory(api)。加载时它可以注册 tools、commands、shortcuts、flags、message renderers、providers、event handlers。但加载期 action 默认是 throwing stub,只有 Runner.bindCore() 后才变成真实动作。

这个设计避免 extension 在 import/load 阶段产生危险副作用。注册是声明,执行要等 runtime 绑定。

原因很实际:加载一个 extension 时,系统可能只是想发现它声明了哪些 command、tool 或资源,而不是允许它立刻发网络请求、写文件、读取 session,甚至弹 UI。Throwing stub 相当于一条硬边界:import 阶段只能登记能力,真正有副作用的 action 必须等到 runner 绑定当前 session、权限和 UI context 之后才能发生。

Hook 分类

  • Input/UI:input、commands、shortcuts、custom editor、overlay
  • Agent lifecycle:before_agent_start、message_start/update/end、turn_start/end
  • Provider:before_provider_request、after_provider_response
  • Tool:tool_call、tool_result
  • Session:session_before_switch、session_before_fork、session_before_compact、session_shutdown
  • Resource:resources_discover

这套 hook 系统让 extension 可以实现 policy、观测、UI 增强、上下文注入和工具扩展,而不需要修改 core。

放到修测试这个例子里

还是“修复失败测试”。一个团队可能想加三件事:跑测试前先确认命令不会太重;测试失败后把日志截成摘要;最后把修复过程发到内部系统。最差的做法,是把这些逻辑都写进 Tool Runtime。

Extension Runtime 更适合处理这类需求。测试命令经过 tool_call hook 时可以被检查,tool_result hook 可以改写长日志,session lifecycle hook 可以在任务结束时做导出。core 仍然只负责 agent 主流程。

UI Context 的价值

Extension UI 不直接依赖 TUI 组件。Pi 暴露 ExtensionUIContext:select、confirm、input、notify、footer/header/status、overlay、widget、autocomplete、editor 操作等。非 interactive 模式可以给 no-op UI context。

这让同一个 extension 可以判断 hasUI,在 TUI 中展示交互,在 print/RPC 中降级。

可迁移的边界

如果自己做 harness,extension 不应该随便改全局状态。更稳的方式是提供明确生命周期点:注册工具、注册 command、监听 input、before_agent_start、before/after tool、shutdown。扩展能力越强,边界越要清楚。

Extension 和 Tool 的区别

Tool 是模型可以请求执行的动作;Extension 是把外部能力接入 runtime 的机制。一个 extension 可以注册 tool,也可以注册 slash command、input hook、before_agent_start hook、before/after tool hook 或 UI context。

如果把 extension 简化成 tool,就解释不了为什么它可以影响 prompt、界面、命令和生命周期。如果把所有 extension 都放开成任意代码,又会让系统难以调试和审计。

为什么 hook 要少而清楚

Hook 越多,扩展越强;hook 越乱,runtime 越不可控。好的 extension runtime 会把可介入的位置设计得很少但很明确:输入进来前、agent start 前、工具执行前后、UI 渲染上下文、关闭时清理资源。

这对 OpenClaw 这类系统尤其重要。channel、plugin、gateway 都需要扩展能力,但底层 agent run 不能被每个插件任意改写。

这里的取舍

  • 取舍:加载期
    • Pi 的倾向:只注册,不执行真实动作
    • 可迁移原则:避免 import 副作用
  • 取舍:Tool policy
    • Pi 的倾向:tool_call / tool_result hook
    • 可迁移原则:策略层不要写死在工具里
  • 取舍:UI
    • Pi 的倾向:ExtensionUIContext
    • 可迁移原则:插件不直接绑定具体 TUI
  • 取舍:Session
    • Pi 的倾向:before_* 可取消
    • 可迁移原则:危险 lifecycle 操作要有拦截点
  • 取舍:Resources
    • Pi 的倾向:resources_discover
    • 可迁移原则:扩展可以声明更多 skills/prompts/themes

MCP 和 Extension 不是一层东西

很多人会问:Claude Code 有 MCP,Pi 有 Extension,它们是不是一回事?不是。

MCP 更像“把外部工具和资源接进来”的协议层。它解决的是:模型或 agent 怎么发现并调用外部 server 提供的工具。

Extension 更像“把能力嵌进 runtime”的插件层。它不只可以加工具,还可以加命令、UI、provider、hook、资源发现、session 生命周期拦截。也就是说,MCP 更偏外部工具协议,Extension 更偏产品运行时扩展。

这两个东西可以共存。一个 Extension 甚至可以负责接入某类 MCP server。但它们解决的问题不同,不能混成一个概念。

如果你在读 OpenClaw

如果你在读 OpenClaw,这一篇尤其重要。OpenClaw 的核心概念之一就是 plugin。Pi 的 Extension Runtime 可以先帮你理解:外部能力怎么注册,什么时候能执行,如何拦截工具调用,UI 能力为什么要通过 context 暴露。

源码里真正能看到的扩展边界

Pi 的扩展系统有一个很清楚的分层:扩展自己保存注册结果,比如命令、工具、快捷键、事件处理器;真正执行动作时,扩展会委托给当前 session 提供的 runtime。这样扩展可以并存,但核心状态由宿主统一仲裁。

扩展能介入的点很多,但不是任意乱入。输入进来时可以拦截或改写;agent start 前可以追加上下文;工具执行前可以阻止;工具执行后可以改写结果;会话切换、fork、压缩和树跳转前都可以取消;UI 交互则通过当前 mode 提供的 UI context 完成。

还有一个很重要的保护:会话替换后,旧扩展上下文会失效。扩展如果异步拿着旧上下文继续操作,会被挡住。这是产品级插件系统必须处理的问题,否则一个旧 session 的插件回调可能污染新 session。

扩展系统真正提供的产品能力

从用户视角看,extension 不是“加载一段代码”,而是把个人工作流、企业策略、外部工具和 UI 交互接进 agent runtime。比如某个扩展可以注册一个命令、拦截危险工具、给 provider 请求加审计头、提供一个选择器 UI,或者在压缩前保留领域状态。

但扩展越强,边界越重要。Pi 的事件和 hook 设计,本质上是在回答:外部能力可以在哪些位置影响 agent,影响结果如何合并,失败时怎么报告,冲突时谁优先。

阅读时可以用的三问

第一,扩展是在加载期声明能力,还是在运行期介入任务。两者混在一起,会让调试很困难。

第二,扩展影响的是输入、工具、上下文、UI,还是关闭清理。每个介入点都应该有名字和边界。

第三,扩展失败时会怎样。一个插件报错不应该悄悄污染整个 agent run,也不应该让用户完全不知道发生了什么。

如果只记住一句话

扩展系统的关键不是能加东西,而是能安全地加东西。

小结

Pi 的 Extension Runtime 把“可扩展”做成运行时边界,而不是产品口号。它允许第三方能力进入 agent,但通过注册期/运行期、context 分层、UI 抽象和 lifecycle hook 控制副作用。