Bucle principal del Agent
codex no corre el modelo en una sola llamada, sino en un bucle: envía petición → recibe SSE → parsea tool call → ejecuta en sandbox → realimenta el resultado → vuelve a enviar, hasta que el modelo hace end_turn. Este bucle lo impulsa Session::submission_loop; la entrada es CodexThread::submit(Op::UserInput{...}). El Op entra al loop por un canal mpsc, y el handler lo procesa de forma asíncrona produciendo un flujo de Event de vuelta al cliente. En escenarios multi-agent, AgentControl monta por encima de este bucle la derivación de sub-agents y la comunicación inter-agent.
Responsabilidades
- Cola única de Op:
CodexThread::submitenvíaOpaSessionIo::tx_sub; el handler ensubmission_loopdispatcha por variante deOpauser_input_or_turn,interrupt,exec_approvalu otros.codex-rs/core/src/session/handlers.rs:710-777 - Ciclo de vida:
SessionIo::shutdown_and_waitenvíaOp::Shutdowny luego espera a que el futuresession_loop_terminationse complete, garantizando una salida limpia.codex-rs/core/src/session/mod.rs:808-817 - Registro multi-agent:
AgentRegistrymantiene conagent_tree: HashMap<AgentPath, AgentMetadata>el root y todos los sub-agents generados;reserve_spawn_slotlimita la concurrencia;next_thread_spawn_depthlimita la profundidad de recursión.codex-rs/core/src/agent/registry.rs:23-76 - Comunicación inter-agent:
AgentControl::send_inter_agent_communicationenvuelveInterAgentCommunicationcomoOp::InterAgentCommunicationy lo manda por la misma cola de submission; cuando el sub-agent termina,maybe_start_completion_watcherdespacha automáticamente la notificación al parent.codex-rs/core/src/agent/control.rs:167-225
Motivación de diseño
Al principio codex era un único hilo con un único bucle: el TUI envía un Op::UserInput, submission_loop corre un turn y vuelca el flujo de Event al TUI. Todos los cambios de estado entran en serie por la misma cola, evitando carreras de concurrencia. El escenario multi-agent desafía ese modelo: cada sub-agent es también un CodexThread, con su propio submission_loop y su propio historial de sesión, pero comparten el mismo handle AgentControl. AgentControl usa un Weak<ThreadManagerState> como referencia inversa al thread manager global, evitando referencias circulares; AgentRegistry envuelve agent_tree en un Mutex, porque varios sub-agents pueden consultar/modificar concurrentemente desde distintas tokio tasks.
El enum Op es el único tipo de mensaje en la cola de submission. Mezclar user input, interrupt, approval, inter-agent communication y settings en el mismo enum tiene una ventaja: el orden que ve el handler es el orden de la intención del usuario. Si el usuario primero escribe «cambia model» y luego manda un mensaje, el handler ve primero ThreadSettings y luego UserInput, sin desorden. maybe_start_completion_watcher es un detalle destacado: al terminar un sub-agent hay que notificar al parent; lanza un tokio::spawn con una task independiente, llama a subscribe_status para obtener un watch channel, y cuando is_final(&status) decide si ir por la ruta v2 multi-agent (enviar InterAgentCommunication) o por la v1 (inyectar un user message), de modo que el submission_loop del parent no se bloquea.
Archivos clave
codex-rs/core/src/agent/mod.rs:1-11 — entrada del módulo agent, re-exporta AgentControl y exceeds_thread_spawn_depth_limit.codex-rs/core/src/agent/control.rs:95-108 — struct AgentControl, con Weak<ThreadManagerState> + Arc<AgentRegistry> + Arc<RolloutBudget>.codex-rs/core/src/agent/control.rs:435-518 — maybe_start_completion_watcher: notifica asíncronamente al parent cuando el sub-agent termina.codex-rs/core/src/codex_thread.rs:162-203 — struct CodexThread y new, con Arc<Session> + SessionIo + SessionSource.codex-rs/core/src/session/mod.rs:756-806 — SessionIo::submit / submit_with_id, lo que de verdad empuja el Submission al channel.codex-rs/protocol/src/protocol.rs:528-583 — pub enum Op, todos los tipos de mensaje de submission.CodexThread es en realidad una fachada: todos sus métodos delegan en SessionIo; el bucle real está en Session::submission_loop. Esta separación permite que el estado interno de Session (Arc<Session>) se comparta entre varios handles de thread, mientras SessionIo solo expone los extremos de envío y recepción.
// core/src/codex_thread.rs:162-203 — CodexThread es una fachada sobre Session + SessionIo
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 es la implementación real de la entrega al channel. Submission lleva id / op / client_user_message_id / trace; si trace no está fijado, se extrae del span actual el W3C trace context, para que el tracing distribuido se enganche. Si el channel está cerrado, devuelve CodexErr::InternalAgentDied, y el handle_thread_request_result de nivel superior limpia el registro de threads en consecuencia.
// 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 es la entrada a todo el bucle principal del agent. Al entrar un Op, un match sub.op.clone() lo dispatcha al handler correspondiente; cada handler devuelve false (no salir) tras procesar. Solo Op::Shutdown devuelve true para cerrar el bucle. Así, todas las operaciones asíncronas entran en serie por la misma cola, evitando races.
// core/src/session/handlers.rs:710-777 — submission_loop: Op procesado en serie
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
}
// ... otras variantes de Op
}
}.await;
if should_exit { break; }
}
}Flujo de datos
Bordes y fallos
- Propagación de
InternalAgentDied: cuando se cierra el channel, todos lossubmitdevuelvenInternalAgentDied;handle_thread_request_resulthaceremove_thread+release_spawned_thread+ limpia la residencia v2, evitando que queden threads muertos en el registro.codex-rs/core/src/agent/control.rs:238-250 - Límite de profundidad de sub-agent:
exceeds_thread_spawn_depth_limit(depth, max_depth)corta en la faseprepare_thread_spawn, evitando recursión infinita de spawn que reventaría el proceso.codex-rs/core/src/agent/registry.rs:70-76 - Límite superior de agents concurrentes:
AgentExecutionLimiterinicializamax_threadsenwith_session_id;ensure_execution_capacity_for_turn_startcomprueba al inicio de cada turn y rechaza si se supera.codex-rs/core/src/agent/control.rs:126-130 - Arranque cauteloso en idle turn:
try_start_turn_if_idlerechaza los casos con Review en curso, Plan mode o turn ya en cola; devuelveTryStartTurnIfIdleErrordejando los items intactos para que el llamador decida.codex-rs/core/src/codex_thread.rs:313-331
Resumen
La esencia del bucle principal del agent es «un único canal mpsc + un gran match»: todos los Op entran en serie a submission_loop, y los handlers los procesan asíncronamente produciendo un flujo de Event. CodexThread es una fachada que envuelve Session y los dos extremos de SessionIo como API externa. En el escenario multi-agent, AgentControl monta por encima del bucle el registro y la comunicación inter-agent; al terminar un sub-agent, maybe_start_completion_watcher lo notifica asíncronamente al parent. Para los detalles de la llamada a Responses API dentro del bucle, ver Cliente LLM y Responses API; para la ejecución de herramientas y sandbox, ver los capítulos siguientes sobre sandbox.