生产级 Coding Agent 架构解剖:OpenAI Codex CLI 源码导读

本文面向谁?

本文面向刚开始学习 Agent 工程设计、但已经知道“模型可以调用工具”的读者。上一篇 Swarm 架构篇 已经解释了 Agent 的基本概念、控制循环、工具边界和上下文压缩;本文不重复这些基础,而是从 OpenAI Codex CLI 的源码里看一个更接近生产级的 coding agent 如何组织 turn / task / session、上下文增量注入、工具运行时、权限沙箱、流式事件和持久化恢复

源码快照

本文基于本地 clone 的 openai/codex 源码整理,仓库路径为 /home/bmtaoran/apps/codex-src,commit 为 be33f80bc65159c094ecd06bf155afa3061ce23d。文中源码路径与行号均指向该快照,后续上游代码可能变化。


0. 先给结论:Codex 的核心不是“更长 prompt”,而是一个 Agent 操作系统

如果只看表层,Codex CLI 像是一个会读文件、改代码、执行命令的聊天程序。但源码里真正值得学习的是它把 coding agent 拆成了几个相对稳定的工程层:

层次主要问题Codex 中的核心实现
协议层用户、前端、agent 如何异步通信?Submission / Op / EventMsg 的 SQ/EQ 协议
会话层一条长期线程如何保存状态、配置和历史?SessionContextManager、rollout persistence
任务层一个用户请求如何被放到后台执行、取消、替换?SessionTaskRegularTaskspawn_task
turn 循环模型、工具、上下文、压缩如何在一轮里闭环?run_turnrun_sampling_request、stream event handling
上下文层初始环境、AGENTS.md、权限、插件、技能如何进入模型?ContextualUserFragmentWorldState snapshot/diff、reference baseline
工具层工具如何暴露给模型、路由、并发、审计和回填结果?ToolRouterToolRegistryToolCallRuntime、handlers
安全与可观测性如何审批命令、沙箱执行、记录事件和 telemetry?approvals、sandbox permissions、hooks、turn diff、raw response items
模型客户端如何稳定调用 Responses API、处理流式、重试和缓存?ModelClient / ModelClientSession

这就是本文的主线:Codex 不是把所有能力塞进一个 prompt,而是把 LLM 放在一个严格分层的运行时里。


1. 源码鸟瞰:从哪个目录开始读?

Codex 的 Rust workspace 很大,codex-rs/ 下有几十个 crate。初学者不需要一开始就读完全部目录,可以先按下面顺序:

阅读顺序路径你应该关注什么
1codex-rs/protocol/src/protocol.rs前端和 agent 通信的消息协议:SubmissionOpEventMsg
2codex-rs/core/src/session/handlers.rs收到用户输入后如何分发、开 turn、steer 当前 turn
3codex-rs/core/src/tasks/mod.rstasks/regular.rstask 生命周期:启动、取消、后台执行
4codex-rs/core/src/session/turn.rs最核心:agent turn 循环、模型采样、工具调用、自动 compact
5codex-rs/core/src/session/mod.rssession 状态、上下文注入、history 记录、compact 替换历史
6codex-rs/core/src/context_manager/history.rs历史如何进入 prompt、如何 normalize、如何截断工具输出
7codex-rs/core/src/context/world_state/*AGENTS.md、环境信息等如何做 snapshot/diff
8codex-rs/core/src/tools/*工具 spec、router、registry、runtime、handler 的分层
9codex-rs/core/src/client.rsResponses API 请求、WebSocket/HTTP fallback、turn-scoped client session
10codex-rs/core/src/compact.rs长上下文压缩和 replacement history

一个有趣的源码信号是:仓库根目录的 AGENTS.md 对 “Model visible context” 有明确约束:上下文必须增量构建、避免频繁变化导致 cache miss、注入项必须有硬上限、上下文 fragment 应该定义为结构体并实现 ContextualUserFragment。这说明 Codex 的上下文设计不是临时拼 prompt,而是被当作工程不变量来维护。


2. 第一层:SQ/EQ 协议让 Agent 变成异步系统

很多教学版 Agent 是这样写的:

user input -> run_agent(input) -> final answer

Codex 源码不是这个形状。它先定义了一个异步通信协议:

  • SQ(Submission Queue):用户、前端或其他 agent 提交请求;
  • EQ(Event Queue):agent 把进度、工具结果、错误、最终回答发回前端。

源码证据:

  • codex-rs/protocol/src/protocol.rs:1-4 明确写出使用 SQ/EQ pattern;
  • Submissionprotocol.rs:159-170 中定义,包含 idop、客户端消息 id、trace context;
  • Opprotocol.rs:511-590 起定义,包含 UserInputThreadSettingsExecApprovalPatchApprovalRefreshMcpServersCompactThreadRollback 等;
  • Event / EventMsgprotocol.rs:1252-1467 起定义,包含 TurnStartedTurnCompleteAgentMessageExecCommandBegin/EndMcpToolCallBegin/EndHookStarted/CompletedTurnDiff 等事件。

这套设计解决了三个问题:

  1. 前端不需要阻塞等待最终答案:它可以流式收到 reasoning、assistant delta、工具开始/结束、审批请求、diff 等事件。
  2. 用户可以在 turn 运行中继续输入:新的 UserInput 可以被视为 steer input,进入 active turn 的 pending queue。
  3. 工具审批、MCP elicitation、动态工具响应都是同一协议的一部分:它们不是临时 callback,而是统一建模为 OpEventMsg

你可以把 SQ/EQ 看成 Codex 的 “agent 总线”。LLM 只是这个总线后面的一个 actor。


3. 第二层:Session / Task / Turn 三层拆分

Codex 的 agent 循环不是单个 while,而是至少三层:

Session:一条长期线程,保存配置、历史、MCP、hooks、rollout、active turn
  └─ Task:一个后台工作单元,如 regular chat、review、compact、用户 shell 命令
       └─ Turn:一次或多次模型采样 + 工具执行 + 上下文维护的闭环

3.1 Session:长期状态容器

Session 负责的事情很多:

  • 保存会话配置、权限 profile、模型信息;
  • 管理 active turn;
  • 记录 conversation history;
  • 连接 MCP runtime;
  • 持久化 rollout;
  • 发送 EventMsg
  • 启动/取消 task;
  • 提供上下文注入、compact、rollback 等能力。

对初学者来说,重要的是别把 Session 理解成“一次请求”。它更像浏览器 tab 或 IDE 工作区里的长期 agent 线程。

3.2 SessionTask:把不同工作流统一成后台任务

codex-rs/core/src/tasks/mod.rs:206-238 定义了 SessionTask trait。它的职责很明确:

  • kind():说明任务类型,供 UI、telemetry、生命周期使用;
  • span_name():给 tracing span 命名;
  • run(...):真正执行任务,接收 SessionTaskContextTurnContext、初始输入和 cancellation token;
  • abort(...):任务被取消时释放资源。

Session::spawn_task / start_tasktasks/mod.rs:313-450 中实现。关键动作包括:

  1. 替换当前任务时先 abort_all_tasks
  2. 清理 connector selection;
  3. 创建 cancellation token;
  4. 把 pending input 迁移到新 turn state;
  5. 发出 turn start lifecycle;
  6. tokio::spawn 后台运行 task;
  7. 结束时 flush rollout,并统一调用 on_task_finished

这层让 Codex 可以用同一个生命周期框架承载 regular chat、review、compact、用户手动 shell 命令等不同工作流。

3.3 RegularTask:普通对话 turn 的外层循环

codex-rs/core/src/tasks/regular.rs:37-88 是普通 turn 的任务实现。核心结构很短:

  1. 先发 TurnStarted
  2. 获取预热好的 model client session(如果有);
  3. 调用 run_turn(...)
  4. 如果 session input queue 里还有 pending input,就继续跑下一轮 run_turn
  5. 没有 pending input 才返回最终 assistant message。

这意味着 Codex 支持一种很实用的交互:当模型还在执行工具时,用户又追加了指令,这些输入不一定要等整个任务结束才处理,而是可以被记录为 pending input,由 regular task 继续消化。


4. 第三层:一次 UserInput 如何变成后台 agent turn?

看用户输入的入口,应该从 session/handlers.rs 开始。

4.1 Submission loop 分发 Op

submission_loopcodex-rs/core/src/session/handlers.rs:703-848。它不断从 Receiver<Submission> 接收消息,然后 match sub.op

  • Op::UserInput user_input_or_turn
  • Op::ThreadSettings 更新 thread settings;
  • Op::ExecApproval / PatchApproval 通知审批等待方;
  • Op::Compact 启动 compact task;
  • Op::ThreadRollback 回滚历史;
  • Op::RunUserShellCommand 启动用户 shell command task;
  • 其他 realtime / MCP / guardian / shutdown 操作也在这里分发。

这比“收到用户输入就直接调模型”多了一层协议边界,带来两个好处:

  • 所有异步输入都可以排队、有 id、可追踪;
  • UI、CLI、app server、子 agent 通信可以复用同一种调度路径。

4.2 user_input_or_turn:能 steer 就 steer,否则开新 task

user_input_or_turn_innerhandlers.rs:183-275。它的决策逻辑是:

  1. 解析 Op::UserInput 中的 items、json schema、client metadata、additional context、thread settings;
  2. 如有 thread settings,先更新 session 配置并发 ThreadSettingsApplied
  3. 创建新的 TurnContext
  4. 调用 sess.steer_input(...) 尝试把输入注入当前 active turn;
  5. 如果没有 active turn,则把 additional context 和 user input 组装成 TurnInput,调用 spawn_task(..., RegularTask::new())

这就是 Codex 同时支持“开启新 turn”和“运行中追加指令”的关键。

InputQueuecodex-rs/core/src/session/input_queue.rs:28-38 中定义,负责保存 turn-local pending input 和 mailbox mail。get_pending_inputinput_queue.rs:197-225 中把 active turn 的 pending input 与 mailbox input 合并取出。

初学者可以学到什么?

不要把“用户输入”只当成函数参数。生产级 agent 需要把用户输入、子 agent mail、审批结果、动态工具响应都变成可排队、可追踪、可延迟处理的事件。


5. Codex 的核心 agent 循环:run_turn

run_turncodex-rs/core/src/session/turn.rs:128-459,文件注释已经非常清楚:模型在每次 sampling request 中要么返回工具调用,要么返回 assistant message;工具调用执行后,把工具输出再送回模型;assistant final message 则表示 turn 完成。

但源码里的真实流程比教学版 loop 丰富得多。

5.1 一次 run_turn 的主流程

可以把 run_turn 简化成下面这张图:

flowchart TD
    A[run_turn start] --> B[pre-sampling compact]
    B --> C[capture_step_context]
    C --> D[record context updates / set reference baseline]
    D --> E[build skill/plugin/extension injections]
    E --> F[run hooks and record user inputs]
    F --> G[clone history and normalize for prompt]
    G --> H[build tools and Prompt]
    H --> I[stream Responses API events]
    I --> J{output item}
    J -->|tool call| K[record call, execute tool future]
    K --> L[record tool output into history]
    L --> M[needs_follow_up = true]
    J -->|assistant message| N[record assistant item]
    N --> O{needs follow up?}
    M --> O
    O -->|yes and token limit/new window| P[mid-turn auto compact]
    P --> G
    O -->|yes| G
    O -->|no| Q[stop hooks / legacy hooks]
    Q --> R[return last assistant message]

对应源码位置:

  • pre-sampling compact:turn.rs:150-165
  • capture step context:turn.rs:167-173
  • skill/plugin injection:turn.rs:175-205
  • pending input drain:turn.rs:224-237
  • clone history for prompt:turn.rs:270-277
  • sampling request:turn.rs:284-294
  • token status / auto compact:turn.rs:304-368
  • stop hooks 和 final 收束:turn.rs:371-414

5.2 StepContext:一次模型请求看到的动态世界快照

capture_step_contextsession/mod.rs:2832-2875。注释强调它是 request-scoped view of dynamic state。它会把以下内容固定成一个 step:

  • 当前 turn context;
  • 环境 snapshot;
  • selected capability roots;
  • MCP runtime snapshot;
  • 已加载的 AGENTS.md。

为什么要有 StepContext?因为工具列表、上下文、MCP 状态、环境状态都可能变化。如果模型在某次 request 里看到了某组工具和某个工作区状态,工具执行时就应该使用同一份 step 视图,否则会出现“模型看到的世界”和“工具实际执行的世界”不一致。

ToolCallRuntime 也专门保存 step_context,源码注释写明:工具调用可能稍后运行,所以要保留当初 advertised tools 的那一步视图(tools/parallel.rs:41-49)。

5.3 不是一个 loop,而是多个 loop 叠在一起

Codex 至少有这些循环:

循环位置作用
submission loophandlers.rs:703-848持续接收外部 Submission
task loopregular.rs:73-88一个 task 中反复处理 pending input
turn sampling loopturn.rs:224-416模型 工具 模型,直到无需 follow-up
stream event loopturn.rs:1945-2348消费 Responses API 流式事件
retry loopturn.rs:1103-1166stream 失败时按 provider retry budget 重试
tool future drainturn.rs:1853-1877并发工具执行后把输出写回 history
compaction loopcompact.rs:251-320compact 请求失败/超窗时重试或裁剪历史

这正是生产级 agent 和 demo agent 的差距:demo 关注“模型下一步做什么”,生产系统还要处理并发、取消、重试、排队、压缩、权限、事件和持久化。


6. 上下文注入:从“拼 prompt”变成“可 diff 的世界状态”

Agent 初学者最容易犯的错误,是每轮都把所有文件、所有规则、所有历史重新塞进 prompt。Codex 不是这么做的。

Codex 的上下文管理有三个核心概念:

  1. history:已经发生过的消息、工具调用、工具输出、reasoning、compaction 等;
  2. reference context item:上一轮用于 diff 的配置/turn context baseline;
  3. world state snapshot:环境、AGENTS.md 等模型可见状态的持久快照。

6.1 初始上下文里有哪些东西?

build_initial_context_with_world_state_and_mcpsession/mod.rs:3145-3458。它会收集多类 developer / contextual user sections:

  • 模型切换说明;
  • permissions instructions;
  • developer instructions;
  • collaboration mode instructions;
  • realtime updates;
  • personality message;
  • apps / connector instructions;
  • available skills;
  • recommended / available plugins;
  • extension contributors 提供的 thread / turn context;
  • token budget context;
  • world state full render;
  • multi-agent usage hint。

最后它把这些 section 合并成 ResponseItem,并设置 turn id。

注意这里的设计重点:上下文被拆成结构化 section,而不是到处手写字符串拼接。

6.2 首轮全量注入,后续只注入 diff

record_context_updates_and_set_reference_context_itemsession/mod.rs:3549-3632。这是正常 runtime path:

  • 如果没有 reference_context_item,说明下一轮没有 baseline,于是注入完整 initial context;
  • 如果已有 baseline,则只构建 settings update items 和 world-state diff;
  • 如果 TurnContext 变化,再加入 turn-scoped extension context;
  • 只有在 model-visible context 已经记录后,才持久化 world state / turn context baseline;
  • 最后更新 reference_context_item

这一段是 Codex 上下文工程的核心。它实现的是:

第一次:完整告诉模型当前世界
后续:只告诉模型世界相比上次变了什么
compact / rollback 后:baseline 可能失效,于是重新全量注入

这样做有几个好处:

  • 减少 token;
  • 减少 prompt cache miss;
  • 让 context 更新更可审计;
  • rollback / compact 后能重新建立可靠 baseline。

6.3 WorldStateSection:AGENTS.md 和环境都是可快照的 section

codex-rs/core/src/context/world_state/mod.rs:167-190 定义了 WorldStateSection trait:

  • 每个 section 有稳定 ID
  • 可以生成 Snapshot
  • 可以根据 previous snapshot 渲染 diff;
  • 可以识别 legacy fragment。

WorldStateworld_state/mod.rs:192-314 中保存多个 section,并提供:

  • snapshot():生成所有 section 的持久快照;
  • render_full():把所有 section 当作首次出现渲染;
  • render_diff(previous):与上一份 snapshot 比较;
  • render_history_diff(previous, items):当没有精确 snapshot 时,回看保留的 history 兜底。

两个重要内置 section:

Section路径作用
AGENTS.mdcontext/world_state/agents_md.rs:13-80如果 AGENTS.md 变了,渲染 replacement notice;如果不再适用,渲染 removal notice
Environmentscontext/world_state/environment.rs:14-145记录 cwd、shell、环境状态、日期、时区、网络、文件系统、子 agent 状态等

这比“每轮读 AGENTS.md 然后塞进 prompt”强很多,因为它知道什么时候没变、什么时候变了、变了该如何向模型解释。

6.4 ContextualUserFragment:所有注入片段都有类型边界

codex-rs/context-fragments/src/fragment.rs:37-114 定义了 ContextualUserFragment。每个 fragment 必须说明:

  • role:是 user 还是 developer
  • markers:如何用开始/结束 marker 标识自己;
  • body:实际内容;
  • render / into response item 的方式。

例如 additional context 在 additional_context.rs:5-92 中有一个硬上限:每个值最多约 1000 tokens,并且 user fragment 会渲染成 <external_...>...</external_...> 形式。

这体现了 Codex 对上下文注入的一个底层原则:

凡是要进入模型上下文的东西,都应该有类型、有边界、有 marker、有上限。

6.5 History 不是简单数组:进入 prompt 前会 normalize

ContextManagercontext_manager/history.rs:36-57 中保存:

  • items
  • history_version
  • token_info
  • reference_context_item
  • world_state_baseline

for_prompthistory.rs:137-144 中会先 normalize_history。normalize 的不变量在 history.rs:355-368

  1. 每个 function/custom call 都要有对应 output;
  2. 每个 output 都要有对应 call;
  3. 如果模型不支持图片,就从消息和工具输出中剥离图片。

工具输出也不是无限写入。process_itemhistory.rs:370-395 中会按 truncation policy 截断 FunctionCallOutputCustomToolCallOutput

这解决了一个很现实的问题:工具可能输出成千上万行日志,如果全量塞回模型,agent 很快失控。Codex 的做法是:长结果可以存在真实世界里,但给模型的结果必须受策略控制。


7. 工具调用:spec、router、runtime、handler 四层分离

Codex 的工具系统不是一个 HashMap<String, fn>。它至少分四层:

Tool spec:告诉模型有哪些工具、参数 schema 是什么
Tool router:把模型返回的 ResponseItem 解析成内部 ToolCall
Tool runtime:控制并发、取消、执行时序,把结果转成 ResponseInputItem
Tool registry/handler:找到具体工具实现,跑 hooks、权限、telemetry、生命周期事件

7.1 每次 sampling request 都基于 step 构建工具表

built_toolssession/turn.rs:1177-1310。它会根据当前 StepContext 和配置收集:

  • MCP tools;
  • deferred MCP tools;
  • loaded plugins;
  • connector/app 状态;
  • tool suggest candidates;
  • extension tool executors;
  • dynamic tools。

然后构造 ToolRouter::from_context(...)

这说明工具集合不是全局静态的,而是和 turn、step、模型能力、feature flag、插件/MCP 状态相关。

7.2 build_tool_router 同时产生“模型可见 spec”和“运行时 registry”

codex-rs/core/src/tools/spec_plan.rs:160-203 中的 build_tool_router 先构建 planned tools,再生成:

  • model_visible_specs:放进 Responses API request 的工具定义;
  • ToolRegistry:真实执行时用来 dispatch 的 handler 集合。

build_model_visible_specs_and_registryspec_plan.rs:235-272 中体现了这个分离:

  • 遍历 runtime;
  • 根据 ToolExposure 判断是否直接暴露给模型;
  • 生成 spec;
  • merge namespace tools;
  • 同时把所有 runtime 放进 registry。

ToolExposure 的存在很关键:有些工具可以被模型直接看见,有些只作为 deferred tool 通过 tool_search 发现,有些 hidden 但仍可兼容旧调用或内部 dispatch。

7.3 工具来源很多,但都被归一成 runtime

add_tool_sourcesspec_plan.rs:612-624,按顺序加入:

  1. shell / unified exec;
  2. MCP resource tools;
  3. core utility tools;
  4. collaboration / multi-agent tools;
  5. MCP runtime tools;
  6. extension tools;
  7. dynamic tools;
  8. hosted model tools。

几个典型例子:

  • shell 工具:spec_plan.rs:640-681
  • MCP resource tools:spec_plan.rs:698-704
  • plan、request user input、request permissions、new context window、current time、apply patch、view image:spec_plan.rs:708-789
  • MCP runtime tools:spec_plan.rs:885-908
  • dynamic tools:spec_plan.rs:916-946
  • extension tools 去重和过滤:spec_plan.rs:996-1044
  • deferred tools 的 tool_search executor:spec_plan.rs:963-985

这套机制让 Codex 可以扩展很多工具来源,但模型看到的是统一的 Responses API tool schema。

7.4 模型输出如何变成工具执行?

流式事件最终会走到 handle_output_item_done,位置是 codex-rs/core/src/stream_events_utils.rs:405-515

关键逻辑:

  1. 调用 ToolRouter::build_tool_call(item.clone())
  2. 如果是 tool call:
    • 记录模型发出的 tool call;
    • 创建 tool future;
    • 设置 needs_follow_up = true
  3. 如果不是 tool call:
    • 转成 UI 可展示的 turn item;
    • 记录 assistant / reasoning / hosted tool item;
  4. 如果模型发出了不合法工具请求但可以回应模型,则构造 function call output,写回 history,并要求 follow-up。

ToolRouter::build_tool_calltools/router.rs:112-160,负责把不同类型的 response item 映射为内部统一的 ToolCall

  • ResponseItem::FunctionCall
  • ResponseItem::ToolSearchCall
  • ResponseItem::CustomToolCall

7.5 工具输出如何回到模型?

工具 future 会被放进 in_flightdrain_in_flightsession/turn.rs:1853-1877 中等待它们完成:

  • 工具返回 ResponseInputItem
  • 转成 ResponseItem
  • 调用 record_conversation_items 写入 history;
  • 如果涉及外部上下文,标记 thread memory mode polluted。

下一次 run_sampling_request 会从 history clone 出 prompt input,于是模型就能看到工具结果。

这就是 canonical tool loop:

model emits tool call
  -> Codex records call
  -> runtime executes tool
  -> output becomes ResponseInputItem
  -> output is recorded into history
  -> next sampling request includes output

7.6 并发工具不是全开:由每个工具声明 parallel support

ToolCallRuntimetools/parallel.rs:41-64 保存 router、session、step context、turn diff tracker 和一个 RwLock

handle_tool_call_with_sourceparallel.rs:93-202 中:

  • 查询工具是否支持 parallel;
  • 支持 parallel 的工具拿 read lock;
  • 不支持 parallel 的工具拿 write lock;
  • 通过 cancellation token 处理 abort;
  • 被取消时生成 aborted tool response 并发 lifecycle event。

这比简单 join_all(tool_calls) 更安全,因为不是所有工具都可以并发。例如读文件可能可以并发,写文件、patch、shell 进程管理就需要更强约束。

7.7 ToolRegistry 是审计和安全边界

CoreToolRuntime trait 在 tools/registry.rs:44-135。它不仅要求工具能执行,还提供:

  • 是否匹配 payload kind;
  • 是否等待 runtime cancellation;
  • telemetry tags;
  • pre_tool_use payload;
  • post_tool_use payload;
  • hook input rewrite。

真正 dispatch 在 tools/registry.rs:405-670。流程包括:

  1. active turn tool call 计数;
  2. 找 tool;
  3. 检查 payload kind;
  4. notify_tool_start
  5. 跑 pre-tool-use hooks:可以 block,也可以 rewrite input;
  6. 执行 handler,并记录 telemetry;
  7. 成功后跑 post-tool-use hooks:可以追加 context、block result、把反馈替换为模型可见输出;
  8. notify_tool_finish
  9. 将结果或错误转换为模型可见结果。

所以 Codex 的工具调用不是“模型说运行就运行”。它要经过 registry、hooks、权限、沙箱、telemetry 和 lifecycle event。

7.8 shell / exec 是安全设计最集中的地方

shell 相关实现展示了 Codex 如何把危险能力纳入受控运行时。

传统 shell handler 的公共逻辑在 tools/handlers/shell.rs:63-165

  • 根据 turn environment 拿 filesystem;
  • 读取 shell environment policy;
  • 应用已批准的 turn permissions;
  • 校验 additional permissions;
  • 如果请求 sandbox override 但 approval policy 不是 OnRequest,直接拒绝并把错误返回模型;
  • 拦截 apply_patch,让 patch 走专用逻辑;
  • 通过 ToolEmitter 发 shell lifecycle event。

unified exec handler 在 tools/handlers/unified_exec/exec_command.rs:157-180 会根据 filesystem sandbox、network sandbox、Windows sandbox level 选择初始 sandbox;exec_command.rs:243-365 中再处理 sandbox permissions、additional permissions、approval policy、apply_patch intercept 和最终 exec_command 请求。

这就是 Codex 的一个核心工程哲学:

模型可以提出“我要执行命令”,但是否允许、在哪个 cwd、用什么 sandbox、是否需要用户审批、输出如何截断,都由 deterministic runtime 决定。

7.9 MCP 工具也被包装成同一种 runtime

McpHandlertools/handlers/mcp.rs:32-164。它把 MCP ToolInfo 转成 Codex tool spec,并实现 ToolExecutor<ToolInvocation>

  • tool_name() 使用 canonical MCP tool name;
  • spec() 返回 Responses API tool spec;
  • read-only MCP 工具可以支持 parallel;
  • search_info() 生成 deferred tool search 的索引信息;
  • handle_call() 调用 handle_mcp_tool_call,再包装成 McpToolOutput

McpHandler 还实现了 CoreToolRuntime 的 pre/post hook payload,因此 MCP 工具同样能进入 hooks、telemetry 和审计链路。


8. Responses API 流式处理:模型事件不是直接显示,而是转成 turn items

try_run_sampling_requestsession/turn.rs:1887-2390。它做了几件重要事情:

  1. 构造 feedback_tags,记录模型、approval policy、sandbox policy、features 等;
  2. 调用 client_session.stream(...) 获取 Responses API stream;
  3. 循环消费 ResponseEvent
  4. OutputItemAddedOutputTextDeltaToolCallInputDelta、reasoning delta、OutputItemDoneCompleted 等事件分别处理;
  5. OutputItemDone 时调用 handle_output_item_done
  6. Completed 时记录 token usage,并根据 end_turn 决定是否 follow-up;
  7. stream 完成后 drain 工具 futures;
  8. 发 token count 和 turn diff。

这体现了 UI/协议和模型原始流之间的隔离:

  • 模型原始事件是 ResponseEvent
  • Codex 内部把它们转成 TurnItemEventMsg、history item;
  • 前端看到的是稳定协议事件,而不是裸 Responses API 流。

这也方便后续兼容不同模型、不同 provider、不同 UI。


9. Model client:session-scoped 与 turn-scoped 的边界很清楚

codex-rs/core/src/client.rs:1-24 文件头说明了两类 client:

  • ModelClient:session-scoped,保存 auth、provider、conversation id、transport fallback state;
  • ModelClientSession:turn-scoped,一轮里可以复用 Responses WebSocket connection 和 sticky routing token。

ModelClientSession 的注释在 client.rs:253-280 中强调:每个 Codex turn 都要创建新的 session;跨 turn 复用会把上一轮 sticky-routing token 带到下一轮,违反客户端/服务器契约。

Responses API request 的构造在 client.rs:875-890,包含:

  • model
  • instructions
  • input
  • tools
  • tool_choice: auto
  • parallel_tool_calls
  • reasoning
  • stream: true
  • include
  • service_tier
  • prompt_cache_key
  • structured text output schema;
  • client_metadata

stream 方法在 client.rs:1725-1785 中优先走 Responses WebSocket;如果不可用或失败,则 fallback 到 HTTP Responses API。WebSocket fallback 是 session-scoped,某个 turn 激活 HTTP fallback 后,后续 turn 也会使用 HTTP。

对初学者的启发是:模型客户端也应该有生命周期设计。不要把 API 调用封装成无状态 call_llm(prompt) 就结束;生产系统需要考虑连接复用、sticky routing、重试预算、fallback、prompt cache key 和 metadata。


10. Compaction:不是删除历史,而是重建一个可继续工作的上下文窗口

上下文压缩在上一篇 Swarm 文章里已经讲过概念,这里只看 Codex 的工程细节。

10.1 什么时候 compact?

run_turn 里有两处:

  • turn 开始前:run_pre_sampling_compact,位置 turn.rs:797-820
  • turn 中间:如果 needs_follow_up 且 token limit reached 或模型请求新 context window,执行 run_auto_compact,位置 turn.rs:345-368

token 状态由 session/context_window.rs:24-92 计算,既考虑 active context tokens,也考虑 auto compact scope、full context window limit、body-after-prefix scope 等。

token_budget.rs:6-39 还可以在接近 compact 阈值时向模型注入 token budget reminder。

10.2 Pre-turn/manual compact 与 mid-turn compact 不同

compact.rs:55-68 定义了 InitialContextInjection

  • DoNotInject:pre-turn/manual compact 使用。它用 summary 替换历史并清空 reference_context_item,下一次 regular turn 会重新完整注入 initial context;
  • BeforeLastUserMessage(world_state):mid-turn compact 使用。因为模型要在同一个 turn 里继续工具/模型循环,所以 replacement history 里必须把 initial context 插到最后一个真实 user message 或 summary 前。

这是很细但非常重要的区别:

pre-turn compact:先压缩,下一轮再重建完整 context
mid-turn compact:压缩后还要立刻继续本轮,所以 replacement history 必须自带完整 initial context

10.3 Replacement history 是可靠性机制

compact.rs:322-365 中,compact 完成后会:

  1. 从当前 history 取最后 assistant message 作为 summary suffix;
  2. 构建 compacted history;
  3. advance auto compact window;
  4. 根据 InitialContextInjection 构建 initial context;
  5. 必要时插入 replacement history;
  6. 设置 reference_context_item
  7. 调用 replace_compacted_history

Session::replace_compacted_historysession/mod.rs:2978-3022

  • 替换 in-memory history;
  • 如果有 world state baseline,就持久化 full world state;
  • 持久化 CompactedItem
  • 持久化 TurnContextItem
  • 标记 compact 后的 session start source。

因此,Codex 的 compact 不是“把前 N 条消息删掉”。它是在创建一个新的、可恢复的历史窗口,并同步更新 reference context 和 world state baseline。


11. Rollout、raw events、rollback:长任务必须可恢复

record_conversation_itemssession/mod.rs:2778-2795,每次记录 history 都会:

  1. prepare_conversation_items_for_history
  2. 更新 current time reminder 状态;
  3. 写入 ContextManager
  4. persist_rollout_response_items
  5. send_raw_response_items

也就是说,同一个 item 同时进入:

  • 模型下次 prompt 的 history;
  • 持久化 rollout;
  • 前端/调试可以看到的 raw response item event。

线程回滚在 session/handlers.rs:452-554 中实现。它会:

  • 拒绝 active turn 期间 rollback;
  • flush 当前 rollout;
  • 读取持久历史;
  • 追加 rollback marker;
  • 调用 apply_rollout_reconstruction 重建 session;
  • recompute token usage;
  • 持久化 rollback event。

这解释了为什么 Codex 需要把每一步都事件化、持久化:长线程、工具执行和上下文压缩都可能失败,必须能从 transcript/rollout 恢复,而不是只依赖内存里的临时状态。


12. LLM 之外,Codex 最值得学习的工程设计

下面这些点,比“模型会不会推理”更值得 agent 设计初学者学习。

12.1 把协议作为第一等公民

Codex 没有让 UI 直接调用内部函数,而是用 SubmissionEventMsg 把所有动作建模成协议消息。这样可以支持 CLI、TUI、app server、realtime conversation、子 agent、审批和工具事件。

可复用原则:

不要只有 run(input) -> output
要有 submit(op) -> stream(event)

12.2 把生命周期拆成 Session / Task / Turn

很多 Agent demo 把所有状态放进一个函数。Codex 把长期 session、后台 task、一次 turn 的 sampling loop 分开,使取消、替换、pending input、compact、review 等能力有明确归属。

可复用原则:

Session 保存长期状态
Task 表示可取消的后台工作
Turn 表示一次模型/工具闭环

12.3 上下文必须 typed、bounded、diffable

Codex 对上下文有几个强约束:

  • fragment 有类型和 marker;
  • additional context 有 token cap;
  • world state 有 snapshot/diff;
  • history 进入 prompt 前 normalize;
  • 工具输出按 truncation policy 截断;
  • compact 后重建 baseline。

可复用原则:

不要把 prompt 当字符串拼接
要把 context 当数据库记录 + diff patch + render layer

12.4 工具不是函数列表,而是运行时

Codex 的工具要经过 spec planning、router、registry、runtime、handler、hooks、telemetry、permissions、sandbox。模型只负责“请求工具”,程序负责“是否、如何、安全地执行工具”。

可复用原则:

model-visible spec != runtime registry
model tool call != actual execution permission
tool output != unlimited raw stdout

12.5 流式事件要转成稳定内部事件

Codex 不把 Responses stream 原样抛给 UI,而是转成 EventMsgTurnItem。这给 UI、日志、测试、rollout 和未来 provider 兼容留出了稳定边界。

可复用原则:

Provider event -> internal turn item -> frontend event
不要让 UI 依赖 provider 的裸协议

12.6 权限、沙箱、审批必须在 deterministic runtime 中实现

shell 和 exec handler 展示了 Codex 的安全设计:approval policy、sandbox permissions、additional permissions、cwd、network、Windows sandbox level 都由 runtime 判断。模型没有最终决定权。

可复用原则:

LLM proposes, runtime disposes

12.7 可观测性不是锦上添花

Codex 到处有 tracing span、telemetry tags、turn diff、token count、raw response item、hook started/completed、tool lifecycle events。对于 coding agent,这些不是“日志美化”,而是调试复杂行为的必要设施。

可复用原则:

每个不可预测行为都应该发事件:模型输出、工具开始/结束、权限请求、compact、diff、错误

13. 如果你要自己实现一个迷你 Codex,最小架构可以这样切

不要一开始实现 Codex 的全部能力。可以按下面的递进版本做。

13.1 V0:只有单 turn 工具循环

history = []
record(user_input)
loop:
  prompt = normalize(history)
  response = model(prompt, tool_specs)
  record(response.items)
  if response has tool_call:
    output = run_tool(tool_call)
    record(output)
    continue
  else:
    break

这能帮你理解基本 tool loop,但还不是生产级。

13.2 V1:加 Session / Task / Event

加入:

  • Submission { id, op }
  • Event { id, msg }
  • Session 保存 history 和 active task;
  • Task 支持 cancellation;
  • TurnContext 保存模型、cwd、权限、配置快照。

此时你就能做流式 UI、取消、审批请求。

13.3 V2:加 typed context fragment

加入:

  • ContextFragment trait;
  • EnvironmentContext
  • InstructionsContext
  • hard token cap;
  • history normalize;
  • tool output truncation。

此时你能避免 prompt 被无限污染。

13.4 V3:加 world state snapshot/diff

加入:

  • WorldStateSection { id, snapshot, render_diff }
  • reference baseline;
  • full injection / diff injection;
  • compact 后 baseline reset。

此时你开始接近 Codex 的上下文工程。

13.5 V4:加工具 registry/runtime

加入:

  • model-visible spec;
  • runtime registry;
  • router;
  • per-tool parallel support;
  • pre/post hooks;
  • sandbox/approval;
  • telemetry。

此时工具就不再是“函数列表”,而是可审计的执行系统。


14. 和 Swarm 篇的区别:这篇重点不在“Agent 是什么”

上一篇 Swarm 架构篇 更适合理解基础概念:

  • Agent 控制循环;
  • 工具调用;
  • evidence / verification;
  • context compaction;
  • anti-loop guard;
  • 多 agent 的基本分工。

本文看 Codex 源码,重点换成生产级 CLI coding agent 的内核:

  • turn/task/session 分层;
  • SQ/EQ 协议;
  • typed context fragments;
  • world-state snapshot/diff;
  • Responses API streaming event handling;
  • tool spec / router / registry / runtime 四层;
  • hooks / approvals / sandbox;
  • rollout / raw events / rollback;
  • prompt cache 友好的增量上下文;
  • compaction replacement history。

一句话说:

Swarm 篇回答“一个 Agent 原型应该有哪些部件”;本文回答“一个长期运行、能改代码、能执行命令、能恢复和审计的 coding agent,怎样把这些部件工程化”。


15. 源码速查表

问题关键源码
Codex 如何定义异步协议?codex-rs/protocol/src/protocol.rs:1-4, :159-170, :511-590, :1252-1467
用户输入如何分发?codex-rs/core/src/session/handlers.rs:703-848
新 turn 如何启动?codex-rs/core/src/session/handlers.rs:183-275, codex-rs/core/src/tasks/mod.rs:313-450
普通任务如何循环处理 pending input?codex-rs/core/src/tasks/regular.rs:37-88
核心模型/工具循环在哪里?codex-rs/core/src/session/turn.rs:128-459
prompt 如何构造?codex-rs/core/src/session/turn.rs:1043-1060, :1103-1116
上下文如何全量/增量注入?codex-rs/core/src/session/mod.rs:3145-3458, :3549-3632
history 如何 normalize?codex-rs/core/src/context_manager/history.rs:137-144, :355-368
工具输出如何截断?codex-rs/core/src/context_manager/history.rs:370-395
typed context fragment 在哪里?codex-rs/context-fragments/src/fragment.rs:37-114
additional context 上限在哪里?codex-rs/context-fragments/src/additional_context.rs:5-92
world state snapshot/diff 在哪里?codex-rs/core/src/context/world_state/mod.rs:167-314
AGENTS.md diff 在哪里?codex-rs/core/src/context/world_state/agents_md.rs:13-80
环境 diff 在哪里?codex-rs/core/src/context/world_state/environment.rs:14-145
工具 router 如何构建?codex-rs/core/src/tools/spec_plan.rs:160-203, :235-272, :612-624
模型输出如何识别为工具调用?codex-rs/core/src/tools/router.rs:112-160
工具调用如何执行并回填?codex-rs/core/src/stream_events_utils.rs:405-515, codex-rs/core/src/session/turn.rs:1853-1877
工具并发/取消在哪里?codex-rs/core/src/tools/parallel.rs:41-202
工具 hooks/telemetry/dispatch 在哪里?codex-rs/core/src/tools/registry.rs:44-135, :405-670
shell 权限和沙箱在哪里?codex-rs/core/src/tools/handlers/shell.rs:63-165, codex-rs/core/src/tools/handlers/unified_exec/exec_command.rs:157-180, :243-365
MCP 工具如何包装?codex-rs/core/src/tools/handlers/mcp.rs:32-164
Responses API stream 在哪里处理?codex-rs/core/src/session/turn.rs:1887-2390
ModelClient 生命周期在哪里?codex-rs/core/src/client.rs:1-24, :235-280, :875-890, :1725-1785
compact replacement history 在哪里?codex-rs/core/src/compact.rs:55-85, :322-365, codex-rs/core/src/session/mod.rs:2978-3022

16. 最后总结

Codex CLI 的源码给 agent 初学者最重要的启发不是“写一个更聪明的 prompt”,而是:

  1. 用协议把外部输入和内部事件标准化;
  2. 用 Session / Task / Turn 分离长期状态、后台工作和模型循环;
  3. 用 typed fragment 和 world-state diff 管理上下文;
  4. 用 registry/runtime 而不是函数列表管理工具;
  5. 用 deterministic runtime 做权限、沙箱、审批、截断和审计;
  6. 用 rollout、raw events、compact replacement history 支撑长任务恢复;
  7. 把模型客户端、streaming、prompt cache、sticky routing 当成工程问题,而不是简单 HTTP 调用。

如果只能带走一句话:

生产级 coding agent 的关键,不是让 LLM 自己记住一切、决定一切、执行一切;而是让 LLM 在一个可观测、可恢复、可约束的运行时里做决策。