From be051e056e23d0945f286f8b3784b7971f6d02ee Mon Sep 17 00:00:00 2001 From: Friel Date: Fri, 27 Mar 2026 16:29:37 -0700 Subject: [PATCH] feat(core): force forked agents to inherit parent model Co-authored-by: Codex --- .../src/tools/handlers/multi_agents/spawn.rs | 18 ++-- .../src/tools/handlers/multi_agents_tests.rs | 92 +++++++++++++++++++ .../tools/handlers/multi_agents_v2/spawn.rs | 18 ++-- 3 files changed, 112 insertions(+), 16 deletions(-) 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 9bb7b6055c..a29d4d172a 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs @@ -61,14 +61,16 @@ impl ToolHandler for Handler { .await; let mut config = build_agent_spawn_config(&session.get_base_instructions().await, turn.as_ref())?; - apply_requested_spawn_agent_model_overrides( - &session, - turn.as_ref(), - &mut config, - args.model.as_deref(), - args.reasoning_effort, - ) - .await?; + if !args.fork_context { + apply_requested_spawn_agent_model_overrides( + &session, + turn.as_ref(), + &mut config, + args.model.as_deref(), + args.reasoning_effort, + ) + .await?; + } apply_role_to_config(&mut config, role_name) .await .map_err(FunctionCallError::RespondToModel)?; diff --git a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs index 8006ee2204..676a187dfc 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs @@ -295,6 +295,98 @@ async fn spawn_agent_uses_explorer_role_and_preserves_approval_policy() { assert_eq!(snapshot.model_provider_id, "ollama"); } +#[tokio::test] +async fn spawn_agent_fork_context_ignores_child_model_overrides() { + let (mut session, turn) = make_session_and_context().await; + let manager = thread_manager(); + let root = manager + .start_thread((*turn.config).clone()) + .await + .expect("root thread should start"); + session.services.agent_control = manager.agent_control(); + session.conversation_id = root.thread_id; + let expected_model = turn.model_info.slug.clone(); + let expected_reasoning_effort = turn.reasoning_effort; + + let output = SpawnAgentHandler + .handle(invocation( + Arc::new(session), + Arc::new(turn), + "spawn_agent", + function_payload(json!({ + "message": "inspect this repo", + "model": "not-a-real-model", + "reasoning_effort": "low", + "fork_context": true + })), + )) + .await + .expect("spawn_agent should succeed"); + let (content, _) = expect_text_output(output); + let result: serde_json::Value = + serde_json::from_str(&content).expect("spawn_agent result should be json"); + let agent_id = parse_agent_id( + result["agent_id"] + .as_str() + .expect("spawn_agent result should include agent_id"), + ); + let snapshot = manager + .get_thread(agent_id) + .await + .expect("spawned agent thread should exist") + .config_snapshot() + .await; + + assert_eq!(snapshot.model, expected_model); + assert_eq!(snapshot.reasoning_effort, expected_reasoning_effort); +} + +#[tokio::test] +async fn multi_agent_v2_spawn_fork_context_ignores_child_model_overrides() { + let (mut session, turn) = make_session_and_context().await; + let manager = thread_manager(); + let root = manager + .start_thread((*turn.config).clone()) + .await + .expect("root thread should start"); + session.services.agent_control = manager.agent_control(); + session.conversation_id = root.thread_id; + let expected_model = turn.model_info.slug.clone(); + let expected_reasoning_effort = turn.reasoning_effort; + + let output = SpawnAgentHandlerV2 + .handle(invocation( + Arc::new(session), + Arc::new(turn), + "spawn_agent", + function_payload(json!({ + "message": "inspect this repo", + "model": "not-a-real-model", + "reasoning_effort": "low", + "fork_context": true + })), + )) + .await + .expect("spawn_agent should succeed"); + let (content, _) = expect_text_output(output); + let result: serde_json::Value = + serde_json::from_str(&content).expect("spawn_agent result should be json"); + let agent_id = parse_agent_id( + result["agent_id"] + .as_str() + .expect("spawn_agent result should include agent_id"), + ); + let snapshot = manager + .get_thread(agent_id) + .await + .expect("spawned agent thread should exist") + .config_snapshot() + .await; + + assert_eq!(snapshot.model, expected_model); + assert_eq!(snapshot.reasoning_effort, expected_reasoning_effort); +} + #[tokio::test] async fn spawn_agent_returns_agent_id_without_task_name() { let (mut session, turn) = make_session_and_context().await; 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 ffe128b43e..e88e734aec 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 @@ -64,14 +64,16 @@ impl ToolHandler for Handler { .await; let mut config = build_agent_spawn_config(&session.get_base_instructions().await, turn.as_ref())?; - apply_requested_spawn_agent_model_overrides( - &session, - turn.as_ref(), - &mut config, - args.model.as_deref(), - args.reasoning_effort, - ) - .await?; + if !args.fork_context { + apply_requested_spawn_agent_model_overrides( + &session, + turn.as_ref(), + &mut config, + args.model.as_deref(), + args.reasoning_effort, + ) + .await?; + } apply_role_to_config(&mut config, role_name) .await .map_err(FunctionCallError::RespondToModel)?;