第 8 章:事件与回放 —— UI 看到的不是 Runner,而是事件投影¶
1. 为什么需要 Event V2¶
如果 UI 直接订阅 Session Runner 内存对象,会绑定到:
- 某个进程的 fiber;
- 某个 server 实例;
- 某个旧版消息结构;
- 某个 provider SDK 的 stream event。
Event V2 让执行过程先变成 OpenCode 自己的领域事件,再由 projector、SSE、sync、SDK 和 UI 消费。事件因此同时承担两种职责:
- 实时通知:当前 UI 立即显示 text delta / tool running;
- 回放输入:断线后从 sequence / cursor 继续同步。
2. 三个事件层¶
Provider LLMEvent
→ Session runner publisher
→ EventV2 domain event
→ durable event / projector
→ public API event / SSE / Sync
→ TUI / App / Desktop / ACP
Provider event 是协议层,不能直接公开;Event V2 是领域层,应该跨 provider 稳定;public API event 是 transport projection,负责兼容和鉴权。
3. 事件不是“日志字符串”¶
一个有用的事件至少需要:
- stable type;
- event ID / sequence;
- session / project / location scope;
- typed properties;
- 是否 durable / 是否可以重放;
- 对应 projector 或 public API schema。
packages/opencode/src/event-manifest.ts 维护事件定义集合,API 会从 manifest 生成 Event 联合 schema。这样新事件必须在类型层被声明,不是偷偷发一个任意 JSON。
4. Stream 与 durable projection 的时序¶
sequenceDiagram
participant P as Provider
participant R as Runner
participant E as EventV2
participant D as DB / projector
participant U as UI subscriber
P->>R: text delta / reasoning / tool call
R->>E: publish typed domain event
E-->>U: realtime event
E->>D: append / project durable state
D-->>U: replay after reconnect
R->>R: await local tool settlement
R->>P: next provider turn
实时通知和数据库投影可能不是同一毫秒完成,因此 UI 不能只靠某一类消息判断最终状态。最终一致的事实来自 projected session state 和可重放序列。
5. Sync 为什么还需要存在¶
SSE 适合持续连接,但实际客户端会断线、切换项目或从历史 session 进入。Sync API 可以提供:
- 当前 cursor 之后的 event;
- session / project 的初始快照;
- 增量事件与状态更新;
- 对某些事件的过滤和重放。
这让 Web、TUI、Desktop 都能先 hydrate 状态,再订阅后续 event,而不是从零等待下一次变化。
6. Event V2 Bridge:迁移期的适配器¶
旧代码仍有 V1 的 Session.Event、Permission.Event、Question.Event。EventV2Bridge 的价值是把旧 service 产生的事件接到新的 Event V2 体系中,给迁移留出时间。
阅读桥接代码时要问两个问题:
- 这个事件的 canonical source 是 V1 service 还是 V2 core service?
- 它是实时通知,还是已具备可回放的 durable record?
不要因为 UI 收到了 event,就假设数据库已经写入了同等事实。
7. 事件驱动下的错误处理¶
事件消费者可能失败,provider 也可能失败,工具可能被拒绝。好的事件边界会让错误具有:
- 稳定 type;
- session / call ID;
- 模型可见的 message 或 tool result;
- UI 可显示的详细信息;
- 必要时可 retry / compact / stop 的分类。
Server 层的 SchemaErrorMiddleware 处理 transport decode error;Session 层的 LLMEvent.providerError、tool error 和 permission decline 处理业务执行错误。两种错误不能混为一个“500”。
本章小结¶
Event V2 是 OpenCode 多端体验的神经系统,但它不是单纯 pub/sub。它把 provider stream 变成领域事件,再通过投影、SSE、Sync 和 SDK 让实时显示与断线恢复同时成立。