读源码之前 · 仓库的工程约定
根目录 AGENTS.md 是写给所有贡献者(包括 Codex 自己)的开发规范:crate 命名、少往 codex-core 里加代码、模块 500 行目标、trait 用 RPITIT、模型可见上下文的六条硬规矩、测试与快照、app-server v2 的命名规范。读懂这些约定,很多“代码为什么长这样”就有了答案;本篇最后给出推荐的阅读顺序,以及在近百万行代码里找东西的办法。
读源码之前 · 仓库的工程约定
动手读 Codex 之前,先读一份 320 行的 Markdown:仓库根目录的 AGENTS.md。它是写给贡献者的开发规范,而这个仓库的贡献者包括 Codex 自己。AGENTS.md 本身就是 Codex 的一项功能:Codex 在某个目录下干活时会把这类文件读进上下文,用法见手册 AGENTS.md 项目指令,实现见指令从哪来。所以 openai/codex 用它来约束“在这个仓库里写代码的 agent”,顺带也约束了人。
同类文件还有几份:codex-rs/tui/src/bottom_pane/AGENTS.md 要求改输入框状态机时同步更新模块文档,codex-rs/tui/styles.md 规定终端配色;.codex/skills/ 里的 code-review 会为每个 code-review-* skill 派一个子代理,这些 skill 的内容正是 AGENTS.md 里“Code Review Rules”一节的各条。下面按“读代码时最常碰到”的顺序讲。
写给 clippy 的规矩
AGENTS.md 开头一串条目几乎都能在 lint 上找到对应:format! 参数内联、if 能合并就合并、能用方法引用就不写闭包,分别对应 clippy 的 uninlined_format_args、collapsible_if、redundant_closure_for_method_calls,其中第一、三条在 workspace 里直接设成了 deny。unwrap_used、expect_used 同样是 deny,codex-rs/clippy.toml 只为测试网开一面;它还禁用了一批方法:TUI 不准用 RGB 或索引颜色,SQLite 连接池只能经 codex-state 里的封装创建。codex-rs/core/src/lib.rs 开头还有一句 #![deny(clippy::print_stdout, clippy::print_stderr)],内核不许直接往终端打印。
有一条规矩最容易在代码里认出来:不要用 bool、Option 这类含义不明的位置参数,实在要传,就在字面量前写上参数名注释,由自研的 argument-comment-lint 检查:
pub(crate) async fn submit(&self, op: Op) -> CodexResult<String> {
self.submit_with_trace(
op, /*trace*/ None, /*parent_turn_id*/ None, /*root_turn_id*/ None,
/*residency_guard*/ None,
)
.await
}(codex-rs/core/src/session/mod.rs:948)
另一条是 trait 里的异步方法。AGENTS.md 不鼓励 #[async_trait],也不许用 #[allow(async_fn_in_trait)] 走捷径,推荐写成返回 impl Future + Send 的原生形式(RPITIT),把 Send 约束显式写出来:
fn run(
self: Arc<Self>,
session: Arc<Session>,
ctx: Arc<TurnContext>,
input: Vec<TurnInput>,
cancellation_token: CancellationToken,
) -> impl std::future::Future<Output = SessionTaskResult> + Send;(codex-rs/core/src/tasks/mod.rs:196)
实现方仍可以写 async fn,只要满足这个契约。在这个 tag 上,async-trait 已经不在 workspace 依赖里了。再加上 codex-rs/rustfmt.toml 的 imports_granularity = "Item"(每个 use 只导入一项),你会看到很多文件开头是几十行单项导入,这是刻意的,不是没人整理。
模块多大、代码往哪放
AGENTS.md 对体量有两条规矩。一是模块目标 500 行以内(不算测试),超过约 800 行就该另起新模块,并点名了几个“高频改动”文件,都在 codex-rs/tui/src/ 下:app.rs、bottom_pane/chat_composer.rs、bottom_pane/footer.rs、chatwidget.rs、bottom_pane/mod.rs。二是一次改动一般不超过 800 行,复杂逻辑不超过 500 行。
这是给新代码定的方向,不是现状。codex-rs/ 下去掉测试文件后有 3,005 个 .rs 文件,按去掉 #[cfg(test)] 块后的行数算,412 个超过 500 行,194 个超过 800 行;最大的是 codex-rs/app-server/src/request_processors/thread_processor.rs(6,258 行)、codex-rs/core/src/session/mod.rs(5,111 行)和 codex-rs/tui/src/bottom_pane/chat_composer.rs(5,031 行)。拆分的痕迹也看得出来:chatwidget.rs 本身约 2,000 行,旁边的 chatwidget/ 目录有 98 个文件,其中 78 个写着 impl ChatWidget;app.rs 约 1,200 行,app/ 目录的 88 个文件里有 61 个是 impl App 按主题拆开的续篇。所以读代码时,一个类型的 impl 散在同名目录的多个文件里是常态。
至于代码往哪个 crate 放,AGENTS.md 专门为最大的 crate 写了一节:
Over time, the `codex-core` crate (defined in `codex-rs/core/`) has become bloated because it is the largest crate, so it is often easier to add something new to `codex-core` rather than refactor out the library code you need so your new code neither takes a dependency on, nor contributes to the size of, `codex-core`.
To that end: **resist adding code to codex-core**!(AGENTS.md:74)
新概念先考虑放进已有的其它 crate,或者干脆新开一个 crate,审查时也要顶住往 codex-core 里加代码的 PR。上一篇里那一圈卫星 crate,就是这条规矩的产物。
模型可见上下文的六条硬规矩
这一节最能体现 Codex 对上下文的态度,原文照录:
Codex maintains a context (history of messages) that is sent to the model in inference requests.
1. No history rewrite - the context must be built up incrementally.
2. Avoid frequent changes to context that cause cache misses.
3. No unbounded items - everything injected in the model context must have a bounded size and a hard cap.
4. No items larger than 10K tokens.
5. Highlight new individual items that can cross >1k tokens as P0. These need an additional manual review.
6. All injected fragments must be defined as structs in `core/context` and implement ContextualUserFragment trait(AGENTS.md:93)
前两条关乎提示缓存:历史只追加不改写,发给模型的前缀才能命中缓存;模型客户端能用 WebSocket 时只发增量输入,也建立在这个前提上(见上下文与历史与模型客户端)。第三、四、五条给注入内容设了硬上限:单项不超过 10K token,可能超过 1K token 的新注入项要额外人工审查。
第六条与代码略有出入。ContextualUserFragment 这个 trait 如今定义在独立的 codex-context-fragments crate 里(codex-rs/context-fragments/src/fragment.rs:64)。全仓库 71 处实现分布在 62 个文件里,其中 51 个文件在 codex-rs/core/src/context/ 及其子目录 world_state/ 下,其余在 codex-guardian-context、codex-prompts、codex-context-fragments 和 ext/skills 里。规则的本意没变:注入模型的每一段文字都是一个有名字的结构体,有固定的起止标记和分类,而不是随手拼出来的字符串。
测试怎么组织
- 单元测试放隔壁文件。 新增测试模块写在同级的
*_tests.rs里,用#[path = "..._tests.rs"]挂上,codex-rs/下这样的文件有 1,265 个。 - 界面改动必须有 insta 快照。 任何用户看得见的 UI 变化都要附带快照测试,
codex-rs/tui下有 1,326 个.snap文件,读 TUI 时它们就是现成的“效果图”。 - agent 行为用集成测试。 改 agent 逻辑优先写集成测试,放在
codex-rs/core/tests/suite/(205 个.rs文件),由codex-rs/core/tests/all.rs汇总成一个测试二进制;测试用test_codex搭一个 Codex 实例,用 wiremock 起一个假的 Responses API(mount_sse_once、ev_response_created等辅助函数)。app-server 的集成测试则应当只走公开的 JSON-RPC 接口。 - 用
just test,不直接cargo test。 前者走 nextest 与仓库的默认配置。
还有一条读代码时会碰到的:AGENTS.md 禁止改动与 CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR、CODEX_SANDBOX_ENV_VAR 相关的代码。Codex 在沙箱里执行命令时会设置这两个环境变量,不少测试的作者知道 Codex 跑不了它们,见到这两个变量就提前返回。
app-server v2 的命名规范
新接口只加在 v2,v1 不再增加任何接口。方法名是 <resource>/<method>,资源名用单数;请求、响应、通知的载荷分别叫 *Params、*Response、*Notification;字段在线上一律 camelCase(配置相关的 RPC 例外,沿用 config.toml 的 snake_case);ID 用字符串,时间戳用 i64 的 Unix 秒并以 _at 结尾;列表接口默认用游标分页。在协议定义里,一个方法是这样登记的:
TurnStart => "turn/start" {
params: v2::TurnStartParams,
inspect_params: true,
serialization: thread_id(params.thread_id),
response: v2::TurnStartResponse,
},(codex-rs/app-server-protocol/src/protocol/common.rs:1032)
serialization 一项声明同一线程上的请求要排队串行处理,inspect_params 表示只有部分字段属于实验性接口。改了载荷要跑 just write-app-server-schema 重新生成 schema,细节见 v2 协议。
推荐的阅读顺序
| 顺序 | 入口 | 看什么 | 对应篇目 |
|---|---|---|---|
| 1 | codex-rs/cli/src/main.rs(Subcommand 在 143 行)、codex-rs/arg0/src/lib.rs | 命令怎么分派 | 构建与运行 |
| 2 | codex-rs/tui/src/lib.rs、codex-rs/tui/src/app_server_session.rs | 前端怎么连上 app-server | TUI 架构 |
| 3 | codex-rs/app-server/src/message_processor.rs、request_processors/ | 请求怎么分派 | app-server |
| 4 | codex-rs/core/src/thread_manager.rs、codex_thread.rs、session/mod.rs | 线程与会话 | ThreadManager |
| 5 | codex-rs/core/src/session/turn.rs(run_turn 在 163 行)、client.rs | 主循环与模型调用 | run_turn |
| 6 | codex-rs/core/src/tools/ 的 router.rs、registry.rs、orchestrator.rs | 工具与审批 | 工具系统 |
| 7 | codex-rs/protocol/src/protocol.rs(Op 在 590 行,EventMsg 在 1358 行) | 内核的词汇表 | 全书 |
先读下一篇把这些文件串成一条线,再按需深入。
在近百万行里找东西
- 从协议倒查。 方法名字符串(如
turn/start)在common.rs里对应一个ClientRequest变体,拿变体名去搜message_processor.rs,就找到处理它的 processor;反方向,内核事件EventMsg的每个变体在codex-rs/app-server/src/bespoke_event_handling.rs里被翻译成 v2 通知。 - 从测试倒查。
core/tests/suite/的文件名基本就是功能名(approvals.rs、agents_md.rs、apply_patch_cli.rs……),是最可靠的行为说明;UI 以快照为准。 - 看 span 名。 代码里到处是 tracing 的 span(
AGENTS.md也要求给异步函数的定义加#[tracing::instrument(...)]),session_task.turn、run_turn、handle_responses这类 span 名可以直接当搜索锚点。日志用RUST_LOG控制,codex -c log_dir=./.codex-log能让 TUI 另写一份纯文本日志,just log从 SQLite 日志库里跟踪。 - 用调试子命令。
codex debug prompt-input "..."把模型可见的输入列表打成 JSON;codex debug app-server send-message-v2 "..."起一个codex app-server子进程,打印initialize、thread/start、turn/start的响应,以及之后的turn/started、item/started、消息增量、item/completed、turn/completed等通知;设CODEX_TUI_RECORD_SESSION=1,TUI 会把收发的消息记进session-*.jsonl。 - 读最小样例。
codex-rs/thread-manager-sample/src/main.rs不到 500 行,经codex-core-api这个门面直接驱动ThreadManager,适合在不碰 app-server 的情况下看清内核怎么用。 - 文档可能过时。
codex-rs/docs/protocol_v1.md自己声明代码可能与它不完全一致,它链接的core/src/agent.rs这个文件、描述的Op::ConfigureSession在这个 tag 上都已不存在。一切以代码为准。
和《从 LLM 到 Coding Agent》对照
那本书的上下文注入与提示缓存两篇讲的是道理:往上下文里塞什么、怎么保住缓存前缀。Codex 把这些道理写成了贡献规范里的硬规矩:只追加、有上限、每个注入片段都是具名结构体,可能超过 1K token 的新注入项还要标成 P0 额外审查。教学实现可以随手拼字符串,生产代码得有人专门守着这道门。