跳转到内容

Codex 的 Agent 核心循环实现

Codex 的 Agent 核心循环实现

学习目标

  • 理解 Codex 的 Session/Turn 两层循环模型的具体实现
  • 掌握 Codex 的 SQ/EQ 事件驱动架构的代码结构
  • 分析 Codex 的单任务串行 + 可中断设计

前置知识

本章涉及 Agent 核心循环的通用原理,建议先阅读:

下文假设你已理解上述概念,直接聚焦 Codex 的具体实现。


项目实践

架构概览

Codex 的 Agent 核心循环位于 codex-rs/core/src/session/ 目录,由 Session 结构体驱动:

关键设计Session 是 Codex Agent 的”心脏”,所有用户操作都通过它处理,所有 Agent 输出都通过它发出。

Session 的核心字段

pub(crate) struct Session {
pub(crate) conversation_id: ThreadId, // 线程唯一标识
pub(crate) tx_event: Sender<Event>, // 事件发送端(EQ)
pub(crate) agent_status: watch::Sender<AgentStatus>, // Agent 状态广播
pub(crate) state: Mutex<SessionState>, // Session 级状态
pub(crate) active_turn: Mutex<Option<ActiveTurn>>, // 当前活跃 Turn
pub(crate) input_queue: InputQueue, // 输入队列(SQ)
pub(crate) goal_runtime: GoalRuntimeState, // 目标运行时
pub(crate) services: SessionServices, // 外部服务集合
}

tx_event 是整个系统的输出口:Session 通过它向 TUI、日志系统、MCP 客户端等消费者发送事件。使用 async_channel::Sender 实现无界通道,保证事件不会丢失。

active_turn 是并发控制的关键:Mutex<Option<ActiveTurn>> 确保同时只有一个 Turn 在执行。新用户输入到来时,如果 active_turn 不为 None,新输入进入 input_queue 排队。

Turn 上下文隔离

每次用户提交创建一个 TurnContext,携带完整的配置快照:

pub(crate) struct TurnContext {
pub(crate) model: ModelConfig, // 模型参数
pub(crate) sandbox: SandboxPolicy, // 沙箱策略
pub(crate) permissions: PermissionSet, // 权限集合
pub(crate) environment: EnvSelection, // 环境变量选择
pub(crate) approval_policy: ApprovalPolicy, // 审批策略
pub(crate) tools_config: ToolsConfig, // 工具配置
// ... 更多字段
}

设计亮点TurnContext 在 Turn 开始时创建,此后即使 Session 级别的配置被修改(如用户切换了模型),当前 Turn 仍然使用创建时的快照。这种”不可变快照”模式彻底消除了并发修改风险。

事件流处理

Codex 的事件流处理在 stream_events_utils.rs 中,将模型 API 的原始 SSE 事件映射为内部 Event 协议:

事件分类

  • ItemStarted / ItemCompleted:工具或消息的开始和完成
  • ExecCommandOutputDeltaEvent:命令输出的增量(流式渲染)
  • TurnAbortedEvent:回合中止
  • ContextCompactedEvent:上下文压缩完成

输入队列

InputQueue 不仅管理用户输入,还承载 Agent 间通信(Inter-Agent Communication):

pub(crate) struct InputQueue {
user_inputs: VecDeque<UserInput>,
inter_agent_messages: VecDeque<IacMessage>,
}

优先级:当 Agent 收到外部 Agent 的消息时,这些消息通常优先于普通用户输入处理,确保协作任务的及时响应。


问题与规避

Turn 中断时的状态一致性

Codex 处理中断时,已执行的部分工具可能已修改文件系统。Codex 的对策是:

  • 工具执行结果在 Turn 完成前不入库,仅在内存中累积
  • 中断后,内存中的中间结果被丢弃,不会污染持久化状态
  • 文件系统修改由沙箱隔离,影响范围可控

事件流背压

Codex 使用 async_channel 的无界通道发送事件。虽然这避免了阻塞,但在极端情况下可能导致内存增长。Codex 的缓解策略:

  • TUI 消费端使用跳帧渲染,处理不过来时丢弃过期的 delta 事件
  • 命令输出使用增量压缩,减少事件数量

并发工具调用

Codex 在单个 Turn 内支持并行工具调用。并发控制通过 ToolCallRuntime 的读写锁实现:

  • 无依赖的工具获取读锁,并行执行
  • 有资源冲突的工具获取写锁,串行执行

设计取舍

为什么使用无界事件通道?

Codex 选择 async_channel 的无界通道而非有界通道。优势是事件永不丢失,代价是极端场景下可能内存增长。考虑到 TUI 消费速度通常跟得上模型输出速度,且跳帧渲染作为兜底,这种权衡是合理的。

为什么 Session 同时只能有一个 Turn?

Codex 采用严格的单任务串行模型。这与 Claude Code 等竞品一致——用户的注意力是单线程的,同时展示多个任务的进度反而会造成认知负担。代价是用户必须等待当前任务完成才能提交新指令。


参考来源