Codex Handbook
core/src/agent/control.rs 731 lines
use crate::agent::AgentStatus;use crate::agent::registry::AgentMetadata;use crate::agent::registry::AgentRegistry;use crate::agent::role::DEFAULT_ROLE_NAME;use crate::agent::role::resolve_role_config;use crate::agent::status::is_final;use crate::codex_thread::ThreadConfigSnapshot;use crate::config::Config;use crate::environment_selection::TurnEnvironmentSnapshot;use crate::session::emit_subagent_session_started;use crate::session_prefix::format_inter_agent_completion_message;use crate::session_prefix::format_subagent_context_line;use crate::session_prefix::format_subagent_notification_message;use crate::thread_manager::ResumeThreadWithHistoryOptions;use crate::thread_manager::ThreadManagerState;use crate::thread_rollout_truncation::truncate_rollout_to_last_n_fork_turns;use codex_protocol::AgentPath;use codex_protocol::SessionId;use codex_protocol::ThreadId;use codex_protocol::error::CodexErr;use codex_protocol::error::Result as CodexResult;use codex_protocol::models::ContentItem;use codex_protocol::models::MessagePhase;use codex_protocol::models::ResponseItem;use codex_protocol::protocol::InitialHistory;use codex_protocol::protocol::InterAgentCommunication;use codex_protocol::protocol::MultiAgentVersion;use codex_protocol::protocol::Op;use codex_protocol::protocol::ResumedHistory;use codex_protocol::protocol::RolloutItem;use codex_protocol::protocol::SessionSource;use codex_protocol::protocol::SubAgentSource;use codex_protocol::protocol::ThreadSource;use codex_protocol::protocol::TurnEnvironmentSelection;use codex_protocol::user_input::UserInput;use codex_state::DirectionalThreadSpawnEdgeStatus;use codex_thread_store::ReadThreadParams;use serde::Serialize;use std::collections::HashMap;use std::collections::VecDeque;use std::sync::Arc;use std::sync::Weak;use tokio::sync::watch;use tracing::warn;pub(crate) use self::execution::AgentExecutionGuard;use self::execution::AgentExecutionLimiter;use self::residency::V2Residency;const ROOT_LAST_TASK_MESSAGE: &str = "Main thread";mod execution;mod legacy;mod residency;mod spawn;#[derive(Clone, Debug, PartialEq, Eq)]pub(crate) enum SpawnAgentForkMode {    FullHistory,    LastNTurns(usize),}#[derive(Clone, Debug, Default)]pub(crate) struct SpawnAgentOptions {    pub(crate) fork_parent_spawn_call_id: Option<String>,    pub(crate) fork_mode: Option<SpawnAgentForkMode>,    pub(crate) parent_thread_id: Option<ThreadId>,    pub(crate) environments: Option<Vec<TurnEnvironmentSelection>>,}#[derive(Clone, Debug)]pub(crate) struct LiveAgent {    pub(crate) thread_id: ThreadId,    pub(crate) metadata: AgentMetadata,    pub(crate) status: AgentStatus,}#[derive(Clone, Debug, Serialize, PartialEq, Eq)]pub(crate) struct ListedAgent {    pub(crate) agent_name: String,    pub(crate) agent_status: AgentStatus,    pub(crate) last_task_message: Option<String>,}/// Control-plane handle for multi-agent operations./// `AgentControl` is held by each session (via `SessionServices`). It provides capability to/// spawn new agents and the inter-agent communication layer./// An `AgentControl` instance is intended to be created at most once per root thread/session/// tree. That same `AgentControl` is then shared with every sub-agent spawned from that root,/// which keeps the registry scoped to that root thread rather than the entire `ThreadManager`.#[derive(Clone, Default)]pub(crate) struct AgentControl {    /// ID shared by the whole agent control session. This means every sub-agents from a common    /// root share the same session ID.    session_id: SessionId,    /// Weak handle back to the global thread registry/state.    /// This is `Weak` to avoid reference cycles and shadow persistence of the form    /// `ThreadManagerState -> CodexThread -> Session -> SessionServices -> ThreadManagerState`.    manager: Weak<ThreadManagerState>,    state: Arc<AgentRegistry>,    v2_residency: Arc<V2Residency>,    agent_execution_limiter: Arc<AgentExecutionLimiter>,}impl AgentControl {    /// Construct a new `AgentControl` that can spawn/message agents via the given manager state.    pub(crate) fn new(manager: Weak<ThreadManagerState>) -> Self {        Self {            manager,            ..Default::default()        }    }    pub(crate) fn with_session_id(mut self, session_id: SessionId, max_threads: usize) -> Self {        self.session_id = session_id;        self.agent_execution_limiter.initialize(max_threads);        self    }    pub(crate) fn session_id(&self) -> SessionId {        self.session_id    }    /// Send rich user input items to an existing agent thread.    pub(crate) async fn send_input(        &self,        agent_id: ThreadId,        initial_operation: Op,    ) -> CodexResult<String> {        let state = self.upgrade()?;        self.ensure_execution_capacity_for_op(agent_id, &initial_operation)            .await?;        self.send_input_after_capacity_check(agent_id, &state, initial_operation)            .await    }    async fn send_input_after_capacity_check(        &self,        agent_id: ThreadId,        state: &Arc<ThreadManagerState>,        initial_operation: Op,    ) -> CodexResult<String> {        let last_task_message = match &initial_operation {            Op::InterAgentCommunication { communication } => {                last_task_message_from_communication(communication)            }            _ => non_empty_task_message(render_input_preview(&initial_operation)),        };        let result = self            .handle_thread_request_result(                agent_id,                state,                state.send_op(agent_id, initial_operation).await,            )            .await;        if result.is_ok() {            match last_task_message {                Some(last_task_message) => self                    .state                    .update_last_task_message(agent_id, last_task_message),                None => self.state.clear_last_task_message(agent_id),            }        }        result    }    pub(crate) async fn send_inter_agent_communication(        &self,        agent_id: ThreadId,        communication: InterAgentCommunication,    ) -> CodexResult<String> {        let last_task_message = last_task_message_from_communication(&communication);        let state = self.upgrade()?;        let op = Op::InterAgentCommunication { communication };        self.ensure_execution_capacity_for_op(agent_id, &op).await?;        let result = self            .handle_thread_request_result(agent_id, &state, state.send_op(agent_id, op).await)            .await;        if result.is_ok() {            match last_task_message {                Some(last_task_message) => self                    .state                    .update_last_task_message(agent_id, last_task_message),                None => self.state.clear_last_task_message(agent_id),            }        }        result    }    /// Interrupt the current task for an existing agent thread.    pub(crate) async fn interrupt_agent(&self, agent_id: ThreadId) -> CodexResult<String> {        let state = self.upgrade()?;        self.handle_thread_request_result(            agent_id,            &state,            state.send_op(agent_id, Op::Interrupt).await,        )        .await    }    async fn handle_thread_request_result(        &self,        agent_id: ThreadId,        state: &Arc<ThreadManagerState>,        result: CodexResult<String>,    ) -> CodexResult<String> {        if matches!(result, Err(CodexErr::InternalAgentDied)) {            let _ = state.remove_thread(&agent_id).await;            self.forget_v2_residency(agent_id);            self.state.release_spawned_thread(agent_id);        }        result    }    /// Fetch the last known status for `agent_id`, returning `NotFound` when unavailable.    pub(crate) async fn get_status(&self, agent_id: ThreadId) -> AgentStatus {        let Ok(state) = self.upgrade() else {            // No agent available if upgrade fails.            return AgentStatus::NotFound;        };        let Ok(thread) = state.get_thread(agent_id).await else {            return AgentStatus::NotFound;        };        thread.agent_status().await    }    pub(crate) fn register_session_root(        &self,        current_thread_id: ThreadId,        current_parent_thread_id: Option<ThreadId>,    ) {        if current_parent_thread_id.is_none() {            self.state.register_root_thread(current_thread_id);        }    }    pub(crate) fn get_agent_metadata(&self, agent_id: ThreadId) -> Option<AgentMetadata> {        self.state.agent_metadata_for_thread(agent_id)    }    pub(crate) fn ensure_agent_known(&self, agent_id: ThreadId) -> CodexResult<AgentMetadata> {        self.state            .agent_metadata_for_thread(agent_id)            .ok_or(CodexErr::ThreadNotFound(agent_id))    }    pub(crate) async fn list_live_agent_subtree_thread_ids(        &self,        agent_id: ThreadId,    ) -> CodexResult<Vec<ThreadId>> {        let mut thread_ids = vec![agent_id];        thread_ids.extend(self.live_thread_spawn_descendants(agent_id).await?);        Ok(thread_ids)    }    pub(crate) async fn get_agent_config_snapshot(        &self,        agent_id: ThreadId,    ) -> Option<ThreadConfigSnapshot> {        let Ok(state) = self.upgrade() else {            return None;        };        let Ok(thread) = state.get_thread(agent_id).await else {            return None;        };        Some(thread.config_snapshot().await)    }    pub(crate) async fn resolve_agent_reference(        &self,        _current_thread_id: ThreadId,        current_session_source: &SessionSource,        agent_reference: &str,    ) -> CodexResult<ThreadId> {        let current_agent_path = current_session_source            .get_agent_path()            .unwrap_or_else(AgentPath::root);        let agent_path = current_agent_path            .resolve(agent_reference)            .map_err(CodexErr::UnsupportedOperation)?;        if let Some(thread_id) = self.state.agent_id_for_path(&agent_path) {            return Ok(thread_id);        }        Err(CodexErr::UnsupportedOperation(format!(            "live agent path `{}` not found",            agent_path.as_str()        )))    }    /// Subscribe to status updates for `agent_id`, yielding the latest value and changes.    pub(crate) async fn subscribe_status(        &self,        agent_id: ThreadId,    ) -> CodexResult<watch::Receiver<AgentStatus>> {        let state = self.upgrade()?;        let thread = state.get_thread(agent_id).await?;        Ok(thread.subscribe_status())    }    pub(crate) async fn format_environment_context_subagents(        &self,        parent_thread_id: ThreadId,    ) -> String {        let Ok(agents) = self.open_thread_spawn_children(parent_thread_id).await else {            return String::new();        };        agents            .into_iter()            .map(|(thread_id, metadata)| {                let reference = metadata                    .agent_path                    .as_ref()                    .map(|agent_path| agent_path.name().to_string())                    .unwrap_or_else(|| thread_id.to_string());                format_subagent_context_line(reference.as_str(), metadata.agent_nickname.as_deref())            })            .collect::<Vec<_>>()            .join("\n")    }    pub(crate) async fn list_agents(        &self,        current_session_source: &SessionSource,        path_prefix: Option<&str>,    ) -> CodexResult<Vec<ListedAgent>> {        let state = self.upgrade()?;        let resolved_prefix = path_prefix            .map(|prefix| {                current_session_source                    .get_agent_path()                    .unwrap_or_else(AgentPath::root)                    .resolve(prefix)                    .map_err(CodexErr::UnsupportedOperation)            })            .transpose()?;        let mut live_agents = self.state.live_agents();        live_agents.sort_by(|left, right| {            left.agent_path                .as_deref()                .unwrap_or_default()                .cmp(right.agent_path.as_deref().unwrap_or_default())                .then_with(|| {                    left.agent_id                        .map(|id| id.to_string())                        .unwrap_or_default()                        .cmp(&right.agent_id.map(|id| id.to_string()).unwrap_or_default())                })        });        let root_path = AgentPath::root();        let mut agents = Vec::with_capacity(live_agents.len().saturating_add(1));        if resolved_prefix            .as_ref()            .is_none_or(|prefix| agent_matches_prefix(Some(&root_path), prefix))            && let Some(root_thread_id) = self.state.agent_id_for_path(&root_path)            && let Ok(root_thread) = state.get_thread(root_thread_id).await        {            agents.push(ListedAgent {                agent_name: root_path.to_string(),                agent_status: root_thread.agent_status().await,                last_task_message: Some(ROOT_LAST_TASK_MESSAGE.to_string()),            });        }        for metadata in live_agents {            let Some(thread_id) = metadata.agent_id else {                continue;            };            if resolved_prefix                .as_ref()                .is_some_and(|prefix| !agent_matches_prefix(metadata.agent_path.as_ref(), prefix))            {                continue;            }            let Ok(thread) = state.get_thread(thread_id).await else {                continue;            };            let agent_name = metadata                .agent_path                .as_ref()                .map(ToString::to_string)                .unwrap_or_else(|| thread_id.to_string());            let last_task_message = metadata.last_task_message.clone();            agents.push(ListedAgent {                agent_name,                agent_status: thread.agent_status().await,                last_task_message,            });        }        Ok(agents)    }    /// Starts a detached watcher for sub-agents spawned from another thread.    ///    /// This is only enabled for `SubAgentSource::ThreadSpawn`, where a parent thread exists and    /// can receive completion notifications.    fn maybe_start_completion_watcher(        &self,        child_thread_id: ThreadId,        session_source: Option<SessionSource>,        child_reference: String,        child_agent_path: Option<AgentPath>,    ) {        let Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {            parent_thread_id, ..        })) = session_source        else {            return;        };        let control = self.clone();        tokio::spawn(async move {            let status = match control.subscribe_status(child_thread_id).await {                Ok(mut status_rx) => {                    let mut status = status_rx.borrow().clone();                    while !is_final(&status) {                        if status_rx.changed().await.is_err() {                            status = control.get_status(child_thread_id).await;                            break;                        }                        status = status_rx.borrow().clone();                    }                    status                }                Err(_) => control.get_status(child_thread_id).await,            };            if !is_final(&status) {                return;            }            let Ok(state) = control.upgrade() else {                return;            };            let child_thread = state.get_thread(child_thread_id).await.ok();            let child_uses_multi_agent_v2 = match child_thread.as_ref() {                Some(child_thread) => {                    child_thread.multi_agent_version() == Some(MultiAgentVersion::V2)                }                None => true,            };            if child_agent_path.is_some() && child_uses_multi_agent_v2 {                let Some(child_agent_path) = child_agent_path.clone() else {                    return;                };                let Some(parent_agent_path) = child_agent_path                    .as_str()                    .rsplit_once('/')                    .and_then(|(parent, _)| AgentPath::try_from(parent).ok())                else {                    return;                };                let Some(message) = format_inter_agent_completion_message(                    parent_agent_path.clone(),                    child_agent_path.clone(),                    &status,                ) else {                    return;                };                let communication = InterAgentCommunication::new(                    child_agent_path,                    parent_agent_path,                    Vec::new(),                    message,                    /*trigger_turn*/ false,                );                let _ = control                    .send_inter_agent_communication(parent_thread_id, communication)                    .await;                return;            }            let message = format_subagent_notification_message(child_reference.as_str(), &status);            let Ok(parent_thread) = state.get_thread(parent_thread_id).await else {                return;            };            parent_thread                .inject_user_message_without_turn(message)                .await;        });    }    #[allow(clippy::too_many_arguments)]    fn prepare_thread_spawn(        &self,        reservation: &mut crate::agent::registry::SpawnReservation,        config: &Config,        parent_thread_id: ThreadId,        depth: i32,        agent_path: Option<AgentPath>,        agent_role: Option<String>,        preferred_agent_nickname: Option<String>,    ) -> CodexResult<(SessionSource, AgentMetadata)> {        if depth == 1 {            self.state.register_root_thread(parent_thread_id);        }        if let Some(agent_path) = agent_path.as_ref() {            reservation.reserve_agent_path(agent_path)?;        }        let candidate_names = spawn::agent_nickname_candidates(config, agent_role.as_deref());        let candidate_name_refs: Vec<&str> = candidate_names.iter().map(String::as_str).collect();        let agent_nickname = Some(reservation.reserve_agent_nickname_with_preference(            &candidate_name_refs,            preferred_agent_nickname.as_deref(),        )?);        let session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn {            parent_thread_id,            depth,            agent_path: agent_path.clone(),            agent_nickname: agent_nickname.clone(),            agent_role: agent_role.clone(),        });        let agent_metadata = AgentMetadata {            agent_id: None,            agent_path,            agent_nickname,            agent_role,            last_task_message: None,        };        Ok((session_source, agent_metadata))    }    fn upgrade(&self) -> CodexResult<Arc<ThreadManagerState>> {        self.manager            .upgrade()            .ok_or_else(|| CodexErr::UnsupportedOperation("thread manager dropped".to_string()))    }    async fn inherited_environments_for_source(        &self,        state: &Arc<ThreadManagerState>,        session_source: Option<&SessionSource>,    ) -> Option<TurnEnvironmentSnapshot> {        let Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {            parent_thread_id, ..        })) = session_source        else {            return None;        };        let parent_thread = state.get_thread(*parent_thread_id).await.ok()?;        Some(            parent_thread                .codex                .session                .services                .turn_environments                .snapshot()                .await,        )    }    async fn inherited_exec_policy_for_source(        &self,        state: &Arc<ThreadManagerState>,        session_source: Option<&SessionSource>,        child_config: &Config,    ) -> Option<Arc<crate::exec_policy::ExecPolicyManager>> {        let Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {            parent_thread_id, ..        })) = session_source        else {            return None;        };        let parent_thread = state.get_thread(*parent_thread_id).await.ok()?;        let parent_config = parent_thread.codex.session.get_config().await;        if !crate::exec_policy::child_uses_parent_exec_policy(&parent_config, child_config) {            return None;        }        Some(Arc::clone(            &parent_thread.codex.session.services.exec_policy,        ))    }    async fn open_thread_spawn_children(        &self,        parent_thread_id: ThreadId,    ) -> CodexResult<Vec<(ThreadId, AgentMetadata)>> {        let mut children_by_parent = self.live_thread_spawn_children().await?;        Ok(children_by_parent            .remove(&parent_thread_id)            .unwrap_or_default())    }    async fn live_thread_spawn_children(        &self,    ) -> CodexResult<HashMap<ThreadId, Vec<(ThreadId, AgentMetadata)>>> {        let state = self.upgrade()?;        let mut children_by_parent = HashMap::<ThreadId, Vec<(ThreadId, AgentMetadata)>>::new();        for (parent_thread_id, child_thread_id) in state.list_live_thread_spawn_edges().await {            children_by_parent                .entry(parent_thread_id)                .or_default()                .push((                    child_thread_id,                    self.state                        .agent_metadata_for_thread(child_thread_id)                        .unwrap_or(AgentMetadata {                            agent_id: Some(child_thread_id),                            ..Default::default()                        }),                ));        }        for children in children_by_parent.values_mut() {            children.sort_by(|left, right| {                left.1                    .agent_path                    .as_deref()                    .unwrap_or_default()                    .cmp(right.1.agent_path.as_deref().unwrap_or_default())                    .then_with(|| left.0.to_string().cmp(&right.0.to_string()))            });        }        Ok(children_by_parent)    }    async fn persist_thread_spawn_edge_for_source(        &self,        thread: &crate::CodexThread,        child_thread_id: ThreadId,        session_source: Option<&SessionSource>,    ) {        let Some(parent_thread_id) = session_source.and_then(SessionSource::parent_thread_id)        else {            return;        };        let Some(state_db_ctx) = thread.state_db() else {            return;        };        if let Err(err) = state_db_ctx            .upsert_thread_spawn_edge(                parent_thread_id,                child_thread_id,                DirectionalThreadSpawnEdgeStatus::Open,            )            .await        {            warn!("failed to persist thread-spawn edge: {err}");        }    }    async fn live_thread_spawn_descendants(        &self,        root_thread_id: ThreadId,    ) -> CodexResult<Vec<ThreadId>> {        let mut children_by_parent = self.live_thread_spawn_children().await?;        let mut descendants = Vec::new();        let mut stack = children_by_parent            .remove(&root_thread_id)            .unwrap_or_default()            .into_iter()            .map(|(child_thread_id, _)| child_thread_id)            .rev()            .collect::<Vec<_>>();        while let Some(thread_id) = stack.pop() {            descendants.push(thread_id);            if let Some(children) = children_by_parent.remove(&thread_id) {                for (child_thread_id, _) in children.into_iter().rev() {                    stack.push(child_thread_id);                }            }        }        Ok(descendants)    }}fn agent_matches_prefix(agent_path: Option<&AgentPath>, prefix: &AgentPath) -> bool {    if prefix.is_root() {        return true;    }    agent_path.is_some_and(|agent_path| {        agent_path == prefix            || agent_path                .as_str()                .strip_prefix(prefix.as_str())                .is_some_and(|suffix| suffix.starts_with('/'))    })}pub(crate) fn render_input_preview(initial_operation: &Op) -> String {    match initial_operation {        Op::UserInput { items, .. } => items            .iter()            .map(|item| match item {                UserInput::Text { text, .. } => text.clone(),                UserInput::Image { .. } => "[image]".to_string(),                UserInput::LocalImage { path, .. } => {                    format!("[local_image:{}]", path.display())                }                UserInput::Skill { name, path, .. } => {                    format!("[skill:${name}]({})", path.display())                }                UserInput::Mention { name, path, .. } => format!("[mention:${name}]({path})"),                _ => "[input]".to_string(),            })            .collect::<Vec<_>>()            .join("\n"),        Op::InterAgentCommunication { communication } => communication.content.clone(),        _ => String::new(),    }}fn last_task_message_from_communication(communication: &InterAgentCommunication) -> Option<String> {    if communication.encrypted_content.is_some() {        return None;    }    non_empty_task_message(communication.content.clone())}fn non_empty_task_message(message: String) -> Option<String> {    (!message.is_empty()).then_some(message)}fn thread_spawn_depth(session_source: &SessionSource) -> Option<i32> {    match session_source {        SessionSource::SubAgent(SubAgentSource::ThreadSpawn { depth, .. }) => Some(*depth),        _ => None,    }}#[cfg(test)]#[path = "control_tests.rs"]mod tests;