6 min read教程

M07. 事件:运行时如何向外部汇报

拆解订阅与扩展钩子两条事件管道,理解运行时事实如何投影到 UI 与观测系统。

M07. 事件:运行时如何向外部汇报#

Agent 运行时会产生大量变化:文本在流式增长、工具开始和结束、turn 切换、队列变化、压缩开始和结束。若每个模块互相调用,任何新 UI 或新观测需求都会侵入核心 Loop。事件系统把“发生了什么”与“谁关心”分开。

1. 先分清两条管道#

Pi 里至少要区分两类使用方式:

  • session.subscribe():SDK 使用者观察会话事件,适合 UI、日志、指标和业务投影。
  • pi.on():扩展订阅生命周期或操作事件,某些钩子可以等待、修改或阻断行为。

它们可能看到相近的事实,但责任不同。观察者不应偷偷变成策略执行器;策略钩子也不应承担整个 UI 的渲染。

2. 事件名表达的是时间点#

常见事件可以按时间线理解:

text
agent_start
  → turn_start
  → message_start / message_update / message_end
  → tool_execution_start / update / end
  → turn_end
  → agent_end

队列更新、自动重试和 compaction 会在这条主线上插入自己的生命周期事件。不要只监听最终 agent_end:那样你无法知道任务是在模型、工具还是压缩阶段耗时。

3. 订阅者应该做 projection#

事件本身是内部协议,前端通常需要的是更小的公共协议:

ts
type UiEvent =
  | { type: "answer_delta"; text: string }
  | { type: "tool_state"; name: string; state: "running" | "done" | "error" }
  | { type: "run_state"; state: "idle" | "working" | "compacting" };

把 Pi 事件投影成自己的 UiEvent 有两个好处:一是避免内部字段泄漏到产品 API,二是允许不同版本的 Pi 在 adapter 中兼容。未知事件应被记录或忽略,而不是让整个 UI 崩溃。

4. 什么时候要等待#

日志和指标通常应该快速返回;权限检查、用户确认和工具前置策略可能需要等待。事件处理器一旦变慢,就会影响后续运行时,所以每个监听器都要明确自己属于哪一类。

一个实用原则是:

  • 观察:复制事件、做轻量转换、异步投递。
  • 干预:只在有明确阻断/修改语义的钩子里等待。
  • 副作用:不要在每个事件上做不可幂等写入。

5. 事件与持久化的关系#

流式 delta 不等于已提交消息。适合恢复的持久化时机通常是 message_end 或 session manager 明确追加 entry 的时刻。这样,断电时不会把半句话误认为完整 assistant 回复。

同时,事件也不是 session file 的替代品。事件只描述这次运行发生了什么;session file 保存之后重启需要重建的历史。

6. 事件钩子中的递归风险#

如果 turn_end 处理器无条件调用 followUp(),每一轮都会制造下一轮;如果 tool_execution_end 触发一个会再次调用该工具的动作,就可能形成隐蔽递归。所有自动续跑都需要计数、预算、停止条件和可观测原因。

7. 设计事件协议的检查表#

  • 事件是否有稳定的 type 判别字段?
  • 增量事件是否能被丢弃后从最终状态重建?
  • 事件里是否含有不该出现在日志中的原始参数?
  • 监听器失败会不会阻断 Agent?
  • 事件顺序是否有文档和测试?
  • 断线重连后是补发事件,还是从 session 重建?

8. 事件系统的可靠性边界#

事件通常是瞬时通知,不应被当成唯一事实来源。可重建的状态应来自 session 或运行时快照;事件只负责让外部尽快得到变化。这样,事件重复时可以幂等处理,事件丢失时可以重新投影,事件乱序时也不会改写已经提交的历史。对无法恢复的缺口,系统应显示“未知”,而不是猜一个看似完整的状态。

资料

本文依据 Pi SDK 文档Extensions 文档Agent 核心源码重新创作。

相关文章