返回课程首页

25

事件系统与拦截链

追踪一次请求经过的观察、修改和阻止事件。

源码基线:Pi v0.82.0 · commit 518855d

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_startturn_end、Execution Events
变换型 inputcontext、Provider Request、Message End
拦截型 tool_call、Session Before Events 可 Block/Cancel
资源型 resources_discover 累积 Path
生命周期 Session/Agent/Turn/Message 多数观察
状态通知 model_selectthinking_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_startpi.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.systemPromptctx.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 时:

  1. tool_execution_start 按源顺序发出;
  2. tool_call Preflight 顺序执行;
  3. 允许的 Tool 并行执行;
  4. Update 可交错;
  5. tool_result 与 Execution End 按完成顺序交错;
  6. 最终 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_startcontext

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. 自测

  1. inputbefore_agent_start 分别看到哪一阶段的 Prompt?
  2. Context Filter 是否会删除 Session Entry?
  3. Header Hook 与 Payload Hook 的修改方式有什么区别?
  4. 为什么 tool_execution_start 不代表 Tool 一定执行?
  5. tool_call Mutation 后会重新 Schema Validation 吗?
  6. tool_call Handler 抛错与 tool_result Handler 抛错有何不同?
  7. Parallel Tool Mode 中哪些顺序是确定的?
  8. agent_endagent_settled 的差别是什么?
  9. 多个 Session Before Handler 都返回结果时怎样避免冲突?
  10. 怎样防止 Event Handler 触发无限递归?
  11. user_bash 多 Handler 的 Winner 怎样决定?