From d0474f2bc1eb246c206c1e9c02338f52bf8d992e Mon Sep 17 00:00:00 2001 From: jif-oai Date: Wed, 1 Apr 2026 11:26:12 +0200 Subject: [PATCH] Use message string in v2 spawn_agent (#16406) ## Summary - switch MultiAgentV2 spawn_agent to accept a single message string instead of items - update v2 spawn tool schema and focused handler/spec tests ## Verification - cargo test -p codex-tools spawn_agent_tool_v2_requires_task_name_and_lists_visible_models - cargo test -p codex-core multi_agent_v2_spawn - just fix -p codex-tools - just fix -p codex-core - just argument-comment-lint Co-authored-by: Codex --- .../src/tools/handlers/multi_agents_tests.rs | 46 +++++++++---------- .../tools/handlers/multi_agents_v2/spawn.rs | 4 +- codex-rs/core/src/tools/spec_tests.rs | 6 +-- codex-rs/tools/src/agent_tool.rs | 9 +++- codex-rs/tools/src/agent_tool_tests.rs | 6 +-- 5 files changed, 38 insertions(+), 33 deletions(-) diff --git a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs index 36294c49c..ce026d8e8 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs @@ -347,7 +347,7 @@ async fn multi_agent_v2_spawn_requires_task_name() { Arc::new(turn), "spawn_agent", function_payload(json!({ - "items": [{"type": "text", "text": "inspect this repo"}] + "message": "inspect this repo" })), ); let Err(err) = SpawnAgentHandlerV2.handle(invocation).await else { @@ -360,7 +360,7 @@ async fn multi_agent_v2_spawn_requires_task_name() { } #[tokio::test] -async fn multi_agent_v2_spawn_rejects_legacy_message_field() { +async fn multi_agent_v2_spawn_rejects_legacy_items_field() { let (mut session, mut turn) = make_session_and_context().await; let manager = thread_manager(); let root = manager @@ -387,12 +387,12 @@ async fn multi_agent_v2_spawn_rejects_legacy_message_field() { })), ); let Err(err) = SpawnAgentHandlerV2.handle(invocation).await else { - panic!("legacy message field should be rejected"); + panic!("legacy items field should be rejected"); }; let FunctionCallError::RespondToModel(message) = err else { - panic!("legacy message field should surface as a model-facing error"); + panic!("legacy items field should surface as a model-facing error"); }; - assert!(message.contains("unknown field `message`")); + assert!(message.contains("unknown field `items`")); } #[tokio::test] @@ -444,7 +444,7 @@ async fn multi_agent_v2_spawn_returns_path_and_send_message_accepts_relative_pat turn.clone(), "spawn_agent", function_payload(json!({ - "items": [{"type": "text", "text": "inspect this repo"}], + "message": "inspect this repo", "task_name": "test_process" })), )) @@ -539,7 +539,7 @@ async fn multi_agent_v2_spawn_rejects_legacy_fork_context() { Arc::new(turn), "spawn_agent", function_payload(json!({ - "items": [{"type": "text", "text": "inspect this repo"}], + "message": "inspect this repo", "task_name": "worker", "fork_context": true })), @@ -578,7 +578,7 @@ async fn multi_agent_v2_spawn_rejects_invalid_fork_turns_string() { Arc::new(turn), "spawn_agent", function_payload(json!({ - "items": [{"type": "text", "text": "inspect this repo"}], + "message": "inspect this repo", "task_name": "worker", "fork_turns": "banana" })), @@ -617,7 +617,7 @@ async fn multi_agent_v2_spawn_rejects_zero_fork_turns() { Arc::new(turn), "spawn_agent", function_payload(json!({ - "items": [{"type": "text", "text": "inspect this repo"}], + "message": "inspect this repo", "task_name": "worker", "fork_turns": "0" })), @@ -731,7 +731,7 @@ async fn multi_agent_v2_list_agents_returns_completed_status_and_last_task_messa turn.clone(), "spawn_agent", function_payload(json!({ - "items": [{"type": "text", "text": "inspect this repo"}], + "message": "inspect this repo", "task_name": "worker" })), )) @@ -909,7 +909,7 @@ async fn multi_agent_v2_list_agents_omits_closed_agents() { turn.clone(), "spawn_agent", function_payload(json!({ - "items": [{"type": "text", "text": "inspect this repo"}], + "message": "inspect this repo", "task_name": "worker" })), )) @@ -973,7 +973,7 @@ async fn multi_agent_v2_send_message_rejects_structured_items() { turn.clone(), "spawn_agent", function_payload(json!({ - "items": [{"type": "text", "text": "boot worker"}], + "message": "boot worker", "task_name": "worker" })), )) @@ -1031,7 +1031,7 @@ async fn multi_agent_v2_send_message_rejects_interrupt_parameter() { turn.clone(), "spawn_agent", function_payload(json!({ - "items": [{"type": "text", "text": "boot worker"}], + "message": "boot worker", "task_name": "worker" })), )) @@ -1104,7 +1104,7 @@ async fn multi_agent_v2_assign_task_interrupts_busy_child_without_losing_message turn.clone(), "spawn_agent", function_payload(json!({ - "items": [{"type": "text", "text": "boot worker"}], + "message": "boot worker", "task_name": "worker" })), )) @@ -1233,7 +1233,7 @@ async fn multi_agent_v2_assign_task_completion_notifies_parent_on_every_turn() { turn.clone(), "spawn_agent", function_payload(json!({ - "items": [{"type": "text", "text": "boot worker"}], + "message": "boot worker", "task_name": "worker" })), )) @@ -1438,7 +1438,7 @@ async fn multi_agent_v2_spawn_includes_agent_id_key_when_named() { Arc::new(turn), "spawn_agent", function_payload(json!({ - "items": [{"type": "text", "text": "inspect this repo"}], + "message": "inspect this repo", "task_name": "test_process" })), )) @@ -1476,7 +1476,7 @@ async fn multi_agent_v2_spawn_surfaces_task_name_validation_errors() { Arc::new(turn), "spawn_agent", function_payload(json!({ - "items": [{"type": "text", "text": "inspect this repo"}], + "message": "inspect this repo", "task_name": "BadName" })), ); @@ -2103,7 +2103,7 @@ async fn multi_agent_v2_wait_agent_accepts_timeout_only_argument() { turn.clone(), "spawn_agent", function_payload(json!({ - "items": [{"type": "text", "text": "boot worker"}], + "message": "boot worker", "task_name": "worker" })), )) @@ -2349,7 +2349,7 @@ async fn multi_agent_v2_wait_agent_returns_summary_for_mailbox_activity() { turn.clone(), "spawn_agent", function_payload(json!({ - "items": [{"type": "text", "text": "inspect this repo"}], + "message": "inspect this repo", "task_name": "test_process" })), )) @@ -2440,7 +2440,7 @@ async fn multi_agent_v2_wait_agent_waits_for_new_mail_after_start() { turn.clone(), "spawn_agent", function_payload(json!({ - "items": [{"type": "text", "text": "boot worker"}], + "message": "boot worker", "task_name": "worker" })), )) @@ -2540,7 +2540,7 @@ async fn multi_agent_v2_wait_agent_wakes_on_any_mailbox_notification() { turn.clone(), "spawn_agent", function_payload(json!({ - "items": [{"type": "text", "text": format!("boot {task_name}")}], + "message": format!("boot {task_name}"), "task_name": task_name })), )) @@ -2627,7 +2627,7 @@ async fn multi_agent_v2_wait_agent_does_not_return_completed_content() { turn.clone(), "spawn_agent", function_payload(json!({ - "items": [{"type": "text", "text": "boot worker"}], + "message": "boot worker", "task_name": "worker" })), )) @@ -2713,7 +2713,7 @@ async fn multi_agent_v2_close_agent_accepts_task_name_target() { turn.clone(), "spawn_agent", function_payload(json!({ - "items": [{"type": "text", "text": "inspect this repo"}], + "message": "inspect this repo", "task_name": "worker" })), )) diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs index c710a6e21..3dc8d7202 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs @@ -40,7 +40,7 @@ impl ToolHandler for Handler { .map(str::trim) .filter(|role| !role.is_empty()); - let initial_operation = parse_collab_input(/*message*/ None, Some(args.items))?; + let initial_operation = parse_collab_input(Some(args.message), /*items*/ None)?; let prompt = render_input_preview(&initial_operation); let session_source = turn.session_source.clone(); @@ -202,7 +202,7 @@ impl ToolHandler for Handler { #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] struct SpawnAgentArgs { - items: Vec, + message: String, task_name: String, agent_type: Option, model: Option, diff --git a/codex-rs/core/src/tools/spec_tests.rs b/codex-rs/core/src/tools/spec_tests.rs index 8bc27a072..631d560a4 100644 --- a/codex-rs/core/src/tools/spec_tests.rs +++ b/codex-rs/core/src/tools/spec_tests.rs @@ -497,13 +497,13 @@ fn test_build_specs_multi_agent_v2_uses_task_names_and_hides_resume() { panic!("spawn_agent should use object params"); }; assert!(properties.contains_key("task_name")); - assert!(properties.contains_key("items")); + assert!(properties.contains_key("message")); assert!(properties.contains_key("fork_turns")); - assert!(!properties.contains_key("message")); + assert!(!properties.contains_key("items")); assert!(!properties.contains_key("fork_context")); assert_eq!( required.as_ref(), - Some(&vec!["task_name".to_string(), "items".to_string()]) + Some(&vec!["task_name".to_string(), "message".to_string()]) ); let output_schema = output_schema .as_ref() diff --git a/codex-rs/tools/src/agent_tool.rs b/codex-rs/tools/src/agent_tool.rs index 84cead4f2..867749e09 100644 --- a/codex-rs/tools/src/agent_tool.rs +++ b/codex-rs/tools/src/agent_tool.rs @@ -66,7 +66,7 @@ pub fn create_spawn_agent_tool_v2(options: SpawnAgentToolOptions<'_>) -> ToolSpe defer_loading: None, parameters: JsonSchema::Object { properties, - required: Some(vec!["task_name".to_string(), "items".to_string()]), + required: Some(vec!["task_name".to_string(), "message".to_string()]), additional_properties: Some(false.into()), }, output_schema: Some(spawn_agent_output_schema_v2()), @@ -585,7 +585,12 @@ fn spawn_agent_common_properties_v1(agent_type_description: &str) -> BTreeMap BTreeMap { BTreeMap::from([ - ("items".to_string(), create_collab_input_items_schema()), + ( + "message".to_string(), + JsonSchema::String { + description: Some("Initial plain-text task for the new agent.".to_string()), + }, + ), ( "agent_type".to_string(), JsonSchema::String { diff --git a/codex-rs/tools/src/agent_tool_tests.rs b/codex-rs/tools/src/agent_tool_tests.rs index 1a6a905a5..2ba8c8827 100644 --- a/codex-rs/tools/src/agent_tool_tests.rs +++ b/codex-rs/tools/src/agent_tool_tests.rs @@ -56,9 +56,9 @@ fn spawn_agent_tool_v2_requires_task_name_and_lists_visible_models() { assert!(description.contains("visible display (`visible-model`)")); assert!(!description.contains("hidden display (`hidden-model`)")); assert!(properties.contains_key("task_name")); - assert!(properties.contains_key("items")); + assert!(properties.contains_key("message")); assert!(properties.contains_key("fork_turns")); - assert!(!properties.contains_key("message")); + assert!(!properties.contains_key("items")); assert!(!properties.contains_key("fork_context")); assert_eq!( properties.get("agent_type"), @@ -68,7 +68,7 @@ fn spawn_agent_tool_v2_requires_task_name_and_lists_visible_models() { ); assert_eq!( required, - Some(vec!["task_name".to_string(), "items".to_string()]) + Some(vec!["task_name".to_string(), "message".to_string()]) ); assert_eq!( output_schema.expect("spawn_agent output schema")["required"],