diff --git a/codex-rs/core/src/agent/control.rs b/codex-rs/core/src/agent/control.rs index 10be510870..7e87bf73d6 100644 --- a/codex-rs/core/src/agent/control.rs +++ b/codex-rs/core/src/agent/control.rs @@ -5,7 +5,6 @@ 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::multi_agent_runtime::MultiAgentRuntime; use crate::session::emit_subagent_session_started; use crate::session_prefix::format_subagent_context_line; use crate::session_prefix::format_subagent_notification_message; @@ -213,26 +212,13 @@ impl AgentControl { async fn spawn_agent_internal( &self, - mut config: crate::config::Config, + config: crate::config::Config, initial_operation: Op, session_source: Option, options: SpawnAgentOptions, ) -> CodexResult { let state = self.upgrade()?; - let mut multi_agent_runtime = state.resolve_multi_agent_runtime(&config).await; - if let Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { depth, .. })) = - session_source.as_ref() - && *depth >= config.agent_max_depth - && !multi_agent_runtime.multi_agent_v2_enabled() - { - let _ = config.features.disable(Feature::SpawnCsv); - let _ = config.features.disable(Feature::Collab); - multi_agent_runtime.disable_collab_tools(); - } - let mut reservation = self - .state - .reserve_spawn_slot(multi_agent_runtime.agent_max_threads(&config))?; - let multi_agent_v2_enabled = multi_agent_runtime.multi_agent_v2_enabled(); + 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()) .await; @@ -268,7 +254,6 @@ impl AgentControl { Box::pin(self.spawn_forked_thread( &state, config, - multi_agent_runtime, session_source, &options, inherited_shell_snapshot, @@ -354,7 +339,7 @@ impl AgentControl { self.send_input(new_thread.thread_id, initial_operation) .await?; - if !multi_agent_v2_enabled { + if !new_thread.thread.enabled(Feature::MultiAgentV2) { let child_reference = agent_metadata .agent_path .as_ref() @@ -379,7 +364,6 @@ impl AgentControl { &self, state: &Arc, config: crate::config::Config, - multi_agent_runtime: MultiAgentRuntime, session_source: SessionSource, options: &SpawnAgentOptions, inherited_shell_snapshot: Option>, @@ -434,14 +418,8 @@ impl AgentControl { } let multi_agent_v2_usage_hint_texts_to_filter: Vec = if let Some(parent_thread) = parent_thread.as_ref() { - let parent_config = parent_thread.codex.session.get_config().await; - if parent_thread - .codex - .session - .multi_agent_runtime() - .await - .multi_agent_v2_enabled() - { + if parent_thread.enabled(Feature::MultiAgentV2) { + let parent_config = parent_thread.codex.session.get_config().await; [ parent_config .multi_agent_v2 @@ -458,7 +436,7 @@ impl AgentControl { } else { Vec::new() } - } else if multi_agent_runtime.multi_agent_v2_enabled() { + } else if config.features.enabled(Feature::MultiAgentV2) { [ config.multi_agent_v2.root_agent_usage_hint_text.clone(), config.multi_agent_v2.subagent_usage_hint_text.clone(), @@ -494,7 +472,7 @@ impl AgentControl { } } if preserve_reference_context_item - && multi_agent_runtime.multi_agent_v2_enabled() + && config.features.enabled(Feature::MultiAgentV2) && let Some(subagent_usage_hint_text) = config.multi_agent_v2.subagent_usage_hint_text.clone() && let Some(subagent_usage_hint_message) = @@ -603,21 +581,16 @@ impl AgentControl { thread_id: ThreadId, session_source: SessionSource, ) -> CodexResult { - let state = self.upgrade()?; - let mut multi_agent_runtime = state.resolve_multi_agent_runtime(&config).await; if let SessionSource::SubAgent(SubAgentSource::ThreadSpawn { depth, .. }) = &session_source && *depth >= config.agent_max_depth - && !multi_agent_runtime.multi_agent_v2_enabled() + && !config.features.enabled(Feature::MultiAgentV2) { let _ = config.features.disable(Feature::SpawnCsv); let _ = config.features.disable(Feature::Collab); - multi_agent_runtime.disable_collab_tools(); } + let state = self.upgrade()?; let state_db_ctx = state.state_db(); - let mut reservation = self - .state - .reserve_spawn_slot(multi_agent_runtime.agent_max_threads(&config))?; - let multi_agent_v2_enabled = multi_agent_runtime.multi_agent_v2_enabled(); + let mut reservation = self.state.reserve_spawn_slot(config.agent_max_threads)?; let (session_source, agent_metadata) = match session_source { SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id, @@ -686,7 +659,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_enabled { + if !resumed_thread.thread.enabled(Feature::MultiAgentV2) { let child_reference = agent_metadata .agent_path .as_ref() @@ -1071,17 +1044,12 @@ impl AgentControl { }; let child_thread = state.get_thread(child_thread_id).await.ok(); let message = format_subagent_notification_message(child_reference.as_str(), &status); - let child_multi_agent_v2_enabled = if let Some(child_thread) = child_thread.as_ref() { - child_thread - .codex - .session - .multi_agent_runtime() - .await - .multi_agent_v2_enabled() - } else { - true - }; - if child_agent_path.is_some() && child_multi_agent_v2_enabled { + if child_agent_path.is_some() + && child_thread + .as_ref() + .map(|thread| thread.enabled(Feature::MultiAgentV2)) + .unwrap_or(true) + { let Some(child_agent_path) = child_agent_path.clone() else { return; }; diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index aa5784d4fe..286c87b226 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -60,6 +60,7 @@ pub use codex_mcp::SandboxState; mod mcp_openai_file; mod mcp_tool_call; pub(crate) mod mention_syntax; +mod tool_mode; pub(crate) mod utils; pub use mention_syntax::PLUGIN_TEXT_MENTION_SIGIL; pub use mention_syntax::TOOL_MENTION_SIGIL; diff --git a/codex-rs/core/src/multi_agent_runtime.rs b/codex-rs/core/src/multi_agent_runtime.rs deleted file mode 100644 index 439640c2f7..0000000000 --- a/codex-rs/core/src/multi_agent_runtime.rs +++ /dev/null @@ -1,142 +0,0 @@ -use crate::config::Config; -use crate::config::DEFAULT_AGENT_MAX_THREADS; -use crate::config::ManagedFeatures; -use codex_features::Feature; -use codex_models_manager::manager::RefreshStrategy; -use codex_models_manager::manager::SharedModelsManager; -use codex_protocol::openai_models::ModelInfo; -use codex_protocol::openai_models::MultiAgentVersion; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct MultiAgentRuntime { - version: Option, - collab_tools_disabled: bool, -} - -impl MultiAgentRuntime { - #[cfg(test)] - pub(crate) fn from_features(features: &ManagedFeatures) -> Self { - Self { - version: multi_agent_version_from_features(features), - collab_tools_disabled: false, - } - } - - pub(crate) fn resolve(model_info: &ModelInfo, features: &ManagedFeatures) -> Self { - Self { - version: model_info - .multi_agent_version - .or_else(|| multi_agent_version_from_features(features)), - collab_tools_disabled: false, - } - } - - pub(crate) async fn resolve_for_config( - models_manager: &SharedModelsManager, - config: &Config, - ) -> Self { - let model = models_manager - .get_default_model(&config.model, RefreshStrategy::Offline) - .await; - let model_info = models_manager - .get_model_info(&model, &config.to_models_manager_config()) - .await; - Self::resolve(&model_info, &config.features) - } - - pub(crate) fn with_model(self, model_info: &ModelInfo, features: &ManagedFeatures) -> Self { - Self { - collab_tools_disabled: self.collab_tools_disabled, - ..Self::resolve(model_info, features) - } - } - - pub(crate) fn collab_tools_enabled(self) -> bool { - !self.collab_tools_disabled && self.version.is_some() - } - - pub(crate) fn multi_agent_v2_enabled(self) -> bool { - self.version == Some(MultiAgentVersion::V2) - } - - pub(crate) fn agent_max_threads(self, config: &Config) -> Option { - if self.multi_agent_v2_enabled() { - Some( - config - .multi_agent_v2 - .max_concurrent_threads_per_session - .saturating_sub(1), - ) - } else if config.features.enabled(Feature::MultiAgentV2) { - DEFAULT_AGENT_MAX_THREADS - } else { - config.agent_max_threads - } - } - - pub(crate) fn disable_collab_tools(&mut self) { - self.collab_tools_disabled = true; - } -} - -fn multi_agent_version_from_features(features: &ManagedFeatures) -> Option { - if features.enabled(Feature::MultiAgentV2) { - Some(MultiAgentVersion::V2) - } else if features.enabled(Feature::Collab) { - Some(MultiAgentVersion::V1) - } else { - None - } -} - -#[cfg(test)] -mod tests { - use super::*; - use codex_features::Features; - - fn features(enabled: &[Feature]) -> ManagedFeatures { - let mut features = Features::default(); - for feature in enabled { - features.enable(*feature); - } - features.into() - } - - fn model_info(multi_agent_version: Option) -> ModelInfo { - let mut model_info = codex_models_manager::model_info::model_info_from_slug("test-model"); - model_info.multi_agent_version = multi_agent_version; - model_info - } - - #[test] - fn omitted_selector_follows_feature_flags() { - let runtime = MultiAgentRuntime::resolve( - &model_info(/*multi_agent_version*/ None), - &features(&[Feature::MultiAgentV2]), - ); - - assert_eq!( - runtime, - MultiAgentRuntime { - version: Some(MultiAgentVersion::V2), - collab_tools_disabled: false, - } - ); - } - - #[test] - fn explicit_selector_overrides_feature_flags() { - let runtime = MultiAgentRuntime::resolve( - &model_info(Some(MultiAgentVersion::V1)), - &features(&[Feature::MultiAgentV2]), - ); - - assert_eq!( - runtime, - MultiAgentRuntime { - version: Some(MultiAgentVersion::V1), - collab_tools_disabled: false, - } - ); - } -} diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index aae57ce91d..8c535cd728 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -31,7 +31,6 @@ use crate::context::PersonalitySpecInstructions; use crate::default_skill_metadata_budget; use crate::environment_selection::ResolvedTurnEnvironments; use crate::exec_policy::ExecPolicyManager; -use crate::multi_agent_runtime::MultiAgentRuntime; use crate::parse_turn_item; use crate::path_utils::normalize_for_native_workdir; use crate::realtime_conversation::RealtimeConversationManager; @@ -486,6 +485,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) @@ -511,7 +518,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 { @@ -536,19 +543,6 @@ impl Codex { let model_info = models_manager .get_model_info(model.as_str(), &config.to_models_manager_config()) .await; - let mut multi_agent_runtime = MultiAgentRuntime::resolve(&model_info, &config.features); - { - let config = Arc::make_mut(&mut config); - if let SessionSource::SubAgent(SubAgentSource::ThreadSpawn { depth, .. }) = - session_source - && depth >= config.agent_max_depth - && !multi_agent_runtime.multi_agent_v2_enabled() - { - let _ = config.features.disable(Feature::SpawnCsv); - let _ = config.features.disable(Feature::Collab); - multi_agent_runtime.disable_collab_tools(); - } - } let base_instructions = config .base_instructions .clone() @@ -579,7 +573,6 @@ impl Codex { let session_configuration = SessionConfiguration { provider: config.model_provider.clone(), collaboration_mode, - multi_agent_runtime, model_reasoning_summary: config.model_reasoning_summary, service_tier, developer_instructions: config.developer_instructions.clone(), @@ -1457,11 +1450,6 @@ impl Session { .clone() } - pub(crate) async fn multi_agent_runtime(&self) -> MultiAgentRuntime { - let state = self.state.lock().await; - state.session_configuration.multi_agent_runtime - } - pub(crate) async fn provider(&self) -> ModelProviderInfo { let state = self.state.lock().await; state.session_configuration.provider.clone() @@ -1662,7 +1650,7 @@ impl Session { turn_context: &TurnContext, msg: &EventMsg, ) { - if !turn_context.multi_agent_v2_enabled() { + if !self.enabled(Feature::MultiAgentV2) { return; } diff --git a/codex-rs/core/src/session/multi_agents.rs b/codex-rs/core/src/session/multi_agents.rs index 2da193a0f2..2cab13cecd 100644 --- a/codex-rs/core/src/session/multi_agents.rs +++ b/codex-rs/core/src/session/multi_agents.rs @@ -1,4 +1,5 @@ use crate::session::turn_context::TurnContext; +use codex_features::Feature; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; @@ -6,7 +7,7 @@ pub(super) fn usage_hint_text<'a>( turn_context: &'a TurnContext, session_source: &SessionSource, ) -> Option<&'a str> { - if !turn_context.multi_agent_v2_enabled() { + if !turn_context.features.enabled(Feature::MultiAgentV2) { return None; } diff --git a/codex-rs/core/src/session/review.rs b/codex-rs/core/src/session/review.rs index bbb51a1821..ddd9585eb4 100644 --- a/codex-rs/core/src/session/review.rs +++ b/codex-rs/core/src/session/review.rs @@ -1,5 +1,5 @@ use super::*; -use codex_protocol::openai_models::ToolMode; +use crate::tool_mode::resolve_tool_mode; use std::sync::atomic::AtomicBool; /// Spawn a review thread using the given prompt. @@ -48,15 +48,7 @@ pub(super) async fn spawn_review_thread( let mut per_turn_config = (*config).clone(); per_turn_config.model = Some(model.clone()); per_turn_config.features = review_features.clone(); - let tool_mode = model_info.tool_mode.unwrap_or_else(|| { - if per_turn_config.features.enabled(Feature::CodeModeOnly) { - ToolMode::CodeModeOnly - } else if per_turn_config.features.enabled(Feature::CodeMode) { - ToolMode::CodeMode - } else { - ToolMode::Direct - } - }); + let tool_mode = resolve_tool_mode(&model_info, &per_turn_config.features); 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!( diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 1dc7093b59..c9e1807fe6 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -2,7 +2,6 @@ use super::input_queue::InputQueue; use super::*; use crate::config::ConstraintError; use crate::goals::GoalRuntimeState; -use crate::multi_agent_runtime::MultiAgentRuntime; use crate::skills::SkillError; use crate::state::ActiveTurn; use codex_protocol::SessionId; @@ -46,7 +45,6 @@ pub(crate) struct SessionConfiguration { pub(super) provider: ModelProviderInfo, pub(super) collaboration_mode: CollaborationMode, - pub(super) multi_agent_runtime: MultiAgentRuntime, pub(super) model_reasoning_summary: Option, pub(super) service_tier: Option, diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 6da3797fb5..62321756db 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -61,7 +61,6 @@ use crate::goals::ExternalGoalPreviousStatus; use crate::goals::ExternalGoalSet; use crate::goals::GoalRuntimeEvent; use crate::goals::SetGoalRequest; -use crate::multi_agent_runtime::MultiAgentRuntime; use crate::rollout::recorder::RolloutRecorder; use crate::state::ActiveTurn; use crate::state::TaskKind; @@ -3072,7 +3071,6 @@ async fn set_rate_limits_retains_previous_credits() { let session_configuration = SessionConfiguration { provider: config.model_provider.clone(), collaboration_mode, - multi_agent_runtime: MultiAgentRuntime::from_features(&config.features), model_reasoning_summary: config.model_reasoning_summary, developer_instructions: config.developer_instructions.clone(), user_instructions: config.user_instructions.clone(), @@ -3178,7 +3176,6 @@ async fn set_rate_limits_updates_plan_type_when_present() { let session_configuration = SessionConfiguration { provider: config.model_provider.clone(), collaboration_mode, - multi_agent_runtime: MultiAgentRuntime::from_features(&config.features), model_reasoning_summary: config.model_reasoning_summary, developer_instructions: config.developer_instructions.clone(), user_instructions: config.user_instructions.clone(), @@ -3707,7 +3704,6 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati SessionConfiguration { provider: config.model_provider.clone(), collaboration_mode, - multi_agent_runtime: MultiAgentRuntime::from_features(&config.features), model_reasoning_summary: config.model_reasoning_summary, developer_instructions: config.developer_instructions.clone(), user_instructions: config.user_instructions.clone(), @@ -4452,7 +4448,6 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() { let session_configuration = SessionConfiguration { provider: config.model_provider.clone(), collaboration_mode, - multi_agent_runtime: MultiAgentRuntime::from_features(&config.features), model_reasoning_summary: config.model_reasoning_summary, developer_instructions: config.developer_instructions.clone(), user_instructions: config.user_instructions.clone(), @@ -4563,7 +4558,6 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { let session_configuration = SessionConfiguration { provider: config.model_provider.clone(), collaboration_mode, - multi_agent_runtime: MultiAgentRuntime::from_features(&config.features), model_reasoning_summary: config.model_reasoning_summary, developer_instructions: config.developer_instructions.clone(), user_instructions: config.user_instructions.clone(), @@ -4799,7 +4793,6 @@ async fn make_session_with_config_and_rx( let session_configuration = SessionConfiguration { provider: config.model_provider.clone(), collaboration_mode, - multi_agent_runtime: MultiAgentRuntime::from_features(&config.features), model_reasoning_summary: config.model_reasoning_summary, developer_instructions: config.developer_instructions.clone(), user_instructions: config.user_instructions.clone(), @@ -4904,7 +4897,6 @@ async fn make_session_with_history_source_and_agent_control_and_rx( let session_configuration = SessionConfiguration { provider: config.model_provider.clone(), collaboration_mode, - multi_agent_runtime: MultiAgentRuntime::from_features(&config.features), model_reasoning_summary: config.model_reasoning_summary, developer_instructions: config.developer_instructions.clone(), user_instructions: config.user_instructions.clone(), @@ -6409,7 +6401,6 @@ where let session_configuration = SessionConfiguration { provider: config.model_provider.clone(), collaboration_mode, - multi_agent_runtime: MultiAgentRuntime::from_features(&config.features), model_reasoning_summary: config.model_reasoning_summary, developer_instructions: config.developer_instructions.clone(), user_instructions: config.user_instructions.clone(), diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index 638bb91198..ee67e915c2 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::tool_mode::resolve_tool_mode; use codex_model_provider::SharedModelProvider; use codex_model_provider::create_model_provider; use codex_protocol::SessionId; @@ -164,6 +165,14 @@ impl TurnContext { self.goal_tools_supported && self.features.get().enabled(Feature::Goals) } + pub(crate) fn code_mode_enabled(&self) -> bool { + matches!(self.tool_mode, ToolMode::CodeMode | ToolMode::CodeModeOnly) + } + + pub(crate) fn code_mode_only_enabled(&self) -> bool { + self.tool_mode == ToolMode::CodeModeOnly + } + pub(crate) async fn with_model( &self, model: String, @@ -174,15 +183,7 @@ impl TurnContext { let model_info = models_manager .get_model_info(model.as_str(), &config.to_models_manager_config()) .await; - let tool_mode = model_info.tool_mode.unwrap_or_else(|| { - if config.features.enabled(Feature::CodeModeOnly) { - ToolMode::CodeModeOnly - } else if config.features.enabled(Feature::CodeMode) { - ToolMode::CodeMode - } else { - ToolMode::Direct - } - }); + let tool_mode = resolve_tool_mode(&model_info, &config.features); let truncation_policy = model_info.truncation_policy.into(); let supported_reasoning_levels = model_info .supported_reasoning_levels @@ -487,15 +488,7 @@ impl Session { ); let mut per_turn_config = per_turn_config; - let tool_mode = model_info.tool_mode.unwrap_or_else(|| { - if per_turn_config.features.enabled(Feature::CodeModeOnly) { - ToolMode::CodeModeOnly - } else if per_turn_config.features.enabled(Feature::CodeMode) { - ToolMode::CodeMode - } else { - ToolMode::Direct - } - }); + let tool_mode = resolve_tool_mode(&model_info, &per_turn_config.features); per_turn_config.service_tier = get_service_tier( per_turn_config.service_tier, per_turn_config.features.enabled(Feature::FastMode), diff --git a/codex-rs/core/src/tasks/mod.rs b/codex-rs/core/src/tasks/mod.rs index 6938bb2252..38a8a96276 100644 --- a/codex-rs/core/src/tasks/mod.rs +++ b/codex-rs/core/src/tasks/mod.rs @@ -27,7 +27,6 @@ use crate::goals::GoalRuntimeEvent; use crate::hook_runtime::inspect_pending_input; use crate::hook_runtime::record_additional_contexts; use crate::hook_runtime::record_pending_input; -use crate::multi_agent_runtime::MultiAgentRuntime; use crate::session::TurnInput; use crate::session::session::Session; use crate::session::turn_context::TurnContext; @@ -71,11 +70,11 @@ pub(crate) enum InterruptedTurnHistoryMarker { } impl InterruptedTurnHistoryMarker { - pub(crate) fn from_config(config: &Config, multi_agent_runtime: MultiAgentRuntime) -> Self { + pub(crate) fn from_config(config: &Config) -> Self { if !config.agent_interrupt_message_enabled { return Self::Disabled; } - if multi_agent_runtime.multi_agent_v2_enabled() { + if config.features.enabled(Feature::MultiAgentV2) { Self::Developer } else { Self::ContextualUser @@ -841,11 +840,9 @@ 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_runtime, - )) + && let Some(marker) = interrupted_turn_history_marker( + InterruptedTurnHistoryMarker::from_config(task.turn_context.config.as_ref()), + ) { 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 0f8caf10f8..3eec8315b1 100644 --- a/codex-rs/core/src/thread_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -7,7 +7,6 @@ use crate::config::ThreadStoreConfig; use crate::environment_selection::default_thread_environment_selections; use crate::environment_selection::resolve_environment_selections; use crate::mcp::McpManager; -use crate::multi_agent_runtime::MultiAgentRuntime; use crate::rollout::truncation; use crate::session::Codex; use crate::session::CodexSpawnArgs; @@ -638,11 +637,10 @@ impl ThreadManager { )) })?; let history = stored_thread_to_initial_history(stored_thread, fork_source.rollout_path())?; - let multi_agent_runtime = self.resolve_multi_agent_runtime(&options.config).await; options.initial_history = fork_history_from_snapshot( ForkSnapshot::Interrupted, history, - InterruptedTurnHistoryMarker::from_config(&options.config, multi_agent_runtime), + InterruptedTurnHistoryMarker::from_config(&options.config), ); self.start_thread_with_options_and_fork_source(options, Some(forked_from_thread_id)) .await @@ -897,9 +895,7 @@ impl ThreadManager { InitialHistory::Forked(_) => history.forked_from_id(), InitialHistory::New | InitialHistory::Cleared => None, }; - let multi_agent_runtime = self.resolve_multi_agent_runtime(&config).await; - let interrupted_marker = - InterruptedTurnHistoryMarker::from_config(&config, multi_agent_runtime); + 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(), @@ -926,10 +922,6 @@ impl ThreadManager { AgentControl::new(Arc::downgrade(&self.state)) } - async fn resolve_multi_agent_runtime(&self, config: &Config) -> MultiAgentRuntime { - self.state.resolve_multi_agent_runtime(config).await - } - #[cfg(test)] pub(crate) fn captured_ops(&self) -> Vec<(ThreadId, Op)> { self.state @@ -941,10 +933,6 @@ impl ThreadManager { } impl ThreadManagerState { - pub(crate) async fn resolve_multi_agent_runtime(&self, config: &Config) -> MultiAgentRuntime { - MultiAgentRuntime::resolve_for_config(&self.models_manager, config).await - } - pub(crate) fn state_db(&self) -> Option { self.state_db.clone() } diff --git a/codex-rs/core/src/tools/handlers/agent_jobs.rs b/codex-rs/core/src/tools/handlers/agent_jobs.rs index 28ce32f51a..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 agent_max_threads = turn.multi_agent_runtime.agent_max_threads(&turn.config); - if agent_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, agent_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/handlers/multi_agents/spawn.rs b/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs index 9fda80fcc8..a6461b3581 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs @@ -109,7 +109,7 @@ async fn handle_spawn_agent( ) .await?; apply_spawn_agent_runtime_overrides(&mut config, turn.as_ref())?; - apply_spawn_agent_overrides(&mut config, turn.multi_agent_runtime, child_depth); + apply_spawn_agent_overrides(&mut config, child_depth); let result = Box::pin(session.services.agent_control.spawn_agent_with_metadata( config, diff --git a/codex-rs/core/src/tools/handlers/multi_agents_common.rs b/codex-rs/core/src/tools/handlers/multi_agents_common.rs index 31c1bfc31a..f1e7bcd429 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_common.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_common.rs @@ -3,7 +3,6 @@ use crate::config::Config; use crate::config::DEFAULT_MULTI_AGENT_V2_MIN_WAIT_TIMEOUT_MS; use crate::config::HARD_MAX_MULTI_AGENT_V2_TIMEOUT_MS; use crate::function_tool::FunctionCallError; -use crate::multi_agent_runtime::MultiAgentRuntime; use crate::session::session::Session; use crate::session::turn_context::TurnContext; use crate::tools::context::FunctionToolOutput; @@ -217,7 +216,7 @@ pub(crate) fn build_agent_resume_config( child_depth: i32, ) -> Result { let mut config = build_agent_shared_config(turn)?; - apply_spawn_agent_overrides(&mut config, turn.multi_agent_runtime, child_depth); + apply_spawn_agent_overrides(&mut config, child_depth); // For resume, keep base instructions sourced from rollout/session metadata. config.base_instructions = None; Ok(config) @@ -281,12 +280,8 @@ pub(crate) fn apply_spawn_agent_runtime_overrides( Ok(()) } -pub(crate) fn apply_spawn_agent_overrides( - config: &mut Config, - multi_agent_runtime: MultiAgentRuntime, - child_depth: i32, -) { - if child_depth >= config.agent_max_depth && !multi_agent_runtime.multi_agent_v2_enabled() { +pub(crate) fn apply_spawn_agent_overrides(config: &mut Config, child_depth: i32) { + if child_depth >= config.agent_max_depth && !config.features.enabled(Feature::MultiAgentV2) { let _ = config.features.disable(Feature::SpawnCsv); let _ = config.features.disable(Feature::Collab); } @@ -315,6 +310,7 @@ pub(crate) async fn apply_requested_spawn_agent_model_overrides( .models_manager .get_model_info(&selected_model_name, &config.to_models_manager_config()) .await; + config.model = Some(selected_model_name.clone()); if let Some(reasoning_effort) = requested_reasoning_effort { validate_spawn_agent_reasoning_effort( @@ -368,6 +364,7 @@ pub(crate) async fn apply_spawn_agent_service_tier( .models_manager .get_model_info(model.as_str(), &config.to_models_manager_config()) .await; + if let Some(requested_service_tier) = requested_service_tier && !model_info.supports_service_tier(requested_service_tier) { diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs index ae41543893..9a4e39195d 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs @@ -108,7 +108,7 @@ async fn handle_spawn_agent( ) .await?; apply_spawn_agent_runtime_overrides(&mut config, turn.as_ref())?; - apply_spawn_agent_overrides(&mut config, turn.multi_agent_runtime, child_depth); + apply_spawn_agent_overrides(&mut config, child_depth); let spawn_source = thread_spawn_source( session.conversation_id, diff --git a/codex-rs/core/src/tools/spec_plan.rs b/codex-rs/core/src/tools/spec_plan.rs index fcfb80f47c..b81b4d41dd 100644 --- a/codex-rs/core/src/tools/spec_plan.rs +++ b/codex-rs/core/src/tools/spec_plan.rs @@ -60,7 +60,7 @@ use codex_mcp::ToolInfo; use codex_protocol::dynamic_tools::DynamicToolSpec; use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::InputModality; -use codex_protocol::openai_models::ToolMode; +use codex_protocol::openai_models::MultiAgentVersion; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; use codex_tools::DiscoverableTool; @@ -231,10 +231,8 @@ fn spec_for_model_request( exposure: ToolExposure, spec: ToolSpec, ) -> ToolSpec { - if matches!( - turn_context.tool_mode, - ToolMode::CodeMode | ToolMode::CodeModeOnly - ) && exposure != ToolExposure::DirectModelOnly + if code_mode_enabled(turn_context) + && exposure != ToolExposure::DirectModelOnly && codex_code_mode::is_code_mode_nested_tool(spec.name()) { codex_tools::augment_tool_spec_for_code_mode(spec) @@ -285,12 +283,32 @@ fn namespace_tools_enabled(turn_context: &TurnContext) -> bool { turn_context.provider.capabilities().namespace_tools } +fn code_mode_enabled(turn_context: &TurnContext) -> bool { + turn_context.code_mode_enabled() +} + +fn code_mode_only_enabled(turn_context: &TurnContext) -> bool { + turn_context.code_mode_only_enabled() +} + fn multi_agent_v2_enabled(turn_context: &TurnContext) -> bool { - turn_context.multi_agent_v2_enabled() + multi_agent_version(turn_context) == Some(MultiAgentVersion::V2) } fn collab_tools_enabled(turn_context: &TurnContext) -> bool { - turn_context.collab_tools_enabled() + multi_agent_version(turn_context).is_some() +} + +fn multi_agent_version(turn_context: &TurnContext) -> Option { + turn_context.model_info.multi_agent_version.or_else(|| { + if turn_context.features.get().enabled(Feature::MultiAgentV2) { + Some(MultiAgentVersion::V2) + } else if turn_context.features.get().enabled(Feature::Collab) { + Some(MultiAgentVersion::V1) + } else { + None + } + }) } fn goal_tools_enabled(turn_context: &TurnContext) -> bool { @@ -393,7 +411,7 @@ fn is_hidden_by_code_mode_only( tool_name: &ToolName, exposure: ToolExposure, ) -> bool { - turn_context.tool_mode == ToolMode::CodeModeOnly + code_mode_only_enabled(turn_context) && exposure != ToolExposure::DirectModelOnly && codex_code_mode::is_code_mode_nested_tool(&codex_tools::code_mode_name_for_tool_name( tool_name, @@ -405,10 +423,7 @@ fn build_code_mode_executors( executors: &[Arc], deferred_tools_available: bool, ) -> Vec> { - if !matches!( - turn_context.tool_mode, - ToolMode::CodeMode | ToolMode::CodeModeOnly - ) { + if !code_mode_enabled(turn_context) { return vec![]; } @@ -442,7 +457,7 @@ fn build_code_mode_executors( create_code_mode_tool( &enabled_tools, &namespace_descriptions, - turn_context.tool_mode == ToolMode::CodeModeOnly, + code_mode_only_enabled(turn_context), deferred_tools_available, ), code_mode_nested_tool_specs, @@ -845,10 +860,7 @@ fn append_extension_tool_executors( .iter() .map(|executor| executor.tool_name()) .collect::>(); - if matches!( - turn_context.tool_mode, - ToolMode::CodeMode | ToolMode::CodeModeOnly - ) { + if code_mode_enabled(turn_context) { reserved_tool_names.insert(ToolName::plain(codex_code_mode::PUBLIC_TOOL_NAME)); reserved_tool_names.insert(ToolName::plain(codex_code_mode::WAIT_TOOL_NAME)); } diff --git a/codex-rs/core/src/tools/spec_plan_tests.rs b/codex-rs/core/src/tools/spec_plan_tests.rs index e059b5f4af..bf781c80eb 100644 --- a/codex-rs/core/src/tools/spec_plan_tests.rs +++ b/codex-rs/core/src/tools/spec_plan_tests.rs @@ -13,6 +13,7 @@ use codex_protocol::dynamic_tools::DynamicToolSpec; use codex_protocol::openai_models::ApplyPatchToolType; use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::InputModality; +use codex_protocol::openai_models::MultiAgentVersion; use codex_protocol::openai_models::ToolMode; use codex_protocol::openai_models::WebSearchToolType; use codex_protocol::protocol::SessionSource; @@ -32,6 +33,7 @@ use serde_json::json; use crate::session::tests::make_session_and_context; use crate::session::turn_context::TurnContext; +use crate::tool_mode::resolve_tool_mode; use crate::tools::handlers::multi_agents_spec::MULTI_AGENT_V1_NAMESPACE; use crate::tools::router::ToolRouter; use crate::tools::router::ToolRouterParams; @@ -216,15 +218,7 @@ fn set_feature(turn: &mut TurnContext, feature: Feature, enabled: bool) { .expect("test feature should be disableable in config"); } turn.config = Arc::new(config); - turn.tool_mode = turn.model_info.tool_mode.unwrap_or_else(|| { - if turn.config.features.enabled(Feature::CodeModeOnly) { - ToolMode::CodeModeOnly - } else if turn.config.features.enabled(Feature::CodeMode) { - ToolMode::CodeMode - } else { - ToolMode::Direct - } - }); + resolve_tool_mode_for_turn(turn); } fn set_features(turn: &mut TurnContext, features: &[Feature]) { @@ -239,6 +233,10 @@ fn update_config(turn: &mut TurnContext, update: impl FnOnce(&mut crate::config: turn.config = Arc::new(config); } +fn resolve_tool_mode_for_turn(turn: &mut TurnContext) { + turn.tool_mode = resolve_tool_mode(&turn.model_info, &turn.config.features); +} + fn set_web_search_mode(turn: &mut TurnContext, mode: WebSearchMode) { update_config(turn, |config| { config @@ -812,7 +810,7 @@ async fn tool_mode_selector_overrides_feature_flags() { let direct = probe(|turn| { set_features(turn, &[Feature::CodeMode, Feature::CodeModeOnly]); turn.model_info.tool_mode = Some(ToolMode::Direct); - turn.tool_mode = ToolMode::Direct; + resolve_tool_mode_for_turn(turn); }) .await; direct.assert_visible_lacks(&[ @@ -821,6 +819,40 @@ async fn tool_mode_selector_overrides_feature_flags() { ]); } +#[tokio::test] +async fn multi_agent_version_selector_overrides_feature_flags() { + let v1 = probe(|turn| { + set_feature(turn, Feature::MultiAgentV2, /*enabled*/ true); + turn.model_info.multi_agent_version = Some(MultiAgentVersion::V1); + }) + .await; + + v1.assert_visible_contains(&[ + "spawn_agent", + "send_input", + "resume_agent", + "wait_agent", + "close_agent", + ]); + v1.assert_visible_lacks(&["send_message", "followup_task", "list_agents"]); + + let v2 = probe(|turn| { + set_feature(turn, Feature::Collab, /*enabled*/ false); + set_feature(turn, Feature::MultiAgentV2, /*enabled*/ false); + turn.model_info.multi_agent_version = Some(MultiAgentVersion::V2); + }) + .await; + + v2.assert_visible_contains(&[ + "spawn_agent", + "send_message", + "followup_task", + "wait_agent", + "close_agent", + "list_agents", + ]); +} + #[tokio::test] async fn v1_multi_agent_tools_defer_when_tool_search_available() { let plan = probe(|turn| {