一、前言
最近在重新翻 Pi Agent 的几个包时,我开始注意到一个之前没单独拆开看的东西:Extension。
一个很直接的原因是,前面两篇把架构、Session、Context、Compaction 都讲了一圈,但凡是讲到 extension state 的地方,都只是一笔带过。而我自己在实际折腾 Agent 时发现,真正决定「这个 Agent 能不能贴着我的工作流跑」的,往往不是核心 Loop 本身,而是 Loop 之外那些能悄悄插手的扩展点。
所以这篇文章不想再去重复 Agent Loop 怎么转,而是想回到一个更具体的问题:
Pi Agent 里的扩展,到底是怎么被加载、在什么位置介入、又通过什么方式影响 Agent 的运行?
本文会以 Pi Agent 为例,看看 Extension 在分层架构里处在什么位置、它的生命周期有哪些阶段、以及最关键的 hook 机制是怎么把「外部逻辑」缝进 Agent 执行过程中的。
二、扩展在分层中的位置
2.1 扩展不是第五个核心包
回顾一下 Pi Agent 的四个核心模块:
-
pi-ai:管模型、Provider 接入。
-
pi-agent-core:管执行流程、状态、工具调用循环。
-
pi-tui:管终端 UI、输入框、消息渲染。
-
pi-coding-agent:管 CLI / Harness、会话、配置、工具、上下文、安全信任,依赖上面三个包。
扩展并不属于这四个包里的任何一个。
更准确地说,Extension 是一层「外部介入面」------它由 resourceLoader 从 cwd 相关的资源里加载进来,然后被注入到 AgentSession 的 extension state 中。它不替 Agent 做决策,只在某些关键节点上改变 Agent 看到的上下文、拦截一次工具调用、或者接管一部分 UI。
所以扩展和四个核心包的关系可以概括成:
pi-coding-agent
│ 依赖
├─ pi-agent-core (执行循环 / 状态 / 工具调用)
├─ pi-ai (模型 / Provider)
└─ pi-tui (终端 UI)
Extension(不在上述四个包内)
│ 由 resourceLoader 加载
├─ 元信息 (name / description / location)
├─ hook 定义 (before_agent_start / beforeToolCall / ...)
├─ UI 扩展 (命令 / 面板)
└─ 运行态 (ExtensionRunner / UI context / handlers)
2.2 为什么扩展必须跟着 Session 走
前面讲 Services 时提到过一个关键点:Session 往往与当前 cwd 强绑定,而 cwd 会决定项目配置、AGENTS.md、Skills、还有扩展本身。
因此扩展并不是全局挂载一次就完事。当一个 Session 被新建、恢复、Fork 或切换时,它依赖的那套运行环境会先被重建,扩展也会随之重新加载和绑定。
也就是说,扩展的生命周期,是挂在 Session 生命周期下面的。这一点后面讲 rebind 时会再回来。
三、扩展的加载与生命周期
3.1 加载时机
扩展的加载,发生在 resourceLoader 初始化当前 cwd 相关资源的过程中。它会把当前项目(以及全局)下声明的扩展读进来,交给 AgentSession 登记到 extension state。
这里最关键的一点是:
❝
扩展在进入 Agent Loop 之前就已经就位,但它本身并不进入 messages 数组------它只是静静地等着被各个 hook 点位触发。
3.2 生命周期阶段
一个扩展从加载到退场,大致会经历这样几个阶段:
加载 Extension(resourceLoader 读入)
↓
before_agent_start
│ (可临时改写 system prompt / 注入上下文)
↓
Agent Loop 运行
│ ├─ beforeToolCall (工具调用前)
│ ├─ afterToolCall (工具调用后)
│ └─ ... 其他 hook 点位
↓
agent_end
↓
shutdown / abort / error handler(收尾与清理)
简单来说,扩展的「存在感」主要来自两头:一头是 Loop 启动前的 before_agent_start,一头是 Loop 运行过程中散布的各个 hook。中间那段 Loop,扩展并不主动参与,只在被 hook 命中的瞬间插一句话。
四、hook 机制
4.1 什么是 hook
hook 是扩展介入 Agent 运行的唯一通道。
它本质上是一个「在某件事发生前 / 发生后,允许外部逻辑先跑一段」的约定。Pi Agent 不把扩展的逻辑硬编码进核心 Loop,而是把若干关键节点暴露成 hook 点位,扩展按需挂载。
因此,对核心 Loop 来说,它看到的不是「某个扩展在做某事」,而是「在这个节点上,如果注册了 hook,就先执行它」。
4.2 常见的 hook 点位
从前面提到的 extension state 字段,可以梳理出几个典型的 hook 点位:
-
before_agent_start:Agent 真正开始跑之前。适合做上下文准备,比如临时改写 system prompt、注入项目级约束。
-
beforeToolCall:每次工具调用执行前。适合做校验、审计、改写参数。
-
afterToolCall:工具执行之后、结果写回 Context 之前。适合做结果转换、脱敏、追加日志。
-
shutdown / abort / error handler:Agent 结束、被中止或出错时的收尾。适合释放资源、保存状态。
例:
❝
/demo 帮我在这个项目里加一条「每次写文件前先备份」的规则
这条规则最终不是写进核心 Loop,而是被实现成一个挂载在 beforeToolCall 上的扩展。
4.3 hook 改写上下文,但不留脏状态
before_agent_start 有个很实用的用法:临时改 system prompt。
但这里有个容易踩的坑------如果扩展在启动阶段改了 system prompt,而跑完之后不还原,那么后续每一轮 Loop 都会带着这份被改过的提示词,污染整个会话。
所以 Pi Agent 的处理方式是:扩展在 before_agent_start 里写的是「本轮临时 override」,等这一轮执行结束,再把它清掉,恢复成基础 system prompt。
before_agent_start
↓
改写 system prompt(本轮临时 override)
↓
Agent Loop 执行
↓
执行结束
↓
清掉 override,恢复原 system prompt
核心可以一句话概括:
❝
扩展对上下文的改写是「会话级临时生效」,不是「永久持久化」。
五、扩展与 UI / 宿主的交互
5.1 Extension UI context
扩展不只是逻辑,它还能带一部分 UI。
在 extension state 里能看到 ExtensionRunner、扩展 UI context、扩展命令上下文这些字段。也就是说,扩展可以注册自己的命令、面板,甚至在某些节点上接管终端里的展示。
对 Agent 来说,这部分和核心 Loop 是解耦的:Loop 只负责推理和工具,UI 的呈现交给 pi-tui 与扩展各自协商。
5.2 切换 Session 时的 rebind
前面埋的那个点,这里收回。
当用户切换 Session 时,旧 Session 被销毁、新 Session 创建,这套运行环境会被重建。此时扩展既然是挂在 Session 下的,它的 UI 绑定也必须跟着重新接上。
这一步由 AgentSessionRuntime 的 rebind hooks 完成:
切换 Session
↓
AgentSessionRuntime.rebind hooks
↓
通知 UI / Host 重新绑定当前 session
↓
Extension UI context 重建
因此,扩展的 UI 不是「全局常驻」,而是「随当前 Session 动态绑定」。这保证了不同项目下的扩展不会串台。
六、扩展的权限边界
6.1 扩展能做什么,不能做什么
扩展能做的事,严格受它挂载的 hook 点位约束:
-
它能在
before_agent_start改写上下文,但不能凭空创造一轮新的 Agent Loop; -
它能在
beforeToolCall拦截和改写工具参数,但不能绕过工具直接改 messages; -
它能在收尾阶段释放资源,但不能阻止 Agent 已经做出的决策。
一句话:
❝
扩展是 Agent 运行过程中的「介入面」,不是「替代品」。
它改变的是 Agent 在某个节点上「基于哪些信息、以什么约束」去做决策,而不是替 Agent 把决策做了。
七、整体来看
如果把扩展机制压成一条链,就是:
Extension 被 resourceLoader 加载
↓
注入 AgentSession.extension state
↓
before_agent_start 改写上下文
↓
Loop 中 hook 介入工具 / 消息
↓
agent_end / shutdown 收尾清理
↓
Extension 本身不进入消息流,
只通过 hook 与上下文影响 Agent
所以 Extension 本质上不是 Agent 内部的「功能模块」,而是一种运行过程中的「外部介入面」:它不替 Agent 思考,只在关键节点悄悄改写上下文、拦截工具、接管 UI,然后把自己清理干净。
如果说 Agent Loop 负责的是「决策怎么发生」,那么 Extension 负责的就是「决策发生的环境与约束长什么样」------它让一套固定的核心 Loop,能够贴着不同项目、不同工作流去「个性化」地运行。