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::submitOpSessionIo::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_communicationInterAgentCommunicationOp::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> で逆参照して循環参照を避け、AgentRegistryMutexagent_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 に通知するため、独立 task を tokio::spawn し、watch channel で subscribe_status し、is_final(&status) になってから v2 multi-agent path(InterAgentCommunication を送る)か v1 path(user message を注入する)かに分岐する。これで parent の submission_loop はブロックされない。

主要ファイル

codex-rs/core/src/agent/mod.rs:1-11agent モジュール入口。AgentControlexceeds_thread_spawn_depth_limit を re-export。codex-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 構造体と newArc<Session> + SessionIo + SessionSource を保持。codex-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 への投函の本当の実装だ。Submissionid / 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 が閉じた後は全ての submitInternalAgentDied を返す。handle_thread_request_result はこれを受けて remove_thread + release_spawned_thread + v2 residency のクリアを行い、レジストリに死んだ thread が残るのを防ぐ。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」だ——全ての Opsubmission_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 章を参照。