阶段 4:事件总线
对应代码:stage04_events.py
学习目标
理解"事件既是给消费者的消息,也是驱动派生状态的输入",并实现 terminal_error 的单写多读模式。
源码锚点
| 概念 | 位置 | 说明 |
|---|---|---|
EventMsg 枚举 |
protocol/src/protocol.rs(约 L1900-3700) |
数十种事件;11.1 节的五种投影形态 |
| 三层发送链 | core/src/session/mod.rs:1828-2077 |
send_event → send_event_raw_with_persistence → deliver_event_raw |
| terminal_error 锁存 | mod.rs:1828-1841 |
Error 事件副作用写入,终态读取(14.6 节"单写多读") |
| AgentStatus watch | mod.rs:2069-2077 |
事件同时更新进程内派生状态 |
代码走读
EventBus.send() 的顺序就是真实 send_event 的骨架:
python
if msg == ERROR and affects_turn_status:
self.terminal_error = payload # 先副作用:锁存错误
if (s := self.agent_status_from(msg)): # 再更新派生状态
self.agent_status = s
self._queue.put_nowait(Event(...)) # 最后投递
两个教学要点:
- 锁存的过滤条件 是
affects_turn_status------真实版里 16 个CodexErrorInfo变体只有 2 个返回 false(14.1 节)。演示里的stream_error不锁存(重试中间态),error锁存------这正是willRetry语义的分界(11.7 节)。 - 队列 unbounded:发送方永不阻塞,但消费者必须持续 drain(3.7 节的警告同样适用)。
运行与预期输出
bash
python stage04_events.py
# 打印 7 条事件;最后两行:
# 最终 agent_status = AgentStatus.completed <- 最后一个生命周期事件决定
# 锁存的 terminal_error = boom <- 终态投影 failed 的依据
注意:agent_status 是 completed 而 terminal_error 是 boom------两者不冲突,因为真实系统的 AgentStatus 描述 Core 生命周期,"Turn 失败"是 App Server 的投影(5.3 节的两层状态)。
练习
- 给
send()加legacy双发:某些事件同时投递旧名字(对应 11.2 节的四条扇出路径之一)。 - 把队列改成
maxsize=10并一次塞 20 条,观察背压------然后回答:为什么真实设计选 unbounded 事件通道 + 有界提交通道? - 实现
affects_turn_status(msg)函数表,把过滤条件从布尔参数改成查表。
与真实实现的差距
- 真实
send_event在 raw 层之前还有 trace、父代理转发、realtime 镜像、legacy 双发四条扇出(11.2 节"四条扇出路径")。 - 持久化过滤在 raw 层内(
send_event_raw_with_persistence);阶段 9 会补。
代码
python
"""积木 4:事件总线。
真实对应物:
- codex-rs/protocol/src/protocol.rs 的 EventMsg 枚举(数十种事件)
- codex-rs/core/src/session/mod.rs:1828-2077 的三层发送链:
send_event -> send_event_raw_with_persistence -> deliver_event_raw
- terminal_error 锁存(mod.rs:1828-1841,分析文档 14.6 节"单写多读")
- AgentStatus watch(mod.rs:2069-2077)
核心认知:事件既是给消费者的消息,也是驱动派生状态的输入
(terminal_error、AgentStatus 都由事件副作用维护)。
运行:python stage04_events.py
"""
from __future__ import annotations
import asyncio
from dataclasses import dataclass
from enum import Enum
class TurnAbortReason(str, Enum):
"""对应 protocol.rs:4209-4214。"""
interrupted = "interrupted"
replaced = "replaced"
class AgentStatus(str, Enum):
"""对应 core/src/agent/status.rs。"""
pending_init = "pendingInit"
running = "running"
completed = "completed"
interrupted = "interrupted"
errored = "errored"
@dataclass
class Event:
"""对应 Event { id: turn 子 id, msg }。payload 是教学版附加的文本。"""
id: str
msg: str
payload: str = ""
# ------ 教学版认得的全部"事件"(真实版每个都是独立 dataclass)------
TURN_STARTED = "turn_started"
ITEM_STARTED = "item_started"
ITEM_COMPLETED = "item_completed"
AGENT_DELTA = "agent_message_delta"
ERROR = "error"
TURN_COMPLETE = "turn_complete"
TURN_ABORTED = "turn_aborted"
class EventBus:
"""对应 tx_event(unbounded) + deliver_event_raw 的 AgentStatus watch。"""
def __init__(self):
self._queue: asyncio.Queue[Event] = asyncio.Queue() # 真实:unbounded
self.agent_status = AgentStatus.pending_init # 对应 watch 值
self.terminal_error: str | None = None # 对应 TurnContext.terminal_error
def agent_status_from(self, msg: str) -> AgentStatus | None:
"""对应 agent_status_from_event() 的最小子集。"""
return {
TURN_STARTED: AgentStatus.running,
TURN_COMPLETE: AgentStatus.completed,
TURN_ABORTED: AgentStatus.interrupted,
ERROR: AgentStatus.errored,
}.get(msg)
async def send(
self,
turn_id: str,
msg: str,
affects_turn_status: bool = False,
payload: str = "",
) -> None:
"""对应 send_event():先副作用(锁存错误),再投递,再更新派生状态。
真实版在 raw 层之前还有 trace / 父代理转发 / realtime 镜像 / legacy 双发
(分析文档 11.2 节的四条扇出路径)。
"""
if msg == ERROR and affects_turn_status:
self.terminal_error = payload or msg # mod.rs:1828-1841 的锁存语义
if (s := self.agent_status_from(msg)) is not None:
self.agent_status = s
self._queue.put_nowait(Event(turn_id, msg, payload))
async def next_event(self) -> Event:
return await self._queue.get()
async def demo() -> None:
bus = EventBus()
turn_id = "t1"
await bus.send(turn_id, TURN_STARTED)
await bus.send(turn_id, ITEM_STARTED, payload="c1")
await bus.send(turn_id, AGENT_DELTA, payload="正在跑命令...")
await bus.send(turn_id, ITEM_COMPLETED, payload="c1")
# 一次不影响 Turn 状态的警告(真实版:StreamError -> willRetry,不锁存)
await bus.send(turn_id, "stream_error", payload="Reconnecting... 1/5")
# 一次影响 Turn 状态的错误 -> 锁存进 terminal_error
await bus.send(turn_id, ERROR, affects_turn_status=True, payload="boom")
await bus.send(turn_id, TURN_COMPLETE)
while not bus._queue.empty():
ev = await bus.next_event()
print(f" * {ev.id} {ev.msg:18} {ev.payload}")
print("最终 agent_status =", bus.agent_status)
print("锁存的 terminal_error =", bus.terminal_error) # 终态投影 failed 的依据
if __name__ == "__main__":
asyncio.run(demo())