diff --git a/codex-rs/core/src/agent/control.rs b/codex-rs/core/src/agent/control.rs index 8e37cc95dd..d88b4f435d 100644 --- a/codex-rs/core/src/agent/control.rs +++ b/codex-rs/core/src/agent/control.rs @@ -218,12 +218,6 @@ impl AgentControl { options: SpawnAgentOptions, ) -> CodexResult { let state = self.upgrade()?; - let multi_agent_v2 = match session_source.as_ref() { - Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { agent_path, .. })) => { - agent_path.is_some() - } - _ => config.features.enabled(Feature::MultiAgentV2), - }; let mut reservation = self.state.reserve_spawn_slot(config.agent_max_threads)?; let inherited_shell_snapshot = self .inherited_shell_snapshot_for_source(&state, session_source.as_ref()) @@ -345,7 +339,7 @@ impl AgentControl { self.send_input(new_thread.thread_id, initial_operation) .await?; - if !multi_agent_v2 { + if agent_metadata.agent_path.is_none() { let child_reference = agent_metadata .agent_path .as_ref() @@ -422,10 +416,9 @@ impl AgentControl { forked_rollout_items = truncate_rollout_to_last_n_fork_turns(&forked_rollout_items, *last_n_turns); } - let multi_agent_v2 = session_source.get_agent_path().is_some(); let multi_agent_v2_usage_hint_texts_to_filter: Vec = if let Some(parent_thread) = parent_thread.as_ref() { - if multi_agent_v2 { + if session_source.get_agent_path().is_some() { let parent_config = parent_thread.codex.session.get_config().await; [ parent_config @@ -443,7 +436,7 @@ impl AgentControl { } else { Vec::new() } - } else if multi_agent_v2 { + } else if session_source.get_agent_path().is_some() { [ config.multi_agent_v2.root_agent_usage_hint_text.clone(), config.multi_agent_v2.subagent_usage_hint_text.clone(), @@ -479,7 +472,7 @@ impl AgentControl { } } if preserve_reference_context_item - && multi_agent_v2 + && session_source.get_agent_path().is_some() && let Some(subagent_usage_hint_text) = config.multi_agent_v2.subagent_usage_hint_text.clone() && let Some(subagent_usage_hint_message) = @@ -595,15 +588,9 @@ impl AgentControl { thread_id: ThreadId, session_source: SessionSource, ) -> CodexResult { - let multi_agent_v2 = match &session_source { - SessionSource::SubAgent(SubAgentSource::ThreadSpawn { agent_path, .. }) => { - agent_path.is_some() - } - _ => config.features.enabled(Feature::MultiAgentV2), - }; if let SessionSource::SubAgent(SubAgentSource::ThreadSpawn { depth, .. }) = &session_source && *depth >= config.agent_max_depth - && !multi_agent_v2 + && !config.features.enabled(Feature::MultiAgentV2) { let _ = config.features.disable(Feature::SpawnCsv); let _ = config.features.disable(Feature::Collab); @@ -679,7 +666,7 @@ impl AgentControl { // Resumed threads are re-registered in-memory and need the same listener // attachment path as freshly spawned threads. state.notify_thread_created(resumed_thread.thread_id); - if !multi_agent_v2 { + if agent_metadata.agent_path.is_none() { let child_reference = agent_metadata .agent_path .as_ref() diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 15f911076d..5817aa386c 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -226,7 +226,6 @@ use self::turn::collect_explicit_app_ids_from_skill_items; use self::turn::realtime_text_for_event; use self::turn_context::TurnContext; use self::turn_context::TurnSkillsContext; -use self::turn_context::resolve_multi_agent_version; #[cfg(test)] mod rollout_reconstruction_tests; @@ -487,6 +486,14 @@ impl Codex { let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY); let (tx_event, rx_event) = async_channel::unbounded(); + if let SessionSource::SubAgent(SubAgentSource::ThreadSpawn { depth, .. }) = session_source + && depth >= config.agent_max_depth + && !config.features.enabled(Feature::MultiAgentV2) + { + let _ = config.features.disable(Feature::SpawnCsv); + let _ = config.features.disable(Feature::Collab); + } + let primary_environment = environment_selections.primary_environment(); let mut user_instruction_warnings = Vec::new(); let user_instructions = AgentsMdManager::new(&config) @@ -512,7 +519,7 @@ impl Codex { ) }; - let mut config = Arc::new(config); + let config = Arc::new(config); let refresh_strategy = if session_source.is_non_root_agent() { codex_models_manager::manager::RefreshStrategy::Offline } else { @@ -537,16 +544,6 @@ impl Codex { let model_info = models_manager .get_model_info(model.as_str(), &config.to_models_manager_config()) .await; - let multi_agent_version = - resolve_multi_agent_version(&model_info, &config, &session_source); - if let SessionSource::SubAgent(SubAgentSource::ThreadSpawn { depth, .. }) = &session_source - && *depth >= config.agent_max_depth - && multi_agent_version != Some(MultiAgentVersion::V2) - { - let config = Arc::make_mut(&mut config); - let _ = config.features.disable(Feature::SpawnCsv); - let _ = config.features.disable(Feature::Collab); - } let base_instructions = config .base_instructions .clone() @@ -1654,6 +1651,10 @@ impl Session { turn_context: &TurnContext, msg: &EventMsg, ) { + if turn_context.multi_agent_version != Some(MultiAgentVersion::V2) { + return; + } + if !matches!(msg, EventMsg::TurnComplete(_) | EventMsg::TurnAborted(_)) { return; } diff --git a/codex-rs/core/src/session/review.rs b/codex-rs/core/src/session/review.rs index ec8bfeaff2..ba2496304c 100644 --- a/codex-rs/core/src/session/review.rs +++ b/codex-rs/core/src/session/review.rs @@ -57,11 +57,6 @@ pub(super) async fn spawn_review_thread( ToolMode::Direct } }); - let multi_agent_version = resolve_multi_agent_version( - &model_info, - &per_turn_config, - &parent_turn_context.session_source, - ); if let Err(err) = per_turn_config.web_search_mode.set(review_web_search_mode) { let fallback_value = per_turn_config.web_search_mode.value(); tracing::warn!( @@ -112,7 +107,7 @@ pub(super) async fn spawn_review_thread( auth_manager: auth_manager_for_context, model_info: model_info.clone(), tool_mode, - multi_agent_version, + multi_agent_version: None, session_telemetry: session_telemetry_for_context, provider: provider_for_context, reasoning_effort, diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index 1498aec6bd..e6bd1aad43 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -2,6 +2,7 @@ use super::*; use crate::SkillLoadOutcome; use crate::config::GhostSnapshotConfig; use crate::environment_selection::ResolvedTurnEnvironments; +use crate::guardian::is_guardian_reviewer_source; use codex_model_provider::SharedModelProvider; use codex_model_provider::create_model_provider; use codex_protocol::SessionId; @@ -110,14 +111,27 @@ pub(crate) fn resolve_multi_agent_version( config: &Config, session_source: &SessionSource, ) -> Option { + if is_guardian_reviewer_source(session_source) + || matches!( + session_source, + SessionSource::SubAgent(SubAgentSource::Review) + ) + { + return None; + } match session_source { SessionSource::SubAgent(SubAgentSource::ThreadSpawn { agent_path: Some(_), .. }) => Some(MultiAgentVersion::V2), + SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + depth, + agent_path: None, + .. + }) if *depth < config.agent_max_depth => Some(MultiAgentVersion::V1), SessionSource::SubAgent(SubAgentSource::ThreadSpawn { agent_path: None, .. - }) => Some(MultiAgentVersion::V1), + }) => None, _ => model_info.multi_agent_version.or_else(|| { if config.features.enabled(Feature::MultiAgentV2) { Some(MultiAgentVersion::V2) diff --git a/codex-rs/core/src/tasks/mod.rs b/codex-rs/core/src/tasks/mod.rs index 4c43d52c64..cf0607454d 100644 --- a/codex-rs/core/src/tasks/mod.rs +++ b/codex-rs/core/src/tasks/mod.rs @@ -71,20 +71,11 @@ pub(crate) enum InterruptedTurnHistoryMarker { } impl InterruptedTurnHistoryMarker { - pub(crate) fn from_config( - config: &Config, - multi_agent_version: Option, - ) -> Self { + pub(crate) fn from_config(config: &Config) -> Self { if !config.agent_interrupt_message_enabled { return Self::Disabled; } - if multi_agent_version.or_else(|| { - config - .features - .enabled(Feature::MultiAgentV2) - .then_some(MultiAgentVersion::V2) - }) == Some(MultiAgentVersion::V2) - { + if config.features.enabled(Feature::MultiAgentV2) { Self::Developer } else { Self::ContextualUser @@ -850,11 +841,15 @@ impl Session { .await; if reason == TurnAbortReason::Interrupted - && let Some(marker) = - interrupted_turn_history_marker(InterruptedTurnHistoryMarker::from_config( - task.turn_context.config.as_ref(), - task.turn_context.multi_agent_version, - )) + && let Some(marker) = interrupted_turn_history_marker( + if !task.turn_context.config.agent_interrupt_message_enabled { + InterruptedTurnHistoryMarker::Disabled + } else if task.turn_context.multi_agent_version == Some(MultiAgentVersion::V2) { + InterruptedTurnHistoryMarker::Developer + } else { + InterruptedTurnHistoryMarker::ContextualUser + }, + ) { self.record_conversation_items( task.turn_context.as_ref(), diff --git a/codex-rs/core/src/thread_manager.rs b/codex-rs/core/src/thread_manager.rs index d7c0fccdd4..3eec8315b1 100644 --- a/codex-rs/core/src/thread_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -640,10 +640,7 @@ impl ThreadManager { options.initial_history = fork_history_from_snapshot( ForkSnapshot::Interrupted, history, - InterruptedTurnHistoryMarker::from_config( - &options.config, - /*multi_agent_version*/ None, - ), + InterruptedTurnHistoryMarker::from_config(&options.config), ); self.start_thread_with_options_and_fork_source(options, Some(forked_from_thread_id)) .await @@ -898,8 +895,7 @@ impl ThreadManager { InitialHistory::Forked(_) => history.forked_from_id(), InitialHistory::New | InitialHistory::Cleared => None, }; - let interrupted_marker = - InterruptedTurnHistoryMarker::from_config(&config, /*multi_agent_version*/ None); + let interrupted_marker = InterruptedTurnHistoryMarker::from_config(&config); let history = fork_history_from_snapshot(snapshot, history, interrupted_marker); let environments = default_thread_environment_selections( self.state.environment_manager.as_ref(), diff --git a/codex-rs/core/src/thread_manager_tests.rs b/codex-rs/core/src/thread_manager_tests.rs index cf8bc1221b..76880eeb9e 100644 --- a/codex-rs/core/src/thread_manager_tests.rs +++ b/codex-rs/core/src/thread_manager_tests.rs @@ -14,7 +14,6 @@ use codex_protocol::models::ContentItem; use codex_protocol::models::ReasoningItemReasoningSummary; use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::ModelsResponse; -use codex_protocol::openai_models::MultiAgentVersion; use codex_protocol::protocol::AgentMessageEvent; use codex_protocol::protocol::InitialHistory; use codex_protocol::protocol::InternalSessionSource; @@ -65,23 +64,6 @@ fn developer_interrupted_marker() -> ResponseItem { .expect("developer interrupted marker should be enabled") } -#[tokio::test] -async fn interrupted_turn_history_marker_uses_resolved_multi_agent_version() { - let mut config = test_config().await; - let _ = config.features.enable(Feature::MultiAgentV2); - - assert_eq!( - ( - InterruptedTurnHistoryMarker::from_config(&config, Some(MultiAgentVersion::V1),), - InterruptedTurnHistoryMarker::from_config(&config, Some(MultiAgentVersion::V2),), - ), - ( - InterruptedTurnHistoryMarker::ContextualUser, - InterruptedTurnHistoryMarker::Developer, - ) - ); -} - #[test] fn truncates_before_requested_user_message() { let items = [ diff --git a/codex-rs/core/src/tools/handlers/agent_jobs.rs b/codex-rs/core/src/tools/handlers/agent_jobs.rs index 3f5d1ff8a8..4c3cd34c2e 100644 --- a/codex-rs/core/src/tools/handlers/agent_jobs.rs +++ b/codex-rs/core/src/tools/handlers/agent_jobs.rs @@ -119,13 +119,13 @@ async fn build_runner_options( "agent depth limit reached; this session cannot spawn more subagents".to_string(), )); } - let max_threads = turn.config.agent_max_threads; - if max_threads == Some(0) { + if turn.config.agent_max_threads == Some(0) { return Err(FunctionCallError::RespondToModel( "agent thread limit reached; this session cannot spawn more subagents".to_string(), )); } - let max_concurrency = normalize_concurrency(requested_concurrency, max_threads); + let max_concurrency = + normalize_concurrency(requested_concurrency, turn.config.agent_max_threads); let base_instructions = session.get_base_instructions().await; let spawn_config = build_agent_spawn_config(&base_instructions, turn.as_ref())?; Ok(JobRunnerOptions { diff --git a/codex-rs/core/src/tools/spec_plan.rs b/codex-rs/core/src/tools/spec_plan.rs index 8bc64b09ee..35f937dd80 100644 --- a/codex-rs/core/src/tools/spec_plan.rs +++ b/codex-rs/core/src/tools/spec_plan.rs @@ -1,4 +1,3 @@ -use crate::guardian::is_guardian_reviewer_source; use crate::session::turn_context::TurnContext; use crate::tools::code_mode::execute_spec::create_code_mode_tool; use crate::tools::context::ToolInvocation; @@ -640,20 +639,6 @@ fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools) { let turn_context = context.turn_context; - if is_guardian_reviewer_source(&turn_context.session_source) - || matches!( - &turn_context.session_source, - SessionSource::SubAgent(SubAgentSource::Review) - ) - || matches!( - &turn_context.session_source, - SessionSource::SubAgent(SubAgentSource::ThreadSpawn { depth, .. }) - if turn_context.multi_agent_version == Some(MultiAgentVersion::V1) - && *depth >= turn_context.config.agent_max_depth - ) - { - return; - } if turn_context.multi_agent_version.is_some() { if turn_context.multi_agent_version == Some(MultiAgentVersion::V2) { let exposure = if turn_context.config.multi_agent_v2.non_code_mode_only { diff --git a/codex-rs/core/src/tools/spec_plan_tests.rs b/codex-rs/core/src/tools/spec_plan_tests.rs index 6ebaa439dc..ed0cd33408 100644 --- a/codex-rs/core/src/tools/spec_plan_tests.rs +++ b/codex-rs/core/src/tools/spec_plan_tests.rs @@ -35,6 +35,7 @@ use serde_json::json; use crate::guardian::GUARDIAN_REVIEWER_NAME; use crate::session::tests::make_session_and_context; use crate::session::turn_context::TurnContext; +use crate::session::turn_context::resolve_multi_agent_version; use crate::tools::handlers::multi_agents_spec::MULTI_AGENT_V1_NAMESPACE; use crate::tools::router::ToolRouter; use crate::tools::router::ToolRouterParams; @@ -882,8 +883,12 @@ async fn multi_agent_version_selector_overrides_feature_flags() { ]); let review = probe(|turn| { - turn.multi_agent_version = Some(MultiAgentVersion::V2); turn.session_source = SessionSource::SubAgent(SubAgentSource::Review); + turn.multi_agent_version = resolve_multi_agent_version( + &turn.model_info, + turn.config.as_ref(), + &turn.session_source, + ); }) .await; review.assert_visible_lacks(&[ @@ -897,9 +902,13 @@ async fn multi_agent_version_selector_overrides_feature_flags() { ]); let guardian_review = probe(|turn| { - turn.multi_agent_version = Some(MultiAgentVersion::V2); turn.session_source = SessionSource::SubAgent(SubAgentSource::Other(GUARDIAN_REVIEWER_NAME.to_string())); + turn.multi_agent_version = resolve_multi_agent_version( + &turn.model_info, + turn.config.as_ref(), + &turn.session_source, + ); }) .await; guardian_review.assert_visible_lacks(&[ @@ -913,7 +922,6 @@ async fn multi_agent_version_selector_overrides_feature_flags() { ]); let max_depth_v1 = probe(|turn| { - turn.multi_agent_version = Some(MultiAgentVersion::V1); turn.session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id: ThreadId::new(), depth: turn.config.agent_max_depth, @@ -921,6 +929,11 @@ async fn multi_agent_version_selector_overrides_feature_flags() { agent_nickname: None, agent_role: None, }); + turn.multi_agent_version = resolve_multi_agent_version( + &turn.model_info, + turn.config.as_ref(), + &turn.session_source, + ); }) .await; max_depth_v1.assert_visible_lacks(&[MULTI_AGENT_V1_NAMESPACE]);