Codex Handbook
core/src/agent/registry.rs 358 lines
use codex_protocol::AgentPath;use codex_protocol::ThreadId;use codex_protocol::error::CodexErr;use codex_protocol::error::Result;use codex_protocol::protocol::SessionSource;use codex_protocol::protocol::SubAgentSource;use rand::prelude::IndexedRandom;use std::collections::HashMap;use std::collections::HashSet;use std::collections::hash_map::Entry;use std::sync::Arc;use std::sync::Mutex;use std::sync::atomic::AtomicUsize;use std::sync::atomic::Ordering;/// This structure is used to add some limits on the multi-agent capabilities for Codex. In/// the current implementation, it limits:/// * Total number of sub-agents (i.e. threads) per user session////// This structure is shared by all agents in the same user session (because the `AgentControl`/// is).#[derive(Default)]pub(crate) struct AgentRegistry {    active_agents: Mutex<ActiveAgents>,    total_count: AtomicUsize,}#[derive(Default)]struct ActiveAgents {    agent_tree: HashMap<String, AgentMetadata>,    used_agent_nicknames: HashSet<String>,    nickname_reset_count: usize,}#[derive(Clone, Debug, Default)]pub(crate) struct AgentMetadata {    pub(crate) agent_id: Option<ThreadId>,    pub(crate) agent_path: Option<AgentPath>,    pub(crate) agent_nickname: Option<String>,    pub(crate) agent_role: Option<String>,    pub(crate) last_task_message: Option<String>,}fn format_agent_nickname(name: &str, nickname_reset_count: usize) -> String {    match nickname_reset_count {        0 => name.to_string(),        reset_count => {            let value = reset_count + 1;            let suffix = match value % 100 {                11..=13 => "th",                _ => match value % 10 {                    1 => "st", // codespell:ignore                    2 => "nd", // codespell:ignore                    3 => "rd", // codespell:ignore                    _ => "th", // codespell:ignore                },            };            format!("{name} the {value}{suffix}")        }    }}fn session_depth(session_source: &SessionSource) -> i32 {    match session_source {        SessionSource::SubAgent(SubAgentSource::ThreadSpawn { depth, .. }) => *depth,        SessionSource::SubAgent(_) => 0,        _ => 0,    }}pub(crate) fn next_thread_spawn_depth(session_source: &SessionSource) -> i32 {    session_depth(session_source).saturating_add(1)}pub(crate) fn exceeds_thread_spawn_depth_limit(depth: i32, max_depth: i32) -> bool {    depth > max_depth}impl AgentRegistry {    pub(crate) fn reserve_spawn_slot(        self: &Arc<Self>,        max_threads: Option<usize>,    ) -> Result<SpawnReservation> {        if let Some(max_threads) = max_threads {            if !self.try_increment_spawned(max_threads) {                return Err(CodexErr::AgentLimitReached { max_threads });            }        } else {            self.total_count.fetch_add(1, Ordering::AcqRel);        }        Ok(SpawnReservation {            state: Arc::clone(self),            active: true,            reserved_agent_nickname: None,            reserved_agent_path: None,        })    }    pub(crate) fn release_spawned_thread(&self, thread_id: ThreadId) {        let removed_counted_agent = {            let mut active_agents = self                .active_agents                .lock()                .unwrap_or_else(std::sync::PoisonError::into_inner);            let removed_key = active_agents                .agent_tree                .iter()                .find_map(|(key, metadata)| (metadata.agent_id == Some(thread_id)).then_some(key))                .cloned();            removed_key                .and_then(|key| active_agents.agent_tree.remove(key.as_str()))                .is_some_and(|metadata| {                    !metadata.agent_path.as_ref().is_some_and(AgentPath::is_root)                })        };        if removed_counted_agent {            self.total_count.fetch_sub(1, Ordering::AcqRel);        }    }    pub(crate) fn register_root_thread(&self, thread_id: ThreadId) {        let mut active_agents = self            .active_agents            .lock()            .unwrap_or_else(std::sync::PoisonError::into_inner);        active_agents            .agent_tree            .entry(AgentPath::ROOT.to_string())            .or_insert_with(|| AgentMetadata {                agent_id: Some(thread_id),                agent_path: Some(AgentPath::root()),                ..Default::default()            });    }    pub(crate) fn agent_id_for_path(&self, agent_path: &AgentPath) -> Option<ThreadId> {        self.active_agents            .lock()            .unwrap_or_else(std::sync::PoisonError::into_inner)            .agent_tree            .get(agent_path.as_str())            .and_then(|metadata| metadata.agent_id)    }    pub(crate) fn agent_metadata_for_thread(&self, thread_id: ThreadId) -> Option<AgentMetadata> {        self.active_agents            .lock()            .unwrap_or_else(std::sync::PoisonError::into_inner)            .agent_tree            .values()            .find(|metadata| metadata.agent_id == Some(thread_id))            .cloned()    }    pub(crate) fn live_agents(&self) -> Vec<AgentMetadata> {        self.active_agents            .lock()            .unwrap_or_else(std::sync::PoisonError::into_inner)            .agent_tree            .values()            .filter(|metadata| {                metadata.agent_id.is_some()                    && !metadata.agent_path.as_ref().is_some_and(AgentPath::is_root)            })            .cloned()            .collect()    }    pub(crate) fn update_last_task_message(&self, thread_id: ThreadId, last_task_message: String) {        let mut active_agents = self            .active_agents            .lock()            .unwrap_or_else(std::sync::PoisonError::into_inner);        if let Some(metadata) = active_agents            .agent_tree            .values_mut()            .find(|metadata| metadata.agent_id == Some(thread_id))        {            metadata.last_task_message = Some(last_task_message);        }    }    pub(crate) fn clear_last_task_message(&self, thread_id: ThreadId) {        let mut active_agents = self            .active_agents            .lock()            .unwrap_or_else(std::sync::PoisonError::into_inner);        if let Some(metadata) = active_agents            .agent_tree            .values_mut()            .find(|metadata| metadata.agent_id == Some(thread_id))        {            metadata.last_task_message = None;        }    }    fn register_spawned_thread(&self, agent_metadata: AgentMetadata) {        let Some(thread_id) = agent_metadata.agent_id else {            return;        };        let mut active_agents = self            .active_agents            .lock()            .unwrap_or_else(std::sync::PoisonError::into_inner);        let key = agent_metadata            .agent_path            .as_ref()            .map(ToString::to_string)            .unwrap_or_else(|| format!("thread:{thread_id}"));        if let Some(agent_nickname) = agent_metadata.agent_nickname.clone() {            active_agents.used_agent_nicknames.insert(agent_nickname);        }        active_agents.agent_tree.insert(key, agent_metadata);    }    fn reserve_agent_nickname(&self, names: &[&str], preferred: Option<&str>) -> Option<String> {        let mut active_agents = self            .active_agents            .lock()            .unwrap_or_else(std::sync::PoisonError::into_inner);        let agent_nickname = if let Some(preferred) = preferred {            preferred.to_string()        } else {            if names.is_empty() {                return None;            }            let available_names: Vec<String> = names                .iter()                .map(|name| format_agent_nickname(name, active_agents.nickname_reset_count))                .filter(|name| !active_agents.used_agent_nicknames.contains(name))                .collect();            if let Some(name) = available_names.choose(&mut rand::rng()) {                name.clone()            } else {                active_agents.used_agent_nicknames.clear();                active_agents.nickname_reset_count += 1;                if let Some(metrics) = codex_otel::global() {                    let _ = metrics.counter(                        "codex.multi_agent.nickname_pool_reset",                        /*inc*/ 1,                        &[],                    );                }                format_agent_nickname(                    names.choose(&mut rand::rng())?,                    active_agents.nickname_reset_count,                )            }        };        active_agents            .used_agent_nicknames            .insert(agent_nickname.clone());        Some(agent_nickname)    }    fn reserve_agent_path(&self, agent_path: &AgentPath) -> Result<()> {        let mut active_agents = self            .active_agents            .lock()            .unwrap_or_else(std::sync::PoisonError::into_inner);        match active_agents.agent_tree.entry(agent_path.to_string()) {            Entry::Occupied(_) => Err(CodexErr::UnsupportedOperation(format!(                "agent path `{agent_path}` already exists"            ))),            Entry::Vacant(entry) => {                entry.insert(AgentMetadata {                    agent_path: Some(agent_path.clone()),                    ..Default::default()                });                Ok(())            }        }    }    fn release_reserved_agent_path(&self, agent_path: &AgentPath) {        let mut active_agents = self            .active_agents            .lock()            .unwrap_or_else(std::sync::PoisonError::into_inner);        if active_agents            .agent_tree            .get(agent_path.as_str())            .is_some_and(|metadata| metadata.agent_id.is_none())        {            active_agents.agent_tree.remove(agent_path.as_str());        }    }    fn try_increment_spawned(&self, max_threads: usize) -> bool {        let mut current = self.total_count.load(Ordering::Acquire);        loop {            if current >= max_threads {                return false;            }            match self.total_count.compare_exchange_weak(                current,                current + 1,                Ordering::AcqRel,                Ordering::Acquire,            ) {                Ok(_) => return true,                Err(updated) => current = updated,            }        }    }}pub(crate) struct SpawnReservation {    state: Arc<AgentRegistry>,    active: bool,    reserved_agent_nickname: Option<String>,    reserved_agent_path: Option<AgentPath>,}impl SpawnReservation {    pub(crate) fn reserve_agent_nickname_with_preference(        &mut self,        names: &[&str],        preferred: Option<&str>,    ) -> Result<String> {        let agent_nickname = self            .state            .reserve_agent_nickname(names, preferred)            .ok_or_else(|| {                CodexErr::UnsupportedOperation("no available agent nicknames".to_string())            })?;        self.reserved_agent_nickname = Some(agent_nickname.clone());        Ok(agent_nickname)    }    pub(crate) fn reserve_agent_path(&mut self, agent_path: &AgentPath) -> Result<()> {        self.state.reserve_agent_path(agent_path)?;        self.reserved_agent_path = Some(agent_path.clone());        Ok(())    }    pub(crate) fn commit(mut self, agent_metadata: AgentMetadata) {        self.reserved_agent_nickname = None;        self.reserved_agent_path = None;        self.state.register_spawned_thread(agent_metadata);        self.active = false;    }}impl Drop for SpawnReservation {    fn drop(&mut self) {        if self.active {            if let Some(agent_path) = self.reserved_agent_path.take() {                self.state.release_reserved_agent_path(&agent_path);            }            self.state.total_count.fetch_sub(1, Ordering::AcqRel);        }    }}#[cfg(test)]#[path = "registry_tests.rs"]mod tests;