mirror of
https://github.com/openai/codex.git
synced 2026-10-20 00:18:49 +00:00
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:
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}))
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user