diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 62b6556ca1..f7a922c676 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -1824,6 +1824,10 @@ }, "usage_hint_text": { "type": "string" + }, + "wait_agent_enabled": { + "description": "Expose the multi-agent v2 `wait_agent` tool.", + "type": "boolean" } }, "type": "object" diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 78a4a71ef5..53ea21f98a 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -10602,6 +10602,7 @@ multi_agent_mode_hint_text = "Custom mode guidance." tool_namespace = "agents" hide_spawn_agent_metadata = true expose_spawn_agent_model_overrides = false +wait_agent_enabled = false non_code_mode_only = true [agents] @@ -10649,6 +10650,7 @@ max_concurrent_threads_per_session = 9 ); assert!(config.multi_agent_v2.hide_spawn_agent_metadata); assert!(!config.multi_agent_v2.expose_spawn_agent_model_overrides); + assert!(!config.multi_agent_v2.wait_agent_enabled); assert!(config.multi_agent_v2.non_code_mode_only); Ok(()) @@ -10752,6 +10754,48 @@ fn multi_agent_v2_exposes_model_overrides_by_default() { ); } +#[tokio::test] +async fn multi_agent_v2_allows_disabled_wait_agent_without_sleep_tool() -> std::io::Result<()> { + for config_toml in [ + r#" +[features.multi_agent_v2] +enabled = true +wait_agent_enabled = false +"#, + r#" +[features.multi_agent_v2] +enabled = true +wait_agent_enabled = false + +[features.current_time_reminder] +enabled = true +sleep_tool = false +"#, + r#" +[features.multi_agent_v2] +enabled = true +wait_agent_enabled = false + +[features.current_time_reminder] +enabled = false +sleep_tool = true +"#, + ] { + let codex_home = tempdir()?; + let config_toml = toml::from_str(config_toml).expect("TOML should deserialize"); + let config = Config::load_from_base_config_with_overrides( + config_toml, + ConfigOverrides::default(), + codex_home.abs(), + ) + .await?; + + assert!(!config.multi_agent_v2.wait_agent_enabled); + } + + Ok(()) +} + #[test] fn multi_agent_v2_preserves_empty_mode_hint_override() { let config_toml = toml::from_str( diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index bddfe7d602..0d59d618f9 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -1177,6 +1177,7 @@ pub struct MultiAgentV2Config { pub tool_namespace: Option, pub hide_spawn_agent_metadata: bool, pub expose_spawn_agent_model_overrides: bool, + pub wait_agent_enabled: bool, pub non_code_mode_only: bool, } @@ -1200,6 +1201,7 @@ impl MultiAgentV2Config { tool_namespace: Some(DEFAULT_MULTI_AGENT_V2_TOOL_NAMESPACE.to_string()), hide_spawn_agent_metadata: true, expose_spawn_agent_model_overrides: true, + wait_agent_enabled: true, non_code_mode_only: true, } } @@ -2568,6 +2570,9 @@ fn resolve_multi_agent_v2_config(config_toml: &ConfigToml) -> MultiAgentV2Config 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 wait_agent_enabled = base + .and_then(|config| config.wait_agent_enabled) + .unwrap_or(default.wait_agent_enabled); 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 { @@ -2612,6 +2617,7 @@ fn resolve_multi_agent_v2_config(config_toml: &ConfigToml) -> MultiAgentV2Config tool_namespace, hide_spawn_agent_metadata, expose_spawn_agent_model_overrides, + wait_agent_enabled, non_code_mode_only, } } diff --git a/codex-rs/core/src/tools/spec_plan.rs b/codex-rs/core/src/tools/spec_plan.rs index 0705ee5631..a0bae39a92 100644 --- a/codex-rs/core/src/tools/spec_plan.rs +++ b/codex-rs/core/src/tools/spec_plan.rs @@ -835,13 +835,15 @@ fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mu multi_agent_v2_handler(FollowupTaskHandlerV2, tool_namespace), exposure, )); - planned_tools.add_arc(override_tool_exposure( - multi_agent_v2_handler( - WaitAgentHandlerV2::new(context.wait_agent_timeouts), - tool_namespace, - ), - exposure, - )); + if turn_context.config.multi_agent_v2.wait_agent_enabled { + planned_tools.add_arc(override_tool_exposure( + multi_agent_v2_handler( + WaitAgentHandlerV2::new(context.wait_agent_timeouts), + tool_namespace, + ), + exposure, + )); + } planned_tools.add_arc(override_tool_exposure( multi_agent_v2_handler(InterruptAgentHandler, tool_namespace), exposure, diff --git a/codex-rs/core/src/tools/spec_plan_tests.rs b/codex-rs/core/src/tools/spec_plan_tests.rs index 43c1e27c86..94cc92e9a8 100644 --- a/codex-rs/core/src/tools/spec_plan_tests.rs +++ b/codex-rs/core/src/tools/spec_plan_tests.rs @@ -1340,6 +1340,30 @@ async fn multi_agent_v2_message_schemas_are_encrypted() { } } +#[tokio::test] +async fn multi_agent_v2_can_disable_wait_agent() { + let plan = probe(|turn| { + set_feature(turn, Feature::MultiAgentV2, /*enabled*/ true); + update_config(turn, |config| { + config.multi_agent_v2.wait_agent_enabled = false; + }); + }) + .await; + + assert_eq!( + plan.namespace_function_names(MULTI_AGENT_V2_NAMESPACE), + &[ + "followup_task".to_string(), + "interrupt_agent".to_string(), + "list_agents".to_string(), + "send_message".to_string(), + "spawn_agent".to_string(), + ] + ); + plan.assert_visible_lacks(&["clock"]); + plan.assert_registered_lacks(&["collaboration.wait_agent", "clock.sleep"]); +} + #[tokio::test] async fn tool_mode_selector_overrides_feature_flags() { let direct = probe(|turn| { diff --git a/codex-rs/core/tests/suite/spawn_agent_description.rs b/codex-rs/core/tests/suite/spawn_agent_description.rs index dcdeac9106..6478829050 100644 --- a/codex-rs/core/tests/suite/spawn_agent_description.rs +++ b/codex-rs/core/tests/suite/spawn_agent_description.rs @@ -32,6 +32,7 @@ use test_case::test_case; use tokio::time::sleep; const MULTI_AGENT_V1_NAMESPACE: &str = "multi_agent_v1"; +const MULTI_AGENT_V2_NAMESPACE: &str = "collaboration"; const SPAWN_AGENT_TOOL_NAME: &str = "spawn_agent"; fn spawn_agent_description(body: &Value) -> Option { @@ -304,3 +305,59 @@ async fn configured_agent_roles_control_spawn_agent_type( ); Ok(()) } + +#[test_case(true, false; "wait agent remains available without clock sleep")] +#[test_case(true, true; "wait agent remains available with clock sleep")] +#[test_case(false, false; "wait agent can be disabled without clock sleep")] +#[test_case(false, true; "wait agent can be disabled with clock sleep")] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn multi_agent_v2_wait_agent_tool_follows_configuration( + wait_agent_enabled: bool, + sleep_tool_enabled: bool, +) -> Result<()> { + let current_time_reminder = if sleep_tool_enabled { + r#" +[features.current_time_reminder] +enabled = true +sleep_tool = true +"# + } else { + "" + }; + let config_toml = format!( + r#" +[features.multi_agent_v2] +enabled = true +wait_agent_enabled = {wait_agent_enabled} +{current_time_reminder}"# + ); + 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_pre_build_hook(move |home| { + std::fs::write(home.join("config.toml"), &config_toml) + .expect("write multi-agent configuration"); + }) + .build_with_auto_env(&server) + .await?; + + test.submit_turn("hello").await?; + + let request = response.single_request(); + let body = request.body_json(); + assert!(namespace_child_tool(&body, MULTI_AGENT_V2_NAMESPACE, SPAWN_AGENT_TOOL_NAME).is_some()); + assert_eq!( + namespace_child_tool(&body, MULTI_AGENT_V2_NAMESPACE, "wait_agent").is_some(), + wait_agent_enabled + ); + assert_eq!( + namespace_child_tool(&body, "clock", "sleep").is_some(), + sleep_tool_enabled + ); + + Ok(()) +} diff --git a/codex-rs/features/src/feature_configs.rs b/codex-rs/features/src/feature_configs.rs index 88062d554d..6b7ca18d61 100644 --- a/codex-rs/features/src/feature_configs.rs +++ b/codex-rs/features/src/feature_configs.rs @@ -66,6 +66,9 @@ pub struct MultiAgentV2ConfigToml { /// corresponding guidance to root and subagent usage hints. #[serde(skip_serializing_if = "Option::is_none")] pub expose_spawn_agent_model_overrides: Option, + /// Expose the multi-agent v2 `wait_agent` tool. + #[serde(skip_serializing_if = "Option::is_none")] + pub wait_agent_enabled: Option, #[serde(skip_serializing_if = "Option::is_none")] pub non_code_mode_only: Option, } diff --git a/codex-rs/features/src/tests.rs b/codex-rs/features/src/tests.rs index 46588295fe..b7238a8d03 100644 --- a/codex-rs/features/src/tests.rs +++ b/codex-rs/features/src/tests.rs @@ -412,6 +412,7 @@ multi_agent_mode_hint_text = "Custom mode guidance." tool_namespace = "agents" hide_spawn_agent_metadata = true expose_spawn_agent_model_overrides = true +wait_agent_enabled = false non_code_mode_only = true "#, ) @@ -437,6 +438,7 @@ non_code_mode_only = true tool_namespace: Some("agents".to_string()), hide_spawn_agent_metadata: Some(true), expose_spawn_agent_model_overrides: Some(true), + wait_agent_enabled: Some(false), non_code_mode_only: Some(true), })) );