use crate::TurnInputRequest; use crate::TurnInputSubmission; use crate::TurnStartOptions; use crate::agent::AgentStatus; use crate::agent::role::DEFAULT_ROLE_NAME; use crate::agent::role::resolve_role_config; use crate::agent::status::is_final; use crate::agent::types::AgentMetadata; use crate::agent::types::LiveAgent; use crate::agent_communication::AgentCommunicationContext; use crate::agent_communication::AgentCommunicationKind; use crate::config::Config; use crate::config::RolloutBudgetConfig; use crate::context::ContextualUserFragment; use crate::context::SubagentNotification; use crate::environment_selection::TurnEnvironmentSnapshot; use crate::session::emit_subagent_session_started; use crate::session_prefix::format_inter_agent_completion_message; use crate::thread_manager::ResumeThreadWithHistoryOptions; use crate::thread_manager::ThreadIdGenerator; use crate::thread_manager::ThreadManagerState; use crate::thread_manager::default_thread_id_generator; use crate::turn_timing::now_unix_timestamp_ms; use codex_history::InitialHistory; use codex_history::ResumedHistory; use codex_history::RolloutItem; use codex_protocol::AgentPath; use codex_protocol::SessionId; use codex_protocol::ThreadId; use codex_protocol::error::CodexErr; use codex_protocol::error::CodexErrorDetails; use codex_protocol::error::Result as CodexResult; use codex_protocol::items::SubAgentActivityItem; use codex_protocol::items::TurnItem; use codex_protocol::models::ContentItem; use codex_protocol::models::ContentItemMetadata; use codex_protocol::models::ContentItemNamespace; use codex_protocol::models::MessagePhase; use codex_protocol::models::ResponseItem; use codex_protocol::protocol::Event; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::HasLegacyEvent; use codex_protocol::protocol::InterAgentCommunication; use codex_protocol::protocol::ItemCompletedEvent; use codex_protocol::protocol::ItemStartedEvent; use codex_protocol::protocol::MultiAgentVersion; use codex_protocol::protocol::Op; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; use codex_protocol::protocol::ThreadHistoryMode; use codex_protocol::protocol::ThreadSource; use codex_protocol::user_input::UserInput; use codex_thread_store::LoadThreadHistoryParams; use codex_thread_store::ReadThreadParams; use futures::StreamExt; use std::collections::VecDeque; use std::sync::Arc; use std::sync::Weak; use tracing::warn; use uuid::Uuid; pub(crate) use self::runtime::AgentControlInit; pub(crate) use self::runtime::AgentTreeMembership; pub(crate) use self::runtime::AgentTreeShutdownState; pub(crate) use self::runtime::AgentTreeTeardownGuard; pub(crate) use self::runtime::LocalAgentRuntime; pub(crate) use self::watch::StatusSubscription; mod api; mod budget; mod completion; mod delivery; mod execution; mod inspection; mod interrupt; mod legacy; mod mailbox; mod residency; pub use residency::ThreadEvictionOutcome; mod resume; mod root_handoff; mod runtime; mod runtime_context; mod sender_context; mod service_tier; mod spawn; mod spawn_guard; mod spawn_telemetry; mod target; mod user_authorization; mod watch; /// Per-session controller handle for a local agent tree. /// Handles retain a session identity and share their tree's `LocalAgentRuntime`. /// Local startup preserves that state when creating or resuming children. #[derive(Clone)] pub(crate) struct LocalAgentControl { /// session_id is equal to the root thread's ID. session_id: SessionId, pub(crate) runtime: LocalAgentRuntime, } impl Default for LocalAgentControl { fn default() -> Self { Self::new( Weak::default(), default_thread_id_generator(), /*rollout_budget*/ None, ) } } impl LocalAgentControl { /// Construct a new `LocalAgentControl` that can spawn/message agents via the given manager state. pub(crate) fn new( manager: Weak, thread_id_generator: ThreadIdGenerator, rollout_budget: Option, ) -> Self { Self { session_id: SessionId::default(), runtime: LocalAgentRuntime::new(manager, thread_id_generator, rollout_budget), } } pub(crate) fn with_session_id(mut self, session_id: SessionId, max_threads: usize) -> Self { self.session_id = session_id; self.runtime.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, input: Vec, start_options: TurnStartOptions, ) -> CodexResult { let state = self.runtime.upgrade()?; let thread = state.get_thread(agent_id).await?; let result = match thread .start_or_steer_turn(TurnInputRequest::user_input(input).on_start(start_options)) .await { Ok(TurnInputSubmission::Started { turn_id, .. }) => Ok(turn_id), Ok(TurnInputSubmission::Steered { .. }) => { // MAv1 exposes an opaque `submission_id` to the model. The legacy // `Op::UserInput` path returned a fresh ID for every steer, while the // turn-input API returns the active turn ID. Keep the tool-visible ID // unique without adding a submission receipt back to Core. Ok(Uuid::now_v7().to_string()) } Ok(TurnInputSubmission::NotSubmitted { reason }) => Err(CodexErr::InvalidRequest( format!("turn input was not submitted: {reason:?}"), )), Err(err) => Err(err), }; self.handle_thread_request_result(agent_id, &state, result) .await } pub(crate) async fn send_inter_agent_communication( &self, agent_id: ThreadId, communication: InterAgentCommunication, agent_communication_context: AgentCommunicationContext, start_options: TurnStartOptions, ) -> CodexResult { let state = self.runtime.upgrade()?; if communication.trigger_turn { let thread = state.get_thread(agent_id).await?; thread .ensure_execution_capacity_for_turn_start(self) .await?; } self.send_inter_agent_communication_after_capacity_check( agent_id, &state, communication, agent_communication_context, start_options, ) .await } pub(crate) async fn emit_sub_agent_activity( &self, thread_id: ThreadId, turn_id: String, item: SubAgentActivityItem, ) -> CodexResult<()> { let state = self.runtime.upgrade()?; let thread = state.get_thread(thread_id).await?; let started_at_ms = now_unix_timestamp_ms(); let item = TurnItem::SubAgentActivity(item); thread .session .send_event_raw(Event { id: turn_id.clone(), msg: EventMsg::ItemStarted(ItemStartedEvent { thread_id, turn_id: turn_id.clone(), item: item.clone(), started_at_ms, }), }) .await; let completed_at_ms = now_unix_timestamp_ms(); let completed = ItemCompletedEvent { thread_id, turn_id: turn_id.clone(), item, started_at_ms: Some(started_at_ms), completed_at_ms, }; thread .session .send_event_raw(Event { id: turn_id.clone(), msg: EventMsg::ItemCompleted(completed.clone()), }) .await; for legacy in completed.as_legacy_events(/*show_raw_agent_reasoning*/ false) { thread .session .send_event_raw(Event { id: turn_id.clone(), msg: legacy, }) .await; } Ok(()) } async fn send_inter_agent_communication_after_capacity_check( &self, agent_id: ThreadId, state: &Arc, communication: InterAgentCommunication, context: AgentCommunicationContext, start_options: TurnStartOptions, ) -> CodexResult { self.submit_inter_agent_communication( agent_id, state, communication, context, start_options, ) .await } async fn submit_inter_agent_communication( &self, agent_id: ThreadId, state: &Arc, communication: InterAgentCommunication, context: AgentCommunicationContext, start_options: TurnStartOptions, ) -> CodexResult { let communication_for_log = crate::agent_communication::logging_enabled().then(|| communication.clone()); { // Keep unloaded delivery atomic with publication of a reloaded session. let threads = state.threads.read().await; if !communication.trigger_turn && !threads.contains_key(&agent_id) { let _membership = self.runtime.admit_start()?; self.runtime.ensure_agent_known(agent_id)?; let submission_id = uuid::Uuid::now_v7().to_string(); self.runtime.mailboxes.enqueue( agent_id, Some(submission_id.clone()), vec![communication], )?; if let Some(communication) = communication_for_log { crate::agent_communication::emit_agent_communication_send( &submission_id, &context, &communication, agent_id, ); } return Ok(submission_id); } } // Loaded recipients retain submission ordering with follow-ups and interrupts. let (parent_turn_id, root_turn_id) = if communication.trigger_turn { ( start_options.parent_turn_id.clone(), start_options.root_turn_id.clone(), ) } else { (None, None) }; let result = self .handle_thread_request_result( agent_id, state, state .send_op( agent_id, Op::InterAgentCommunication { communication, start_options, }, parent_turn_id, root_turn_id, ) .await, ) .await; if let (Some(communication), Ok(communication_id)) = (communication_for_log, result.as_ref()) { crate::agent_communication::emit_agent_communication_send( communication_id, &context, &communication, agent_id, ); } result } /// Interrupt the current task for an existing agent thread. pub(crate) async fn interrupt_agent(&self, agent_id: ThreadId) -> CodexResult { let state = self.runtime.upgrade()?; self.handle_thread_request_result( agent_id, &state, state .send_op( agent_id, Op::Interrupt, /*parent_turn_id*/ None, /*root_turn_id*/ None, ) .await, ) .await } async fn handle_thread_request_result( &self, agent_id: ThreadId, state: &Arc, result: CodexResult, ) -> CodexResult { if result .as_ref() .is_err_and(|err| matches!(err.details(), CodexErrorDetails::InternalAgentDied)) { let _ = state.remove_thread(&agent_id).await; self.forget_v2_residency(agent_id); self.runtime.registry.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.runtime.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 get_agent_metadata(&self, agent_id: ThreadId) -> Option { self.runtime.registry.agent_metadata_for_thread(agent_id) } pub(crate) async fn list_agents( &self, current_session_source: &SessionSource, path_prefix: Option<&str>, ) -> CodexResult> { let state = self.runtime.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.runtime.registry.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.runtime.registry.agent_id_for_path(&root_path) && let Ok(root_thread) = state.get_thread(root_thread_id).await { agents.push(LiveAgent { thread_id: root_thread_id, metadata: AgentMetadata { agent_id: Some(root_thread_id), agent_path: Some(root_path), ..Default::default() }, status: root_thread.agent_status().await, }); } 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; }; agents.push(LiveAgent { thread_id, metadata, status: thread.agent_status().await, }); } 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, child_reference: String, child_agent_path: Option, ) { let Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id, .. })) = session_source else { return; }; let Ok(membership) = self.runtime.admit_start() else { return; }; let teardown = membership.into_teardown_guard("completion_watcher", Some(child_thread_id)); let control = self.clone(); let watcher = async move { let status = match control.subscribe_status(child_thread_id).await { Ok(mut updates) => { let mut final_status = None; while let Some(Ok(snapshot)) = tokio::select! { biased; _ = control.runtime.shutdown.cancelled() => return, update = updates.next() => update, } { if let Some(status) = snapshot.status() && is_final(status) { final_status = Some(status.clone()); break; } } match final_status { Some(status) => status, None => control.get_status(child_thread_id).await, } } Err(_) => control.get_status(child_thread_id).await, }; // Tree shutdown is a lifecycle handoff, not a child result for the parent. if !is_final(&status) || (matches!(&status, AgentStatus::Shutdown) && control.runtime.shutdown.is_cancelled()) { return; } let Ok(state) = control.runtime.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 context = AgentCommunicationContext::new(AgentCommunicationKind::Result, child_thread_id); let _ = control .send_inter_agent_communication( parent_thread_id, communication, context, TurnStartOptions::default(), ) .await; return; } let Ok(parent_thread) = state.get_thread(parent_thread_id).await else { return; }; let config = parent_thread.config().await; let namespace = match parent_thread .multi_agent_version() .unwrap_or_else(|| config.multi_agent_version_from_features()) { MultiAgentVersion::Disabled => None, MultiAgentVersion::V1 => Some(ContentItemNamespace::MultiAgentV1), MultiAgentVersion::V2 => Some( config .multi_agent_v2 .tool_namespace .clone() .map(ContentItemNamespace::from) .unwrap_or(ContentItemNamespace::Functions), ), }; parent_thread .inject_fragment_without_turn( SubagentNotification::new(child_reference.as_str(), status) .with_metadata(ContentItemMetadata::tool(namespace)), ) .await; }; tokio::spawn(async move { watcher.await; teardown.complete(); }); } fn prepare_agent_metadata( &self, reservation: &mut crate::agent::registry::SpawnReservation, config: &Config, agent_path: Option, agent_role: Option, preferred_agent_nickname: Option, ) -> CodexResult { 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(), )?); Ok(AgentMetadata { agent_id: None, agent_path, agent_nickname, agent_role, }) } #[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, agent_role: Option, preferred_agent_nickname: Option, ) -> CodexResult<(SessionSource, AgentMetadata)> { if depth == 1 { self.runtime.registry.register_root_thread(parent_thread_id); } let agent_metadata = self.prepare_agent_metadata( reservation, config, agent_path, agent_role, preferred_agent_nickname, )?; let session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id, depth, agent_path: agent_metadata.agent_path.clone(), agent_nickname: agent_metadata.agent_nickname.clone(), agent_role: agent_metadata.agent_role.clone(), }); Ok((session_source, agent_metadata)) } async fn inherited_environments_for_source( &self, state: &Arc, session_source: Option<&SessionSource>, ) -> Option { 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 .session .services .turn_environments .snapshot() .await, ) } async fn inherited_exec_policy_for_source( &self, state: &Arc, session_source: Option<&SessionSource>, child_config: &Config, ) -> Option> { 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.session.get_config().await; if !crate::exec_policy::child_uses_parent_exec_policy(&parent_config, child_config) { return None; } Some(Arc::clone(&parent_thread.session.services.exec_policy)) } async fn persist_thread_spawn_edge_for_source( &self, child_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; }; if child_thread.config_snapshot().await.ephemeral { return; } let Ok(state) = self.runtime.upgrade() else { return; }; let Some(agent_graph_store) = state.agent_graph_store() else { return; }; if let Err(err) = agent_graph_store .upsert_thread_spawn_edge( parent_thread_id, child_thread_id, codex_agent_graph_store::ThreadSpawnEdgeStatus::Open, ) .await { warn!("failed to persist thread-spawn edge: {err}"); } } } 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(input: &[UserInput]) -> String { input .iter() .map(|item| match item { UserInput::Text { text, .. } => text.clone(), UserInput::Image { .. } => "[image]".to_string(), UserInput::LocalImage { path, .. } => { format!("[local_image:{}]", path.display()) } UserInput::Audio { .. } => "[audio]".to_string(), UserInput::LocalAudio { path } => { format!("[local_audio:{}]", path.display()) } UserInput::Skill { name, path, .. } => { format!("[skill:${name}]({})", path.display()) } UserInput::Mention { name, path, .. } => format!("[mention:${name}]({path})"), _ => "[input]".to_string(), }) .collect::>() .join("\n") } fn thread_spawn_depth(session_source: &SessionSource) -> Option { match session_source { SessionSource::SubAgent(SubAgentSource::ThreadSpawn { depth, .. }) => Some(*depth), _ => None, } } #[cfg(test)] #[path = "control_tests.rs"] mod tests;