Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions codex-rs/core/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
54 changes: 51 additions & 3 deletions codex-rs/core/src/config/config_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
"#,
)?;
Expand Down Expand Up @@ -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(())
Expand All @@ -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,
Expand Down Expand Up @@ -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)
}))
);
}

Expand All @@ -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);
}
Expand Down
36 changes: 31 additions & 5 deletions codex-rs/core/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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,
}

Expand All @@ -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,
}
}
Expand Down Expand Up @@ -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())
Expand All @@ -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);
Expand All @@ -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,
}
}
Expand Down Expand Up @@ -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,
Expand Down
16 changes: 12 additions & 4 deletions codex-rs/core/src/tools/handlers/multi_agents_spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
}

Expand Down Expand Up @@ -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(),
Expand Down
46 changes: 43 additions & 3 deletions codex-rs/core/src/tools/handlers/multi_agents_spec_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});

Expand Down Expand Up @@ -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")
Expand All @@ -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,
});

Expand Down Expand Up @@ -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,
});

Expand Down Expand Up @@ -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,
});

Expand All @@ -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"));
}

Expand Down
5 changes: 5 additions & 0 deletions codex-rs/core/src/tools/spec_plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
11 changes: 11 additions & 0 deletions codex-rs/core/src/tools/spec_plan_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
4 changes: 4 additions & 0 deletions codex-rs/features/src/feature_configs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
}
Expand Down
2 changes: 2 additions & 0 deletions codex-rs/features/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
"#,
)
Expand All @@ -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),
}))
);
Expand Down
Loading