diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index 62f0725ca18b..20251a6d4fce 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -2809,7 +2809,12 @@ async fn turn_start_emits_spawn_agent_item_with_model_metadata_v2() -> Result<() |req: &wiremock::Request| body_contains(req, PARENT_PROMPT), responses::sse(vec![ responses::ev_response_created("resp-turn1-1"), - responses::ev_function_call(SPAWN_CALL_ID, "spawn_agent", &spawn_args), + responses::ev_function_call_with_namespace( + SPAWN_CALL_ID, + "multi_agent_v1", + "spawn_agent", + &spawn_args, + ), responses::ev_completed("resp-turn1-1"), ]), ) @@ -3006,7 +3011,12 @@ async fn turn_start_emits_spawn_agent_item_with_effective_role_model_metadata_v2 |req: &wiremock::Request| body_contains(req, PARENT_PROMPT), responses::sse(vec![ responses::ev_response_created("resp-turn1-1"), - responses::ev_function_call(SPAWN_CALL_ID, "spawn_agent", &spawn_args), + responses::ev_function_call_with_namespace( + SPAWN_CALL_ID, + "multi_agent_v1", + "spawn_agent", + &spawn_args, + ), responses::ev_completed("resp-turn1-1"), ]), ) diff --git a/codex-rs/core/src/tools/handlers/multi_agents.rs b/codex-rs/core/src/tools/handlers/multi_agents.rs index d26b64722d0a..00176572e07f 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents.rs @@ -15,6 +15,7 @@ use crate::tools::context::ToolOutput; use crate::tools::context::ToolPayload; use crate::tools::context::boxed_tool_output; pub(crate) use crate::tools::handlers::multi_agents_common::*; +use crate::tools::handlers::multi_agents_spec::MULTI_AGENT_V1_NAMESPACE; use crate::tools::handlers::parse_arguments; use crate::tools::registry::CoreToolRuntime; use crate::tools::registry::ToolExecutor; diff --git a/codex-rs/core/src/tools/handlers/multi_agents/close_agent.rs b/codex-rs/core/src/tools/handlers/multi_agents/close_agent.rs index ce8bd5d2ab3c..bcbde4a6d3e4 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents/close_agent.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents/close_agent.rs @@ -8,7 +8,7 @@ pub(crate) struct Handler; #[async_trait::async_trait] impl ToolExecutor for Handler { fn tool_name(&self) -> ToolName { - ToolName::plain("close_agent") + ToolName::namespaced(MULTI_AGENT_V1_NAMESPACE, "close_agent") } fn spec(&self) -> Option { diff --git a/codex-rs/core/src/tools/handlers/multi_agents/resume_agent.rs b/codex-rs/core/src/tools/handlers/multi_agents/resume_agent.rs index fcb8aeab18b0..ddfe6ee6a79f 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents/resume_agent.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents/resume_agent.rs @@ -10,7 +10,7 @@ pub(crate) struct Handler; #[async_trait::async_trait] impl ToolExecutor for Handler { fn tool_name(&self) -> ToolName { - ToolName::plain("resume_agent") + ToolName::namespaced(MULTI_AGENT_V1_NAMESPACE, "resume_agent") } fn spec(&self) -> Option { diff --git a/codex-rs/core/src/tools/handlers/multi_agents/send_input.rs b/codex-rs/core/src/tools/handlers/multi_agents/send_input.rs index 3a42f425ece7..3322e1b3d472 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents/send_input.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents/send_input.rs @@ -9,7 +9,7 @@ pub(crate) struct Handler; #[async_trait::async_trait] impl ToolExecutor for Handler { fn tool_name(&self) -> ToolName { - ToolName::plain("send_input") + ToolName::namespaced(MULTI_AGENT_V1_NAMESPACE, "send_input") } fn spec(&self) -> Option { diff --git a/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs b/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs index ce87c9f08d62..ceb46083c58e 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs @@ -25,7 +25,7 @@ impl Handler { #[async_trait::async_trait] impl ToolExecutor for Handler { fn tool_name(&self) -> ToolName { - ToolName::plain("spawn_agent") + ToolName::namespaced(MULTI_AGENT_V1_NAMESPACE, "spawn_agent") } fn spec(&self) -> Option { diff --git a/codex-rs/core/src/tools/handlers/multi_agents/wait.rs b/codex-rs/core/src/tools/handlers/multi_agents/wait.rs index 97841b396159..68f3dd7ee4fe 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents/wait.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents/wait.rs @@ -30,7 +30,7 @@ impl Handler { #[async_trait::async_trait] impl ToolExecutor for Handler { fn tool_name(&self) -> ToolName { - ToolName::plain("wait_agent") + ToolName::namespaced(MULTI_AGENT_V1_NAMESPACE, "wait_agent") } fn spec(&self) -> Option { diff --git a/codex-rs/core/src/tools/handlers/multi_agents_spec.rs b/codex-rs/core/src/tools/handlers/multi_agents_spec.rs index e1295b50de11..8aad4284116a 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_spec.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_spec.rs @@ -1,11 +1,16 @@ use codex_protocol::openai_models::ModelPreset; use codex_tools::JsonSchema; +use codex_tools::ResponsesApiNamespace; +use codex_tools::ResponsesApiNamespaceTool; use codex_tools::ResponsesApiTool; use codex_tools::ToolSpec; use serde_json::Value; use serde_json::json; use std::collections::BTreeMap; +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_MODEL_OVERRIDE_DESCRIPTION: &str = "Optional model override for the new agent. Leave unset to inherit the same model as the parent, which is the preferred default. Only set this when the user explicitly asks for a different model or the task clearly requires one."; const SPAWN_AGENT_SERVICE_TIER_OVERRIDE_DESCRIPTION: &str = "Optional service tier override for the new agent. Leave unset unless the user explicitly asks for one."; @@ -48,18 +53,22 @@ pub fn create_spawn_agent_tool_v1(options: SpawnAgentToolOptions) -> ToolSpec { hide_spawn_agent_metadata_options(&mut properties); } - ToolSpec::Function(ResponsesApiTool { - name: "spawn_agent".to_string(), - description: spawn_agent_tool_description( - available_models_description.as_deref(), - return_value_description, - options.include_usage_hint, - options.usage_hint_text, - ), - strict: false, - defer_loading: None, - parameters: JsonSchema::object(properties, /*required*/ None, Some(false.into())), - output_schema: Some(spawn_agent_output_schema_v1()), + ToolSpec::Namespace(ResponsesApiNamespace { + name: MULTI_AGENT_V1_NAMESPACE.to_string(), + description: MULTI_AGENT_V1_NAMESPACE_DESCRIPTION.to_string(), + tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool { + name: "spawn_agent".to_string(), + description: spawn_agent_tool_description( + available_models_description.as_deref(), + return_value_description, + options.include_usage_hint, + options.usage_hint_text, + ), + strict: false, + defer_loading: None, + parameters: JsonSchema::object(properties, /*required*/ None, Some(false.into())), + output_schema: Some(spawn_agent_output_schema_v1()), + })], }) } @@ -122,14 +131,18 @@ pub fn create_send_input_tool_v1() -> ToolSpec { ), ]); - ToolSpec::Function(ResponsesApiTool { - name: "send_input".to_string(), - description: "Send a message to an existing agent. Use interrupt=true to redirect work immediately. You should reuse the agent by send_input if you believe your assigned task is highly dependent on the context of a previous task." - .to_string(), - strict: false, - defer_loading: None, - parameters: JsonSchema::object(properties, Some(vec!["target".to_string()]), Some(false.into())), - output_schema: Some(send_input_output_schema()), + ToolSpec::Namespace(ResponsesApiNamespace { + name: MULTI_AGENT_V1_NAMESPACE.to_string(), + description: MULTI_AGENT_V1_NAMESPACE_DESCRIPTION.to_string(), + tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool { + name: "send_input".to_string(), + description: "Send a message to an existing agent. Use interrupt=true to redirect work immediately. You should reuse the agent by send_input if you believe your assigned task is highly dependent on the context of a previous task." + .to_string(), + strict: false, + defer_loading: None, + parameters: JsonSchema::object(properties, Some(vec!["target".to_string()]), Some(false.into())), + output_schema: Some(send_input_output_schema()), + })], }) } @@ -197,27 +210,35 @@ pub fn create_resume_agent_tool() -> ToolSpec { JsonSchema::string(Some("Agent id to resume.".to_string())), )]); - ToolSpec::Function(ResponsesApiTool { - name: "resume_agent".to_string(), - description: - "Resume a previously closed agent by id so it can receive send_input and wait_agent calls." - .to_string(), - strict: false, - defer_loading: None, - parameters: JsonSchema::object(properties, Some(vec!["id".to_string()]), Some(false.into())), - output_schema: Some(resume_agent_output_schema()), + ToolSpec::Namespace(ResponsesApiNamespace { + name: MULTI_AGENT_V1_NAMESPACE.to_string(), + description: MULTI_AGENT_V1_NAMESPACE_DESCRIPTION.to_string(), + tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool { + name: "resume_agent".to_string(), + description: + "Resume a previously closed agent by id so it can receive send_input and wait_agent calls." + .to_string(), + strict: false, + defer_loading: None, + parameters: JsonSchema::object(properties, Some(vec!["id".to_string()]), Some(false.into())), + output_schema: Some(resume_agent_output_schema()), + })], }) } pub fn create_wait_agent_tool_v1(options: WaitAgentTimeoutOptions) -> ToolSpec { - ToolSpec::Function(ResponsesApiTool { - name: "wait_agent".to_string(), - description: "Wait for agents to reach a final status. Completed statuses may include the agent's final message. Returns empty status when timed out. Once the agent reaches a final status, a notification message will be received containing the same completed status." - .to_string(), - strict: false, - defer_loading: None, - parameters: wait_agent_tool_parameters_v1(options), - output_schema: Some(wait_output_schema_v1()), + ToolSpec::Namespace(ResponsesApiNamespace { + name: MULTI_AGENT_V1_NAMESPACE.to_string(), + description: MULTI_AGENT_V1_NAMESPACE_DESCRIPTION.to_string(), + tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool { + name: "wait_agent".to_string(), + description: "Wait for agents to reach a final status. Completed statuses may include the agent's final message. Returns empty status when timed out. Once the agent reaches a final status, a notification message will be received containing the same completed status." + .to_string(), + strict: false, + defer_loading: None, + parameters: wait_agent_tool_parameters_v1(options), + output_schema: Some(wait_output_schema_v1()), + })], }) } @@ -260,13 +281,17 @@ pub fn create_close_agent_tool_v1() -> ToolSpec { JsonSchema::string(Some("Agent id to close (from spawn_agent).".to_string())), )]); - ToolSpec::Function(ResponsesApiTool { - name: "close_agent".to_string(), - description: "Close an agent and any open descendants when they are no longer needed, and return the target agent's previous status before shutdown was requested. Don't keep agents open for too long if they are not needed anymore.".to_string(), - strict: false, - defer_loading: None, - parameters: JsonSchema::object(properties, Some(vec!["target".to_string()]), Some(false.into())), - output_schema: Some(close_agent_output_schema()), + ToolSpec::Namespace(ResponsesApiNamespace { + name: MULTI_AGENT_V1_NAMESPACE.to_string(), + description: MULTI_AGENT_V1_NAMESPACE_DESCRIPTION.to_string(), + tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool { + name: "close_agent".to_string(), + description: "Close an agent and any open descendants when they are no longer needed, and return the target agent's previous status before shutdown was requested. Don't keep agents open for too long if they are not needed anymore.".to_string(), + strict: false, + defer_loading: None, + parameters: JsonSchema::object(properties, Some(vec!["target".to_string()]), Some(false.into())), + output_schema: Some(close_agent_output_schema()), + })], }) } diff --git a/codex-rs/core/src/tools/handlers/multi_agents_spec_tests.rs b/codex-rs/core/src/tools/handlers/multi_agents_spec_tests.rs index 711db2fd1134..6f9eac1fff11 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_spec_tests.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_spec_tests.rs @@ -120,11 +120,17 @@ fn spawn_agent_tool_v1_keeps_legacy_fork_context_field() { max_concurrent_threads_per_session: None, }); - let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = tool else { - panic!("spawn_agent should be a function tool"); + let ToolSpec::Namespace(namespace) = tool else { + panic!("spawn_agent v1 should be a namespace tool"); + }; + assert_eq!(namespace.name, MULTI_AGENT_V1_NAMESPACE); + let Some(ResponsesApiNamespaceTool::Function(ResponsesApiTool { parameters, .. })) = + namespace.tools.first() + else { + panic!("spawn_agent should be a namespace function tool"); }; assert_eq!( - parameters.schema_type, + parameters.schema_type.clone(), Some(JsonSchemaType::Single(JsonSchemaPrimitiveType::Object)) ); let properties = parameters diff --git a/codex-rs/core/src/tools/spec_plan_tests.rs b/codex-rs/core/src/tools/spec_plan_tests.rs index 57bab6e39f9e..22276af550b2 100644 --- a/codex-rs/core/src/tools/spec_plan_tests.rs +++ b/codex-rs/core/src/tools/spec_plan_tests.rs @@ -27,6 +27,7 @@ use serde_json::json; use crate::session::tests::make_session_and_context; use crate::session::turn_context::TurnContext; +use crate::tools::handlers::multi_agents_spec::MULTI_AGENT_V1_NAMESPACE; use crate::tools::router::ToolRouter; use crate::tools::router::ToolRouterParams; @@ -602,14 +603,27 @@ async fn multi_agent_feature_selects_one_agent_tool_family() { set_feature(turn, Feature::MultiAgentV2, /*enabled*/ false); }) .await; - v1.assert_visible_contains(&[ + v1.assert_visible_contains(&[MULTI_AGENT_V1_NAMESPACE]); + v1.assert_visible_lacks(&[ "spawn_agent", "send_input", "resume_agent", "wait_agent", "close_agent", + "send_message", + "followup_task", + "list_agents", ]); - v1.assert_visible_lacks(&["send_message", "followup_task", "list_agents"]); + assert_eq!( + v1.namespace_function_names(MULTI_AGENT_V1_NAMESPACE), + &[ + "close_agent".to_string(), + "resume_agent".to_string(), + "send_input".to_string(), + "spawn_agent".to_string(), + "wait_agent".to_string(), + ] + ); let v2 = probe(|turn| { set_feature(turn, Feature::MultiAgentV2, /*enabled*/ true); @@ -678,8 +692,19 @@ async fn v1_multi_agent_tools_defer_when_tool_search_available() { "wait_agent", "close_agent", ] { - plan.assert_registered_contains(&[tool_name]); - assert_eq!(plan.exposure(tool_name), ToolExposure::Deferred); + let namespaced_tool_name = ToolName::namespaced(MULTI_AGENT_V1_NAMESPACE, tool_name); + let namespaced_tool_name = namespaced_tool_name.to_string(); + assert!( + plan.registered_names.contains(&namespaced_tool_name), + "expected namespaced runtime for {tool_name}" + ); + assert!( + !plan + .registered_names + .contains(&ToolName::plain(tool_name).to_string()), + "expected no plain runtime for deferred {tool_name}" + ); + assert_eq!(plan.exposure(&namespaced_tool_name), ToolExposure::Deferred); } let ToolSpec::ToolSearch { description, .. } = plan.visible_spec("tool_search") else { panic!("expected visible tool_search spec"); diff --git a/codex-rs/core/tests/suite/approvals.rs b/codex-rs/core/tests/suite/approvals.rs index f08262eb6f4b..924253d8b80c 100644 --- a/codex-rs/core/tests/suite/approvals.rs +++ b/codex-rs/core/tests/suite/approvals.rs @@ -25,6 +25,7 @@ use core_test_support::responses::ev_apply_patch_custom_tool_call; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; use core_test_support::responses::ev_function_call; +use core_test_support::responses::ev_function_call_with_namespace; use core_test_support::responses::ev_response_created; use core_test_support::responses::mount_sse_once; use core_test_support::responses::mount_sse_once_match; @@ -2373,7 +2374,12 @@ async fn spawned_subagent_execpolicy_amendment_propagates_to_parent_session() -> |req: &Request| body_contains(req, PARENT_PROMPT), sse(vec![ ev_response_created("resp-parent-1"), - ev_function_call(SPAWN_CALL_ID, "spawn_agent", &spawn_args), + ev_function_call_with_namespace( + SPAWN_CALL_ID, + "multi_agent_v1", + "spawn_agent", + &spawn_args, + ), ev_completed("resp-parent-1"), ]), ) diff --git a/codex-rs/core/tests/suite/responses_api_proxy_headers.rs b/codex-rs/core/tests/suite/responses_api_proxy_headers.rs index f396a825a125..c18b4319844f 100644 --- a/codex-rs/core/tests/suite/responses_api_proxy_headers.rs +++ b/codex-rs/core/tests/suite/responses_api_proxy_headers.rs @@ -13,7 +13,7 @@ use core_test_support::responses::ResponseMock; use core_test_support::responses::ResponsesRequest; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; -use core_test_support::responses::ev_function_call; +use core_test_support::responses::ev_function_call_with_namespace; use core_test_support::responses::ev_response_created; use core_test_support::responses::mount_sse_once_match; use core_test_support::responses::sse; @@ -46,7 +46,12 @@ async fn responses_api_parent_and_subagent_requests_include_identity_headers() - }, sse(vec![ ev_response_created("resp-parent-1"), - ev_function_call(SPAWN_CALL_ID, "spawn_agent", &spawn_args), + ev_function_call_with_namespace( + SPAWN_CALL_ID, + "multi_agent_v1", + "spawn_agent", + &spawn_args, + ), ev_completed("resp-parent-1"), ]), ) diff --git a/codex-rs/core/tests/suite/search_tool.rs b/codex-rs/core/tests/suite/search_tool.rs index ca5c146e5a9e..706ff4871e05 100644 --- a/codex-rs/core/tests/suite/search_tool.rs +++ b/codex-rs/core/tests/suite/search_tool.rs @@ -745,13 +745,25 @@ async fn tool_search_returns_deferred_v1_multi_agent_tools() -> Result<()> { ); let tools = tool_search_output_tools(&requests[1], call_id); - let spawn_agent = tools - .iter() - .find(|tool| { + assert!( + !tools.iter().any(|tool| { tool.get("type").and_then(Value::as_str) == Some("function") && tool.get("name").and_then(Value::as_str) == Some("spawn_agent") - }) - .unwrap_or_else(|| panic!("expected tool_search to return spawn_agent: {tools:?}")); + }), + "spawn_agent should be returned as a namespace child, not a flat function: {tools:?}" + ); + assert!( + tools.iter().any(|tool| { + tool.get("type").and_then(Value::as_str) == Some("namespace") + && tool.get("name").and_then(Value::as_str) == Some("multi_agent_v1") + }), + "expected tool_search to return multi_agent_v1 namespace: {tools:?}" + ); + let output = tool_search_output_item(&requests[1], call_id); + let spawn_agent = namespace_child_tool(&output, "multi_agent_v1", "spawn_agent") + .unwrap_or_else(|| { + panic!("expected tool_search to return multi_agent_v1.spawn_agent: {output:?}") + }); assert_eq!( spawn_agent.get("defer_loading").and_then(Value::as_bool), Some(true) diff --git a/codex-rs/core/tests/suite/spawn_agent_description.rs b/codex-rs/core/tests/suite/spawn_agent_description.rs index c70f9bce37c0..9a7d70adebb1 100644 --- a/codex-rs/core/tests/suite/spawn_agent_description.rs +++ b/codex-rs/core/tests/suite/spawn_agent_description.rs @@ -20,6 +20,7 @@ use core_test_support::responses::ev_completed; use core_test_support::responses::ev_response_created; use core_test_support::responses::mount_models_once; use core_test_support::responses::mount_sse_once; +use core_test_support::responses::namespace_child_tool; use core_test_support::responses::sse; use core_test_support::responses::start_mock_server; use core_test_support::test_codex::test_codex; @@ -28,22 +29,14 @@ use std::time::Duration; use std::time::Instant; use tokio::time::sleep; +const MULTI_AGENT_V1_NAMESPACE: &str = "multi_agent_v1"; const SPAWN_AGENT_TOOL_NAME: &str = "spawn_agent"; fn spawn_agent_description(body: &Value) -> Option { - body.get("tools") - .and_then(Value::as_array) - .and_then(|tools| { - tools.iter().find_map(|tool| { - if tool.get("name").and_then(Value::as_str) == Some(SPAWN_AGENT_TOOL_NAME) { - tool.get("description") - .and_then(Value::as_str) - .map(str::to_string) - } else { - None - } - }) - }) + namespace_child_tool(body, MULTI_AGENT_V1_NAMESPACE, SPAWN_AGENT_TOOL_NAME) + .and_then(|tool| tool.get("description")) + .and_then(Value::as_str) + .map(str::to_string) } fn test_model_info( diff --git a/codex-rs/core/tests/suite/subagent_notifications.rs b/codex-rs/core/tests/suite/subagent_notifications.rs index f56df240d582..b648cb3d44ce 100644 --- a/codex-rs/core/tests/suite/subagent_notifications.rs +++ b/codex-rs/core/tests/suite/subagent_notifications.rs @@ -7,12 +7,13 @@ use codex_protocol::openai_models::ReasoningEffort; use core_test_support::responses::ResponsesRequest; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; -use core_test_support::responses::ev_function_call; +use core_test_support::responses::ev_function_call_with_namespace; use core_test_support::responses::ev_response_created; use core_test_support::responses::ev_tool_search_call; use core_test_support::responses::mount_response_once_match; use core_test_support::responses::mount_sse_once_match; use core_test_support::responses::mount_sse_sequence; +use core_test_support::responses::namespace_child_tool; use core_test_support::responses::sse; use core_test_support::responses::sse_response; use core_test_support::responses::start_mock_server; @@ -30,6 +31,7 @@ use tokio::time::sleep; use wiremock::MockServer; const SPAWN_CALL_ID: &str = "spawn-call-1"; +const MULTI_AGENT_V1_NAMESPACE: &str = "multi_agent_v1"; const TURN_0_FORK_PROMPT: &str = "seed fork context"; const TURN_1_PROMPT: &str = "spawn a child and continue"; const TURN_2_NO_WAIT_PROMPT: &str = "follow up without wait"; @@ -76,15 +78,6 @@ fn tool_parameter_description(tool: &Value, parameter_name: &str) -> Option Vec { - request - .tool_search_output(call_id) - .get("tools") - .and_then(Value::as_array) - .cloned() - .unwrap_or_default() -} - fn role_block(description: &str, role_name: &str) -> Option { let role_header = format!("{role_name}: {{"); let mut lines = description.lines().skip_while(|line| *line != role_header); @@ -144,7 +137,7 @@ async fn setup_turn_one_with_spawned_child( server: &MockServer, child_response_delay: Option, ) -> Result<(TestCodex, String)> { - setup_turn_one_with_custom_spawned_child( + let (test, spawned_id, _child_request_log) = setup_turn_one_with_custom_spawned_child( server, json!({ "message": CHILD_PROMPT, @@ -153,7 +146,8 @@ async fn setup_turn_one_with_spawned_child( /*wait_for_parent_notification*/ true, |builder| builder, ) - .await + .await?; + Ok((test, spawned_id)) } async fn setup_turn_one_with_custom_spawned_child( @@ -164,7 +158,11 @@ async fn setup_turn_one_with_custom_spawned_child( configure_test: impl FnOnce( core_test_support::test_codex::TestCodexBuilder, ) -> core_test_support::test_codex::TestCodexBuilder, -) -> Result<(TestCodex, String)> { +) -> Result<( + TestCodex, + String, + core_test_support::responses::ResponseMock, +)> { let spawn_args = serde_json::to_string(&spawn_args)?; mount_sse_once_match( @@ -172,7 +170,12 @@ async fn setup_turn_one_with_custom_spawned_child( |req: &wiremock::Request| body_contains(req, TURN_1_PROMPT), sse(vec![ ev_response_created("resp-turn1-1"), - ev_function_call(SPAWN_CALL_ID, "spawn_agent", &spawn_args), + ev_function_call_with_namespace( + SPAWN_CALL_ID, + MULTI_AGENT_V1_NAMESPACE, + "spawn_agent", + &spawn_args, + ), ev_completed("resp-turn1-1"), ]), ) @@ -249,7 +252,7 @@ async fn setup_turn_one_with_custom_spawned_child( } let spawned_id = wait_for_spawned_thread_id(&test).await?; - Ok((test, spawned_id)) + Ok((test, spawned_id, child_request_log)) } async fn spawn_child_and_capture_snapshot( @@ -259,7 +262,7 @@ async fn spawn_child_and_capture_snapshot( core_test_support::test_codex::TestCodexBuilder, ) -> core_test_support::test_codex::TestCodexBuilder, ) -> Result { - let (test, spawned_id) = setup_turn_one_with_custom_spawned_child( + let (test, spawned_id, _child_request_log) = setup_turn_one_with_custom_spawned_child( server, spawn_args, /*child_response_delay*/ None, @@ -328,7 +331,12 @@ async fn spawned_child_receives_forked_parent_context() -> Result<()> { |req: &wiremock::Request| body_contains(req, TURN_1_PROMPT), sse(vec![ ev_response_created("resp-turn1-1"), - ev_function_call(SPAWN_CALL_ID, "spawn_agent", &spawn_args), + ev_function_call_with_namespace( + SPAWN_CALL_ID, + MULTI_AGENT_V1_NAMESPACE, + "spawn_agent", + &spawn_args, + ), ev_completed("resp-turn1-1"), ]), ) @@ -434,15 +442,22 @@ async fn spawned_multi_agent_v2_child_inherits_parent_developer_context() -> Res |req: &wiremock::Request| body_contains(req, TURN_1_PROMPT), sse(vec![ ev_response_created("resp-turn1-1"), - ev_function_call(SPAWN_CALL_ID, "spawn_agent", &spawn_args), + ev_function_call_with_namespace( + SPAWN_CALL_ID, + MULTI_AGENT_V1_NAMESPACE, + "spawn_agent", + &spawn_args, + ), ev_completed("resp-turn1-1"), ]), ) .await; - let _child_request_log = mount_sse_once_match( + let child_request_log = mount_sse_once_match( &server, - |req: &wiremock::Request| body_contains(req, CHILD_PROMPT), + |req: &wiremock::Request| { + body_contains(req, CHILD_PROMPT) && !body_contains(req, SPAWN_CALL_ID) + }, sse(vec![ ev_response_created("resp-child-1"), ev_completed("resp-child-1"), @@ -452,9 +467,7 @@ async fn spawned_multi_agent_v2_child_inherits_parent_developer_context() -> Res let _turn1_followup = mount_sse_once_match( &server, - |req: &wiremock::Request| { - body_contains(req, "function_call_output") && body_contains(req, "/root/worker") - }, + |req: &wiremock::Request| body_contains(req, SPAWN_CALL_ID), sse(vec![ ev_response_created("resp-turn1-2"), ev_assistant_message("msg-turn1-2", "parent done"), @@ -478,29 +491,12 @@ async fn spawned_multi_agent_v2_child_inherits_parent_developer_context() -> Res test.submit_turn(TURN_1_PROMPT).await?; - let deadline = Instant::now() + Duration::from_secs(2); - let child_request = loop { - if let Some(request) = server - .received_requests() - .await - .unwrap_or_default() - .into_iter() - .find(|request| { - body_contains(request, CHILD_PROMPT) && !body_contains(request, SPAWN_CALL_ID) - }) - { - break request; - } - if Instant::now() >= deadline { - anyhow::bail!("timed out waiting for spawned child request with developer context"); - } - sleep(Duration::from_millis(10)).await; - }; - assert!(body_contains( - &child_request, - "Parent developer instructions." - )); - assert!(body_contains(&child_request, CHILD_PROMPT)); + let child_requests = wait_for_requests(&child_request_log).await?; + let child_request = child_requests + .last() + .expect("child request log should capture at least one request"); + assert!(child_request.body_contains_text("Parent developer instructions.")); + assert!(child_request.body_contains_text(CHILD_PROMPT)); Ok(()) } @@ -519,15 +515,22 @@ async fn skills_toggle_skips_instructions_for_parent_and_spawned_child() -> Resu |req: &wiremock::Request| body_contains(req, TURN_1_PROMPT), sse(vec![ ev_response_created("resp-turn1-1"), - ev_function_call(SPAWN_CALL_ID, "spawn_agent", &spawn_args), + ev_function_call_with_namespace( + SPAWN_CALL_ID, + MULTI_AGENT_V1_NAMESPACE, + "spawn_agent", + &spawn_args, + ), ev_completed("resp-turn1-1"), ]), ) .await; - let _child_request_log = mount_sse_once_match( + let child_request_log = mount_sse_once_match( &server, - |req: &wiremock::Request| body_contains(req, CHILD_PROMPT), + |req: &wiremock::Request| { + body_contains(req, CHILD_PROMPT) && !body_contains(req, SPAWN_CALL_ID) + }, sse(vec![ ev_response_created("resp-child-1"), ev_completed("resp-child-1"), @@ -537,9 +540,7 @@ async fn skills_toggle_skips_instructions_for_parent_and_spawned_child() -> Resu let _turn1_followup = mount_sse_once_match( &server, - |req: &wiremock::Request| { - body_contains(req, "function_call_output") && body_contains(req, "/root/worker") - }, + |req: &wiremock::Request| body_contains(req, SPAWN_CALL_ID), sse(vec![ ev_response_created("resp-turn1-2"), ev_assistant_message("msg-turn1-2", "parent done"), @@ -572,26 +573,12 @@ async fn skills_toggle_skips_instructions_for_parent_and_spawned_child() -> Resu assert!(!parent_request.body_contains_text("")); assert!(!parent_request.body_contains_text("demo-skill")); - let deadline = Instant::now() + Duration::from_secs(2); - let child_request = loop { - if let Some(request) = server - .received_requests() - .await - .unwrap_or_default() - .into_iter() - .find(|request| { - body_contains(request, CHILD_PROMPT) && !body_contains(request, SPAWN_CALL_ID) - }) - { - break request; - } - if Instant::now() >= deadline { - anyhow::bail!("timed out waiting for spawned child request"); - } - sleep(Duration::from_millis(10)).await; - }; - assert!(!body_contains(&child_request, "")); - assert!(!body_contains(&child_request, "demo-skill")); + let child_requests = wait_for_requests(&child_request_log).await?; + let child_request = child_requests + .last() + .expect("child request log should capture at least one request"); + assert!(!child_request.body_contains_text("")); + assert!(!child_request.body_contains_text("demo-skill")); Ok(()) } @@ -695,14 +682,11 @@ async fn spawn_agent_tool_description_mentions_role_locked_settings() -> Result< let requests = resp_mock.requests(); assert_eq!(requests.len(), 2); - let tools = tool_search_output_tools(&requests[1], call_id); - let spawn_agent = tools - .iter() - .find(|tool| { - tool.get("type").and_then(Value::as_str) == Some("function") - && tool.get("name").and_then(Value::as_str) == Some("spawn_agent") - }) - .unwrap_or_else(|| panic!("expected tool_search to return spawn_agent: {tools:?}")); + let output = requests[1].tool_search_output(call_id); + let spawn_agent = namespace_child_tool(&output, "multi_agent_v1", "spawn_agent") + .unwrap_or_else(|| { + panic!("expected tool_search to return multi_agent_v1.spawn_agent: {output:?}") + }); let agent_type_description = tool_parameter_description(spawn_agent, "agent_type") .expect("spawn_agent agent_type description"); let custom_role_description =