diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 8a66fca0e..6401605ee 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -12,6 +12,7 @@ use crate::CodexAuth; use crate::SandboxState; use crate::agent::AgentControl; use crate::agent::AgentStatus; +use crate::agent::MAX_THREAD_SPAWN_DEPTH; use crate::agent::agent_status_from_event; use crate::compact; use crate::compact::run_inline_auto_compact_task; @@ -52,6 +53,7 @@ use codex_protocol::protocol::RawResponseItemEvent; use codex_protocol::protocol::ReviewRequest; use codex_protocol::protocol::RolloutItem; use codex_protocol::protocol::SessionSource; +use codex_protocol::protocol::SubAgentSource; use codex_protocol::protocol::TurnAbortReason; use codex_protocol::protocol::TurnContextItem; use codex_protocol::protocol::TurnStartedEvent; @@ -240,7 +242,7 @@ impl Codex { /// Spawn a new [`Codex`] and initialize the session. #[allow(clippy::too_many_arguments)] pub(crate) async fn spawn( - config: Config, + mut config: Config, auth_manager: Arc, models_manager: Arc, skills_manager: Arc, @@ -262,6 +264,12 @@ impl Codex { ); } + if let SessionSource::SubAgent(SubAgentSource::ThreadSpawn { depth, .. }) = session_source + && depth >= MAX_THREAD_SPAWN_DEPTH + { + config.features.disable(Feature::Collab); + } + let enabled_skills = loaded_skills.enabled_skills(); let user_instructions = get_user_instructions(&config, Some(&enabled_skills)).await; @@ -3587,6 +3595,7 @@ mod tests { use crate::shell::default_user_shell; use crate::tools::format_exec_output_str; + use codex_protocol::ThreadId; use codex_protocol::models::FunctionCallOutputPayload; use crate::protocol::CompactedItem; diff --git a/codex-rs/core/src/tools/handlers/collab.rs b/codex-rs/core/src/tools/handlers/collab.rs index 83ee53905..00190648f 100644 --- a/codex-rs/core/src/tools/handlers/collab.rs +++ b/codex-rs/core/src/tools/handlers/collab.rs @@ -1,8 +1,10 @@ use crate::agent::AgentStatus; +use crate::agent::exceeds_thread_spawn_depth_limit; use crate::codex::Session; use crate::codex::TurnContext; use crate::config::Config; use crate::error::CodexErr; +use crate::features::Feature; use crate::function_tool::FunctionCallError; use crate::tools::context::ToolInvocation; use crate::tools::context::ToolOutput; @@ -78,7 +80,7 @@ impl ToolHandler for CollabHandler { mod spawn { use super::*; use crate::agent::AgentRole; - use crate::agent::MAX_THREAD_SPAWN_DEPTH; + use crate::agent::exceeds_thread_spawn_depth_limit; use crate::agent::next_thread_spawn_depth; use codex_protocol::protocol::SessionSource; @@ -113,9 +115,9 @@ mod spawn { let session_source = turn.client.get_session_source(); let child_depth = next_thread_spawn_depth(&session_source); if exceeds_thread_spawn_depth_limit(child_depth) { - return Err(FunctionCallError::RespondToModel(format!( - "agent depth limit reached: max depth is {MAX_THREAD_SPAWN_DEPTH}" - ))); + return Err(FunctionCallError::RespondToModel( + "Agent depth limit reached. Solve the task yourself.".to_string(), + )); } session .send_event( @@ -128,8 +130,11 @@ mod spawn { .into(), ) .await; - let mut config = - build_agent_spawn_config(&session.get_base_instructions().await, turn.as_ref())?; + let mut config = build_agent_spawn_config( + &session.get_base_instructions().await, + turn.as_ref(), + child_depth, + )?; agent_role .apply_to_config(&mut config) .map_err(FunctionCallError::RespondToModel)?; @@ -582,6 +587,7 @@ fn collab_agent_error(agent_id: ThreadId, err: CodexErr) -> FunctionCallError { fn build_agent_spawn_config( base_instructions: &BaseInstructions, turn: &TurnContext, + child_depth: i32, ) -> Result { let base_config = turn.client.config(); let mut config = (*base_config).clone(); @@ -607,6 +613,12 @@ fn build_agent_spawn_config( .map_err(|err| { FunctionCallError::RespondToModel(format!("sandbox_policy is invalid: {err}")) })?; + + // If the new agent will be at max depth: + if exceeds_thread_spawn_depth_limit(child_depth + 1) { + config.features.disable(Feature::Collab); + } + Ok(config) } @@ -778,9 +790,9 @@ mod tests { }; assert_eq!( err, - FunctionCallError::RespondToModel(format!( - "agent depth limit reached: max depth is {MAX_THREAD_SPAWN_DEPTH}" - )) + FunctionCallError::RespondToModel( + "Agent depth limit reached. Solve the task yourself.".to_string() + ) ); } @@ -1144,7 +1156,7 @@ mod tests { turn.approval_policy = AskForApproval::Never; turn.sandbox_policy = SandboxPolicy::DangerFullAccess; - let config = build_agent_spawn_config(&base_instructions, &turn).expect("spawn config"); + let config = build_agent_spawn_config(&base_instructions, &turn, 0).expect("spawn config"); let mut expected = (*turn.client.config()).clone(); expected.base_instructions = Some(base_instructions.text); expected.model = Some(turn.client.get_model()); @@ -1189,7 +1201,7 @@ mod tests { text: "base".to_string(), }; - let config = build_agent_spawn_config(&base_instructions, &turn).expect("spawn config"); + let config = build_agent_spawn_config(&base_instructions, &turn, 0).expect("spawn config"); assert_eq!(config.user_instructions, base_config.user_instructions); }