# 读源码之前 · 仓库的工程约定

> 根目录 AGENTS.md 是写给所有贡献者（包括 Codex 自己）的开发规范：crate 命名、少往 codex-core 里加代码、模块 500 行目标、trait 用 RPITIT、模型可见上下文的六条硬规矩、测试与快照、app-server v2 的命名规范。读懂这些约定，很多“代码为什么长这样”就有了答案；本篇最后给出推荐的阅读顺序，以及在近百万行代码里找东西的办法。

- 作者：David（道雾轩）
- 专栏：Codex 源码解读（https://daiw.net/manual/codex-source.md）
- 最后更新：2026-09-29
- 原文：https://daiw.net/manual/codex-source/reading-the-source
- 转载与引用：请注明出处并附原文链接（https://daiw.net/about/copyright）

# 读源码之前 · 仓库的工程约定

动手读 Codex 之前，先读一份 320 行的 Markdown：仓库根目录的 `AGENTS.md`。它是写给贡献者的开发规范，而这个仓库的贡献者包括 Codex 自己。`AGENTS.md` 本身就是 Codex 的一项功能：Codex 在某个目录下干活时会把这类文件读进上下文，用法见手册 [AGENTS.md 项目指令](https://daiw.net/manual/codex/agents-md)，实现见[指令从哪来](https://daiw.net/manual/codex-source/instructions)。所以 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` 检查：

```rust
    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` 约束显式写出来：

```rust
    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 写了一节：

```markdown
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。[上一篇](https://daiw.net/manual/codex-source/workspace-map)里那一圈卫星 crate，就是这条规矩的产物。

## 模型可见上下文的六条硬规矩

这一节最能体现 Codex 对上下文的态度，原文照录：

```markdown
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 时只发增量输入，也建立在这个前提上（见[上下文与历史](https://daiw.net/manual/codex-source/context-history)与[模型客户端](https://daiw.net/manual/codex-source/model-client)）。第三、四、五条给注入内容设了硬上限：单项不超过 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` 结尾；列表接口默认用游标分页。在协议定义里，一个方法是这样登记的：

```rust
    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 协议](https://daiw.net/manual/codex-source/app-server-protocol)。

## 推荐的阅读顺序

| 顺序 | 入口 | 看什么 | 对应篇目 |
| --- | --- | --- | --- |
| 1 | `codex-rs/cli/src/main.rs`（`Subcommand` 在 143 行）、`codex-rs/arg0/src/lib.rs` | 命令怎么分派 | [构建与运行](https://daiw.net/manual/codex-source/build-and-run) |
| 2 | `codex-rs/tui/src/lib.rs`、`codex-rs/tui/src/app_server_session.rs` | 前端怎么连上 app-server | [TUI 架构](https://daiw.net/manual/codex-source/tui-architecture) |
| 3 | `codex-rs/app-server/src/message_processor.rs`、`request_processors/` | 请求怎么分派 | [app-server](https://daiw.net/manual/codex-source/app-server-architecture) |
| 4 | `codex-rs/core/src/thread_manager.rs`、`codex_thread.rs`、`session/mod.rs` | 线程与会话 | [ThreadManager](https://daiw.net/manual/codex-source/thread-manager) |
| 5 | `codex-rs/core/src/session/turn.rs`（`run_turn` 在 163 行）、`client.rs` | 主循环与模型调用 | [run_turn](https://daiw.net/manual/codex-source/turn-loop) |
| 6 | `codex-rs/core/src/tools/` 的 `router.rs`、`registry.rs`、`orchestrator.rs` | 工具与审批 | [工具系统](https://daiw.net/manual/codex-source/tool-architecture) |
| 7 | `codex-rs/protocol/src/protocol.rs`（`Op` 在 590 行，`EventMsg` 在 1358 行） | 内核的词汇表 | 全书 |

先读[下一篇](https://daiw.net/manual/codex-source/message-lifecycle)把这些文件串成一条线，再按需深入。

## 在近百万行里找东西

```mermaid
flowchart LR
  Q[想查一个功能] --> K{手里有什么线索}
  K -->|命令行参数| C[cli/src/main.rs]
  K -->|界面上的文字| S[tui 的 .snap 快照]
  K -->|协议方法名| P[app-server-protocol<br/>common.rs]
  K -->|模型看到的内容| M[codex debug prompt-input]
  C --> P
  S --> P
  P --> R[message_processor.rs<br/>request_processors]
  R --> O[core 的 Op 与 session/handlers.rs]
  O --> T[run_turn 与 tools]
  M --> T
```

- **从协议倒查。** 方法名字符串（如 `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》对照

那本书的[上下文注入](https://daiw.net/manual/llm-to-agent/context-injection)与[提示缓存](https://daiw.net/manual/llm-to-agent/prompt-caching)两篇讲的是道理：往上下文里塞什么、怎么保住缓存前缀。Codex 把这些道理写成了贡献规范里的硬规矩：只追加、有上限、每个注入片段都是具名结构体，可能超过 1K token 的新注入项还要标成 P0 额外审查。教学实现可以随手拼字符串，生产代码得有人专门守着这道门。

---

上一篇：[workspace 全景 · 153 个成员怎么分层](https://daiw.net/manual/codex-source/workspace-map) · 下一篇：[一条消息的生命周期 · 从按下回车到看到回复](https://daiw.net/manual/codex-source/message-lifecycle)
