From 54bd07d28c8c8684217fd5ab83c0e7d943d06d91 Mon Sep 17 00:00:00 2001 From: Thibault Sottiaux Date: Mon, 20 Apr 2026 15:34:08 -0700 Subject: [PATCH] [codex] prefer inherited spawn agent model (#18701) This updates the spawn-agent tool contract so subagents are presented as inheriting the parent model by default. The visible model list is now framed as optional overrides, the model parameter tells callers to leave it unset and the delegation guidance no longer nudges models toward picking a smaller/mini override. Fixes reports that 5.4 would occasionally pick 5.2 or lower as sub-agents. --- codex-rs/core/src/tools/spec_tests.rs | 6 +++-- .../tests/suite/spawn_agent_description.rs | 21 ++++++++++++++++ codex-rs/tools/src/agent_tool.rs | 25 +++++++++++-------- codex-rs/tools/src/agent_tool_tests.rs | 17 +++++++++++++ 4 files changed, 57 insertions(+), 12 deletions(-) diff --git a/codex-rs/core/src/tools/spec_tests.rs b/codex-rs/core/src/tools/spec_tests.rs index d37d94c13..a1e685df7 100644 --- a/codex-rs/core/src/tools/spec_tests.rs +++ b/codex-rs/core/src/tools/spec_tests.rs @@ -742,10 +742,11 @@ async fn spawn_agent_description_omits_usage_hint_when_disabled() { assert_regex_match( r#"(?sx) ^\s* - No\ picker-visible\ models\ are\ currently\ loaded\. + No\ picker-visible\ model\ overrides\ are\ currently\ loaded\. \s+Spawns\ an\ agent\ to\ work\ on\ the\ specified\ task\.\ If\ your\ current\ task\ is\ `/root/task1`\ and\ you\ spawn_agent\ with\ task_name\ "task_3"\ the\ agent\ will\ have\ canonical\ task\ name\ `/root/task1/task_3`\. \s+You\ are\ then\ able\ to\ refer\ to\ this\ agent\ as\ `task_3`\ or\ `/root/task1/task_3`\ interchangeably\.\ However\ an\ agent\ `/root/task2/task_3`\ would\ only\ be\ able\ to\ communicate\ with\ this\ agent\ via\ its\ canonical\ name\ `/root/task1/task_3`\. \s+The\ spawned\ agent\ will\ have\ the\ same\ tools\ as\ you\ and\ the\ ability\ to\ spawn\ its\ own\ subagents\. + \s+Spawned\ agents\ inherit\ your\ current\ model\ by\ default\.\ Omit\ `model`\ to\ use\ that\ preferred\ default;\ set\ `model`\ only\ when\ an\ explicit\ override\ is\ needed\. \s+It\ will\ be\ able\ to\ send\ you\ and\ other\ running\ agents\ messages,\ and\ its\ final\ answer\ will\ be\ provided\ to\ you\ when\ it\ finishes\. \s+The\ new\ agent's\ canonical\ task\ name\ will\ be\ provided\ to\ it\ along\ with\ the\ message\. \s*$ @@ -766,10 +767,11 @@ async fn spawn_agent_description_uses_configured_usage_hint_text() { assert_regex_match( r#"(?sx) ^\s* - No\ picker-visible\ models\ are\ currently\ loaded\. + No\ picker-visible\ model\ overrides\ are\ currently\ loaded\. \s+Spawns\ an\ agent\ to\ work\ on\ the\ specified\ task\.\ If\ your\ current\ task\ is\ `/root/task1`\ and\ you\ spawn_agent\ with\ task_name\ "task_3"\ the\ agent\ will\ have\ canonical\ task\ name\ `/root/task1/task_3`\. \s+You\ are\ then\ able\ to\ refer\ to\ this\ agent\ as\ `task_3`\ or\ `/root/task1/task_3`\ interchangeably\.\ However\ an\ agent\ `/root/task2/task_3`\ would\ only\ be\ able\ to\ communicate\ with\ this\ agent\ via\ its\ canonical\ name\ `/root/task1/task_3`\. \s+The\ spawned\ agent\ will\ have\ the\ same\ tools\ as\ you\ and\ the\ ability\ to\ spawn\ its\ own\ subagents\. + \s+Spawned\ agents\ inherit\ your\ current\ model\ by\ default\.\ Omit\ `model`\ to\ use\ that\ preferred\ default;\ set\ `model`\ only\ when\ an\ explicit\ override\ is\ needed\. \s+It\ will\ be\ able\ to\ send\ you\ and\ other\ running\ agents\ messages,\ and\ its\ final\ answer\ will\ be\ provided\ to\ you\ when\ it\ finishes\. \s+The\ new\ agent's\ canonical\ task\ name\ will\ be\ provided\ to\ it\ along\ with\ the\ message\. \s+Custom\ delegation\ guidance\ only\. diff --git a/codex-rs/core/tests/suite/spawn_agent_description.rs b/codex-rs/core/tests/suite/spawn_agent_description.rs index 1d2bd1ed0..a8b3bab95 100644 --- a/codex-rs/core/tests/suite/spawn_agent_description.rs +++ b/codex-rs/core/tests/suite/spawn_agent_description.rs @@ -170,6 +170,23 @@ async fn spawn_agent_description_lists_visible_models_and_reasoning_efforts() -> description.contains("- Visible Model (`visible-model`): Fast and capable"), "expected visible model summary in spawn_agent description: {description:?}" ); + assert!( + description + .contains("Available model overrides (optional; inherited parent model is preferred):"), + "expected model choices to be framed as overrides in spawn_agent description: {description:?}" + ); + assert!( + description.contains( + "Spawned agents inherit your current model by default. Omit `model` to use that preferred default; set `model` only when an explicit override is needed." + ), + "expected inherited-model guidance in spawn_agent description: {description:?}" + ); + assert!( + description.contains( + "Do not set the `model` field unless the user explicitly asks for a different model or there is a clear task-specific reason." + ), + "expected model override usage guidance in spawn_agent description: {description:?}" + ); assert!( description.contains("Default reasoning effort: medium."), "expected default reasoning effort in spawn_agent description: {description:?}" @@ -200,6 +217,10 @@ async fn spawn_agent_description_lists_visible_models_and_reasoning_efforts() -> ), "expected agent-role clarification in spawn_agent description: {description:?}" ); + assert!( + !description.contains("A mini model can solve many tasks faster than the main model."), + "spawn_agent description should not encourage choosing a smaller model by default: {description:?}" + ); Ok(()) } diff --git a/codex-rs/tools/src/agent_tool.rs b/codex-rs/tools/src/agent_tool.rs index aa1699a4e..346eaf8ae 100644 --- a/codex-rs/tools/src/agent_tool.rs +++ b/codex-rs/tools/src/agent_tool.rs @@ -6,6 +6,9 @@ use serde_json::Value; use serde_json::json; use std::collections::BTreeMap; +const SPAWN_AGENT_INHERITED_MODEL_GUIDANCE: &str = "Spawned agents inherit your current model by default. Omit `model` to use that preferred default; set `model` only when an explicit override is needed."; +const SPAWN_AGENT_MODEL_OVERRIDE_DESCRIPTION: &str = "Optional model override for the new agent. Leave unset to inherit the same model as the parent, which is the preferred default. Only set this when the user explicitly asks for a different model or the task clearly requires one."; + #[derive(Debug, Clone)] pub struct SpawnAgentToolOptions<'a> { pub available_models: &'a [ModelPreset], @@ -527,8 +530,7 @@ fn spawn_agent_common_properties_v1(agent_type_description: &str) -> BTreeMap BTreeMap String { let visible_models: Vec<&ModelPreset> = models.iter().filter(|model| model.show_in_picker).collect(); if visible_models.is_empty() { - return "No picker-visible models are currently loaded.".to_string(); + return "No picker-visible model overrides are currently loaded.".to_string(); } - visible_models + let model_descriptions = visible_models .into_iter() .map(|model| { let efforts = model @@ -706,7 +708,10 @@ fn spawn_agent_models_description(models: &[ModelPreset]) -> String { ) }) .collect::>() - .join("\n") + .join("\n"); + format!( + "Available model overrides (optional; inherited parent model is preferred):\n{model_descriptions}" + ) } fn wait_agent_tool_parameters_v1(options: WaitAgentTimeoutOptions) -> JsonSchema { diff --git a/codex-rs/tools/src/agent_tool_tests.rs b/codex-rs/tools/src/agent_tool_tests.rs index e592e2b69..e9fd3546b 100644 --- a/codex-rs/tools/src/agent_tool_tests.rs +++ b/codex-rs/tools/src/agent_tool_tests.rs @@ -61,6 +61,11 @@ fn spawn_agent_tool_v2_requires_task_name_and_lists_visible_models() { .expect("spawn_agent should use object params"); assert!(description.contains("Spawns an agent to work on the specified task.")); assert!(description.contains("The spawned agent will have the same tools as you")); + assert!(description.contains(SPAWN_AGENT_INHERITED_MODEL_GUIDANCE)); + assert!( + description + .contains("Available model overrides (optional; inherited parent model is preferred):") + ); assert!(description.contains("visible display (`visible-model`)")); assert!(!description.contains("hidden display (`hidden-model`)")); assert!(properties.contains_key("task_name")); @@ -72,6 +77,12 @@ fn spawn_agent_tool_v2_requires_task_name_and_lists_visible_models() { properties.get("agent_type"), Some(&JsonSchema::string(Some("role help".to_string()))) ); + assert_eq!( + properties + .get("model") + .and_then(|schema| schema.description.as_deref()), + Some(SPAWN_AGENT_MODEL_OVERRIDE_DESCRIPTION) + ); assert_eq!( parameters.required.as_ref(), Some(&vec!["task_name".to_string(), "message".to_string()]) @@ -106,6 +117,12 @@ fn spawn_agent_tool_v1_keeps_legacy_fork_context_field() { assert!(properties.contains_key("fork_context")); assert!(!properties.contains_key("fork_turns")); + assert_eq!( + properties + .get("model") + .and_then(|schema| schema.description.as_deref()), + Some(SPAWN_AGENT_MODEL_OVERRIDE_DESCRIPTION) + ); } #[test]