Extension 真正强大的地方不是“能收到很多 Event”,而是不同 Event 有不同合并语义:
- 有的只能观察;
- 有的可以替换数据;
- 有的按 Middleware 链式传递;
- 有的遇到 Block/Cancel 就短路;
- Handler 抛错时,有的隔离继续,有的为了安全直接让 Tool 失败。
理解这些差异,才能写出可预测的 Extension。
1. 一次 Prompt 的主干
flowchart TD
I["input<br/>可变换/处理"] --> B["before_agent_start<br/>消息/Prompt"]
B --> A["agent_start"]
A --> TS["turn_start"]
TS --> D{"有新注入消息?"}
D -->|有| UM["user/custom<br/>message_start/end"]
D -->|无| C["context<br/>修改消息"]
UM --> C
C --> H["before_provider_headers"]
H --> P["before_provider_request"]
P --> AR["after_provider_response"]
AR --> MS["assistant<br/>message_start/update/end"]
MS --> X{"有 Tool Call?"}
X -->|有| TX["Tool Events"]
TX --> TR["ToolResult<br/>message_start/end"]
X -->|无| TE["turn_end"]
TR --> TE
TE --> Q{"是否需要继续下一 Turn?"}
Q -->|"Tool Call 未终止 / 有排队消息"| TS
Q -->|否| AE["agent_end"]
AE --> S["agent_settled"]
agent_end 只表示一个底层 Run 结束。自动 Retry、Compaction Retry 或 Queue Continuation 仍
可能继续;真正需要把 UI 状态置为稳定,应看 agent_settled。
2. 事件分类
| 类型 | 例子 | 返回值是否影响流程 |
|---|---|---|
| 观察型 | agent_start、turn_end、Execution Events |
否 |
| 变换型 | input、context、Provider Request、Message End |
是 |
| 拦截型 | tool_call、Session Before Events |
可 Block/Cancel |
| 资源型 | resources_discover |
累积 Path |
| 生命周期 | Session/Agent/Turn/Message | 多数观察 |
| 状态通知 | model_select、thinking_level_select |
否 |
分类是教学模型,不是源码导出的 Enum。
3. 输入事件的位置
处理顺序:
Idle:
Extension Command → input → Skill/Template 展开
→ Preflight → before_agent_start → Agent Run
Streaming:
Extension Command → input → Skill/Template 展开
→ Steer/Follow-up Queue → return
Streaming 分支不会为这条排队消息触发自己的 before_agent_start。pi.sendUserMessage() 会
触发 input,但调用时关闭 Extension Command、Skill 与 Template 展开。
input 看见的是原始文本。它返回:
{ action: "continue" }
{ action: "transform", text, images? }
{ action: "handled" }
多个 Handler 顺序运行;Transform 的结果传给下一位,Handled 立即短路。
4. before_agent_start
它看到 Skill/Template 展开后的 Prompt,以及已构建的 Base System Prompt:
return {
message: {
customType: "policy",
content: "当前工作区只允许只读操作。",
},
systemPrompt: event.systemPrompt + "\n\n只读模式。",
};
合并规则:
- 每个返回的 Custom Message 都累积;
- System Prompt 按 Handler 顺序链式替换;
- 后一 Handler 的
event.systemPrompt和ctx.getSystemPrompt()都能看到前一结果; - 若下一次用户提交/Agent Run 没有任何 Prompt Override,AgentSession 恢复 Base Prompt; 一次 Override 会覆盖当前 Agent Run 中的多个 Tool-loop Turn。
5. Context 与 Session Transcript 不相同
context 在 Agent Loop 的每个模型 Turn 前触发:
pi.on("context", (event) => ({
messages: event.messages.filter(shouldSend),
}));
Runner 先 structuredClone(messages),再把前一个 Handler 返回的 Messages 传给下一个。
这只改变本次模型看到的 Context,不等于删除 Session JSONL 中的 Entry。要做“对模型隐藏 但审计仍保留”,这是正确扩展点。
Compaction 与 Branch Summary 的直接 Summarization Call 不经过 Agent Loop
transformContext,所以不能概括成“所有 LLM Call 都触发 context”。
6. Provider 的三层 Event
Context
→ Auth + Headers 组装
→ before_provider_headers
→ Provider-specific serialization
→ before_provider_request
→ HTTP response
→ after_provider_response
→ consume stream
before_provider_headers
原地修改 Headers;null 删除 Header。Provider 内部 HTTP Retry 复用已组装 Header,不会
再次触发;外层 Agent Auto Retry 或 Summarization Retry 是新的模型调用,会再次触发。
before_provider_request
返回任意非 undefined 值替换 Payload,并链给后续 Handler。这里的修改不会反映到
ctx.getSystemPrompt(),因为它已是 Provider Serialized Payload。
after_provider_response
观察 Status 与可用 Response Headers,发生在 Stream Body 消费前。Transport 不一定暴露 全部 Header。
Summarization 复用包装后的 Stream Function,因此会经过 Header Hook;它不会自动带上主
Agent 配置的 onPayload/onResponse,所以三个 Provider Event 不是所有模型请求固定成套
出现的三联链。
7. Message 生命周期
message_start
→ assistant only: message_update × N
→ message_end
Start/End 覆盖 User、Assistant 和 ToolResult;Update 只用于 Assistant Streaming。
message_end 可以返回 Replacement Message。多个 Handler 链式处理,但 Replacement 必须
保持 Role;违反时 Runner 记录 Extension Error 并忽略这次替换。
AgentSession 会原地同步最终 Message,使 Agent State、后续 Event、Session Persistence 看到同一个版本。
8. Tool 的精确事件顺序
单个 Tool:
sequenceDiagram
participant A as Agent Core
participant S as AgentSession
participant R as ExtensionRunner
participant T as Tool
A->>S: tool_execution_start
S->>R: observe start
A->>R: tool_call(input)
alt blocked / handler error
R-->>A: block or throw
else allowed
R-->>A: continue
A->>T: execute
T-->>A: updates/result
A->>R: tool_result
R-->>A: patched result
end
A->>S: tool_execution_end
随后还会形成最终 ToolResult Message 的 Start/End Event。
9. tool_call:修改和阻止
event.input 可原地修改:
if (isToolCallEventType("read", event)) {
event.input.path = normalizeAllowedPath(event.input.path);
}
规则:
- 后续 Handler 看见前一 Handler 的 Mutation;
- Mutation 影响真正执行;
- Mutation 后不会重新做 Schema Validation;
- Return Value 只用
{ block, reason }; - 第一个
block: true立即停止链。
“修改后不重新校验”意味着 Handler 自己必须保持 Argument 符合 Tool Contract。
10. tool_call 的错误是特殊边界
多数 Runner Emit 会捕获 Handler Error、报告给 Error Listener,再继续后续 Handler。
emitToolCall() 不做这层 Catch。AgentSession 的 Before Tool Hook 会把错误重新抛出,使该
Tool 不执行并形成失败结果。这是 Fail Closed:
审批 Handler 崩溃
≠ 默认放行
→ Tool 执行失败
不要笼统说“Extension Handler Error 都被隔离”。
11. tool_result:链式 Patch
执行结束后,Handler 可返回任意字段的 Partial Patch:
return {
content: redact(event.content),
isError: event.isError,
};
可改:
- Content;
- Details;
- isError;
- Usage。
每个 Handler 看见之前的最新值。Handler 抛错会被记录并隔离,后续 Handler 继续。
这改变的是送入后续 Agent 流程的 Tool Result;做 Redaction 时还要确认原始数据是否已写入 其他 Log。
12. Parallel Tool Mode
同一 Assistant Message 有多个 Tool Call 时:
tool_execution_start按源顺序发出;tool_callPreflight 顺序执行;- 允许的 Tool 并行执行;
- Update 可交错;
tool_result与 Execution End 按完成顺序交错;- 最终 ToolResult Message 仍按 Assistant Source Order 发出。
第 2 步也会运行在随后被 Block 或 Handler Error 阻止的 Tool 上;Missing Tool 与 Schema
Validation Failure 才会在它之前变成 Immediate Error。第 5 步的
tool_result → tool_execution_end 只适用于真正执行完成的 Tool;Block/tool_call Error
在 Source-order Preflight 中直接产生 Error End,不触发 Extension tool_result。被
Provider 标记为输出截断的 Tool Call 还会完全跳过正常 Preflight。
所以:
- 审批顺序不等于完成顺序;
- 兄弟
tool_call看不到尚未完成的兄弟 Result; - 需要依赖的 Tool 应设 Sequential 或拆成多个 Turn。
13. Agent 与 Turn 生命周期
一次 Agent Run:
agent_start
turn_start #0
...模型/Tool...
turn_end #0
turn_start #1
...模型最终回答...
turn_end #1
agent_end
Turn Index 由 AgentSession 从 0 开始维护。turn_end 提供本 Turn 的 Assistant Message 与
ToolResults。
agent_settled 在外层自动动作全部结束后发出。若 Extension 在 Settled Handler 又启动
新 Run,ctx.isIdle() 可能随之变化。
14. Session 拦截事件
以下事件可以 Cancel 或定制:
| Before Event | 作用 |
|---|---|
session_before_switch |
New/Resume |
session_before_fork |
Fork/Clone |
session_before_compact |
Cancel 或给 Custom Compaction |
session_before_tree |
Cancel 或给 Branch Summary/Options |
Runner 按顺序执行;遇到 cancel: true 立即返回。没有 Cancel 时,后返回的结果可能成为最终
Result,所以多个会产出 Custom Summary 的 Extension 应建立唯一 Owner。
成功替换 Session 时:
before
→ old session_shutdown
→ reload/rebind
→ new session_start
→ resources_discover
观察型 Session Event 还包括:
session_start:Reason 为startup | reload | new | resume | fork;session_info_changed:Session Name 设置或清除;session_compact:成功压缩后的 Entry、Reason、WillRetry;session_tree:成功导航后的 New/Old Leaf 与可选 Summary;session_shutdown:Reason 为quit | reload | new | resume | fork。
New/Resume/Fork 后会重新执行 Resource Discovery,但其 Reason 仍为 startup;只有显式
Reload 使用 resources_discover.reason = "reload"。
15. Resource Event
resources_discover 在 Session Start 后执行。Runner 收集所有 Handler 返回的 Skill、
Prompt、Theme Paths,并标记它们来自哪个 Extension。
AgentSession 将 Path 交回 Resource Loader,随后重建 Base System Prompt。
这不是每 Turn Event;需要动态随 Turn 改变的上下文,应使用 before_agent_start 或
context。
16. Project Trust Event
它使用受限的 ProjectTrustContext,而不是完整 ExtensionContext。只有 Pre-trust 可加载的
用户/全局、CLI Extension 与 SDK Inline Factory 参与;项目本地 Extension 此时尚未加载。
每个 Handler 可返回:
yes / no / undecided
第一个 Yes/No 胜出;Undecided 继续。Handler Error 被收集,不自动将项目判为 Trusted。
17. User Bash 的专用合并
用户直接输入 ! / !! 时触发 user_bash。Handler 可返回:
- Custom
operations; - 已完全处理的 Bash
result。
Runner 按 Extension/注册顺序运行,第一个非 undefined Result 胜出并短路。Handler Error
被记录并隔离,继续寻找后续 Handler。这与模型调用 Bash Tool 的 tool_call 链不是同一
入口。
18. Model 与 Thinking Event
model_select:New Model、Previous Model、Source;thinking_level_select:New Level、Previous Level。
两者都是 Notification。返回值不会撤销选择。需要阻止模型使用,应在宿主模型选择入口做 Policy,而不是依赖 Notification Handler。
19. 同步与异步 Handler
类型允许:
(event, ctx) => result
async (event, ctx) => result
Runner 对每个 Handler await,同一 Event Chain 顺序执行。一个慢 Handler 会增加该事件点
的延迟。
没有自动 Handler Timeout。网络请求应:
- 使用
ctx.signal; - 设置自身 Timeout;
- 处理 Abort;
- 不在高频
message_update中执行昂贵工作。
20. 错误隔离矩阵
| Event | Handler 抛错 |
|---|---|
| 普通 Lifecycle / Context / Result / Input | 报 Extension Error,继续 |
project_trust |
收集 Error,继续找 Decision |
tool_call |
传播到 Tool Hook,Tool 不执行 |
| Extension Factory | 记录 Load Error,该 Extension 不加载 |
Error Listener 本身由宿主 Binding。没有 Listener 时,产品必须确认诊断不会悄悄丢失。
21. 事件递归
Handler 可调用 pi.sendUserMessage()、pi.sendMessage({ triggerTurn: true }) 或触发
Compaction,从而启动新的事件链。
避免:
agent_end handler
→ sendUserMessage
→ agent_end
→ sendUserMessage
→ ...
使用 Session Entry、内存 Guard、Message CustomType 或 Run ID 判断是否已处理。Pi 不会自动 推断业务递归。
22. 为桌面 Agent 建立 Event Adapter
不要让界面直接订阅每个 Extension Event。建议:
Pi AgentSessionEvent
+
Extension Error/Approval Channel
↓
Desktop Event Adapter
↓
Conversation Store / Tool Store / Approval Store
Adapter 把底层事件转换成稳定的应用 State。第 33 章会完成这层映射。
23. 常见误区
“所有 Event 都能修改流程”
错误。大量 Lifecycle Event 只是观察。
“所有 Handler 错误都会忽略”
错误。tool_call Error 会阻止 Tool。
“tool_execution_start 后 Tool 已经执行”
错误。后面还有 tool_call Preflight,可能 Block。
“agent_end 就可以关闭 Loading”
不一定。外层可能 Retry、Compact 或继续 Queue;用 agent_settled。
“Context 过滤会删除 Session History”
错误。它改变的是本次 LLM Context。
24. 本章小结
- Event 的观察、变换、拦截和累积语义不同;
- Input 在 Skill/Template 展开前,before_agent_start 在展开后;
- Context 修改本次模型消息,不直接改 Session JSONL;
- Provider Headers、Payload 与 Response 位于不同阶段;
- Message End 可链式替换,但 Role 必须保持;
- Tool Start 发生在 Preflight 前,tool_call 可 Mutation/Block;
- tool_call Handler Error Fail Closed,其他多数错误隔离继续;
- Tool Result Partial Patch 按 Extension 顺序串联;
- Parallel Mode 的 Preflight、Execution、Completion 与最终 Message 顺序不同;
- agent_settled 比 agent_end 更适合桌面稳定状态;
- Session Before Event 可 Cancel,Resource Event 会累积 Path;
- User Bash 由第一个返回结果的 Handler 接管,错误隔离后继续;
- Handler 没有自动 Timeout,递归和性能需要 Extension 自己治理。
25. 自测
input与before_agent_start分别看到哪一阶段的 Prompt?- Context Filter 是否会删除 Session Entry?
- Header Hook 与 Payload Hook 的修改方式有什么区别?
- 为什么
tool_execution_start不代表 Tool 一定执行? tool_callMutation 后会重新 Schema Validation 吗?tool_callHandler 抛错与tool_resultHandler 抛错有何不同?- Parallel Tool Mode 中哪些顺序是确定的?
agent_end与agent_settled的差别是什么?- 多个 Session Before Handler 都返回结果时怎样避免冲突?
- 怎样防止 Event Handler 触发无限递归?
user_bash多 Handler 的 Winner 怎样决定?