Agent-Hauptschleife
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
- Einzelne Op-Warteschlange:
CodexThread::submitsendetOpanSessionIo::tx_sub; der Handler verteilt insubmission_loopnachOp-Variante aufuser_input_or_turn,interrupt,exec_approvalusw.codex-rs/core/src/session/handlers.rs:710-777 - Lebenszyklus:
SessionIo::shutdown_and_waitsendetOp::Shutdownund wartet anschließend auf den Abschluss dessession_loop_termination-Future für einen sauberen Ausstieg.codex-rs/core/src/session/mod.rs:808-817 - Multi-Agent-Registry:
AgentRegistryhält inagent_tree: HashMap<AgentPath, AgentMetadata>den Root und alle gespawnten Sub-Agenten;reserve_spawn_slotbegrenzt Nebenläufigkeit,next_thread_spawn_depthdie Rekursionstiefe.codex-rs/core/src/agent/registry.rs:23-76 - Inter-Agent-Kommunikation:
AgentControl::send_inter_agent_communicationverpacktInterAgentCommunicationalsOp::InterAgentCommunicationund läuft über dieselbe Submission-Warteschlange; ist der Sub-Agent fertig, löstmaybe_start_completion_watcherautomatisch 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-108 — AgentControl-Struktur, hält Weak<ThreadManagerState> + Arc<AgentRegistry> + Arc<RolloutBudget>.codex-rs/core/src/agent/control.rs:435-518 — maybe_start_completion_watcher, benachrichtigt den Parent asynchron nach Abschluss des Sub-Agent.codex-rs/core/src/codex_thread.rs:162-203 — CodexThread-Struktur und new; hält Arc<Session> + SessionIo + SessionSource.codex-rs/core/src/session/mod.rs:756-806 — SessionIo::submit / submit_with_id, schiebt Submission in den Channel.codex-rs/protocol/src/protocol.rs:528-583 — pub 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.
// 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.
// 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.
// 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 allesubmit-AufrufeInternalAgentDiedzurück;handle_thread_request_resultmacht darausremove_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 inprepare_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:
AgentExecutionLimiterinitialisiert beimwith_session_iddasmax_threads;ensure_execution_capacity_for_turn_startprü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_idlelehnt ab, wenn ein Review läuft, der Plan-Modus aktiv ist oder bereits Turns anstehen;TryStartTurnIfIdleErrorgibt 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.