Agent 主迴圈
codex 跑模型不是一次呼叫,而是一個迴圈:發請求 → 收 SSE → 解析 tool call → 沙箱執行 → 把結果回灌 → 再發請求,直到模型自己 end_turn。這個迴圈由 Session::submission_loop 驅動,入口是 CodexThread::submit(Op::UserInput{...}),Op 透過 mpsc channel 進 loop,handler 非同步處理並產生 Event 流回客戶端。多 agent 場景下,AgentControl 在這條迴圈之上掛了 sub-agent 派生與 inter-agent 通訊層。
職責
- 單一 Op 佇列:
CodexThread::submit把Op發到SessionIo::tx_sub,handler 在submission_loop裡按Op變體分發到user_input_or_turn、interrupt、exec_approval等處理函式。codex-rs/core/src/session/handlers.rs:710-777 - 生命週期:
SessionIo::shutdown_and_wait發Op::Shutdown後等session_loop_terminationfuture 完成,保證乾淨退出。codex-rs/core/src/session/mod.rs:808-817 - 多 agent registry:
AgentRegistry用agent_tree: HashMap<AgentPath, AgentMetadata>維護 root + 所有 spawned sub-agent,reserve_spawn_slot限並發,next_thread_spawn_depth限遞迴深度。codex-rs/core/src/agent/registry.rs:23-76 - Inter-agent 通訊:
AgentControl::send_inter_agent_communication把InterAgentCommunication包成Op::InterAgentCommunication走同一個 submission 佇列,子 agent 完成時maybe_start_completion_watcher自動派發完成通知給 parent。codex-rs/core/src/agent/control.rs:167-225
設計動機
早期 codex 就是單執行緒單迴圈:TUI 發一個 Op::UserInput,submission_loop 跑一輪 turn,把 Event 流回 TUI。所有狀態變更串行進同一佇列,避免並發競爭。多 agent 場景挑戰了這套模型:每個 sub-agent 也是 CodexThread,有獨立 submission_loop 和會話歷史,但共享同一個 AgentControl 句柄。AgentControl 用 Weak<ThreadManagerState> 反向參照全域 thread manager,避免循環參照;AgentRegistry 用 Mutex 包住 agent_tree,因為多個 sub-agent 可能在不同 tokio task 裡並發查/改。
Op 列舉是 submission 佇列裡唯一的訊息型別。把 user input、interrupt、approval、inter-agent communication、settings 全塞進同一個 enum 的好處是 handler 順序就是使用者意圖順序——使用者先敲「換 model」再發訊息,handler 看到的就是先 ThreadSettings 後 UserInput,不會被打亂。maybe_start_completion_watcher 是個細節亮點:sub-agent 完成後要通知 parent,它 tokio::spawn 獨立 task,subscribe_status 拿 watch channel,等到 is_final(&status) 再決定走 v2 multi-agent path(發 InterAgentCommunication)還是 v1 path(注入 user message),這樣 parent 的 submission_loop 不會被阻塞。
關鍵檔案
codex-rs/core/src/agent/mod.rs:1-11 — agent 模組入口,re-export AgentControl、exceeds_thread_spawn_depth_limit。codex-rs/core/src/agent/control.rs:95-108 — AgentControl 結構體,持 Weak<ThreadManagerState> + Arc<AgentRegistry> + Arc<RolloutBudget>。codex-rs/core/src/agent/control.rs:435-518 — maybe_start_completion_watcher,子 agent 完成後非同步通知 parent。codex-rs/core/src/codex_thread.rs:162-203 — CodexThread 結構體與 new,持 Arc<Session> + SessionIo + SessionSource。codex-rs/core/src/session/mod.rs:756-806 — SessionIo::submit / submit_with_id,實際把 Submission 推 channel。codex-rs/protocol/src/protocol.rs:528-583 — pub enum Op,所有 submission 訊息型別。CodexThread 實際是個 facade:所有方法都委派給 SessionIo,真正的迴圈在 Session::submission_loop。這樣分層讓 Session 的內部狀態(Arc<Session>)可以被多個 thread 句柄共享,而 SessionIo 只暴露收發兩端。
// core/src/codex_thread.rs:162-203 — CodexThread 是 Session + SessionIo 的 facade
pub struct CodexThread {
pub(crate) session: Arc<Session>,
pub(crate) io: SessionIo,
pub(crate) session_source: SessionSource,
session_configured: SessionConfiguredEvent,
rollout_path: Option<PathBuf>,
out_of_band_elicitations: Mutex<OutOfBandElicitations>,
}
impl CodexThread {
pub(crate) fn new(session: Arc<Session>, io: SessionIo, /* ... */) -> Self { /* ... */ }
pub async fn submit(&self, op: Op) -> CodexResult<String> {
self.io.submit(op).await
}
}SessionIo::submit_with_id 是 channel 投遞的真正實現。Submission 持有 id / op / client_user_message_id / trace,trace 沒設就從當前 span 提取 W3C trace context,讓分散式追蹤能串起來。channel 關閉時回傳 CodexErr::InternalAgentDied,上層 handle_thread_request_result 據此清理 thread 註冊表。
// core/src/session/mod.rs:797-817 — submit_with_id + shutdown_and_wait
pub(crate) async fn submit_with_id(&self, mut sub: Submission) -> CodexResult<()> {
if sub.trace.is_none() {
sub.trace = current_span_w3c_trace_context();
}
self.tx_sub
.send(sub)
.await
.map_err(|_| CodexErr::InternalAgentDied)?;
Ok(())
}
pub(crate) async fn shutdown_and_wait(&self) -> CodexResult<()> {
let session_loop_termination = self.session_loop_termination.clone();
match self.submit(Op::Shutdown).await {
Ok(_) => {}
Err(CodexErr::InternalAgentDied) => {}
Err(err) => return Err(err),
}
session_loop_termination.await;
Ok(())
}submission_loop 是整個 agent 主迴圈的入口。Op 進來後 match sub.op.clone() 分發到對應 handler,每個 handler 處理完回傳 false(不退出),只有 Op::Shutdown 回傳 true 讓迴圈退出。這樣所有非同步操作都串行進同一佇列,避免 race。
// core/src/session/handlers.rs:710-777 — submission_loop: Op 串行处理
pub(super) async fn submission_loop(sess: Arc<Session>, config: Arc<Config>, rx_sub: Receiver<Submission>) {
let mut shutdown_received = false;
while let Ok(sub) = rx_sub.recv().await {
debug!(?sub, "Submission");
let dispatch_span = submission_dispatch_span(&sub);
let should_exit = async {
match sub.op.clone() {
Op::Interrupt => { interrupt(&sess).await; false }
Op::UserInput { .. } => {
user_input_or_turn(&sess, sub.id.clone(), sub.op, sub.client_user_message_id).await;
false
}
Op::ThreadSettings { thread_settings } => {
update_thread_settings(&sess, sub.id.clone(), thread_settings).await;
false
}
Op::InterAgentCommunication { communication } => {
inter_agent_communication(&sess, sub.id.clone(), communication).await;
false
}
// ... 其他 Op 变体
}
}.await;
if should_exit { break; }
}
}資料流
邊界與失敗
InternalAgentDied傳播:channel 關閉後所有submit回傳InternalAgentDied,handle_thread_request_result據此remove_thread+release_spawned_thread+ 清 v2 residency,避免註冊表殘留死執行緒。codex-rs/core/src/agent/control.rs:238-250- sub-agent 深度限制:
exceeds_thread_spawn_depth_limit(depth, max_depth)在prepare_thread_spawn階段攔截,防止無限遞迴 spawn 把行程撐爆。codex-rs/core/src/agent/registry.rs:70-76 - 並發 agent 上限:
AgentExecutionLimiter在with_session_id時初始化max_threads,ensure_execution_capacity_for_turn_start在每個 turn 開始前檢查,超限拒絕。codex-rs/core/src/agent/control.rs:126-130 - idle turn 謹慎啟動:
try_start_turn_if_idle拒絕 Review 進行中、Plan mode、已有排隊 turn 的場景,回傳TryStartTurnIfIdleError把 items 原樣還回讓呼叫方決定。codex-rs/core/src/codex_thread.rs:313-331
小結
agent 主迴圈的本質是「單一 mpsc channel + 大 match」——所有 Op 串行進 submission_loop,handler 非同步處理產生 Event 流。CodexThread 是個 facade,把 Session 和 SessionIo 收發兩端包裝成對外 API。多 agent 場景下 AgentControl 在迴圈之上掛了 registry 和 inter-agent 通訊,sub-agent 完成透過 maybe_start_completion_watcher 非同步通知 parent。迴圈裡呼叫 Responses API 的細節見 LLM 客戶端與 Responses API,工具呼叫與沙箱執行見後續 sandbox 章節。