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

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

作者 David更新于 第 4 篇(共 57 篇)

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

动手读 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 协议。

推荐的阅读顺序

顺序入口看什么对应篇目
1codex-rs/cli/src/main.rs(Subcommand 在 143 行)、codex-rs/arg0/src/lib.rs命令怎么分派构建与运行
2codex-rs/tui/src/lib.rs、codex-rs/tui/src/app_server_session.rs前端怎么连上 app-serverTUI 架构
3codex-rs/app-server/src/message_processor.rs、request_processors/请求怎么分派app-server
4codex-rs/core/src/thread_manager.rs、codex_thread.rs、session/mod.rs线程与会话ThreadManager
5codex-rs/core/src/session/turn.rs(run_turn 在 163 行)、client.rs主循环与模型调用run_turn
6codex-rs/core/src/tools/ 的 router.rs、registry.rs、orchestrator.rs工具与审批工具系统
7codex-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 额外审查。教学实现可以随手拼字符串,生产代码得有人专门守着这道门。


上一篇:workspace 全景 · 153 个成员怎么分层 · 下一篇:一条消息的生命周期 · 从按下回车到看到回复

本页目录