mirror of
https://github.com/openai/codex.git
synced 2026-10-20 08:19:50 +00:00
Expose spawn agent types only when roles are configured (#33572)
## Why The `agent_type` argument is only useful when agent roles are configured. Avoid advertising an inapplicable override in the model-visible `spawn_agent` schema. ## What changed - Include `agent_type` in the V1 and V2 spawn tools only when configured roles are available. - Clarify that `agent_type` is an explicit override and document its interaction with context forking. - Remove the unused `agents.support_agent_type` configuration field. ## Testing - Add coverage for hiding `agent_type` without roles and exposing it when a role is configured. GitOrigin-RevId: 32e0f123b15f317e9cdb71774029375bc073d9ad
This commit is contained in:
@@ -688,9 +688,6 @@ pub struct AgentsToml {
|
||||
pub max_concurrent_threads_per_session: Option<usize>,
|
||||
/// Maximum nesting depth for V1 agent threads. Ignored by V2.
|
||||
pub max_depth: Option<i32>,
|
||||
/// Reserved for controlling whether the spawn tool supports agent types.
|
||||
/// Currently ignored.
|
||||
pub support_agent_type: Option<bool>,
|
||||
/// Reserved default model for spawned subagents. Currently ignored.
|
||||
pub default_subagent_model: Option<String>,
|
||||
/// Reserved default reasoning effort for spawned subagents. Currently ignored.
|
||||
|
||||
@@ -72,10 +72,6 @@
|
||||
"description": "Maximum nesting depth for V1 agent threads. Ignored by V2.",
|
||||
"format": "int32",
|
||||
"type": "integer"
|
||||
},
|
||||
"support_agent_type": {
|
||||
"description": "Reserved for controlling whether the spawn tool supports agent types. Currently ignored.",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
|
||||
@@ -241,10 +241,7 @@ pub(crate) mod spawn_tool_spec {
|
||||
}
|
||||
}
|
||||
|
||||
format!(
|
||||
"Optional type name for the new agent. If omitted, `{DEFAULT_ROLE_NAME}` is used.\nAvailable roles:\n{}",
|
||||
formatted_roles.join("\n"),
|
||||
)
|
||||
format!("Available roles:\n{}", formatted_roles.join("\n"))
|
||||
}
|
||||
|
||||
fn format_role(name: &str, declaration: &AgentRoleConfig) -> String {
|
||||
|
||||
@@ -7351,7 +7351,6 @@ async fn load_config_rejects_missing_agent_role_config_file() -> std::io::Result
|
||||
enabled: None,
|
||||
max_concurrent_threads_per_session: None,
|
||||
max_depth: None,
|
||||
support_agent_type: None,
|
||||
default_subagent_model: None,
|
||||
default_subagent_reasoning_effort: None,
|
||||
job_max_runtime_seconds: None,
|
||||
@@ -8279,7 +8278,6 @@ async fn load_config_resolves_agent_controls() -> std::io::Result<()> {
|
||||
agents: Some(AgentsToml {
|
||||
enabled: Some(false),
|
||||
max_depth: Some(2),
|
||||
support_agent_type: Some(true),
|
||||
default_subagent_model: Some("gpt-5.6-terra".to_string()),
|
||||
default_subagent_reasoning_effort: Some(ReasoningEffort::High),
|
||||
interrupt_message: Some(false),
|
||||
@@ -8299,7 +8297,6 @@ async fn load_config_resolves_agent_controls() -> std::io::Result<()> {
|
||||
(
|
||||
config.agents_enabled,
|
||||
config.agent_max_depth,
|
||||
config.agent_support_agent_type,
|
||||
config.agent_default_subagent_model.as_deref(),
|
||||
config.agent_default_subagent_reasoning_effort,
|
||||
config.agent_interrupt_message_enabled,
|
||||
@@ -8307,7 +8304,6 @@ async fn load_config_resolves_agent_controls() -> std::io::Result<()> {
|
||||
(
|
||||
false,
|
||||
2,
|
||||
Some(true),
|
||||
Some("gpt-5.6-terra"),
|
||||
Some(ReasoningEffort::High),
|
||||
false,
|
||||
@@ -8346,7 +8342,6 @@ async fn load_config_normalizes_agent_role_nickname_candidates() -> std::io::Res
|
||||
enabled: None,
|
||||
max_concurrent_threads_per_session: None,
|
||||
max_depth: None,
|
||||
support_agent_type: None,
|
||||
default_subagent_model: None,
|
||||
default_subagent_reasoning_effort: None,
|
||||
job_max_runtime_seconds: None,
|
||||
@@ -8393,7 +8388,6 @@ async fn load_config_rejects_empty_agent_role_nickname_candidates() -> std::io::
|
||||
enabled: None,
|
||||
max_concurrent_threads_per_session: None,
|
||||
max_depth: None,
|
||||
support_agent_type: None,
|
||||
default_subagent_model: None,
|
||||
default_subagent_reasoning_effort: None,
|
||||
job_max_runtime_seconds: None,
|
||||
@@ -8434,7 +8428,6 @@ async fn load_config_rejects_duplicate_agent_role_nickname_candidates() -> std::
|
||||
enabled: None,
|
||||
max_concurrent_threads_per_session: None,
|
||||
max_depth: None,
|
||||
support_agent_type: None,
|
||||
default_subagent_model: None,
|
||||
default_subagent_reasoning_effort: None,
|
||||
job_max_runtime_seconds: None,
|
||||
@@ -8475,7 +8468,6 @@ async fn load_config_rejects_unsafe_agent_role_nickname_candidates() -> std::io:
|
||||
enabled: None,
|
||||
max_concurrent_threads_per_session: None,
|
||||
max_depth: None,
|
||||
support_agent_type: None,
|
||||
default_subagent_model: None,
|
||||
default_subagent_reasoning_effort: None,
|
||||
job_max_runtime_seconds: None,
|
||||
|
||||
@@ -863,9 +863,6 @@ pub struct Config {
|
||||
/// User-configured maximum number of spawned agent threads per session.
|
||||
pub agent_max_threads: Option<usize>,
|
||||
|
||||
/// Reserved setting for whether the spawn tool supports agent types.
|
||||
pub agent_support_agent_type: Option<bool>,
|
||||
|
||||
/// Reserved default model for spawned subagents.
|
||||
pub agent_default_subagent_model: Option<String>,
|
||||
|
||||
@@ -3606,10 +3603,6 @@ impl Config {
|
||||
.as_ref()
|
||||
.and_then(|agents| agents.max_depth)
|
||||
.unwrap_or(DEFAULT_AGENT_MAX_DEPTH);
|
||||
let agent_support_agent_type = cfg
|
||||
.agents
|
||||
.as_ref()
|
||||
.and_then(|agents| agents.support_agent_type);
|
||||
let agent_default_subagent_model = cfg
|
||||
.agents
|
||||
.as_ref()
|
||||
@@ -3970,7 +3963,6 @@ impl Config {
|
||||
tool_output_token_limit: cfg.tool_output_token_limit,
|
||||
agents_enabled,
|
||||
agent_max_threads,
|
||||
agent_support_agent_type,
|
||||
agent_default_subagent_model,
|
||||
agent_default_subagent_reasoning_effort,
|
||||
agent_max_depth,
|
||||
|
||||
@@ -180,7 +180,6 @@ fn save_config_resolved_fields(
|
||||
agents.enabled = Some(config.agents_enabled);
|
||||
agents.max_concurrent_threads_per_session = config.agent_max_threads;
|
||||
agents.max_depth = Some(config.agent_max_depth);
|
||||
agents.support_agent_type = config.agent_support_agent_type;
|
||||
agents.default_subagent_model = config.agent_default_subagent_model.clone();
|
||||
agents.default_subagent_reasoning_effort =
|
||||
config.agent_default_subagent_reasoning_effort.clone();
|
||||
|
||||
@@ -15,6 +15,7 @@ pub const MULTI_AGENT_V1_NAMESPACE: &str = "multi_agent_v1";
|
||||
const MULTI_AGENT_V1_NAMESPACE_DESCRIPTION: &str = "Tools for spawning and managing sub-agents.";
|
||||
|
||||
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_TYPE_OVERRIDE_DESCRIPTION_V1: &str = "Agent type override for the new agent. Omit to inherit the parent agent type with a full-history fork; otherwise, `default` is used.";
|
||||
const SPAWN_AGENT_MODEL_OVERRIDE_DESCRIPTION: &str =
|
||||
"Model override for the new agent. Omit unless an explicit override is needed.";
|
||||
const SPAWN_AGENT_SERVICE_TIER_OVERRIDE_DESCRIPTION: &str =
|
||||
@@ -25,6 +26,7 @@ const MAX_REASONING_EFFORT_CHARS_IN_SPAWN_AGENT_DESCRIPTION: usize = 64;
|
||||
pub struct SpawnAgentToolOptions {
|
||||
pub available_models: Vec<ModelPreset>,
|
||||
pub agent_type_description: String,
|
||||
pub expose_agent_type: bool,
|
||||
pub hide_agent_type_model_reasoning: bool,
|
||||
pub expose_spawn_agent_model_overrides: bool,
|
||||
pub multi_agent_version: MultiAgentVersion,
|
||||
@@ -36,6 +38,7 @@ impl Default for SpawnAgentToolOptions {
|
||||
Self {
|
||||
available_models: Vec::new(),
|
||||
agent_type_description: String::new(),
|
||||
expose_agent_type: true,
|
||||
hide_agent_type_model_reasoning: false,
|
||||
expose_spawn_agent_model_overrides: false,
|
||||
multi_agent_version: MultiAgentVersion::Disabled,
|
||||
@@ -70,6 +73,9 @@ pub fn create_spawn_agent_tool_v1(options: SpawnAgentToolOptions) -> ToolSpec {
|
||||
let return_value_description =
|
||||
"Returns the spawned agent id plus the user-facing nickname when available.";
|
||||
let mut properties = spawn_agent_common_properties_v1(&options.agent_type_description);
|
||||
if !options.expose_agent_type {
|
||||
properties.remove("agent_type");
|
||||
}
|
||||
if options.hide_agent_type_model_reasoning {
|
||||
hide_spawn_agent_metadata_options(&mut properties);
|
||||
}
|
||||
@@ -101,8 +107,10 @@ pub fn create_spawn_agent_tool_v2(options: SpawnAgentToolOptions) -> ToolSpec {
|
||||
&& !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 {
|
||||
if !options.expose_agent_type {
|
||||
properties.remove("agent_type");
|
||||
}
|
||||
if options.hide_agent_type_model_reasoning {
|
||||
properties.remove("service_tier");
|
||||
}
|
||||
if !options.expose_spawn_agent_model_overrides {
|
||||
@@ -582,7 +590,9 @@ fn spawn_agent_common_properties_v1(agent_type_description: &str) -> BTreeMap<St
|
||||
("items".to_string(), create_collab_input_items_schema()),
|
||||
(
|
||||
"agent_type".to_string(),
|
||||
JsonSchema::string(Some(agent_type_description.to_string())),
|
||||
JsonSchema::string(Some(format!(
|
||||
"{SPAWN_AGENT_TYPE_OVERRIDE_DESCRIPTION_V1}\n{agent_type_description}"
|
||||
))),
|
||||
),
|
||||
(
|
||||
"fork_context".to_string(),
|
||||
@@ -624,7 +634,9 @@ fn spawn_agent_common_properties_v2(agent_type_description: &str) -> BTreeMap<St
|
||||
),
|
||||
(
|
||||
"agent_type".to_string(),
|
||||
JsonSchema::string(Some(agent_type_description.to_string())),
|
||||
JsonSchema::string(Some(format!(
|
||||
"Agent type override for the new agent. Omit unless explicitly asked. Set `fork_turns` to `none` or a positive integer when an explicit override is needed.\n{agent_type_description}"
|
||||
))),
|
||||
),
|
||||
(
|
||||
"fork_turns".to_string(),
|
||||
|
||||
@@ -48,6 +48,7 @@ fn spawn_agent_tool_v2_requires_task_name_and_lists_visible_models() {
|
||||
incompatible,
|
||||
],
|
||||
agent_type_description: "role help".to_string(),
|
||||
expose_agent_type: true,
|
||||
hide_agent_type_model_reasoning: false,
|
||||
expose_spawn_agent_model_overrides: true,
|
||||
multi_agent_version: MultiAgentVersion::V2,
|
||||
@@ -95,10 +96,6 @@ fn spawn_agent_tool_v2_requires_task_name_and_lists_visible_models() {
|
||||
assert!(properties.contains_key("fork_turns"));
|
||||
assert!(!properties.contains_key("items"));
|
||||
assert!(!properties.contains_key("fork_context"));
|
||||
assert_eq!(
|
||||
properties.get("agent_type"),
|
||||
Some(&JsonSchema::string(Some("role help".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
properties
|
||||
.get("model")
|
||||
@@ -132,6 +129,7 @@ fn spawn_agent_tool_v1_keeps_legacy_fork_context_field() {
|
||||
let tool = create_spawn_agent_tool_v1(SpawnAgentToolOptions {
|
||||
available_models: Vec::new(),
|
||||
agent_type_description: "role help".to_string(),
|
||||
expose_agent_type: true,
|
||||
hide_agent_type_model_reasoning: false,
|
||||
expose_spawn_agent_model_overrides: true,
|
||||
multi_agent_version: MultiAgentVersion::V1,
|
||||
@@ -158,6 +156,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("agent_type"),
|
||||
Some(&JsonSchema::string(Some(format!(
|
||||
"{SPAWN_AGENT_TYPE_OVERRIDE_DESCRIPTION_V1}\nrole help"
|
||||
))))
|
||||
);
|
||||
assert_eq!(
|
||||
properties
|
||||
.get("message")
|
||||
@@ -190,6 +194,7 @@ fn spawn_agent_tool_caps_visible_model_summaries() {
|
||||
model_preset("sixth", /*show_in_picker*/ true),
|
||||
],
|
||||
agent_type_description: "role help".to_string(),
|
||||
expose_agent_type: true,
|
||||
hide_agent_type_model_reasoning: false,
|
||||
expose_spawn_agent_model_overrides: true,
|
||||
multi_agent_version: MultiAgentVersion::V2,
|
||||
@@ -235,6 +240,7 @@ 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(),
|
||||
expose_agent_type: false,
|
||||
hide_agent_type_model_reasoning: true,
|
||||
expose_spawn_agent_model_overrides: true,
|
||||
multi_agent_version: MultiAgentVersion::V2,
|
||||
@@ -267,6 +273,7 @@ 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(),
|
||||
expose_agent_type: false,
|
||||
hide_agent_type_model_reasoning: true,
|
||||
expose_spawn_agent_model_overrides: false,
|
||||
multi_agent_version: MultiAgentVersion::V2,
|
||||
|
||||
@@ -798,16 +798,15 @@ fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mu
|
||||
.flatten();
|
||||
let agent_type_description =
|
||||
agent_type_description(turn_context, context.default_agent_type_description);
|
||||
// TODO(jif): Wire `agents.support_agent_type` into the V2 spawn tool surface.
|
||||
let hide_spawn_agent_metadata =
|
||||
turn_context.config.multi_agent_v2.hide_spawn_agent_metadata;
|
||||
planned_tools.add_arc(override_tool_exposure(
|
||||
multi_agent_v2_handler(
|
||||
SpawnAgentHandlerV2::new(SpawnAgentToolOptions {
|
||||
available_models: turn_context.available_models.clone(),
|
||||
agent_type_description,
|
||||
hide_agent_type_model_reasoning: turn_context
|
||||
.config
|
||||
.multi_agent_v2
|
||||
.hide_spawn_agent_metadata,
|
||||
expose_agent_type: !turn_context.config.agent_roles.is_empty(),
|
||||
hide_agent_type_model_reasoning: hide_spawn_agent_metadata,
|
||||
expose_spawn_agent_model_overrides: turn_context
|
||||
.config
|
||||
.multi_agent_v2
|
||||
@@ -854,6 +853,7 @@ fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mu
|
||||
SpawnAgentHandler::new(SpawnAgentToolOptions {
|
||||
available_models: turn_context.available_models.clone(),
|
||||
agent_type_description,
|
||||
expose_agent_type: !turn_context.config.agent_roles.is_empty(),
|
||||
hide_agent_type_model_reasoning: false,
|
||||
expose_spawn_agent_model_overrides: true,
|
||||
multi_agent_version: turn_context.multi_agent_version,
|
||||
|
||||
@@ -1207,12 +1207,13 @@ async fn multi_agent_feature_selects_one_agent_tool_family() {
|
||||
.properties
|
||||
.as_ref()
|
||||
.expect("spawn_agent should use object params");
|
||||
for property in ["agent_type", "model", "reasoning_effort", "service_tier"] {
|
||||
for property in ["model", "reasoning_effort", "service_tier"] {
|
||||
assert!(
|
||||
properties.contains_key(property),
|
||||
"expected v1 spawn_agent to expose `{property}`"
|
||||
);
|
||||
}
|
||||
assert!(!properties.contains_key("agent_type"));
|
||||
|
||||
let v2 = probe(|turn| {
|
||||
set_feature(turn, Feature::MultiAgentV2, /*enabled*/ true);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use anyhow::Result;
|
||||
use codex_core::config::AgentRoleConfig;
|
||||
use codex_features::Feature;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_models_manager::manager::RefreshStrategy;
|
||||
@@ -27,6 +28,7 @@ use core_test_support::test_codex::test_codex;
|
||||
use serde_json::Value;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use test_case::test_case;
|
||||
use tokio::time::sleep;
|
||||
|
||||
const MULTI_AGENT_V1_NAMESPACE: &str = "multi_agent_v1";
|
||||
@@ -39,6 +41,12 @@ fn spawn_agent_description(body: &Value) -> Option<String> {
|
||||
.map(str::to_string)
|
||||
}
|
||||
|
||||
fn spawn_agent_exposes_agent_type(body: &Value, namespace: &str) -> bool {
|
||||
namespace_child_tool(body, namespace, SPAWN_AGENT_TOOL_NAME)
|
||||
.and_then(|tool| tool.pointer("/parameters/properties/agent_type"))
|
||||
.is_some()
|
||||
}
|
||||
|
||||
fn test_model_info(
|
||||
slug: &str,
|
||||
display_name: &str,
|
||||
@@ -242,3 +250,57 @@ async fn spawn_agent_description_lists_visible_models_and_reasoning_efforts() ->
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(false, false, MULTI_AGENT_V1_NAMESPACE; "v1 hides agent type without roles")]
|
||||
#[test_case(true, true, "collaboration"; "v2 exposes agent type with a role")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn configured_agent_roles_control_spawn_agent_type(
|
||||
multi_agent_v2: bool,
|
||||
has_agent_role: bool,
|
||||
namespace: &str,
|
||||
) -> Result<()> {
|
||||
let server = start_mock_server().await;
|
||||
let response = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![ev_response_created("resp1"), ev_completed("resp1")]),
|
||||
)
|
||||
.await;
|
||||
let test = test_codex()
|
||||
.with_config(move |config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::Collab)
|
||||
.expect("test config should allow feature update");
|
||||
if multi_agent_v2 {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::MultiAgentV2)
|
||||
.expect("test config should allow feature update");
|
||||
} else {
|
||||
config
|
||||
.features
|
||||
.disable(Feature::MultiAgentV2)
|
||||
.expect("test config should allow feature update");
|
||||
}
|
||||
if has_agent_role {
|
||||
config.agent_roles.insert(
|
||||
"researcher".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: Some("Research role".to_string()),
|
||||
config_file: None,
|
||||
nickname_candidates: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
})
|
||||
.build_with_auto_env(&server)
|
||||
.await?;
|
||||
|
||||
test.submit_turn("hello").await?;
|
||||
|
||||
assert_eq!(
|
||||
spawn_agent_exposes_agent_type(&response.single_request().body_json(), namespace),
|
||||
has_agent_role
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -242,7 +242,6 @@ fn new_config(model: Option<String>, arg0_paths: Arg0DispatchPaths) -> anyhow::R
|
||||
tool_output_token_limit: None,
|
||||
agents_enabled: true,
|
||||
agent_max_threads: Some(6),
|
||||
agent_support_agent_type: None,
|
||||
agent_default_subagent_model: None,
|
||||
agent_default_subagent_reasoning_effort: None,
|
||||
agent_job_max_runtime_seconds: None,
|
||||
|
||||
@@ -70,11 +70,6 @@ fn render_agents_config_lines(config: &Config) -> Vec<Line<'static>> {
|
||||
config.agent_max_depth
|
||||
)
|
||||
.into(),
|
||||
format!(
|
||||
" - support_agent_type = {} (not yet wired)",
|
||||
format_optional(config.agent_support_agent_type)
|
||||
)
|
||||
.into(),
|
||||
format!(
|
||||
" - default_subagent_model = {} (not yet wired)",
|
||||
format_optional(config.agent_default_subagent_model.as_deref())
|
||||
@@ -654,7 +649,6 @@ mod tests {
|
||||
enabled = false
|
||||
max_concurrent_threads_per_session = 7
|
||||
max_depth = -2
|
||||
support_agent_type = true
|
||||
default_subagent_model = "gpt-5.6-terra"
|
||||
default_subagent_reasoning_effort = "high"
|
||||
job_max_runtime_seconds = 900
|
||||
|
||||
-1
@@ -8,7 +8,6 @@ expression: render_to_text(&render_agents_config_lines(&config))
|
||||
- enabled = false
|
||||
- max_concurrent_threads_per_session = 7
|
||||
- max_depth = -2 (V1 only; ignored by V2)
|
||||
- support_agent_type = true (not yet wired)
|
||||
- default_subagent_model = gpt-5.6-terra (not yet wired)
|
||||
- default_subagent_reasoning_effort = high (not yet wired)
|
||||
- job_max_runtime_seconds = 900
|
||||
|
||||
Reference in New Issue
Block a user