Skip to content

Agent-Hauptschleife

源码版本rust-v0.145.0

codex ruft das Modell nicht mit einem einzigen Aufruf auf, sondern in einer Schleife: Request senden → SSE empfangen → Tool-Call parsen → in der Sandbox ausführen → Ergebnis zurückfüttern → erneut Request senden, bis das Modell selbst end_turn macht. Angetrieben wird diese Schleife von Session::submission_loop; der Einstieg ist CodexThread::submit(Op::UserInput{...}). Op gelangt über einen mpsc-Channel in die Schleife, der Handler verarbeitet asynchron und erzeugt einen Event-Strom zurück an den Client. In Multi-Agent-Szenarien setzt AgentControl über dieser Schleife eine Sub-Agent-Ableitung und eine Inter-Agent-Kommunikationsschicht auf.

Verantwortlichkeiten

  1. Einzelne Op-Warteschlange: CodexThread::submit sendet Op an SessionIo::tx_sub; der Handler verteilt in submission_loop nach Op-Variante auf user_input_or_turn, interrupt, exec_approval usw. codex-rs/core/src/session/handlers.rs:710-777
  2. Lebenszyklus: SessionIo::shutdown_and_wait sendet Op::Shutdown und wartet anschließend auf den Abschluss des session_loop_termination-Future für einen sauberen Ausstieg. codex-rs/core/src/session/mod.rs:808-817
  3. Multi-Agent-Registry: AgentRegistry hält in agent_tree: HashMap<AgentPath, AgentMetadata> den Root und alle gespawnten Sub-Agenten; reserve_spawn_slot begrenzt Nebenläufigkeit, next_thread_spawn_depth die Rekursionstiefe. codex-rs/core/src/agent/registry.rs:23-76
  4. Inter-Agent-Kommunikation: AgentControl::send_inter_agent_communication verpackt InterAgentCommunication als Op::InterAgentCommunication und läuft über dieselbe Submission-Warteschlange; ist der Sub-Agent fertig, löst maybe_start_completion_watcher automatisch eine Fertig-Meldung an den Parent aus. codex-rs/core/src/agent/control.rs:167-225

Entwurfsbeweggründe

Früh war codex ein einzelner Thread mit einer einzelnen Schleife: Die TUI sendet ein Op::UserInput, submission_loop dreht eine Turn und streamt Event zurück an die TUI. Alle Zustandsänderungen gehen seriell in dieselbe Warteschlange, um Nebenläufigkeitskonflikte zu vermeiden. Multi-Agent-Szenarien fordern dieses Modell heraus: Jeder Sub-Agent ist ebenfalls ein CodexThread mit eigener submission_loop und eigener Sitzungshistorie, aber alle teilen sich dasselbe AgentControl-Handle. AgentControl hält eine Weak<ThreadManagerState>-Rückreferenz auf den globalen Thread-Manager, um Zirkelbezüge zu vermeiden; AgentRegistry umwickelt agent_tree mit einem Mutex, weil mehrere Sub-Agenten in verschiedenen Tokio-Tasks nebenläufig lesen/schreiben können.

Die Op-Enum ist der einzige Nachrichtentyp in der Submission-Warteschlange. User Input, Interrupt, Approval, Inter-Agent-Kommunikation und Settings in derselben Enum zu bündeln, hat den Vorteil, dass die Handler-Reihenfolge genau der Nutzerintention entspricht — erst „Modell wechseln", dann Nachricht, sieht der Handler als ThreadSettings gefolgt von UserInput, ohne Vertauschung. maybe_start_completion_watcher ist ein Detail-Highlight: Um den Parent über den fertigen Sub-Agent zu informieren, tokio::spawn-t es einen eigenen Task, holt sich über subscribe_status einen Watch-Channel und entscheidet erst bei is_final(&status), ob der v2-Multi-Agent-Pfad (InterAgentCommunication) oder der v1-Pfad (User-Message-Injektion) genommen wird, sodass die submission_loop des Parent nicht blockiert.

Wichtige Dateien

codex-rs/core/src/agent/mod.rs:1-11 — Modul-Einstieg agent; re-exportiert AgentControl, exceeds_thread_spawn_depth_limit.codex-rs/core/src/agent/control.rs:95-108AgentControl-Struktur, hält Weak<ThreadManagerState> + Arc<AgentRegistry> + Arc<RolloutBudget>.codex-rs/core/src/agent/control.rs:435-518maybe_start_completion_watcher, benachrichtigt den Parent asynchron nach Abschluss des Sub-Agent.codex-rs/core/src/codex_thread.rs:162-203CodexThread-Struktur und new; hält Arc<Session> + SessionIo + SessionSource.codex-rs/core/src/session/mod.rs:756-806SessionIo::submit / submit_with_id, schiebt Submission in den Channel.codex-rs/protocol/src/protocol.rs:528-583pub enum Op, alle Submission-Nachrichtentypen.

CodexThread ist tatsächlich ein Facade: Alle Methoden delegieren an SessionIo, die eigentliche Schleife steckt in Session::submission_loop. Diese Schichtung erlaubt es, den internen Zustand von Session (Arc<Session>) über mehrere Thread-Handles zu teilen, während SessionIo nur Sende- und Empfangsseite nach außen gibt.

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 ist die eigentliche Channel-Zustellung. Submission hält id / op / client_user_message_id / trace; ist trace nicht gesetzt, wird aus dem aktuellen Span der W3C-Trace-Context extrahiert, sodass sich verteiltes Tracing verketten lässt. Ist der Channel geschlossen, wird CodexErr::InternalAgentDied zurückgegeben; der obere Layer handle_thread_request_result bereinigt damit die Thread-Registry.

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 ist der Einstieg der gesamten Agent-Hauptschleife. Kommt ein Op herein, verteilt match sub.op.clone() auf den entsprechenden Handler; jeder Handler gibt nach der Abarbeitung false zurück (nicht beenden), nur Op::Shutdown liefert true und lässt die Schleife aussteigen. So gelangen alle asynchronen Operationen seriell in dieselbe Warteschlange, was Races vermeidet.

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; }
    }
}

Datenfluss

Grenzen und Fehler

  • InternalAgentDied-Propagation: Nach Schließen des Channels geben alle submit-Aufrufe InternalAgentDied zurück; handle_thread_request_result macht daraus remove_thread + release_spawned_thread + Bereinigen der v2-Residency, damit in der Registry keine toten Threads bleiben. codex-rs/core/src/agent/control.rs:238-250
  • Sub-Agent-Tiefenbegrenzung: exceeds_thread_spawn_depth_limit(depth, max_depth) greift bereits in prepare_thread_spawn, um eine unendliche Spawn-Rekursion am Aufblähen des Prozesses zu hindern. codex-rs/core/src/agent/registry.rs:70-76
  • Obere Grenze nebenläufiger Agenten: AgentExecutionLimiter initialisiert beim with_session_id das max_threads; ensure_execution_capacity_for_turn_start prüft vor jedem Turn-Beginn und lehnt bei Überschreitung ab. codex-rs/core/src/agent/control.rs:126-130
  • Vorsichtiger Start bei Idle-Turn: try_start_turn_if_idle lehnt ab, wenn ein Review läuft, der Plan-Modus aktiv ist oder bereits Turns anstehen; TryStartTurnIfIdleError gibt die Items unverändert zurück, damit der Aufrufer entscheidet. codex-rs/core/src/codex_thread.rs:313-331

Zusammenfassung

Die Agent-Hauptschleife ist im Wesentlichen „einzelner mpsc-Channel + großes match" — alle Op gehen seriell in die submission_loop, der Handler verarbeitet asynchron und erzeugt einen Event-Strom. CodexThread ist eine Facade, die Session und SessionIo (Sende-/Empfangsseite) zur nach außen sichtbaren API verpackt. In Multi-Agent-Szenarien setzt AgentControl über der Schleife eine Registry und Inter-Agent-Kommunikation auf; nach Fertigwerden eines Sub-Agent benachrichtigt maybe_start_completion_watcher den Parent asynchron. Details zum Responses-API-Aufruf in der Schleife siehe LLM-Client und Responses API; Werkzeugaufrufe und Sandbox-Ausführung in den folgenden Sandbox-Kapiteln.