阶段 4:事件总线

阶段 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(...))          # 最后投递

两个教学要点:

  1. 锁存的过滤条件 是 affects_turn_status------真实版里 16 个 CodexErrorInfo 变体只有 2 个返回 false(14.1 节)。演示里的 stream_error 不锁存(重试中间态),error 锁存------这正是 willRetry 语义的分界(11.7 节)。
  2. 队列 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 节的两层状态)。

练习

  1. 给 send() 加 legacy 双发:某些事件同时投递旧名字(对应 11.2 节的四条扇出路径之一)。
  2. 把队列改成 maxsize=10 并一次塞 20 条,观察背压------然后回答:为什么真实设计选 unbounded 事件通道 + 有界提交通道?
  3. 实现 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())
相关推荐
子兮曰5 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
回眸&啤酒鸭5 天前
【回眸】Minicart 电商购物车核心功能落地指南
人工智能
一隅论数智5 天前
给AI一张“业务概念地图“:本体如何从哲学走向企业智能
大数据·人工智能·经验分享·笔记·学习·学习方法·政务
AI的探索之旅5 天前
97 个 OpenCV 实例(三十):双目立体,从标定到点云
人工智能·opencv·计算机视觉
AlbertZein5 天前
Step-5-Preview 上手实测:3D 游戏、金融分析、网页设计一次跑完
人工智能·aigc
LaughingZhu5 天前
Product Hunt 每日热榜 | 2026-09-19
人工智能·深度学习·神经网络·搜索引擎·百度
美狐美颜SDK开放平台5 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
1点东西5 天前
做了近两年的Agent开发,其实真正要学的就是这五件事
llm·agent·ai编程
wukangjupingbb5 天前
智能网联汽车安全能力框架
人工智能