拆到最小,Codex 就是一个本地编码 Agent:用户输入、AGENTS.md、环境信息与工具定义被组装成 Prompt,经 Responses API 流式采样;模型返回的工具调用在审批策略与操作系统沙箱约束下执行,结果回填历史再采样,直到模型不再需要工具。

循环本身并不神秘,难的是周围的工程。官方协议用 Model、Codex、Session、Task、Turn 五个实体描述运行模型:Session 同一时刻只允许一个 Task,Task 由多个 Turn 组成,Turn 之间严格因果——上一轮输出就是下一轮输入。

决定体验的是循环之外的取舍:引擎与 UI 通过一对队列(Submission Queue / Event Queue)解耦,一份 core 支撑 TUI、IDE 插件、桌面 App 与 MCP Server;Prompt 只追加不改写以命中前缀缓存;改动以补丁表达;安全上则用审批与沙箱两个正交维度,再叠加 execpolicy 与网络代理。

一个 while 循环:输出即下一轮输入

core/src/session/turn.rs 里的 run_turn() 是引擎心脏。去掉遥测与错误处理,结构大致是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
async fn run_turn(sess, turn_ctx, input) {
run_hooks_and_record_inputs(input);
run_pre_sampling_compact(); // 采样前先检查 token 上限
loop {
let prompt = build_prompt(history, tools, instructions);
let result = run_sampling_request(prompt).await; // 流式请求 Responses API
// Message / Reasoning → 记入历史并推给 UI
// FunctionCall → ToolRouter 执行,输出写回历史
let needs_follow_up = result.model_needs_follow_up || has_pending_input;
if token_limit_reached && needs_follow_up { run_auto_compact(); }
if !needs_follow_up { break; } // 只输出文本 → 结束
}
emit(EventMsg::TurnComplete { last_agent_message });
}

是否继续采样只取决于 needs_follow_up。本轮只要出现工具调用,结果就必须回传模型,因此必然再采样一次;模型只给文本消息时循环自然收敛。流式处理中,OutputItemDone 携带完整 item,由 handle_output_item_done 决定是记录消息还是分发工具,文本增量走 AgentMessageContentDelta;多个允许并行的工具调用交给 tools/parallel.rs 并发执行,再统一收取结果。

五层架构:一份 core,多种前端

从外到内可以切成五层,相邻层只通过明确协议通信:

层 关键组件
① 前端 / UI tui 全屏终端、exec 无头模式、IDE 插件、桌面 App、mcp-server
② 接入 / 协议 cli 与 arg0 角色分发、app-server(JSON-RPC,stdio 或 WebSocket)、protocol(Op、EventMsg、ResponseItem)
③ 引擎 core ThreadManager、Session、SessionTask(regular / review / compact)、run_turn、ContextManager、compact、rollout
④ 工具 / 执行 ToolRouter、ToolRegistry、ToolOrchestrator、unified_exec(PTY)、apply-patch、codex-mcp、execpolicy
⑤ 平台 / 外部 macOS Seatbelt、Linux bubblewrap + seccomp、Windows 受限令牌、network-proxy、Responses API、Ollama / LM Studio

前端与引擎之间是一对队列:UI 把带 sub_id 的 Op 推进 Submission Queue,core 把 EventMsg 推进 Event Queue。正因为接口足够窄,同一份 core 才能同时驱动终端、IDE 与桌面端。面向富客户端的 app-server 在此之上又包了一层 JSON-RPC,把对话抽象成 Thread(thread/start、thread/resume、thread/fork)、Turn(turn/start、turn/interrupt)和 Item(item/started → delta → item/completed)三个原语。

执行模型的约束写在协议文档里:

实体 含义 关键约束
Codex 核心引擎,跑在后台线程或独立进程中 只通过 SQ / EQ 与 UI 通信
Model Responses API 或兼容 Provider 以 SSE 或 WebSocket 流式返回 ResponseItem
Session 当前配置与状态:模型、cwd、审批、沙箱、历史 同一时刻最多运行一个 Task,重新配置会中止当前执行
Task 响应一次用户输入的完整工作 由多个 Turn 组成,新输入或 Op::Interrupt 会终止它
Turn 一次“采样 → 执行工具 → 回填”的迭代 上一轮输出即下一轮输入,无输出则任务结束

一个 Session 只跑一个 Task,是为了避免并发编辑互相覆盖;需要并行时开独立 Thread 或子 Agent(thread_spawn.rs、agent_communication.rs),每个 Thread 仍遵守同样的规则。编辑走 apply_patch 的自定义 diff 语法(*** Begin Patch),比整文件重写输出更短、也更容易审阅。

审批策略 × 沙箱策略:两个正交维度

安全模型由两个维度组合而成:审批策略决定“什么时候问人”,沙箱策略决定“操作系统层面允许做什么”。

AskForApproval 行为
untrusted 除 execpolicy 显式放行的命令外,一律需要审批
on-request 默认值,由模型判断何时申请提权;on-failure 是它的序列化别名
granular 按类别细分开关:shell 提权、execpolicy prompt、技能脚本、MCP elicitation 等
never 从不询问,失败直接返回模型,适合 CI 与 codex exec
SandboxPolicy 文件系统 网络
read-only 全盘只读 默认关闭
workspace-write cwd、writable_roots 与临时目录可写,.git、.codex 只读 默认关闭,可配置
danger-full-access 无限制 无限制
external-sandbox 已在外部容器中,全盘访问 按配置

core/src/tools/orchestrator.rs 把两者编排成一条执行链:

  1. 判定:execpolicy 的 Starlark prefix_rule(allow / prompt / forbidden)与审批策略共同得出 Skip、NeedsApproval 或 Forbidden。
  2. 审批:需要时发出 ExecApprovalRequest,用户可选 Approved、ApprovedForSession(会话缓存)、ApprovedExecpolicyAmendment(写成规则)、Denied 或 Abort;Guardian 也可自动审查代为决策。
  3. 执行与升级:先按 SandboxManager 选出的平台沙箱运行;若因沙箱失败,untrusted(或 granular 中开启沙箱审批)会再次请求审批、批准后脱离沙箱重试;on-request 与 never 不自动升级,只把精简的拒绝信息回给模型,由模型带 require_escalated 显式申请。

平台实现各有取舍:macOS 用 /usr/bin/sandbox-exec 加载动态生成的 SBPL profile;Linux 用 bubblewrap 以 --ro-bind / / 打底、--bind 叠加可写根,再用 seccomp 拦网络系统调用与 io_uring;Windows 用受限令牌或提权沙箱,强制不了就 fail closed。再叠加 execpolicy 与 network proxy,构成规则、审批、OS 沙箱、网络四层防御。

上下文:只追加的 Prompt、压缩与落盘

每次采样都是一次完整的 Responses API 调用:instructions(codex-prompts 按模型族挑选)、tools(built_tools() 依配置、MCP、技能生成)、input(权限说明 → 开发者指令 → AGENTS.md → 环境上下文 → 历史 → 本次用户消息)。AGENTS.md 在 core/src/agents_md.rs 里发现:从 cwd 向上找到 project_root_markers(默认 .git)定根,再从根到 cwd 逐层收集、按根到叶拼接,最后合并 ~/.codex/AGENTS.md,注入为一条 user 消息。

这个顺序是为缓存服务的:Responses API 按前缀精确匹配缓存,Codex 因此让 Prompt 只追加不改写,静态部分在前、历史在后;中途切换沙箱或模型也以追加消息表达。请求默认无状态,每次发送完整历史,推理内容以 encrypted_content 加密回传,兼容零数据保留(ZDR)组织。

历史由 ContextManager 维护原生 ResponseItem,发送前经 normalize.rs 归一化。token 超过 model_auto_compact_token_limit 且仍需继续时触发自动压缩:本地用压缩提示词让模型总结,远程由 Responses API 返回不透明的 Compaction item;再用 SUMMARY_PREFIX 加摘要与最近用户消息(上限约 2 万 token)替换历史,重插初始上下文,同一个 Turn 接着跑。会话以 rollout JSONL 落在 ~/.codex/sessions/ 下,codex resume 重放恢复。

三种读法:详解、动画与演示文稿

同一套机制有三种呈现方式,按由静到动的顺序看更顺。

架构详解:逐章拆开 Agent Loop、Prompt 组装与缓存、SQ / EQ 协议、工具系统、审批与沙箱、压缩与 codex-rs 代码地图,适合对照本文细读:OpenAI Codex 原理与代码架构详解。

原理动画:以“在 TUI 里修复失败的单元测试”为例,21 步走完 5 次采样、1 次沙箱拒绝、1 次提权审批和 1 次上下文压缩,右侧同步显示 needs_follow_up、当前工具、执行环境与 token 占用。可单步、播放或点时间轴跳转:

在新窗口打开完整页面 →

演示文稿:17 页幻灯片,按“产品形态 → Agent Loop → 分层 → 实体 → Prompt → 协议 → 工具 → 安全模型 → 平台沙箱 → Orchestrator → 上下文压缩 → 完整时序 → 代码地图 → 设计取舍”组织,适合分享或快速过一遍全貌。方向键翻页,O 键看总览,F 键全屏:

在新窗口打开完整页面 →

参考与延伸

本系列的其它两篇与总入口: