M11 · SOURCE-GROUNDED TUTORIAL
OpenAI Codex从源码学会它怎么工作 安全、可恢复、可观测和多 Agent 控制面最均衡的企业级基座之一。 我们不把 README 当结论,而是沿主循环、工具、上下文、权限、扩展、协作和状态一路读到实现。
Rust · Secure Multi-Agent Runtime Apache-2.0 3418498f0142 27 个结论 · 63 处引用
这门课怎么读
先建立直觉,再沿一条任务链钻进代码 参考教程的做法不是把 API 名称罗列出来,而是从一个小白能理解的问题开始,先解释“为什么需要这个机制”,再用概念对比、执行链路和固定提交的源码回答“它究竟怎么做”。本页把 OpenAI Codex 的 27 个源码结论重新编排成十节课;每个结论都保留证据等级、文件路径、行号和可点击源码。
你会得到 一张可复述的架构地图 一次完整任务的链路追踪 能迁移到自研 Harness 的设计判断
你不会得到 把 README 功能当成已验证事实 把 prompt 约束说成 OS 沙箱 把一次双模型调用夸成多 Agent 平台
M00 · MAP
先看全景:这个 Agent 的控制面在哪里 下面的图不是产品宣传图,而是把固定提交里最关键的入口、循环、模型、工具、安全、状态和协作节点放在一张地图上。
核心机制 turn 内多 step;不可漂移 snapshot;流式工具 future
上下文 COW 历史;配对/多模态成本;本地压缩自救
适用建设 企业内研发平台、安全执行、复杂长任务、多 Agent
M00.5 · TRACE
跟踪一个任务:从输入到交付 把下面九步当成你读源码时的“地图坐标”。每到一个节点,都能回到后面的章节查具体实现。
01 提交 turn 与环境 任务进入
↓
02 注入 AGENTS/skills/plugins 把结果交给下一层
↓
03 捕获不可漂移 step snapshot 把结果交给下一层
↓
04 SSE/WebSocket 流式采样 把结果交给下一层
↓
05 工具 future 边流边启动 把结果交给下一层
↓
06 审批与 OS 沙箱变换 把结果交给下一层
↓
07 读并行/写排他执行 把结果交给下一层
↓
08 写 rollout 与 metrics 把结果交给下一层
↓
09 follow-up/steer/compact 或结束 交付/续跑
读图提醒 箭头只表示控制面之间的关系,不代表每个实现都同步、串行或拥有 OS 隔离;真正的边界要以对应章节的源码摘录和 caveat 为准。
M01 · ORIENTATION
先把 Agent 看成一台会交付的机器 如果只看 README,你知道它能做什么;钻进源码后,我们要知道它为什么能做、什么时候会停、失败后谁负责收拾。
先用一个生活比喻 把 Agent 想成一间带传送带的工作室:入口收任务,主循环决定下一步,模型负责提出动作,工具负责动手,状态账本负责让下一班人接着干。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
读源码时先问 本课的判断方式 这一层有没有独立证据? 没有就不把其他层的能力冒充成默认行为;回到固定提交继续追调用链。 谁拥有最终控制权? 区分模型输出、框架规则、用户审批和 OS 隔离四种不同力量。
这一章先建立通用概念 固定提交账本没有把该维度单独拆出,但它会在其他章节的源码路径中体现。先沿执行链路阅读,再回到报告页核对证据。
小练习 1 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M02 · LOOP
主循环:模型为什么会继续动 一次模型调用为什么会变成十几步?循环靠什么继续,靠什么停止?
先用一个生活比喻 像一个会看回执的快递员:模型先写行动单,工具返回回执,主循环把回执放回桌面,再让模型决定下一张行动单。
这套实现先回答了什么? 一轮任务可以问模型很多次,但每一次“想一想并行动”的小步都先拍一张现场快照,避免工具清单和提示词在同一步里前后不一致。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 每个 turn 由多个 step 组成,step 内共享一次不可漂移的上下文快照 codex-rs/core/src/session/turn.rs:153一轮任务可以问模型很多次,但每一次“想一想并行动”的小步都先拍一张现场快照,避免工具清单和提示词在同一步里前后不一致。 流式采样与工具 Future 同时推进,并可被新消息抢占 codex-rs/core/src/session/turn.rs:2034模型说到一个完整动作就能开工,不必等整段回答结束;用户中途补充信息时,系统也能在安全位置收住并接新指令。 重试预算属于 turn-scoped client session,窗口超限不当作普通网络错误重试 codex-rs/core/src/session/turn.rs:1176同一轮尽量复用连接和粘性状态;行李箱塞不下不会盲目重拨网络,而是交给压缩逻辑处理。
01 L1 · fact · codex-loop-001
每个 turn 由多个 step 组成,step 内共享一次不可漂移的上下文快照
先看源码事实 run_turn 在采样前处理压缩、skills/plugins、session/input hooks 和待处理输入;随后为一个 step 捕获 StepContext,并用同一视图构造上下文、工具与模型请求。
翻译成白话 一轮任务可以问模型很多次,但每一次“想一想并行动”的小步都先拍一张现场快照,避免工具清单和提示词在同一步里前后不一致。
为什么这对自研重要 配置热更新只能在安全边界生效,换来 prompt cache 和工具调用的确定性。
固定提交源码摘录 复制
153 pub(crate) async fn run_turn(
154 sess: Arc<Session>,
155 turn_context: Arc<TurnContext>,
156 turn_extension_data: Arc<codex_extension_api::ExtensionData>,
157 input: Vec<TurnInput>,
158 prewarmed_client_session: Option<ModelClientSession>,
159 cancellation_token: CancellationToken,
160 ) -> CodexResult<Option<String>> {
161 let mut client_session =
162 prewarmed_client_session.unwrap_or_else(|| sess.services.model_client.new_session());
163 // TODO(ccunningham): Pre-turn compaction runs before context updates and the
164 // new user message are recorded. Estimate pending incoming items (context
165 // diffs/full reinjection + user input) and trigger compaction preemptively
166 // when they would push the thread over the compaction threshold.
167 if let Err(err) = run_pre_sampling_compact(
168 &sess,
169 &turn_context,
170 &mut client_session,
171 &cancellation_token,
172 )
173 .await
174 {
175 if matches!(err.details(), CodexErrorDetails::TurnAborted) {
176 run_hooks_and_record_inputs(&sess, &turn_context, &input).await;
177 return Err(err);
… 86 lines omitted; exact range 153–274 …
264 if run_hooks_and_record_inputs(&sess, &turn_context, &pending_input).await {
265 break;
266 }
267
268 let window_id = sess.current_window_id().await;
269 super::rollout_budget::maybe_record_reminder(
270 sess.as_ref(),
271 turn_context.as_ref(),
272 &window_id,
273 )
274 .await;
为什么相信这条结论?查看 2 处证据
02 L1 · fact · codex-loop-002
流式采样与工具 Future 同时推进,并可被新消息抢占
先看源码事实 try_run_sampling_request 消费流式 ResponseEvent;完整 tool item 到达后进入 FuturesOrdered,工具参数 diff 可边流边展示;若 commentary/reasoning 期间 mailbox 有消息,则返回 needs_follow_up。
翻译成白话 模型说到一个完整动作就能开工,不必等整段回答结束;用户中途补充信息时,系统也能在安全位置收住并接新指令。
为什么这对自研重要 响应更快,但对调用顺序、取消、配对和幂等要求很高。
固定提交源码摘录 复制
2034 tool_runtime: ToolCallRuntime,
2035 sess: Arc<Session>,
2036 turn_context: Arc<TurnContext>,
2037 turn_store: Arc<codex_extension_api::ExtensionData>,
2038 client_session: &mut ModelClientSession,
2039 responses_metadata: &CodexResponsesMetadata,
2040 turn_diff_tracker: SharedTurnDiffTracker,
2041 prompt: &Prompt,
2042 cancellation_token: CancellationToken,
2043 ) -> CodexResult<SamplingRequestResult> {
2044 feedback_tags!(
2045 model = turn_context.model_info.slug.clone(),
2046 approval_policy = turn_context.approval_policy.value(),
2047 sandbox_policy = &turn_context.sandbox_policy(),
2048 effort = turn_context.reasoning_effort,
2049 auth_mode = sess.services.auth_manager.auth_mode(),
2050 features = sess.features.enabled_features(),
2051 );
2052 let inference_trace = sess.services.rollout_thread_trace.inference_trace_context(
2053 turn_context.sub_id.as_str(),
2054 turn_context.model_info.slug.as_str(),
2055 turn_context.provider.info().name.as_str(),
2056 );
2057 let sampling_timing_guard = turn_context.turn_timing_state.begin_sampling();
2058 let uses_sequential_cutoff_reasoning_summaries = turn_context
… 99 lines omitted; exact range 2034–2168 …
2158 let item_id = previous.id();
2159 flush_assistant_text_segments_for_item(
2160 &sess,
2161 &turn_context,
2162 plan_mode_state.as_mut(),
2163 &mut assistant_message_stream_parsers,
2164 &item_id,
2165 )
2166 .await;
2167 }
2168 if let Some(state) = plan_mode_state.as_mut()
为什么相信这条结论?查看 2 处证据
03 L1 · fact · codex-loop-003
重试预算属于 turn-scoped client session,窗口超限不当作普通网络错误重试
先看源码事实 run_sampling_request 在一个 turn 内复用 ModelClientSession;上下文超限和 usage limit 直接上抛,其他可重试错误才按 Provider 的 stream retry 上限重试。
翻译成白话 同一轮尽量复用连接和粘性状态;行李箱塞不下不会盲目重拨网络,而是交给压缩逻辑处理。
为什么这对自研重要 把语义恢复与传输恢复分开,避免无效重试放大费用。
固定提交源码摘录 复制
1176 async fn run_sampling_request(
1177 sess: Arc<Session>,
1178 step_context: Arc<StepContext>,
1179 turn_store: Arc<codex_extension_api::ExtensionData>,
1180 turn_diff_tracker: SharedTurnDiffTracker,
1181 client_session: &mut ModelClientSession,
1182 responses_metadata: &CodexResponsesMetadata,
1183 input: Vec<ResponseItem>,
1184 cancellation_token: CancellationToken,
1185 ) -> CodexResult<(SamplingRequestResult, Vec<ResponseItem>)> {
1186 let turn_context = Arc::clone(&step_context.turn);
1187 let router = Arc::clone(&step_context.tool_router);
1188
1189 let base_instructions = sess.get_base_instructions().await;
1190
1191 let tool_runtime = ToolCallRuntime::new(
1192 Arc::clone(&router),
1193 Arc::clone(&sess),
1194 Arc::clone(&step_context),
1195 Arc::clone(&turn_diff_tracker),
1196 );
1197 let _code_mode_worker = sess.services.code_mode_service.start_turn_worker(
1198 &sess,
1199 Arc::clone(&step_context),
1200 Arc::clone(&router),
… 62 lines omitted; exact range 1176–1273 …
1263 max_retries,
1264 err,
1265 client_session,
1266 &sess,
1267 &turn_context,
1268 ResponsesStreamRequest::Sampling,
1269 )
1270 .await?;
1271 turn_context.turn_timing_state.record_sampling_retry();
1272 }
1273 }
为什么相信这条结论?查看 2 处证据
小练习 2 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M03 · MODEL
模型调用:流式输出如何变成可执行步骤 模型输出的文字、思考、工具调用和错误,经过哪些转换才进入 Agent 状态?
先用一个生活比喻 模型像电话另一端的同事:你听到的不是一整段录音,而是一串实时片段;Harness 要边听边拼装,还要能在电话断线时留下可恢复的记录。
这套实现先回答了什么? 它允许换“接线地址和门禁方式”,但要求对方都说 Responses 这门语言;不是任意 Chat Completions 方言翻译器。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 模型协议只保留 Responses API,但 Provider 端点与认证可扩展 codex-rs/model-provider-info/src/lib.rs:54它允许换“接线地址和门禁方式”,但要求对方都说 Responses 这门语言;不是任意 Chat Completions 方言翻译器。 传输层同时支持 SSE 与可复用 WebSocket,并带 turn 粘性状态 codex-rs/core/src/client.rs:1每轮对话尽量占用一条可复用的高速通道,还带着本轮路由票据;热身失败不会把整轮任务判死。
04 L1 · fact · codex-provider-001
模型协议只保留 Responses API,但 Provider 端点与认证可扩展
先看源码事实 WireApi 枚举只有 Responses,反序列化 chat 会报迁移错误;ModelProviderInfo 允许自定义 base URL、环境密钥、命令认证、AWS SigV4、headers、query 和重试。
翻译成白话 它允许换“接线地址和门禁方式”,但要求对方都说 Responses 这门语言;不是任意 Chat Completions 方言翻译器。
为什么这对自研重要 兼容面更一致,第三方 Provider 必须实现 Responses 语义而非只暴露 chat/completions。
固定提交源码摘录 复制
54 /// Wire protocol that the provider speaks.
55 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, JsonSchema)]
56 #[serde(rename_all = "lowercase")]
57 pub enum WireApi {
58 /// The Responses API exposed by OpenAI at `/v1/responses`.
59 #[default]
60 Responses,
61 }
62
63 impl fmt::Display for WireApi {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 let value = match self {
66 Self::Responses => "responses",
67 };
68 f.write_str(value)
69 }
70 }
71
72 impl<'de> Deserialize<'de> for WireApi {
73 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
74 where
75 D: serde::Deserializer<'de>,
76 {
77 let value = String::deserialize(deserializer)?;
78 match value.as_str() {
79 "responses" => Ok(Self::Responses),
80 "chat" => Err(serde::de::Error::custom(CHAT_WIRE_API_REMOVED_ERROR)),
81 _ => Err(serde::de::Error::unknown_variant(&value, &["responses"])),
82 }
83 }
84 }
为什么相信这条结论?查看 2 处证据
05 L1 · fact · codex-provider-002
传输层同时支持 SSE 与可复用 WebSocket,并带 turn 粘性状态
先看源码事实 ModelClientSession 按 turn 创建,延迟缓存 Responses WebSocket 和 x-codex-turn-state;预热是 generate=false 的 v2 response.create,失败后由常规重试/回退处理。
翻译成白话 每轮对话尽量占用一条可复用的高速通道,还带着本轮路由票据;热身失败不会把整轮任务判死。
为什么这对自研重要 交互延迟低,但 Provider 必须准确声明 WebSocket 能力,AWS 认证当前明确不能与 WebSocket 同开。
固定提交源码摘录 复制
1 //! Session- and turn-scoped helpers for talking to model provider APIs.
2 //!
3 //! `ModelClient` is intended to live for the lifetime of a Codex session and holds the stable
4 //! configuration and state needed to talk to a provider (auth, provider selection, conversation id,
5 //! and transport fallback state).
6 //!
7 //! Per-turn settings (model selection, reasoning controls, telemetry context, and turn metadata)
8 //! are passed explicitly to streaming and unary methods so that the turn lifetime is visible at the
9 //! call site.
10 //!
11 //! A [`ModelClientSession`] is created per turn and is used to stream one or more Responses API
12 //! requests during that turn. It caches a Responses WebSocket connection (opened lazily) and stores
13 //! per-turn state such as the `x-codex-turn-state` token used for sticky routing.
14 //!
15 //! WebSocket prewarm is a v2-only `response.create` with `generate=false`; it waits for completion
16 //! so the next request can reuse the same connection and `previous_response_id`.
17 //!
18 //! Turn execution performs prewarm as a best-effort step before the first stream request so the
19 //! subsequent request can reuse the same connection.
20 //!
21 //! ## Retry-Budget Tradeoff
22 //!
23 //! WebSocket prewarm is treated as the first websocket connection attempt for a turn. If it
24 //! fails, normal stream retry/fallback logic handles recovery on the same turn.
为什么相信这条结论?查看 2 处证据
小练习 3 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M05 · CONTEXT
上下文:有限窗口怎样装下长任务 当对话、工具输出、计划和记忆越来越多,系统怎样决定留下什么、折叠什么、放到哪里?
先用一个生活比喻 上下文不是聊天记录,而是一张会整理的工作台:常用零件放桌面,旧材料装进档案盒,必要时只留下索引卡。
这套实现先回答了什么? 像有版本号的账本:读取者共享同一份快照,真要改时才复制,且每笔工具调用都要能对上回执。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 历史是带版本号的 Copy-on-Write 账本,不是随手拼接的消息数组 codex-rs/core/src/context_manager/history.rs:38像有版本号的账本:读取者共享同一份快照,真要改时才复制,且每笔工具调用都要能对上回执。 上下文裁剪同时维护工具调用配对并计算多模态成本 codex-rs/core/src/context_manager/history.rs:189剪历史不能只撕掉一页:工具问题和答案必须一起处理;图片、音频和加密内容也不能当作零体积。 本地压缩会自救:压缩请求自身超限时逐项删旧记录再重试 codex-rs/core/src/compact.rs:240连“请帮我整理行李”这句话都塞不进去时,它会先扔掉最旧且成对的票据,直到能完成整理。
09 L1 · fact · codex-context-001
历史是带版本号的 Copy-on-Write 账本,不是随手拼接的消息数组
先看源码事实 ContextManager 用 Arc<Vec<ResponseItem>> 保存历史并维护 history_version、token info、reference context 和 world-state baseline;写入前过滤非 API item、截断并在取 prompt 时规范化。
翻译成白话 像有版本号的账本:读取者共享同一份快照,真要改时才复制,且每笔工具调用都要能对上回执。
为什么这对自研重要 适合并发读取和回滚,也让 compaction、fork 与增量世界状态有明确一致性边界。
固定提交源码摘录 复制
38 /// Transcript of thread history
39 #[derive(Debug, Clone, Default)]
40 pub(crate) struct ContextManager {
41 /// The oldest items are at the beginning of the vector. Snapshots share the vector until a
42 /// caller needs to mutate it, avoiding deep copies for read-only history consumers.
43 items: Arc<Vec<ResponseItem>>,
44 /// Bumped whenever history is rewritten, such as compaction or rollback.
45 history_version: u64,
46 token_info: Option<TokenUsageInfo>,
47 /// Reference context snapshot used for diffing and producing model-visible
48 /// settings update items.
49 ///
50 /// This is the baseline for the next regular model turn, and may already
51 /// match the current turn after context updates are persisted.
52 ///
53 /// When this is `None`, settings diffing treats the next turn as having no
54 /// baseline and emits a full reinjection of context state. Rollback may
55 /// also clear this when it trims a mixed initial-context developer bundle
56 /// whose non-diff fragments no longer exist in the surviving history.
57 reference_context_item: Option<TurnContextItem>,
58 /// World state most recently appended to model-visible history.
59 world_state_baseline: Option<WorldStateSnapshot>,
60 }
为什么相信这条结论?查看 2 处证据
10 L1 · fact · codex-context-002
上下文裁剪同时维护工具调用配对并计算多模态成本
先看源码事实 remove_oldest_item 会同步移除对应 call/output 并清 world baseline;normalize 修复配对并过滤不支持的图像/音频,token 估算覆盖 reasoning、compaction、图像、音频和 encrypted content。
翻译成白话 剪历史不能只撕掉一页:工具问题和答案必须一起处理;图片、音频和加密内容也不能当作零体积。
为什么这对自研重要 上下文预算比纯文本字符计数可靠,降低 Provider 因协议不完整而拒绝请求的概率。
固定提交源码摘录 复制
189 pub(crate) fn remove_first_item(&mut self) {
190 if !self.items.is_empty() {
191 // Remove the oldest item (front of the list). Items are ordered from
192 // oldest → newest, so index 0 is the first entry recorded.
193 let items = Arc::make_mut(&mut self.items);
194 let removed = items.remove(0);
195 // If the removed item participates in a call/output pair, also remove
196 // its corresponding counterpart to keep the invariants intact without
197 // running a full normalization pass.
198 normalize::remove_corresponding_for(items, &removed);
199 self.world_state_baseline = None;
200 }
201 }
202
203 pub(crate) fn replace(&mut self, items: Vec<ResponseItem>) {
204 self.items = Arc::new(items);
205 self.history_version = self.history_version.saturating_add(1);
206 self.world_state_baseline = None;
207 }
208
209 /// Drop the last `num_turns` instruction turns from this history.
210 ///
211 /// Instruction turns are history messages that should behave like a new prompt boundary:
212 /// ordinary user messages and structured assistant inter-agent instructions.
213 ///
… 24 lines omitted; exact range 189–248 …
238 let mut cut_idx = if n_from_end >= user_positions.len() {
239 first_instruction_turn_idx
240 } else {
241 user_positions[user_positions.len() - n_from_end]
242 };
243
244 cut_idx =
245 self.trim_pre_turn_context_updates(&snapshot, first_instruction_turn_idx, cut_idx);
246
247 self.replace(snapshot[..cut_idx].to_vec());
248 }
为什么相信这条结论?查看 3 处证据
11 L1 · fact · codex-context-003
本地压缩会自救:压缩请求自身超限时逐项删旧记录再重试
先看源码事实 compact 使用模型生成摘要;若压缩调用本身超出窗口,就删除最老 item 及其配对继续尝试,成功后保留真实 user 消息、插入摘要前缀、推进窗口并重算 usage。
翻译成白话 连“请帮我整理行李”这句话都塞不进去时,它会先扔掉最旧且成对的票据,直到能完成整理。
为什么这对自研重要 长会话更耐用;源码也明确提示多次摘要会逐步降低准确性。
固定提交源码摘录 复制
240 async fn run_compact_task_inner_impl(
241 sess: Arc<Session>,
242 turn_context: Arc<TurnContext>,
243 input: Vec<UserInput>,
244 initial_context_injection: InitialContextInjection,
245 compaction_metadata: CompactionTurnMetadata,
246 ) -> CodexResult<String> {
247 let compaction_item = TurnItem::ContextCompaction(ContextCompactionItem::new());
248 sess.emit_turn_item_started(&turn_context, &compaction_item)
249 .await;
250 let initial_input_for_turn: ResponseInputItem = ResponseInputItem::from(input);
251
252 let mut history = sess.clone_history().await;
253 history.record_items(
254 &[initial_input_for_turn.into()],
255 turn_context.model_info.truncation_policy.into(),
256 );
257
258 let max_retries = turn_context.provider.info().stream_max_retries();
259 let mut retries = 0;
260 let mut client_session = sess.services.model_client.new_session();
261 // Reuse one client session so turn-scoped state (sticky routing, websocket incremental
262 // request tracking)
263 // survives retries within this compact turn.
264 let window_id = sess.current_window_id().await;
… 43 lines omitted; exact range 240–318 …
308 }
309 Err(e) if matches!(e.details(), CodexErrorDetails::ContextWindowExceeded) => {
310 if turn_input_len > 1 {
311 // Trim from the beginning to preserve cache (prefix-based) and keep recent messages intact.
312 error!(
313 "Context window exceeded while compacting; removing oldest history item. Error: {e}"
314 );
315 history.remove_first_item();
316 retries = 0;
317 continue;
318 }
为什么相信这条结论?查看 2 处证据
小练习 5 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M06 · SECURITY
权限与沙箱:能做什么,在哪里做 审批按钮、规则引擎、容器和操作系统沙箱分别解决什么问题?为什么“问过用户”不等于“隔离了风险”?
先用一个生活比喻 审批像门卫问你有没有预约,沙箱像把访客关在指定房间;前者决定是否放行,后者限制放行后能摸到什么。
这套实现先回答了什么? 一条轴决定要不要敲门,另一条轴决定进门后活动范围;“不用问”不等于“拥有整台机器”。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 审批策略把“何时问”与“允许做什么”分成两条轴 codex-rs/protocol/src/protocol.rs:890一条轴决定要不要敲门,另一条轴决定进门后活动范围;“不用问”不等于“拥有整台机器”。 审批缺失默认中止,且可授予一次、本会话或规则/网络修订 codex-rs/core/src/session/mod.rs:2295授权不是一个“永远允许”按钮;可以只放这次、放本会话、或把精确规则写进政策,没人回答则停下。 沙箱按平台变换真实进程:macOS Seatbelt、Linux seccomp/bwrap/landlock、Windows restricted token codex-rs/sandboxing/src/manager.rs:34不是在提示词里说“请别乱动”,而是在启动进程前给命令套上操作系统能执行的限制器。 自定义 permission profile 默认从受限文件系统和受限网络开始 codex-rs/core/src/config/permissions.rs:203自定义政策从“什么都别给”开始逐项开门,而不是先全开再查漏补缺。
12 L2 · fact · codex-permission-001
审批策略把“何时问”与“允许做什么”分成两条轴
先看源码事实 AskForApproval 有 UnlessTrusted、OnRequest、Granular 和 Never;SandboxPolicy 独立描述 DangerFullAccess、ReadOnly、ExternalSandbox 与 WorkspaceWrite。
翻译成白话 一条轴决定要不要敲门,另一条轴决定进门后活动范围;“不用问”不等于“拥有整台机器”。
为什么这对自研重要 企业策略能单独收紧提权频率与文件/网络能力。
固定提交源码摘录 复制
890 /// Determines the conditions under which the user is consulted to approve
891 /// running the command proposed by Codex.
892 #[derive(
893 Debug,
894 Clone,
895 Copy,
896 Default,
897 PartialEq,
898 Eq,
899 Hash,
900 Serialize,
901 Deserialize,
902 Display,
903 JsonSchema,
904 TS,
905 )]
906 #[serde(rename_all = "kebab-case")]
907 #[strum(serialize_all = "kebab-case")]
908 pub enum AskForApproval {
909 /// Under this policy, only "known safe" commands—as determined by
910 /// `is_safe_command()`—that **only read files** are auto‑approved.
911 /// Everything else will ask the user to approve.
912 #[serde(rename = "untrusted")]
913 #[strum(serialize = "untrusted")]
914 UnlessTrusted,
… 7 lines omitted; exact range 890–932 …
922 ///
923 /// When a field is `true`, commands in that category are allowed. When it
924 /// is `false`, those requests are automatically rejected instead of shown
925 /// to the user.
926 #[strum(serialize = "granular")]
927 Granular(GranularApprovalConfig),
928
929 /// Never ask the user to approve commands. Failures are immediately returned
930 /// to the model, and never escalated to the user for approval.
931 Never,
932 }
为什么相信这条结论?查看 2 处证据
13 L1 · fact · codex-permission-002
审批缺失默认中止,且可授予一次、本会话或规则/网络修订
先看源码事实 命令审批包含 command、cwd、reason、network amendment、execpolicy amendment、additional permissions 和 plugin provenance;等待通道消失时默认 Abort。ReviewDecision 支持单次、session、execpolicy/network amendment、拒绝、超时和 abort。
翻译成白话 授权不是一个“永远允许”按钮;可以只放这次、放本会话、或把精确规则写进政策,没人回答则停下。
为什么这对自研重要 失败关闭且授权可结构化沉淀,适合审计。
固定提交源码摘录 复制
2295 pub async fn request_command_approval(
2296 &self,
2297 turn_context: &TurnContext,
2298 call_id: String,
2299 approval_id: Option<String>,
2300 environment_id: Option<String>,
2301 command: Vec<String>,
2302 cwd: AbsolutePathBuf,
2303 reason: Option<String>,
2304 network_approval_context: Option<NetworkApprovalContext>,
2305 proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
2306 additional_permissions: Option<AdditionalPermissionProfile>,
2307 available_decisions: Option<Vec<ReviewDecision>>,
2308 plugin_attribution_override: Option<PluginCommandAttribution>,
2309 ) -> ReviewDecision {
2310 let _elicitation = self.services.elicitations.register();
2311 // command-level approvals use `call_id`.
2312 // `approval_id` is only present for subcommand callbacks (execve intercept)
2313 let effective_approval_id = approval_id.clone().unwrap_or_else(|| call_id.clone());
2314 // Add the tx_approve callback to the map before sending the request.
2315 let (tx_approve, rx_approve) = oneshot::channel();
2316 let prev_entry = {
2317 let mut active = self.active_turn.lock().await;
2318 match active.as_mut() {
2319 Some(at) => {
… 46 lines omitted; exact range 2295–2376 …
2366 cwd,
2367 reason,
2368 network_approval_context,
2369 proposed_execpolicy_amendment,
2370 proposed_network_policy_amendments,
2371 additional_permissions,
2372 available_decisions: Some(available_decisions),
2373 parsed_cmd,
2374 });
2375 self.send_event(turn_context, event).await;
2376 rx_approve.await.unwrap_or(ReviewDecision::Abort)
为什么相信这条结论?查看 2 处证据
14 L1 · fact · codex-sandbox-001
沙箱按平台变换真实进程:macOS Seatbelt、Linux seccomp/bwrap/landlock、Windows restricted token
先看源码事实 SandboxManager 将抽象策略映射到平台后端;Auto 只在策略要求时选沙箱,Require/Forbid 明确覆盖选择,随后把命令变换为对应 launcher。
翻译成白话 不是在提示词里说“请别乱动”,而是在启动进程前给命令套上操作系统能执行的限制器。
为什么这对自研重要 强度取决于平台与后端可用性;报告不把 unsupported 平台推断成同等隔离。
固定提交源码摘录 复制
34 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
35 pub enum SandboxType {
36 None,
37 MacosSeatbelt,
38 LinuxSeccomp,
39 WindowsRestrictedToken,
40 }
41
42 impl SandboxType {
43 pub fn as_metric_tag(self) -> &'static str {
44 match self {
45 SandboxType::None => "none",
46 SandboxType::MacosSeatbelt => "seatbelt",
47 SandboxType::LinuxSeccomp => "seccomp",
48 SandboxType::WindowsRestrictedToken => "windows_sandbox",
49 }
50 }
51 }
52
53 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
54 pub enum SandboxablePreference {
55 Auto,
56 Require,
57 Forbid,
58 }
… 4 lines omitted; exact range 34–73 …
63 } else if cfg!(target_os = "linux") {
64 Some(SandboxType::LinuxSeccomp)
65 } else if cfg!(target_os = "windows") {
66 if windows_sandbox_enabled {
67 Some(SandboxType::WindowsRestrictedToken)
68 } else {
69 None
70 }
71 } else {
72 None
73 }
为什么相信这条结论?查看 2 处证据
15 L1 · fact · codex-sandbox-002
自定义 permission profile 默认从受限文件系统和受限网络开始
先看源码事实 custom profile 编译器先建立 restricted filesystem 与 restricted network,再应用条目;网络只有显式 true 才启用,WorkspaceWrite 默认也关闭网络,并保护 .git hooks 等元数据子路径。
翻译成白话 自定义政策从“什么都别给”开始逐项开门,而不是先全开再查漏补缺。
为什么这对自研重要 默认拒绝更适合作为组织级安全基线。
固定提交源码摘录 复制
203 fn extensible_builtin_parent_profile(profile_name: &str) -> Option<PermissionProfileToml> {
204 let file_system = match profile_name {
205 BUILT_IN_READ_ONLY_PROFILE => FileSystemSandboxPolicy::read_only(),
206 BUILT_IN_WORKSPACE_PROFILE => FileSystemSandboxPolicy::workspace_write(
207 &[],
208 /*exclude_tmpdir_env_var*/ false,
209 /*exclude_slash_tmp*/ false,
210 ),
211 _ => return None,
212 };
213 Some(permission_profile_toml_from_file_system_policy(file_system))
为什么相信这条结论?查看 3 处证据
小练习 6 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M07 · ECOSYSTEM
指令、MCP、Skills 与插件:能力如何接进来 一条系统指令、一个 Skill、一个 MCP server 和一个插件,分别在什么时候进入上下文和执行路径?
先用一个生活比喻 这像给工作室接设备:说明书不是设备,设备也不等于电源;成熟 Harness 会分别治理发现、信任、加载、调用和卸载。
这套实现先回答了什么? 它不是每轮临时扫一遍外接工具,而是维护一套有版本、有健康状态、有认证身份的连接池。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 MCP 是带复用、认证、required gate 和 catalog revision 的运行时 codex-rs/codex-mcp/src/connection_manager.rs:66它不是每轮临时扫一遍外接工具,而是维护一套有版本、有健康状态、有认证身份的连接池。 模型只能看到显式可见且能绑定到同一目录版本的 MCP 工具 codex-rs/codex-mcp/src/connection_manager/tool_catalog.rs:34展示给模型的工具名和真正执行它的连接必须来自同一版目录,不能拿新版菜单去点旧版厨房。 AGENTS.md 从项目根向 cwd 分层合并,局部 override 优先且有总字节预算 codex-rs/core/src/agents_md.rs:1公司规章先读,走进子目录后再叠加本地规章;同一层的 override 像贴在门上的最新通知。 Skills、plugins 和 extensions 都在初始上下文构建期受预算与来源控制 codex-rs/core/src/session/mod.rs:3336扩展不是把所有说明一股脑塞进提示词,而是先做预算,再按可信来源和消息层级装配。
16 L1 · fact · codex-mcp-001
MCP 是带复用、认证、required gate 和 catalog revision 的运行时
先看源码事实 McpConnectionSet 同时管理 server、required servers、工具目录版本、Codex Apps cache、plugin provenance、工具命名前缀和 elicitation;连接配置/OAuth 凭据一致且存活时才复用。
翻译成白话 它不是每轮临时扫一遍外接工具,而是维护一套有版本、有健康状态、有认证身份的连接池。
为什么这对自研重要 工具清单可稳定捕获到 step snapshot,连接变化不会悄悄污染正在执行的一步。
固定提交源码摘录 复制
66 pub(crate) struct McpServerConnection {
67 identity: Option<McpServerConnectionIdentity>,
68 client: AsyncManagedClient,
69 }
70
71 impl McpServerConnection {
72 async fn reusable_client(
73 &self,
74 desired: &McpServerConnectionIdentity,
75 ) -> Option<ManagedClient> {
76 let current = self.identity.as_ref()?;
77 if !current.has_same_connection_config(desired) {
78 return None;
79 }
80 if !self.client.startup_complete.load(Ordering::Acquire) {
81 return None;
82 }
83 let client = self.client.client().await.ok()?;
84 if client.client.is_closed().await {
85 return None;
86 }
87 let Ok(desired_credentials) = desired.oauth_credentials() else {
88 return Some(client);
89 };
90 let reusable = match client.client.managed_oauth_credentials().await {
… 16 lines omitted; exact range 66–117 …
107 fn cancel_startup(&self) {
108 if !self.client.startup_complete.load(Ordering::Acquire) {
109 self.client.cancel_token.cancel();
110 }
111 }
112 }
113
114 impl Drop for McpServerConnection {
115 fn drop(&mut self) {
116 self.client.cancel_token.cancel();
117 }
为什么相信这条结论?查看 2 处证据
17 L1 · fact · codex-mcp-002
模型只能看到显式可见且能绑定到同一目录版本的 MCP 工具
先看源码事实 tool_catalog 过滤 UI visibility,不含 metadata 时默认可见;capture_binding 在读锁下捕获 revision、ready client、过滤后的 tools 和 prepared calls,无法精确绑定的工具被省略。
翻译成白话 展示给模型的工具名和真正执行它的连接必须来自同一版目录,不能拿新版菜单去点旧版厨房。
为什么这对自研重要 减少热刷新导致的工具错配和 TOCTOU 风险。
固定提交源码摘录 复制
34 /// Returns whether a tool may be included in model-facing tool declarations.
35 ///
36 /// Tools without visibility metadata remain visible. Tools with visibility
37 /// metadata are hidden unless they explicitly include `model`.
38 ///
39 /// <https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx#resource-discovery>
40 pub fn tool_is_model_visible(tool: &ToolInfo) -> bool {
41 let Some(visibility) = tool
42 .tool
43 .meta
44 .as_deref()
45 .and_then(|meta| meta.get(MCP_UI_META_KEY))
46 .and_then(serde_json::Value::as_object)
47 .and_then(|ui| ui.get(MCP_UI_VISIBILITY_META_KEY))
48 .and_then(serde_json::Value::as_array)
49 else {
50 return true;
51 };
52 visibility
53 .iter()
54 .any(|target| target.as_str() == Some(MCP_UI_MODEL_VISIBILITY))
55 }
为什么相信这条结论?查看 2 处证据
18 L1 · fact · codex-instructions-001
AGENTS.md 从项目根向 cwd 分层合并,局部 override 优先且有总字节预算
先看源码事实 发现逻辑以 project root marker 截断上行范围,从根到 cwd 每层最多选 AGENTS.override.md、AGENTS.md 或 fallback 文件之一;按总预算读取,超出部分截断。
翻译成白话 公司规章先读,走进子目录后再叠加本地规章;同一层的 override 像贴在门上的最新通知。
为什么这对自研重要 目录作用域明确,但超预算时越靠后的深层说明更可能被截断,需要可视化告警。
固定提交源码摘录 复制
1 //! AGENTS.md discovery and user instruction assembly.
2 //!
3 //! Project-level documentation is primarily stored in files named `AGENTS.md`.
4 //! Additional fallback filenames can be configured via `project_doc_fallback_filenames`.
5 //! We include the concatenation of all files found along the path from the
6 //! project root to the current working directory as follows:
7 //!
8 //! 1. Determine the project root by walking upwards from the current working
9 //! directory until a configured `project_root_markers` entry is found.
10 //! When `project_root_markers` is unset, the default marker list is used
11 //! (`.git`). If no marker is found, only the current working directory is
12 //! considered. An empty marker list disables parent traversal.
13 //! 2. Collect every `AGENTS.md` found from the project root down to the
14 //! current working directory (inclusive) and concatenate their contents in
15 //! that order.
16 //! 3. We do **not** walk past the project root.
为什么相信这条结论?查看 3 处证据
19 L1 · fact · codex-extension-001
Skills、plugins 和 extensions 都在初始上下文构建期受预算与来源控制
先看源码事实 build_initial_context 按 context window 计算 skill metadata budget,超限发 warning;plugins 先按配置加载,再生成推荐插件上下文;extension contributors 可注入 developer、contextual user 或独立 developer fragment。
翻译成白话 扩展不是把所有说明一股脑塞进提示词,而是先做预算,再按可信来源和消息层级装配。
为什么这对自研重要 可扩展性强且照顾 prompt cache;插件推荐与已安装插件被区分。
固定提交源码摘录 复制
3336 pub(crate) async fn build_initial_context_with_world_state(
3337 &self,
3338 turn_context: &TurnContext,
3339 world_state: &WorldState,
3340 ) -> Vec<ResponseItem> {
3341 let mut developer_sections = Vec::<String>::with_capacity(8);
3342 let mut contextual_user_sections = Vec::<String>::with_capacity(2);
3343 let mut separate_developer_sections = Vec::<String>::new();
3344 let (session_source, auto_compact_window_ids) = {
3345 let state = self.state.lock().await;
3346 (
3347 state.session_configuration.session_source.clone(),
3348 state.auto_compact_window_ids(),
3349 )
3350 };
3351 let separate_guardian_developer_message =
3352 crate::guardian::is_guardian_reviewer_source(&session_source);
3353 // Keep the guardian policy prompt out of the aggregated developer bundle so it
3354 // stays isolated as its own top-level developer message for guardian subagents.
3355 if !separate_guardian_developer_message
3356 && let Some(developer_instructions) = turn_context.developer_instructions.as_deref()
3357 && !developer_instructions.is_empty()
3358 {
3359 developer_sections.push(developer_instructions.to_string());
3360 }
… 26 lines omitted; exact range 3336–3397 …
3387 msg: EventMsg::Warning(WarningEvent {
3388 message: warning_message,
3389 }),
3390 })
3391 .await;
3392 }
3393 if !host_catalog_in_world_state {
3394 developer_sections.push(skills_instructions.render());
3395 }
3396 }
3397 }
为什么相信这条结论?查看 2 处证据
20 L1 · fact · codex-hooks-001
Hooks 覆盖 session、prompt、permission、tool、compact、stop 与 subagent 生命周期
先看源码事实 runtime 为 session/subagent start、PreToolUse、PermissionRequest、PostToolUse、UserPromptSubmit 等构造稳定 payload;hook 可阻断、改写输入或注入额外上下文,完成事件同时进入指标和 analytics。
翻译成白话 钩子既能当门卫,也能当翻译器和旁路记录员;每次执行都有开始、结束和耗时记录。
为什么这对自研重要 组织可插入治理逻辑,但 hook 本身应被当作有权限的代码并受项目信任策略保护。
固定提交源码摘录 复制
103 pub(crate) async fn run_pending_session_start_hooks(
104 sess: &Arc<Session>,
105 turn_context: &Arc<TurnContext>,
106 ) -> bool {
107 while let Some(session_start_source) = sess.take_pending_session_start_source().await {
108 // Pending session-start hooks are reused to dispatch thread-spawn subagent
109 // starts. Other subagent sessions are internal/system work and do not run
110 // start hooks.
111 let target = match &turn_context.session_source {
112 SessionSource::SubAgent(SubAgentSource::ThreadSpawn { agent_role, .. })
113 if matches!(
114 session_start_source,
115 codex_hooks::SessionStartSource::Startup
116 ) =>
117 {
118 let context = subagent_hook_context(sess, agent_role);
119 StartHookTarget::SubagentStart {
120 turn_id: turn_context.sub_id.clone(),
121 agent_id: context.agent_id,
122 agent_type: context.agent_type,
123 }
124 }
125 SessionSource::SubAgent(_) => return false,
126 _ => StartHookTarget::SessionStart {
127 source: session_start_source,
… 82 lines omitted; exact range 103–220 …
210 {
211 PreToolUseHookResult::Blocked(format!(
212 "Command blocked by PreToolUse hook: {reason}. Command: {command}"
213 ))
214 } else {
215 PreToolUseHookResult::Blocked(format!(
216 "Tool call blocked by PreToolUse hook: {reason}. Tool: {}",
217 tool_name.name()
218 ))
219 }
220 }
为什么相信这条结论?查看 3 处证据
小练习 7 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M08 · COLLABORATION
子 Agent:把一个大任务拆成可治理的协作 什么时候是普通工具调用,什么时候才算子 Agent?子 Agent 的上下文、预算、取消和结果怎样回到父 Agent?
先用一个生活比喻 不是把同事叫来聊天就叫协作;真正的协作要有工单、权限、截止时间、交付物和回收机制。
这套实现先回答了什么? 每个子 Agent 都有自己的会话账本,但兄弟们共用一张组织架构表、并发配额和总预算。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 多 Agent 是共享控制面的线程树,不是主循环里的递归函数 codex-rs/core/src/agent/control.rs:70每个子 Agent 都有自己的会话账本,但兄弟们共用一张组织架构表、并发配额和总预算。 fork 可选全历史、最近 N 轮或空白;消息可只入队也可触发 turn codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs:39派工时可把整本案卷、最近几页或一张白纸交给下属;便签可以只塞进邮箱,也可以按门铃让他立即处理。 驻留上限与同时执行上限分离,V2 子 Agent 才受执行 limiter codex-rs/core/src/config/mod.rs:1547可以让许多子会话留在通讯录里,但只有有限几个同时开工;已经在干活的人收到补充便签不会再算一个工位。
21 L1 · fact · codex-agent-001
多 Agent 是共享控制面的线程树,不是主循环里的递归函数
先看源码事实 每棵 root thread 树共享 AgentControl、registry、V2 residency、execution limiter 和 rollout budget;spawn 通过 ThreadManager 创建独立 thread,子线程继承环境与 exec policy。
翻译成白话 每个子 Agent 都有自己的会话账本,但兄弟们共用一张组织架构表、并发配额和总预算。
为什么这对自研重要 隔离了对话状态,同时能做全树限流、恢复和协作观测。
固定提交源码摘录 复制
70 pub(crate) fork_parent_spawn_call_id: Option<String>,
71 pub(crate) fork_mode: Option<SpawnAgentForkMode>,
72 pub(crate) parent_thread_id: Option<ThreadId>,
73 pub(crate) environments: Option<Vec<TurnEnvironmentSelection>>,
74 }
75
76 #[derive(Clone, Debug)]
77 pub(crate) struct LiveAgent {
78 pub(crate) thread_id: ThreadId,
79 pub(crate) metadata: AgentMetadata,
80 pub(crate) status: AgentStatus,
81 }
82
83 #[derive(Clone, Debug, Serialize, PartialEq, Eq)]
84 pub(crate) struct ListedAgent {
85 pub(crate) agent_name: String,
86 pub(crate) agent_status: AgentStatus,
87 }
88
89 /// Control-plane handle for multi-agent operations.
90 /// `AgentControl` is held by each session (via `SessionServices`). It provides capability to
91 /// spawn new agents and the inter-agent communication layer.
92 /// An `AgentControl` instance is intended to be created at most once per root thread/session
93 /// tree. That same `AgentControl` is then shared with every sub-agent spawned from that root,
94 /// which keeps the registry scoped to that root thread rather than the entire `ThreadManager`.
… 6 lines omitted; exact range 70–111 …
101 /// This is `Weak` to avoid reference cycles and shadow persistence of the form
102 /// `ThreadManagerState -> CodexThread -> Session -> SessionServices -> ThreadManagerState`.
103 manager: Weak<ThreadManagerState>,
104 state: Arc<AgentRegistry>,
105 v2_residency: Arc<V2Residency>,
106 agent_execution_limiter: Arc<AgentExecutionLimiter>,
107 /// Session-scoped state shared by the root thread and every cloned sub-agent control handle.
108 rollout_budget: Arc<RolloutBudget>,
109 }
110
111 impl AgentControl {
为什么相信这条结论?查看 2 处证据
22 L1 · fact · codex-agent-002
fork 可选全历史、最近 N 轮或空白;消息可只入队也可触发 turn
先看源码事实 spawn_agent V2 解析 fork_turns none/all/正整数,默认 all;send_message 只投递队列,followup 类通信可触发空闲 agent 开新 turn,wait 同时监听 mailbox、steer 和 timeout。
翻译成白话 派工时可把整本案卷、最近几页或一张白纸交给下属;便签可以只塞进邮箱,也可以按门铃让他立即处理。
为什么这对自研重要 上下文成本可控,并避免普通消息总是打断正在执行的子任务。
固定提交源码摘录 复制
39 async fn handle_spawn_agent(
40 invocation: ToolInvocation,
41 ) -> Result<SpawnAgentResult, FunctionCallError> {
42 let ToolInvocation {
43 session,
44 turn,
45 payload,
46 call_id,
47 ..
48 } = invocation;
49 let arguments = function_arguments(payload)?;
50 let args: SpawnAgentArgs = parse_arguments(&arguments)?;
51 let fork_mode = args.fork_mode()?;
52 let role_name = args
53 .agent_type
54 .as_deref()
55 .map(str::trim)
56 .filter(|role| !role.is_empty());
57
58 let message = message_content(args.message)?;
59 let session_source = turn.session_source.clone();
60 let child_depth = next_thread_spawn_depth(&session_source);
61 let mut config =
62 build_agent_spawn_config(&session.get_base_instructions().await, turn.as_ref())?;
63 if let Some(service_tier) = args.service_tier.as_ref() {
… 91 lines omitted; exact range 39–165 …
155
156 let hide_agent_metadata = turn.config.multi_agent_v2.hide_spawn_agent_metadata;
157 if hide_agent_metadata {
158 Ok(SpawnAgentResult::HiddenMetadata { task_name })
159 } else {
160 Ok(SpawnAgentResult::WithNickname {
161 task_name,
162 nickname,
163 })
164 }
165 }
为什么相信这条结论?查看 3 处证据
23 L1 · fact · codex-agent-003
驻留上限与同时执行上限分离,V2 子 Agent 才受执行 limiter
先看源码事实 V2 的 effective_agent_max_threads 从 max_concurrent_threads_per_session 减去 root;AgentExecutionLimiter 只在 V2 SubAgent 启动新 turn 时检查 active 数,已有 active turn 的消息不重复占槽,guard drop 释放名额。
翻译成白话 可以让许多子会话留在通讯录里,但只有有限几个同时开工;已经在干活的人收到补充便签不会再算一个工位。
为什么这对自研重要 资源治理比单一 agent count 精细,恢复/驻留和运行并发可以独立调优。
固定提交源码摘录 复制
1547 pub(crate) fn effective_agent_max_threads(
1548 &self,
1549 multi_agent_version: MultiAgentVersion,
1550 ) -> Option<usize> {
1551 match multi_agent_version {
1552 MultiAgentVersion::V2 => Some(
1553 self.multi_agent_v2
1554 .max_concurrent_threads_per_session
1555 .saturating_sub(1),
1556 ),
1557 MultiAgentVersion::Disabled | MultiAgentVersion::V1 => {
1558 self.agent_max_threads.or(DEFAULT_AGENT_MAX_THREADS)
1559 }
1560 }
为什么相信这条结论?查看 2 处证据
小练习 8 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M09 · STATE
会话、持久化与观测:让一次运行变成可追溯事实 如果进程崩了、用户刷新了、任务跑了一夜,系统凭什么恢复并解释“刚才究竟发生了什么”?
先用一个生活比喻 内存像白板,数据库像目录,append-only journal 像监控录像;可靠 Harness 不只保存最后答案,还保存每次转弯。
这套实现先回答了什么? 先把每一步写成可重放流水账,后台书记员负责落盘;书记员一旦坏掉,后续调用会记得这次故障而不是假装成功。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 会话采用 JSONL rollout 作为事件事实源,后台 writer 支持 persist、flush 与失败记忆 codex-rs/rollout/src/recorder.rs:93先把每一步写成可重放流水账,后台书记员负责落盘;书记员一旦坏掉,后续调用会记得这次故障而不是假装成功。 SQLite 是可查询镜像,并把状态、日志、目标和记忆拆库降低锁竞争 codex-rs/state/src/lib.rs:1流水账负责忠实记录,SQLite 像索引卡片箱,负责快速搜索;不同类型卡片分柜,避免大家抢同一把锁。 观测横跨模型、工具、hooks、MCP、rollout 与 SQLite,不只是一份 CLI 日志 codex-rs/core/src/client.rs:74既能看模型这段花了多久,也能拆出排队、工具处理、钩子、连接器和数据库的时间与故障。
24 L1 · fact · codex-persistence-001
会话采用 JSONL rollout 作为事件事实源,后台 writer 支持 persist、flush 与失败记忆
先看源码事实 RolloutRecorder 将创建/恢复参数与 RolloutItem 送入后台命令队列;writer task 保存 terminal failure,公开 Persist、Flush、Shutdown acknowledgement。
翻译成白话 先把每一步写成可重放流水账,后台书记员负责落盘;书记员一旦坏掉,后续调用会记得这次故障而不是假装成功。
为什么这对自研重要 支持 resume/fork/审计,也给崩溃恢复和一致性测试提供稳定基线。
固定提交源码摘录 复制
93 pub enum RolloutRecorderParams {
94 Create {
95 session_id: SessionId,
96 conversation_id: ThreadId,
97 forked_from_id: Option<ThreadId>,
98 parent_thread_id: Option<ThreadId>,
99 source: Box<SessionSource>,
100 thread_source: Option<ThreadSource>,
101 originator: String,
102 base_instructions: BaseInstructions,
103 dynamic_tools: Vec<DynamicToolSpec>,
104 selected_capability_roots: Vec<SelectedCapabilityRoot>,
105 multi_agent_version: Option<MultiAgentVersion>,
106 history_mode: ThreadHistoryMode,
107 history_base: Option<HistoryPosition>,
108 subagent_history_start_ordinal: Option<u64>,
109 initial_window_id: Option<String>,
110 },
111 Resume {
112 path: PathBuf,
113 },
114 }
115
116 enum RolloutCmd {
117 AddItems(Vec<RolloutItem>),
… 43 lines omitted; exact range 93–171 …
161 }
162
163 /// Return the terminal writer-task failure, if the task exited with an error.
164 fn terminal_failure(&self) -> Option<IoError> {
165 let guard = self
166 .terminal_failure
167 .lock()
168 .unwrap_or_else(std::sync::PoisonError::into_inner);
169 guard.as_ref().map(|err| clone_io_error(err.as_ref()))
170 }
171 }
为什么相信这条结论?查看 2 处证据
25 L1 · fact · codex-persistence-002
SQLite 是可查询镜像,并把状态、日志、目标和记忆拆库降低锁竞争
先看源码事实 state crate 从 JSONL rollout 提取元数据镜像到 SQLite;StateRuntime 分别打开 state、logs、goals、memories 数据库,并明确把日志和分页历史分离以降低锁竞争。
翻译成白话 流水账负责忠实记录,SQLite 像索引卡片箱,负责快速搜索;不同类型卡片分柜,避免大家抢同一把锁。
为什么这对自研重要 兼顾可恢复事件源与 UI 查询性能,代价是要处理 backfill/reconciliation。
固定提交源码摘录 复制
1 //! SQLite-backed state for rollout metadata.
2 //!
3 //! This crate is intentionally small and focused: it extracts rollout metadata
4 //! from JSONL rollouts and mirrors it into a local SQLite database. Backfill
5 //! orchestration and rollout scanning live in `codex-core`.
6
7 const _: () = assert!(
8 libsqlite3_sys::SQLITE_VERSION_NUMBER >= 3_051_003,
9 "bundled SQLite must include the WAL-reset corruption fix",
10 );
为什么相信这条结论?查看 3 处证据
26 L1 · fact · codex-observe-001
观测横跨模型、工具、hooks、MCP、rollout 与 SQLite,不只是一份 CLI 日志
先看源码事实 模型 client 注入 SessionTelemetry 与 rollout inference trace;工具 runtime 记录 dispatch/handler/total timing;hooks 发开始/完成事件与 duration;MCP 记录 tool list/refresh;state 定义初始化、错误、回填与 fallback 指标。
翻译成白话 既能看模型这段花了多久,也能拆出排队、工具处理、钩子、连接器和数据库的时间与故障。
为什么这对自研重要 适合做端到端性能归因和故障回放,但需要明确遥测数据的隐私与导出策略。
固定提交源码摘录 复制
74 use codex_otel::SessionTelemetry;
75 use codex_otel::current_span_w3c_trace_context;
76 use codex_protocol::auth::AuthMode;
77
78 use codex_protocol::ThreadId;
79 use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
80 use codex_protocol::config_types::Verbosity as VerbosityConfig;
81 use codex_protocol::models::ContentItem;
82 use codex_protocol::models::ResponseItem;
83 use codex_protocol::openai_models::ModelInfo;
84 use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
85 use codex_protocol::protocol::InternalSessionSource;
86 use codex_protocol::protocol::SessionSource;
87 use codex_protocol::protocol::W3cTraceContext;
88 use codex_rollout_trace::CompactionTraceContext;
89 use codex_rollout_trace::InferenceTraceAttempt;
90 use codex_rollout_trace::InferenceTraceContext;
91 use codex_tools::create_tools_json_for_responses_api;
为什么相信这条结论?查看 3 处证据
小练习 9 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M10 · ENGINEERING
测试、恢复与工程取舍:把漂亮机制变成可靠产品 哪些行为有测试证明?哪些只是配置或 prompt 约定?当安全、性能、可恢复性冲突时,源码选择了什么?
先用一个生活比喻 这像验收一座桥:图纸说明结构,测试证明承重,故障演练证明断电后还能不能让人安全回来。
这套实现先回答了什么? 它不是一段 CLI 脚本,而是一套带协议、TUI、app server、状态库、插件和跨平台后端的系统工程。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 这是大型、多前端、测试密集的 Rust Harness,许可证为 Apache-2.0 LICENSE:1它不是一段 CLI 脚本,而是一套带协议、TUI、app server、状态库、插件和跨平台后端的系统工程。
27 L3 · fact · codex-maturity-001
这是大型、多前端、测试密集的 Rust Harness,许可证为 Apache-2.0
先看源码事实 固定快照含约 2,780 个 Rust 文件、645 个 TypeScript 文件;源码树中约 487 个 test 命名文件、1,170 个含 Rust test attribute 的文件,根许可证为 Apache License 2.0。
翻译成白话 它不是一段 CLI 脚本,而是一套带协议、TUI、app server、状态库、插件和跨平台后端的系统工程。
为什么这对自研重要 可借鉴性高,但直接复刻意味着承担很大的平台与回归测试成本。
边界与风险 文件数与测试文件数是对该固定 checkout 的机械统计,不等于测试覆盖率。
固定提交源码摘录 复制
1 Apache License
2 Version 2.0, January 2004
3 http://www.apache.org/licenses/
4
5 TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
7 1. Definitions.
8
9 "License" shall mean the terms and conditions for use, reproduction,
10 and distribution as defined by Sections 1 through 9 of this document.
11
12 "Licensor" shall mean the copyright owner or entity authorized by
13 the copyright owner that is granting the License.
14
15 "Legal Entity" shall mean the union of the acting entity and all
16 other entities that control, are controlled by, or are under common
17 control with that entity. For the purposes of this definition,
18 "control" means (i) the power, direct or indirect, to cause the
19 direction or management of such entity, whether by contract or
20 otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 outstanding shares, or (iii) beneficial ownership of such entity.
22
23 "You" (or "Your") shall mean an individual or Legal Entity
24 exercising permissions granted by this License.
25
26 "Source" form shall mean the preferred form for making modifications,
27 including but not limited to software source code, documentation
28 source, and configuration files.
为什么相信这条结论?查看 3 处证据
小练习 10 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M11 · PRACTICE
把读懂变成会判断 下面的练习不要求你先写一个完整 Agent,而是训练你检查设计边界:事实是什么、推断是什么、如果换成自研产品要补哪一层。
Q1 每个 turn 由多个 step 组成,step 内共享一次不可漂移的上下文快照 问题: 如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?
参考答案 配置热更新只能在安全边界生效,换来 prompt cache 和工具调用的确定性。
证据:codex-rs/core/src/session/turn.rs:153 Q2 流式采样与工具 Future 同时推进,并可被新消息抢占 问题: 如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?
参考答案 响应更快,但对调用顺序、取消、配对和幂等要求很高。
证据:codex-rs/core/src/session/turn.rs:2034 Q3 重试预算属于 turn-scoped client session,窗口超限不当作普通网络错误重试 问题: 如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?
参考答案 把语义恢复与传输恢复分开,避免无效重试放大费用。
证据:codex-rs/core/src/session/turn.rs:1176 Q4 模型协议只保留 Responses API,但 Provider 端点与认证可扩展 问题: 如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?
参考答案 兼容面更一致,第三方 Provider 必须实现 Responses 语义而非只暴露 chat/completions。
证据:codex-rs/model-provider-info/src/lib.rs:54 Q5 传输层同时支持 SSE 与可复用 WebSocket,并带 turn 粘性状态 问题: 如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?
参考答案 交互延迟低,但 Provider 必须准确声明 WebSocket 能力,AWS 认证当前明确不能与 WebSocket 同开。
证据:codex-rs/core/src/client.rs:1
APPENDIX · SOURCE INDEX
本课读过的实现文件 文件索引帮助你在课程外继续追踪调用链;每一条路径来自固定提交的证据账本。
01 codex-rs/core/src/session/turn.rsL153–274, L275–455, L2034–2168, L2169–2265, L1176–1273 02 codex-rs/core/src/client.rsL1–24, L1–24, L74–91 03 codex-rs/model-provider-info/src/lib.rsL54–84, L86–144, L156–186 04 codex-rs/core/src/context_manager/history.rsL38–60, L123–146, L189–248, L295–386, L493–558 05 codex-rs/core/src/compact.rsL240–318, L319–392 06 codex-rs/core/src/tools/registry.rsL48–149, L151–188 07 codex-rs/core/src/tools/parallel.rsL74–145, L146–202 08 codex-rs/core/src/tools/spec_plan_tests.rsL637–708, L637–708 09 codex-rs/core/src/tools/spec_plan.rsL310–328, L917–936 10 codex-rs/protocol/src/protocol.rsL890–932, L995–1043, L4094–4135 11 codex-rs/core/src/session/mod.rsL2295–2376, L3336–3397, L3398–3440 12 codex-rs/sandboxing/src/manager.rsL34–73, L280–367 13 codex-rs/core/src/config/permissions.rsL203–213, L347–407, L507–520 14 codex-rs/codex-mcp/src/connection_manager.rsL66–117, L143–199 15 codex-rs/codex-mcp/src/connection_manager/tool_catalog.rsL34–55, L127–230 16 codex-rs/core/src/agents_md.rsL1–16, L89–150, L207–247 17 codex-rs/core/src/hook_runtime.rsL103–220, L222–285, L649–705, L649–691 18 codex-rs/core/src/agent/control.rsL70–111 19 codex-rs/core/src/agent/control/spawn.rsL365–445 20 codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rsL39–165, L173–220 21 codex-rs/core/src/tools/handlers/multi_agents_v2/wait.rsL37–118 22 codex-rs/core/src/config/mod.rsL1547–1560 23 codex-rs/core/src/agent/control/execution.rsL14–118 24 codex-rs/rollout/src/recorder.rsL93–171, L177–290 25 codex-rs/state/src/lib.rsL1–10, L81–95 26 codex-rs/state/src/runtime.rsL71–125, L126–170 27 LICENSEL1–28 28 codex-rs/core/src/agent/control_tests.rsL2047–2174