Expose model overrides for multi-agent v2 spawns (#32749)

## What changed

- Add the `features.multi_agent_v2.expose_spawn_agent_model_overrides` setting, enabled by default, to expose `model` and `reasoning_effort` on the v2 `spawn_agent` tool.
- Keep these controls available when other spawn metadata is hidden, while allowing them to be disabled independently.
- Add root-agent and subagent guidance that overrides require a partial or context-free fork and should only be used when explicitly authorized.

## Testing

- Cover configuration parsing and defaults, usage-hint preservation, and tool-schema behavior with override exposure enabled and disabled.

GitOrigin-RevId: 92370498108c96fbd51f32965624ce531e991d9a
This commit is contained in:
jif
2026-07-13 09:48:33 +00:00
committed by copyberry
parent ea0fd84d94
commit ea15456284
9 changed files with 163 additions and 15 deletions
+4
View File
@@ -1734,6 +1734,10 @@
"enabled": {
"type": "boolean"
},
"expose_spawn_agent_model_overrides": {
"description": "Exposes `model` and `reasoning_effort` on the multi-agent v2 spawn tool and adds corresponding guidance to root and subagent usage hints.",
"type": "boolean"
},
"hide_spawn_agent_metadata": {
"type": "boolean"
},
+51 -3
View File
@@ -10314,6 +10314,7 @@ subagent_usage_hint_text = "Subagent guidance."
multi_agent_mode_hint_text = "Custom mode guidance."
tool_namespace = "agents"
hide_spawn_agent_metadata = true
expose_spawn_agent_model_overrides = false
non_code_mode_only = true
"#,
)?;
@@ -10357,6 +10358,7 @@ non_code_mode_only = true
Some("agents")
);
assert!(config.multi_agent_v2.hide_spawn_agent_metadata);
assert!(!config.multi_agent_v2.expose_spawn_agent_model_overrides);
assert!(config.multi_agent_v2.non_code_mode_only);
Ok(())
@@ -10378,7 +10380,10 @@ enabled = true
.build()
.await?;
assert_eq!(config.multi_agent_v2, MultiAgentV2Config::default());
assert_eq!(
config.multi_agent_v2,
resolve_multi_agent_v2_config(&ConfigToml::default())
);
assert_eq!(
(
config.agent_max_threads,
@@ -10410,7 +10415,50 @@ max_concurrent_threads_per_session = 17
config.subagent_usage_hint_text,
]
.into_iter()
.all(|hint| hint.is_some_and(|hint| hint.ends_with(expected_suffix.as_str())))
.all(|hint| hint.is_some_and(|hint| hint.contains(expected_suffix.as_str())))
);
}
#[test]
fn multi_agent_v2_model_override_exposure_preserves_configured_usage_hints() {
let config_toml = toml::from_str(
r#"[features.multi_agent_v2]
enabled = true
root_agent_usage_hint_text = "Root guidance."
subagent_usage_hint_text = "Subagent guidance."
expose_spawn_agent_model_overrides = true
"#,
)
.expect("multi-agent v2 config should parse");
let config = resolve_multi_agent_v2_config(&config_toml);
assert!(config.expose_spawn_agent_model_overrides);
assert_eq!(
config.root_agent_usage_hint_text.as_deref(),
Some("Root guidance.")
);
assert_eq!(
config.subagent_usage_hint_text.as_deref(),
Some("Subagent guidance.")
);
}
#[test]
fn multi_agent_v2_exposes_model_overrides_by_default() {
let config_toml =
toml::from_str(r#"[features.multi_agent_v2]"#).expect("multi-agent v2 config should parse");
let config = resolve_multi_agent_v2_config(&config_toml);
assert!(config.expose_spawn_agent_model_overrides);
assert!(
[
config.root_agent_usage_hint_text,
config.subagent_usage_hint_text,
]
.into_iter()
.all(|hint| hint.is_some_and(|hint| {
hint.ends_with(DEFAULT_MULTI_AGENT_V2_MODEL_OVERRIDE_USAGE_HINT_TEXT)
}))
);
}
@@ -10425,7 +10473,7 @@ multi_agent_mode_hint_text = ""
let expected = MultiAgentV2Config {
multi_agent_mode_hint_text: Some(String::new()),
..Default::default()
..resolve_multi_agent_v2_config(&ConfigToml::default())
};
assert_eq!(resolve_multi_agent_v2_config(&config_toml), expected);
}
+31 -5
View File
@@ -248,6 +248,7 @@ Payload:
```
You may also see them addressed as to=/root/..., which indicates your identity is /root/...
"#;
const DEFAULT_MULTI_AGENT_V2_MODEL_OVERRIDE_USAGE_HINT_TEXT: &str = "Full-history forks (`fork_turns` omitted or `\"all\"`) inherit the parent model and reasoning effort and do not accept overrides. Only set `model` or `reasoning_effort` when explicitly requested by the user, applicable `AGENTS.md` instructions, or skill instructions; when doing so, set `fork_turns` to `\"none\"` or a positive integer string.";
const DEFAULT_MULTI_AGENT_V2_TOOL_NAMESPACE: &str = "collaboration";
const DEFAULT_MULTI_AGENT_V2_SHARED_USAGE_HINT_TEXT: &str = r#"Note that collaboration tools cannot be called from inside `functions.exec`. Call `spawn_agent`, `send_message`, `followup_task`, `wait_agent`, `interrupt_agent`, and `list_agents` only as direct tool calls using the recipient shown in their tool definitions, such as `to=functions.collaboration.spawn_agent`, since they are intentionally absent from the `functions.exec` `tools.*` namespace. Available tools in `functions.exec` are explicitly described with a `tools` namespace in the developer message.
@@ -1150,6 +1151,7 @@ pub struct MultiAgentV2Config {
pub multi_agent_mode_hint_text: Option<String>,
pub tool_namespace: Option<String>,
pub hide_spawn_agent_metadata: bool,
pub expose_spawn_agent_model_overrides: bool,
pub non_code_mode_only: bool,
}
@@ -1172,6 +1174,7 @@ impl MultiAgentV2Config {
multi_agent_mode_hint_text: None,
tool_namespace: Some(DEFAULT_MULTI_AGENT_V2_TOOL_NAMESPACE.to_string()),
hide_spawn_agent_metadata: true,
expose_spawn_agent_model_overrides: true,
non_code_mode_only: true,
}
}
@@ -2516,13 +2519,31 @@ fn resolve_multi_agent_v2_config(config_toml: &ConfigToml) -> MultiAgentV2Config
.and_then(|config| config.usage_hint_text.as_ref())
.cloned()
.or(default.usage_hint_text);
let hide_spawn_agent_metadata = base
.and_then(|config| config.hide_spawn_agent_metadata)
.unwrap_or(default.hide_spawn_agent_metadata);
let expose_spawn_agent_model_overrides = base
.and_then(|config| config.expose_spawn_agent_model_overrides)
.unwrap_or(default.expose_spawn_agent_model_overrides);
let mut default_root_agent_usage_hint_text = default.root_agent_usage_hint_text;
let mut default_subagent_usage_hint_text = default.subagent_usage_hint_text;
if expose_spawn_agent_model_overrides {
default_root_agent_usage_hint_text = Some(append_usage_hint_text(
default_root_agent_usage_hint_text.as_deref(),
DEFAULT_MULTI_AGENT_V2_MODEL_OVERRIDE_USAGE_HINT_TEXT,
));
default_subagent_usage_hint_text = Some(append_usage_hint_text(
default_subagent_usage_hint_text.as_deref(),
DEFAULT_MULTI_AGENT_V2_MODEL_OVERRIDE_USAGE_HINT_TEXT,
));
}
let root_agent_usage_hint_text = resolve_optional_prompt_text(
base.map(|config| &config.root_agent_usage_hint_text),
default.root_agent_usage_hint_text,
default_root_agent_usage_hint_text,
);
let subagent_usage_hint_text = resolve_optional_prompt_text(
base.map(|config| &config.subagent_usage_hint_text),
default.subagent_usage_hint_text,
default_subagent_usage_hint_text,
);
let multi_agent_mode_hint_text = base
.and_then(|config| config.multi_agent_mode_hint_text.as_ref())
@@ -2532,9 +2553,6 @@ fn resolve_multi_agent_v2_config(config_toml: &ConfigToml) -> MultiAgentV2Config
.and_then(|config| config.tool_namespace.as_ref())
.cloned()
.or(default.tool_namespace);
let hide_spawn_agent_metadata = base
.and_then(|config| config.hide_spawn_agent_metadata)
.unwrap_or(default.hide_spawn_agent_metadata);
let non_code_mode_only = base
.and_then(|config| config.non_code_mode_only)
.unwrap_or(default.non_code_mode_only);
@@ -2550,6 +2568,7 @@ fn resolve_multi_agent_v2_config(config_toml: &ConfigToml) -> MultiAgentV2Config
multi_agent_mode_hint_text,
tool_namespace,
hide_spawn_agent_metadata,
expose_spawn_agent_model_overrides,
non_code_mode_only,
}
}
@@ -2734,6 +2753,13 @@ fn resolve_optional_prompt_text(
}
}
fn append_usage_hint_text(usage_hint_text: Option<&str>, additional_text: &str) -> String {
match usage_hint_text {
Some(usage_hint_text) => format!("{usage_hint_text}\n\n{additional_text}"),
None => additional_text.to_string(),
}
}
fn code_mode_toml_config(features: Option<&FeaturesToml>) -> Option<&CodeModeConfigToml> {
match features?.code_mode.as_ref()? {
FeatureToml::Enabled(_) => None,
@@ -24,6 +24,7 @@ pub struct SpawnAgentToolOptions {
pub available_models: Vec<ModelPreset>,
pub agent_type_description: String,
pub hide_agent_type_model_reasoning: bool,
pub expose_spawn_agent_model_overrides: bool,
pub usage_hint_text: Option<String>,
}
@@ -76,13 +77,20 @@ pub fn create_spawn_agent_tool_v1(options: SpawnAgentToolOptions) -> ToolSpec {
}
pub fn create_spawn_agent_tool_v2(options: SpawnAgentToolOptions) -> ToolSpec {
let available_models_description = (!options.hide_agent_type_model_reasoning)
let available_models_description = options
.expose_spawn_agent_model_overrides
.then(|| spawn_agent_models_description(&options.available_models));
let inherited_model_guidance =
(!options.hide_agent_type_model_reasoning).then_some(SPAWN_AGENT_INHERITED_MODEL_GUIDANCE);
let inherited_model_guidance = (options.expose_spawn_agent_model_overrides
&& !options.hide_agent_type_model_reasoning)
.then_some(SPAWN_AGENT_INHERITED_MODEL_GUIDANCE);
let mut properties = spawn_agent_common_properties_v2(&options.agent_type_description);
if options.hide_agent_type_model_reasoning {
hide_spawn_agent_metadata_options(&mut properties);
properties.remove("agent_type");
properties.remove("service_tier");
}
if !options.expose_spawn_agent_model_overrides {
properties.remove("model");
properties.remove("reasoning_effort");
}
properties.insert(
"task_name".to_string(),
@@ -45,6 +45,7 @@ fn spawn_agent_tool_v2_requires_task_name_and_lists_visible_models() {
],
agent_type_description: "role help".to_string(),
hide_agent_type_model_reasoning: false,
expose_spawn_agent_model_overrides: true,
usage_hint_text: None,
});
@@ -98,6 +99,12 @@ fn spawn_agent_tool_v2_requires_task_name_and_lists_visible_models() {
.and_then(|schema| schema.description.as_deref()),
Some(SPAWN_AGENT_MODEL_OVERRIDE_DESCRIPTION)
);
assert_eq!(
properties
.get("reasoning_effort")
.and_then(|schema| schema.description.as_deref()),
Some("Reasoning effort override for the new agent. Omit to inherit the parent effort.")
);
assert_eq!(
properties
.get("service_tier")
@@ -120,6 +127,7 @@ fn spawn_agent_tool_v1_keeps_legacy_fork_context_field() {
available_models: Vec::new(),
agent_type_description: "role help".to_string(),
hide_agent_type_model_reasoning: false,
expose_spawn_agent_model_overrides: true,
usage_hint_text: None,
});
@@ -176,6 +184,7 @@ fn spawn_agent_tool_caps_visible_model_summaries() {
],
agent_type_description: "role help".to_string(),
hide_agent_type_model_reasoning: false,
expose_spawn_agent_model_overrides: true,
usage_hint_text: None,
});
@@ -214,11 +223,12 @@ fn spawn_agent_tool_caps_reasoning_effort_value_length() {
}
#[test]
fn spawn_agent_tool_hides_service_tier_with_spawn_metadata() {
fn spawn_agent_tool_keeps_model_controls_when_spawn_metadata_is_hidden() {
let tool = create_spawn_agent_tool_v2(SpawnAgentToolOptions {
available_models: vec![model_preset("visible", /*show_in_picker*/ true)],
agent_type_description: "role help".to_string(),
hide_agent_type_model_reasoning: true,
expose_spawn_agent_model_overrides: true,
usage_hint_text: None,
});
@@ -236,10 +246,40 @@ fn spawn_agent_tool_hides_service_tier_with_spawn_metadata() {
.expect("spawn_agent should use object params");
assert!(!properties.contains_key("agent_type"));
assert!(!properties.contains_key("model"));
assert!(!properties.contains_key("reasoning_effort"));
assert!(properties.contains_key("model"));
assert!(properties.contains_key("reasoning_effort"));
assert!(!properties.contains_key("service_tier"));
assert!(!description.contains(SPAWN_AGENT_INHERITED_MODEL_GUIDANCE));
assert!(description.contains("Available model overrides"));
}
#[test]
fn spawn_agent_tool_hides_model_controls_without_override_exposure() {
let tool = create_spawn_agent_tool_v2(SpawnAgentToolOptions {
available_models: vec![model_preset("visible", /*show_in_picker*/ true)],
agent_type_description: "role help".to_string(),
hide_agent_type_model_reasoning: true,
expose_spawn_agent_model_overrides: false,
usage_hint_text: None,
});
let ToolSpec::Function(ResponsesApiTool {
description,
parameters,
..
}) = tool
else {
panic!("spawn_agent should be a function tool");
};
let properties = parameters
.properties
.as_ref()
.expect("spawn_agent should use object params");
for property in ["agent_type", "model", "reasoning_effort", "service_tier"] {
assert!(!properties.contains_key(property));
}
assert!(!description.contains(SPAWN_AGENT_INHERITED_MODEL_GUIDANCE));
assert!(!description.contains("Available model overrides"));
}
+5
View File
@@ -783,6 +783,10 @@ fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mu
.config
.multi_agent_v2
.hide_spawn_agent_metadata,
expose_spawn_agent_model_overrides: turn_context
.config
.multi_agent_v2
.expose_spawn_agent_model_overrides,
usage_hint_text: turn_context.config.multi_agent_v2.usage_hint_text.clone(),
}),
tool_namespace,
@@ -825,6 +829,7 @@ fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mu
available_models: turn_context.available_models.clone(),
agent_type_description,
hide_agent_type_model_reasoning: false,
expose_spawn_agent_model_overrides: true,
usage_hint_text: turn_context.config.multi_agent_v2.usage_hint_text.clone(),
}),
exposure,
@@ -1255,6 +1255,17 @@ async fn multi_agent_feature_selects_one_agent_tool_family() {
else {
panic!("expected spawn_agent in {MULTI_AGENT_V2_NAMESPACE} namespace");
};
let spawn_agent_properties = spawn_agent
.parameters
.properties
.as_ref()
.expect("spawn_agent should use object params");
for property in ["model", "reasoning_effort"] {
assert!(spawn_agent_properties.contains_key(property));
}
for property in ["agent_type", "service_tier"] {
assert!(!spawn_agent_properties.contains_key(property));
}
let spawn_agent_description = spawn_agent.description.as_str();
assert!(!spawn_agent_description.contains("max_concurrent_threads_per_session"));
assert!(spawn_agent_description.contains(
+4
View File
@@ -62,6 +62,10 @@ pub struct MultiAgentV2ConfigToml {
pub tool_namespace: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hide_spawn_agent_metadata: Option<bool>,
/// Exposes `model` and `reasoning_effort` on the multi-agent v2 spawn tool and adds
/// corresponding guidance to root and subagent usage hints.
#[serde(skip_serializing_if = "Option::is_none")]
pub expose_spawn_agent_model_overrides: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub non_code_mode_only: Option<bool>,
}
+2
View File
@@ -651,6 +651,7 @@ subagent_usage_hint_text = "Subagent guidance."
multi_agent_mode_hint_text = "Custom mode guidance."
tool_namespace = "agents"
hide_spawn_agent_metadata = true
expose_spawn_agent_model_overrides = true
non_code_mode_only = true
"#,
)
@@ -675,6 +676,7 @@ non_code_mode_only = true
multi_agent_mode_hint_text: Some("Custom mode guidance.".to_string()),
tool_namespace: Some("agents".to_string()),
hide_spawn_agent_metadata: Some(true),
expose_spawn_agent_model_overrides: Some(true),
non_code_mode_only: Some(true),
}))
);