Skip to content

Agent 主迴圈

源码版本rust-v0.145.0

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 通訊層。

職責

  1. 單一 Op 佇列:CodexThread::submitOp 發到 SessionIo::tx_sub,handler 在 submission_loop 裡按 Op 變體分發到 user_input_or_turninterruptexec_approval 等處理函式。codex-rs/core/src/session/handlers.rs:710-777
  2. 生命週期:SessionIo::shutdown_and_waitOp::Shutdown 後等 session_loop_termination future 完成,保證乾淨退出。codex-rs/core/src/session/mod.rs:808-817
  3. 多 agent registry:AgentRegistryagent_tree: HashMap<AgentPath, AgentMetadata> 維護 root + 所有 spawned sub-agent,reserve_spawn_slot 限並發,next_thread_spawn_depth 限遞迴深度。codex-rs/core/src/agent/registry.rs:23-76
  4. Inter-agent 通訊:AgentControl::send_inter_agent_communicationInterAgentCommunication 包成 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 句柄。AgentControlWeak<ThreadManagerState> 反向參照全域 thread manager,避免循環參照;AgentRegistryMutex 包住 agent_tree,因為多個 sub-agent 可能在不同 tokio task 裡並發查/改。

Op 列舉是 submission 佇列裡唯一的訊息型別。把 user input、interrupt、approval、inter-agent communication、settings 全塞進同一個 enum 的好處是 handler 順序就是使用者意圖順序——使用者先敲「換 model」再發訊息,handler 看到的就是先 ThreadSettingsUserInput,不會被打亂。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-11agent 模組入口,re-export AgentControlexceeds_thread_spawn_depth_limitcodex-rs/core/src/agent/control.rs:95-108AgentControl 結構體,持 Weak<ThreadManagerState> + Arc<AgentRegistry> + Arc<RolloutBudget>codex-rs/core/src/agent/control.rs:435-518maybe_start_completion_watcher,子 agent 完成後非同步通知 parent。codex-rs/core/src/codex_thread.rs:162-203CodexThread 結構體與 new,持 Arc<Session> + SessionIo + SessionSourcecodex-rs/core/src/session/mod.rs:756-806SessionIo::submit / submit_with_id,實際把 Submission 推 channel。codex-rs/protocol/src/protocol.rs:528-583pub enum Op,所有 submission 訊息型別。

CodexThread 實際是個 facade:所有方法都委派給 SessionIo,真正的迴圈在 Session::submission_loop。這樣分層讓 Session 的內部狀態(Arc<Session>)可以被多個 thread 句柄共享,而 SessionIo 只暴露收發兩端。

rust
// 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 註冊表。

rust
// 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。

rust
// 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 上限:AgentExecutionLimiterwith_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,把 SessionSessionIo 收發兩端包裝成對外 API。多 agent 場景下 AgentControl 在迴圈之上掛了 registry 和 inter-agent 通訊,sub-agent 完成透過 maybe_start_completion_watcher 非同步通知 parent。迴圈裡呼叫 Responses API 的細節見 LLM 客戶端與 Responses API,工具呼叫與沙箱執行見後續 sandbox 章節。