diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index f1f008530c..e70aed2aa6 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -1129,6 +1129,12 @@ impl SessionConfiguration { model: self.collaboration_mode.model().to_string(), model_provider_id: self.original_config_do_not_use.model_provider_id.clone(), service_tier: self.service_tier, + plan_mode_reasoning_effort: self.original_config_do_not_use.plan_mode_reasoning_effort, + model_verbosity: self.original_config_do_not_use.model_verbosity, + model_context_window: self.original_config_do_not_use.model_context_window, + model_auto_compact_token_limit: self + .original_config_do_not_use + .model_auto_compact_token_limit, approval_policy: self.approval_policy.value(), approvals_reviewer: self.approvals_reviewer, sandbox_policy: self.sandbox_policy.get().clone(), @@ -1136,6 +1142,7 @@ impl SessionConfiguration { ephemeral: self.original_config_do_not_use.ephemeral, reasoning_effort: self.collaboration_mode.reasoning_effort(), personality: self.personality, + active_profile: self.original_config_do_not_use.active_profile.clone(), session_source: self.session_source.clone(), } } diff --git a/codex-rs/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs index 8b467fc10b..d6af167125 100644 --- a/codex-rs/core/src/codex_thread.rs +++ b/codex-rs/core/src/codex_thread.rs @@ -12,6 +12,7 @@ use codex_features::Feature; use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::config_types::Personality; use codex_protocol::config_types::ServiceTier; +use codex_protocol::config_types::Verbosity; use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ResponseItem; @@ -33,6 +34,10 @@ pub struct ThreadConfigSnapshot { pub model: String, pub model_provider_id: String, pub service_tier: Option, + pub plan_mode_reasoning_effort: Option, + pub model_verbosity: Option, + pub model_context_window: Option, + pub model_auto_compact_token_limit: Option, pub approval_policy: AskForApproval, pub approvals_reviewer: ApprovalsReviewer, pub sandbox_policy: SandboxPolicy, @@ -40,6 +45,7 @@ pub struct ThreadConfigSnapshot { pub ephemeral: bool, pub reasoning_effort: Option, pub personality: Option, + pub active_profile: Option, pub session_source: SessionSource, } 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 3b55b76a91..2e66f77856 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs @@ -79,6 +79,9 @@ impl ToolHandler for Handler { apply_role_to_config(&mut config, role_name) .await .map_err(FunctionCallError::RespondToModel)?; + if fork_context { + restore_forked_spawn_agent_model_config(&mut config, turn.as_ref()); + } apply_spawn_agent_runtime_overrides(&mut config, turn.as_ref())?; apply_spawn_agent_overrides(&mut config, child_depth); 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 4778f6b9f8..3f987578a2 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_common.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_common.rs @@ -238,6 +238,24 @@ fn build_agent_shared_config(turn: &TurnContext) -> Result ThreadManager { ) } +async fn install_role_with_model_provider_and_profile_override(turn: &mut TurnContext) -> String { + let role_name = "fork-context-role".to_string(); + let role_config_path = turn.config.codex_home.join("fork-context-role.toml"); + tokio::fs::write( + &role_config_path, + r#"developer_instructions = "Forked children should keep the parent model config." +model_provider = "role-provider" +model_context_window = 12345 +model_auto_compact_token_limit = 1234 +model_verbosity = "low" +plan_mode_reasoning_effort = "minimal" +profile = "role-profile" +service_tier = "fast" + +[profiles.role-profile] +model_provider = "role-provider" +"#, + ) + .await + .expect("role config should be written"); + + let mut config = (*turn.config).clone(); + let mut role_provider = + built_in_model_providers(/* openai_base_url */ /*openai_base_url*/ None)["openai"].clone(); + role_provider.name = "Role Provider".to_string(); + config + .model_providers + .insert("role-provider".to_string(), role_provider); + config.service_tier = Some(ServiceTier::Flex); + config.plan_mode_reasoning_effort = Some(ReasoningEffort::High); + config.model_verbosity = Some(Verbosity::High); + config.model_context_window = Some(200_000); + config.model_auto_compact_token_limit = Some(180_000); + config.agent_roles.insert( + role_name.clone(), + AgentRoleConfig { + description: Some("Role with model-provider and profile overrides".to_string()), + config_file: Some(role_config_path), + nickname_candidates: None, + fork_context: None, + }, + ); + turn.config = Arc::new(config); + + role_name +} + fn history_contains_inter_agent_communication( history_items: &[ResponseItem], expected: &InterAgentCommunication, @@ -305,7 +356,8 @@ async fn spawn_agent_uses_explorer_role_and_preserves_approval_policy() { #[tokio::test] async fn spawn_agent_fork_context_ignores_child_model_overrides() { - let (mut session, turn) = make_session_and_context().await; + let (mut session, mut turn) = make_session_and_context().await; + let role_name = install_role_with_model_provider_and_profile_override(&mut turn).await; let manager = thread_manager(); let root = manager .start_thread((*turn.config).clone()) @@ -314,7 +366,15 @@ async fn spawn_agent_fork_context_ignores_child_model_overrides() { session.services.agent_control = manager.agent_control(); session.conversation_id = root.thread_id; let expected_model = turn.model_info.slug.clone(); + let expected_model_provider_id = turn.config.model_provider_id.clone(); + let expected_model_provider_name = turn.provider.name.clone(); + let expected_active_profile = turn.config.active_profile.clone(); let expected_reasoning_effort = turn.reasoning_effort; + let expected_service_tier = turn.config.service_tier; + let expected_plan_mode_reasoning_effort = turn.config.plan_mode_reasoning_effort; + let expected_model_verbosity = turn.config.model_verbosity; + let expected_model_context_window = turn.config.model_context_window; + let expected_model_auto_compact_token_limit = turn.config.model_auto_compact_token_limit; let output = SpawnAgentHandler .handle(invocation( @@ -323,6 +383,7 @@ async fn spawn_agent_fork_context_ignores_child_model_overrides() { "spawn_agent", function_payload(json!({ "message": "inspect this repo", + "agent_type": role_name, "model": "not-a-real-model", "reasoning_effort": "low", "fork_context": true @@ -346,12 +407,27 @@ async fn spawn_agent_fork_context_ignores_child_model_overrides() { .await; assert_eq!(snapshot.model, expected_model); + assert_eq!(snapshot.model_provider_id, expected_model_provider_id); + assert_eq!(snapshot.model_provider.name, expected_model_provider_name); + assert_eq!(snapshot.active_profile, expected_active_profile); assert_eq!(snapshot.reasoning_effort, expected_reasoning_effort); + assert_eq!(snapshot.service_tier, expected_service_tier); + assert_eq!( + snapshot.plan_mode_reasoning_effort, + expected_plan_mode_reasoning_effort + ); + assert_eq!(snapshot.model_verbosity, expected_model_verbosity); + assert_eq!(snapshot.model_context_window, expected_model_context_window); + assert_eq!( + snapshot.model_auto_compact_token_limit, + expected_model_auto_compact_token_limit + ); } #[tokio::test] async fn multi_agent_v2_spawn_fork_turns_ignores_child_model_overrides() { - let (mut session, turn) = make_session_and_context().await; + let (mut session, mut turn) = make_session_and_context().await; + let role_name = install_role_with_model_provider_and_profile_override(&mut turn).await; let manager = thread_manager(); let root = manager .start_thread((*turn.config).clone()) @@ -369,7 +445,15 @@ async fn multi_agent_v2_spawn_fork_turns_ignores_child_model_overrides() { ..turn }; let expected_model = turn.model_info.slug.clone(); + let expected_model_provider_id = turn.config.model_provider_id.clone(); + let expected_model_provider_name = turn.provider.name.clone(); + let expected_active_profile = turn.config.active_profile.clone(); let expected_reasoning_effort = turn.reasoning_effort; + let expected_service_tier = turn.config.service_tier; + let expected_plan_mode_reasoning_effort = turn.config.plan_mode_reasoning_effort; + let expected_model_verbosity = turn.config.model_verbosity; + let expected_model_context_window = turn.config.model_context_window; + let expected_model_auto_compact_token_limit = turn.config.model_auto_compact_token_limit; let output = SpawnAgentHandlerV2 .handle(invocation( @@ -377,7 +461,8 @@ async fn multi_agent_v2_spawn_fork_turns_ignores_child_model_overrides() { Arc::new(turn), "spawn_agent", function_payload(json!({ - "items": [{"type": "text", "text": "inspect this repo"}], + "message": "inspect this repo", + "agent_type": role_name, "model": "not-a-real-model", "reasoning_effort": "low", "fork_turns": "all", @@ -404,7 +489,21 @@ async fn multi_agent_v2_spawn_fork_turns_ignores_child_model_overrides() { .await; assert_eq!(snapshot.model, expected_model); + assert_eq!(snapshot.model_provider_id, expected_model_provider_id); + assert_eq!(snapshot.model_provider.name, expected_model_provider_name); + assert_eq!(snapshot.active_profile, expected_active_profile); assert_eq!(snapshot.reasoning_effort, expected_reasoning_effort); + assert_eq!(snapshot.service_tier, expected_service_tier); + assert_eq!( + snapshot.plan_mode_reasoning_effort, + expected_plan_mode_reasoning_effort + ); + assert_eq!(snapshot.model_verbosity, expected_model_verbosity); + assert_eq!(snapshot.model_context_window, expected_model_context_window); + assert_eq!( + snapshot.model_auto_compact_token_limit, + expected_model_auto_compact_token_limit + ); } #[tokio::test] 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 b8a1f5443e..057ad537ec 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 @@ -81,6 +81,9 @@ impl ToolHandler for Handler { apply_role_to_config(&mut config, role_name) .await .map_err(FunctionCallError::RespondToModel)?; + if fork_mode.is_some() { + restore_forked_spawn_agent_model_config(&mut config, turn.as_ref()); + } apply_spawn_agent_runtime_overrides(&mut config, turn.as_ref())?; apply_spawn_agent_overrides(&mut config, child_depth); diff --git a/codex-rs/core/tests/suite/subagent_notifications.rs b/codex-rs/core/tests/suite/subagent_notifications.rs index 85c2347175..72bf00ed23 100644 --- a/codex-rs/core/tests/suite/subagent_notifications.rs +++ b/codex-rs/core/tests/suite/subagent_notifications.rs @@ -224,7 +224,9 @@ async fn setup_turn_one_with_custom_spawned_child( test.submit_turn(TURN_1_PROMPT).await?; if child_response_delay.is_none() && wait_for_parent_notification { let _ = wait_for_requests(&child_request_log).await?; - let rollout_path = test.codex.rollout_path().expect("rollout path"); + let Some(rollout_path) = test.codex.rollout_path() else { + anyhow::bail!("rollout path"); + }; let deadline = Instant::now() + Duration::from_secs(6); loop { test.codex.ensure_rollout_materialized().await;