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> で逆参照して循環参照を避け、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 に通知するため、独立 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-11 — agent モジュール入口。AgentControl、exceeds_thread_spawn_depth_limit を re-export。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 のクリアを行い、レジストリに死んだ 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 上限:
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 章を参照。