Skip to content

Bucle principal del Agent

源码版本rust-v0.145.0

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

  1. Cola única de Op: CodexThread::submit envía Op a SessionIo::tx_sub; el handler en submission_loop dispatcha por variante de Op a user_input_or_turn, interrupt, exec_approval u otros. codex-rs/core/src/session/handlers.rs:710-777
  2. Ciclo de vida: SessionIo::shutdown_and_wait envía Op::Shutdown y luego espera a que el future session_loop_termination se complete, garantizando una salida limpia. codex-rs/core/src/session/mod.rs:808-817
  3. Registro multi-agent: AgentRegistry mantiene con agent_tree: HashMap<AgentPath, AgentMetadata> el root y todos los sub-agents generados; reserve_spawn_slot limita la concurrencia; next_thread_spawn_depth limita la profundidad de recursión. codex-rs/core/src/agent/registry.rs:23-76
  4. Comunicación inter-agent: AgentControl::send_inter_agent_communication envuelve InterAgentCommunication como Op::InterAgentCommunication y lo manda por la misma cola de submission; cuando el sub-agent termina, maybe_start_completion_watcher despacha 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-518maybe_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-806SessionIo::submit / submit_with_id, lo que de verdad empuja el Submission al channel.codex-rs/protocol/src/protocol.rs:528-583pub 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.

rust
// 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.

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 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.

rust
// 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 los submit devuelven InternalAgentDied; handle_thread_request_result hace remove_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 fase prepare_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: AgentExecutionLimiter inicializa max_threads en with_session_id; ensure_execution_capacity_for_turn_start comprueba 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_idle rechaza los casos con Review en curso, Plan mode o turn ya en cola; devuelve TryStartTurnIfIdleError dejando 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.