生产级 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 协议 |
| 会话层 | 一条长期线程如何保存状态、配置和历史? | Session、ContextManager、rollout persistence |
| 任务层 | 一个用户请求如何被放到后台执行、取消、替换? | SessionTask、RegularTask、spawn_task |
| turn 循环 | 模型、工具、上下文、压缩如何在一轮里闭环? | run_turn、run_sampling_request、stream event handling |
| 上下文层 | 初始环境、AGENTS.md、权限、插件、技能如何进入模型? | ContextualUserFragment、WorldState snapshot/diff、reference baseline |
| 工具层 | 工具如何暴露给模型、路由、并发、审计和回填结果? | ToolRouter、ToolRegistry、ToolCallRuntime、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。初学者不需要一开始就读完全部目录,可以先按下面顺序:
| 阅读顺序 | 路径 | 你应该关注什么 |
|---|---|---|
| 1 | codex-rs/protocol/src/protocol.rs | 前端和 agent 通信的消息协议:Submission、Op、EventMsg |
| 2 | codex-rs/core/src/session/handlers.rs | 收到用户输入后如何分发、开 turn、steer 当前 turn |
| 3 | codex-rs/core/src/tasks/mod.rs、tasks/regular.rs | task 生命周期:启动、取消、后台执行 |
| 4 | codex-rs/core/src/session/turn.rs | 最核心:agent turn 循环、模型采样、工具调用、自动 compact |
| 5 | codex-rs/core/src/session/mod.rs | session 状态、上下文注入、history 记录、compact 替换历史 |
| 6 | codex-rs/core/src/context_manager/history.rs | 历史如何进入 prompt、如何 normalize、如何截断工具输出 |
| 7 | codex-rs/core/src/context/world_state/* | AGENTS.md、环境信息等如何做 snapshot/diff |
| 8 | codex-rs/core/src/tools/* | 工具 spec、router、registry、runtime、handler 的分层 |
| 9 | codex-rs/core/src/client.rs | Responses API 请求、WebSocket/HTTP fallback、turn-scoped client session |
| 10 | codex-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 answerCodex 源码不是这个形状。它先定义了一个异步通信协议:
- SQ(Submission Queue):用户、前端或其他 agent 提交请求;
- EQ(Event Queue):agent 把进度、工具结果、错误、最终回答发回前端。
源码证据:
codex-rs/protocol/src/protocol.rs:1-4明确写出使用 SQ/EQ pattern;Submission在protocol.rs:159-170中定义,包含id、op、客户端消息 id、trace context;Op在protocol.rs:511-590起定义,包含UserInput、ThreadSettings、ExecApproval、PatchApproval、RefreshMcpServers、Compact、ThreadRollback等;Event/EventMsg在protocol.rs:1252-1467起定义,包含TurnStarted、TurnComplete、AgentMessage、ExecCommandBegin/End、McpToolCallBegin/End、HookStarted/Completed、TurnDiff等事件。
这套设计解决了三个问题:
- 前端不需要阻塞等待最终答案:它可以流式收到 reasoning、assistant delta、工具开始/结束、审批请求、diff 等事件。
- 用户可以在 turn 运行中继续输入:新的
UserInput可以被视为 steer input,进入 active turn 的 pending queue。 - 工具审批、MCP elicitation、动态工具响应都是同一协议的一部分:它们不是临时 callback,而是统一建模为
Op和EventMsg。
你可以把 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(...):真正执行任务,接收SessionTaskContext、TurnContext、初始输入和 cancellation token;abort(...):任务被取消时释放资源。
Session::spawn_task / start_task 在 tasks/mod.rs:313-450 中实现。关键动作包括:
- 替换当前任务时先
abort_all_tasks; - 清理 connector selection;
- 创建 cancellation token;
- 把 pending input 迁移到新 turn state;
- 发出 turn start lifecycle;
tokio::spawn后台运行 task;- 结束时 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 的任务实现。核心结构很短:
- 先发
TurnStarted; - 获取预热好的 model client session(如果有);
- 调用
run_turn(...); - 如果 session input queue 里还有 pending input,就继续跑下一轮
run_turn; - 没有 pending input 才返回最终 assistant message。
这意味着 Codex 支持一种很实用的交互:当模型还在执行工具时,用户又追加了指令,这些输入不一定要等整个任务结束才处理,而是可以被记录为 pending input,由 regular task 继续消化。
4. 第三层:一次 UserInput 如何变成后台 agent turn?
看用户输入的入口,应该从 session/handlers.rs 开始。
4.1 Submission loop 分发 Op
submission_loop 在 codex-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_inner 在 handlers.rs:183-275。它的决策逻辑是:
- 解析
Op::UserInput中的 items、json schema、client metadata、additional context、thread settings; - 如有 thread settings,先更新 session 配置并发
ThreadSettingsApplied; - 创建新的
TurnContext; - 调用
sess.steer_input(...)尝试把输入注入当前 active turn; - 如果没有 active turn,则把 additional context 和 user input 组装成
TurnInput,调用spawn_task(..., RegularTask::new())。
这就是 Codex 同时支持“开启新 turn”和“运行中追加指令”的关键。
InputQueue 在 codex-rs/core/src/session/input_queue.rs:28-38 中定义,负责保存 turn-local pending input 和 mailbox mail。get_pending_input 在 input_queue.rs:197-225 中把 active turn 的 pending input 与 mailbox input 合并取出。
初学者可以学到什么?
不要把“用户输入”只当成函数参数。生产级 agent 需要把用户输入、子 agent mail、审批结果、动态工具响应都变成可排队、可追踪、可延迟处理的事件。
5. Codex 的核心 agent 循环:run_turn
run_turn 在 codex-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_context 在 session/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 loop | handlers.rs:703-848 | 持续接收外部 Submission |
| task loop | regular.rs:73-88 | 一个 task 中反复处理 pending input |
| turn sampling loop | turn.rs:224-416 | 模型 → 工具 → 模型,直到无需 follow-up |
| stream event loop | turn.rs:1945-2348 | 消费 Responses API 流式事件 |
| retry loop | turn.rs:1103-1166 | stream 失败时按 provider retry budget 重试 |
| tool future drain | turn.rs:1853-1877 | 并发工具执行后把输出写回 history |
| compaction loop | compact.rs:251-320 | compact 请求失败/超窗时重试或裁剪历史 |
这正是生产级 agent 和 demo agent 的差距:demo 关注“模型下一步做什么”,生产系统还要处理并发、取消、重试、排队、压缩、权限、事件和持久化。
6. 上下文注入:从“拼 prompt”变成“可 diff 的世界状态”
Agent 初学者最容易犯的错误,是每轮都把所有文件、所有规则、所有历史重新塞进 prompt。Codex 不是这么做的。
Codex 的上下文管理有三个核心概念:
- history:已经发生过的消息、工具调用、工具输出、reasoning、compaction 等;
- reference context item:上一轮用于 diff 的配置/turn context baseline;
- world state snapshot:环境、AGENTS.md 等模型可见状态的持久快照。
6.1 初始上下文里有哪些东西?
build_initial_context_with_world_state_and_mcp 在 session/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_item 在 session/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。
WorldState 在 world_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.md | context/world_state/agents_md.rs:13-80 | 如果 AGENTS.md 变了,渲染 replacement notice;如果不再适用,渲染 removal notice |
| Environments | context/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
ContextManager 在 context_manager/history.rs:36-57 中保存:
items;history_version;token_info;reference_context_item;world_state_baseline。
for_prompt 在 history.rs:137-144 中会先 normalize_history。normalize 的不变量在 history.rs:355-368:
- 每个 function/custom call 都要有对应 output;
- 每个 output 都要有对应 call;
- 如果模型不支持图片,就从消息和工具输出中剥离图片。
工具输出也不是无限写入。process_item 在 history.rs:370-395 中会按 truncation policy 截断 FunctionCallOutput 和 CustomToolCallOutput。
这解决了一个很现实的问题:工具可能输出成千上万行日志,如果全量塞回模型,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_tools 在 session/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_registry 在 spec_plan.rs:235-272 中体现了这个分离:
- 遍历 runtime;
- 根据
ToolExposure判断是否直接暴露给模型; - 生成 spec;
- merge namespace tools;
- 同时把所有 runtime 放进 registry。
ToolExposure 的存在很关键:有些工具可以被模型直接看见,有些只作为 deferred tool 通过 tool_search 发现,有些 hidden 但仍可兼容旧调用或内部 dispatch。
7.3 工具来源很多,但都被归一成 runtime
add_tool_sources 在 spec_plan.rs:612-624,按顺序加入:
- shell / unified exec;
- MCP resource tools;
- core utility tools;
- collaboration / multi-agent tools;
- MCP runtime tools;
- extension tools;
- dynamic tools;
- 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_searchexecutor: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。
关键逻辑:
- 调用
ToolRouter::build_tool_call(item.clone()); - 如果是 tool call:
- 记录模型发出的 tool call;
- 创建 tool future;
- 设置
needs_follow_up = true;
- 如果不是 tool call:
- 转成 UI 可展示的 turn item;
- 记录 assistant / reasoning / hosted tool item;
- 如果模型发出了不合法工具请求但可以回应模型,则构造 function call output,写回 history,并要求 follow-up。
ToolRouter::build_tool_call 在 tools/router.rs:112-160,负责把不同类型的 response item 映射为内部统一的 ToolCall:
ResponseItem::FunctionCall;ResponseItem::ToolSearchCall;ResponseItem::CustomToolCall。
7.5 工具输出如何回到模型?
工具 future 会被放进 in_flight。drain_in_flight 在 session/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 output7.6 并发工具不是全开:由每个工具声明 parallel support
ToolCallRuntime 在 tools/parallel.rs:41-64 保存 router、session、step context、turn diff tracker 和一个 RwLock。
handle_tool_call_with_source 在 parallel.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。流程包括:
- active turn tool call 计数;
- 找 tool;
- 检查 payload kind;
notify_tool_start;- 跑 pre-tool-use hooks:可以 block,也可以 rewrite input;
- 执行 handler,并记录 telemetry;
- 成功后跑 post-tool-use hooks:可以追加 context、block result、把反馈替换为模型可见输出;
notify_tool_finish;- 将结果或错误转换为模型可见结果。
所以 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
McpHandler 在 tools/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_request 在 session/turn.rs:1887-2390。它做了几件重要事情:
- 构造
feedback_tags,记录模型、approval policy、sandbox policy、features 等; - 调用
client_session.stream(...)获取 Responses API stream; - 循环消费
ResponseEvent; - 对
OutputItemAdded、OutputTextDelta、ToolCallInputDelta、reasoning delta、OutputItemDone、Completed等事件分别处理; - 在
OutputItemDone时调用handle_output_item_done; Completed时记录 token usage,并根据end_turn决定是否 follow-up;- stream 完成后 drain 工具 futures;
- 发 token count 和 turn diff。
这体现了 UI/协议和模型原始流之间的隔离:
- 模型原始事件是
ResponseEvent; - Codex 内部把它们转成
TurnItem、EventMsg、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 context10.3 Replacement history 是可靠性机制
compact.rs:322-365 中,compact 完成后会:
- 从当前 history 取最后 assistant message 作为 summary suffix;
- 构建 compacted history;
- advance auto compact window;
- 根据
InitialContextInjection构建 initial context; - 必要时插入 replacement history;
- 设置
reference_context_item; - 调用
replace_compacted_history。
Session::replace_compacted_history 在 session/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_items 在 session/mod.rs:2778-2795,每次记录 history 都会:
prepare_conversation_items_for_history;- 更新 current time reminder 状态;
- 写入
ContextManager; persist_rollout_response_items;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 直接调用内部函数,而是用 Submission 和 EventMsg 把所有动作建模成协议消息。这样可以支持 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 layer12.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 stdout12.5 流式事件要转成稳定内部事件
Codex 不把 Responses stream 原样抛给 UI,而是转成 EventMsg 和 TurnItem。这给 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 disposes12.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
加入:
ContextFragmenttrait;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”,而是:
- 用协议把外部输入和内部事件标准化;
- 用 Session / Task / Turn 分离长期状态、后台工作和模型循环;
- 用 typed fragment 和 world-state diff 管理上下文;
- 用 registry/runtime 而不是函数列表管理工具;
- 用 deterministic runtime 做权限、沙箱、审批、截断和审计;
- 用 rollout、raw events、compact replacement history 支撑长任务恢复;
- 把模型客户端、streaming、prompt cache、sticky routing 当成工程问题,而不是简单 HTTP 调用。
如果只能带走一句话:
生产级 coding agent 的关键,不是让 LLM 自己记住一切、决定一切、执行一切;而是让 LLM 在一个可观测、可恢复、可约束的运行时里做决策。