阶段 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())
相关推荐
AbrahamCS1 小时前
从角色协同到可运行图:Graph Engineer 与多智能体编排
前端·人工智能·智能体
MacroZheng1 小时前
几行代码给项目集成AI功能,Spring AI 2.0太香了!
java·人工智能·spring boot
zed_231 小时前
给接口加把锁:Bearer Token 鉴权(附冒烟验收)
人工智能
前端开发江鸟1 小时前
学完 Agent 开发基础后,我准备把它真正用在文字创作里
agent
Smoothcloud_润云1 小时前
从“模型服务”到“Agent 调度”:AI 推理基础设施为什么正在重构?
llm·agent·gpu
u0103055271 小时前
昇腾Model Agent模型适配指南
人工智能
Neptune231 小时前
Agent死循环6小时:从“相信LLM判断”到“焊死硬件条件止
人工智能
用户548775431601 小时前
DeepSeek Harness 与 xAgent:两种 Agent Harness 架构路线怎么选
人工智能·deepseek
安逸sgr1 小时前
卷积神经网络 CNN 是什么?为什么适合处理图像?
人工智能·ai·大模型·agent·智能体