diff --git a/codex-rs/core/src/agent/control.rs b/codex-rs/core/src/agent/control.rs index b49d71d824..e21326a6dc 100644 --- a/codex-rs/core/src/agent/control.rs +++ b/codex-rs/core/src/agent/control.rs @@ -215,7 +215,7 @@ 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, @@ -228,12 +228,10 @@ impl AgentControl { options.multi_agent_version, ) .await; - if let Some(multi_agent_version) = inherited_multi_agent_version { - config - .apply_multi_agent_version(multi_agent_version) - .map_err(|err| CodexErr::InvalidRequest(err.to_string()))?; - } - let mut reservation = self.state.reserve_spawn_slot(config.agent_max_threads)?; + let agent_max_threads = config + .effective_agent_max_threads(inherited_multi_agent_version.flatten()) + .map_err(|err| CodexErr::InvalidRequest(err.to_string()))?; + let mut reservation = self.state.reserve_spawn_slot(agent_max_threads)?; let inherited_shell_snapshot = self .inherited_shell_snapshot_for_source(&state, session_source.as_ref()) .await; @@ -614,7 +612,7 @@ impl AgentControl { async fn resume_single_agent_from_rollout( &self, - mut config: crate::config::Config, + config: crate::config::Config, thread_id: ThreadId, session_source: SessionSource, ) -> CodexResult { @@ -652,10 +650,10 @@ impl AgentControl { inherited_multi_agent_version, ) .await; - config - .apply_multi_agent_version(multi_agent_version) + let agent_max_threads = config + .effective_agent_max_threads(multi_agent_version) .map_err(|err| CodexErr::InvalidRequest(err.to_string()))?; - let mut reservation = self.state.reserve_spawn_slot(config.agent_max_threads)?; + let mut reservation = self.state.reserve_spawn_slot(agent_max_threads)?; let (session_source, agent_metadata) = match session_source { SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id, diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 3f22670e28..ac0bad9230 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -86,6 +86,7 @@ use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::permissions::NetworkSandboxPolicy; +use codex_protocol::protocol::MultiAgentVersion; use codex_protocol::protocol::NetworkAccess; use codex_protocol::protocol::RealtimeVoice; use codex_protocol::protocol::SandboxPolicy; @@ -9801,7 +9802,13 @@ non_code_mode_only = true assert_eq!(config.multi_agent_v2.min_wait_timeout_ms, 2500); assert_eq!(config.multi_agent_v2.max_wait_timeout_ms, 120000); assert_eq!(config.multi_agent_v2.default_wait_timeout_ms, 30000); - assert_eq!(config.agent_max_threads, Some(4)); + assert_eq!( + ( + config.agent_max_threads, + config.effective_agent_max_threads(Some(MultiAgentVersion::V2))? + ), + (None, Some(4)) + ); assert!(!config.multi_agent_v2.usage_hint_enabled); assert_eq!( config.multi_agent_v2.usage_hint_text.as_deref(), @@ -9845,7 +9852,13 @@ enabled = true assert_eq!(config.multi_agent_v2.min_wait_timeout_ms, 10_000); assert_eq!(config.multi_agent_v2.max_wait_timeout_ms, 3_600_000); assert_eq!(config.multi_agent_v2.default_wait_timeout_ms, 30_000); - assert_eq!(config.agent_max_threads, Some(3)); + assert_eq!( + ( + config.agent_max_threads, + config.effective_agent_max_threads(Some(MultiAgentVersion::V2))? + ), + (None, Some(3)) + ); assert_eq!( config.multi_agent_v2.root_agent_usage_hint_text.as_deref(), Some(DEFAULT_MULTI_AGENT_V2_ROOT_AGENT_USAGE_HINT_TEXT) @@ -9918,7 +9931,7 @@ async fn multi_agent_version_resolution_prefers_v2_then_v1_then_disabled() -> st .expect("test config should allow feature update"); assert_eq!( config.multi_agent_version_from_features(), - Some(codex_protocol::protocol::MultiAgentVersion::V1) + Some(MultiAgentVersion::V1) ); config @@ -9927,7 +9940,7 @@ async fn multi_agent_version_resolution_prefers_v2_then_v1_then_disabled() -> st .expect("test config should allow feature update"); assert_eq!( config.multi_agent_version_from_features(), - Some(codex_protocol::protocol::MultiAgentVersion::V2) + Some(MultiAgentVersion::V2) ); Ok(()) @@ -9946,13 +9959,13 @@ max_threads = 3 "#, )?; - let mut config = ConfigBuilder::without_managed_config_for_tests() + let config = ConfigBuilder::without_managed_config_for_tests() .codex_home(codex_home.path().to_path_buf()) .fallback_cwd(Some(codex_home.path().to_path_buf())) .build() .await?; let err = config - .apply_multi_agent_version(Some(codex_protocol::protocol::MultiAgentVersion::V2)) + .effective_agent_max_threads(Some(MultiAgentVersion::V2)) .expect_err("agents.max_threads should conflict with multi_agent_v2"); assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); @@ -10217,7 +10230,13 @@ max_concurrent_threads_per_session = 1 .await?; assert_eq!(config.multi_agent_v2.max_concurrent_threads_per_session, 1); - assert_eq!(config.agent_max_threads, Some(0)); + assert_eq!( + ( + config.agent_max_threads, + config.effective_agent_max_threads(Some(MultiAgentVersion::V2))? + ), + (None, Some(0)) + ); Ok(()) } diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 465f00fb5d..8dd069b49e 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -824,10 +824,8 @@ pub struct Config { /// Token budget applied when storing tool/function outputs in the context manager. pub tool_output_token_limit: Option, - /// Maximum number of agent threads that can be open concurrently. + /// User-configured maximum number of agent threads that can be open concurrently. pub agent_max_threads: Option, - /// User-configured legacy thread limit before applying the locked multi-agent runtime. - pub configured_agent_max_threads: Option, /// Maximum runtime in seconds for agent job workers before they are failed. pub agent_job_max_runtime_seconds: Option, @@ -1294,29 +1292,28 @@ impl Config { } } - pub(crate) fn apply_multi_agent_version( - &mut self, + pub(crate) fn effective_agent_max_threads( + &self, multi_agent_version: Option, - ) -> std::io::Result<()> { - self.agent_max_threads = match multi_agent_version { + ) -> std::io::Result> { + match multi_agent_version { Some(MultiAgentVersion::V2) => { - if self.configured_agent_max_threads.is_some() { + if self.agent_max_threads.is_some() { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, "agents.max_threads cannot be set when multi_agent_v2 is enabled", )); } - Some( + Ok(Some( self.multi_agent_v2 .max_concurrent_threads_per_session .saturating_sub(1), - ) + )) } - None | Some(MultiAgentVersion::V1) => self - .configured_agent_max_threads - .or(DEFAULT_AGENT_MAX_THREADS), - }; - Ok(()) + None | Some(MultiAgentVersion::V1) => { + Ok(self.agent_max_threads.or(DEFAULT_AGENT_MAX_THREADS)) + } + } } pub fn legacy_sandbox_policy(&self) -> SandboxPolicy { @@ -3101,23 +3098,13 @@ impl Config { )); } validate_multi_agent_v2_tool_namespace(multi_agent_v2.tool_namespace.as_deref())?; - let configured_agent_max_threads = - cfg.agents.as_ref().and_then(|agents| agents.max_threads); - if configured_agent_max_threads == Some(0) { + let agent_max_threads = cfg.agents.as_ref().and_then(|agents| agents.max_threads); + if agent_max_threads == Some(0) { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, "agents.max_threads must be at least 1", )); } - let agent_max_threads = if features.enabled(Feature::MultiAgentV2) { - Some( - multi_agent_v2 - .max_concurrent_threads_per_session - .saturating_sub(1), - ) - } else { - configured_agent_max_threads.or(DEFAULT_AGENT_MAX_THREADS) - }; let agent_max_depth = cfg .agents .as_ref() @@ -3489,7 +3476,6 @@ impl Config { .collect(), tool_output_token_limit: cfg.tool_output_token_limit, agent_max_threads, - configured_agent_max_threads, agent_max_depth, agent_roles, memories: memories_config, diff --git a/codex-rs/core/src/session/config_lock.rs b/codex-rs/core/src/session/config_lock.rs index a614d6735e..cbb03408bc 100644 --- a/codex-rs/core/src/session/config_lock.rs +++ b/codex-rs/core/src/session/config_lock.rs @@ -172,7 +172,7 @@ fn save_config_resolved_fields( agents.max_threads = if sc.multi_agent_version == Some(MultiAgentVersion::V2) { None } else { - config.agent_max_threads + config.effective_agent_max_threads(sc.multi_agent_version)? }; agents.max_depth = Some(config.agent_max_depth); agents.job_max_runtime_seconds = config.agent_job_max_runtime_seconds; diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 57a3f42d3c..ab1a6186dc 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -489,8 +489,8 @@ impl Codex { let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY); let (tx_event, rx_event) = async_channel::unbounded(); - config - .apply_multi_agent_version(multi_agent_version) + let _ = config + .effective_agent_max_threads(multi_agent_version) .map_err(|err| CodexErr::InvalidRequest(err.to_string()))?; let primary_environment = environment_selections.primary_environment(); diff --git a/codex-rs/core/src/tools/handlers/agent_jobs.rs b/codex-rs/core/src/tools/handlers/agent_jobs.rs index 7660b66805..1ba8de62fb 100644 --- a/codex-rs/core/src/tools/handlers/agent_jobs.rs +++ b/codex-rs/core/src/tools/handlers/agent_jobs.rs @@ -119,13 +119,16 @@ async fn build_runner_options( "agent depth limit reached; this session cannot spawn more subagents".to_string(), )); } - if turn.config.agent_max_threads == Some(0) { + let agent_max_threads = turn + .config + .effective_agent_max_threads(turn.multi_agent_version) + .map_err(|err| FunctionCallError::Fatal(err.to_string()))?; + if 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, turn.config.agent_max_threads); + let max_concurrency = normalize_concurrency(requested_concurrency, 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/thread-manager-sample/src/main.rs b/codex-rs/thread-manager-sample/src/main.rs index b062a963a9..9305a6f668 100644 --- a/codex-rs/thread-manager-sample/src/main.rs +++ b/codex-rs/thread-manager-sample/src/main.rs @@ -224,7 +224,6 @@ fn new_config(model: Option, arg0_paths: Arg0DispatchPaths) -> anyhow::R project_doc_fallback_filenames: Vec::new(), tool_output_token_limit: None, agent_max_threads: Some(6), - configured_agent_max_threads: Some(6), agent_job_max_runtime_seconds: None, agent_interrupt_message_enabled: false, agent_max_depth: 1,