From 4a5635b5a0336274b6ee196140bfe151b18a642d Mon Sep 17 00:00:00 2001 From: jif-oai Date: Thu, 26 Mar 2026 14:01:00 +0000 Subject: [PATCH] feat: clean spawn v1 (#15861) Avoid the usage of path in the v1 spawn --- .../core/src/tools/handlers/multi_agents.rs | 23 +- .../handlers/multi_agents/close_agent.rs | 2 +- .../tools/handlers/multi_agents/send_input.rs | 2 +- .../src/tools/handlers/multi_agents/spawn.rs | 12 +- .../src/tools/handlers/multi_agents/wait.rs | 2 +- .../src/tools/handlers/multi_agents_tests.rs | 73 ++++- .../src/tools/handlers/multi_agents_v2.rs | 4 + .../handlers/multi_agents_v2/close_agent.rs | 125 +++++++++ codex-rs/core/src/tools/spec.rs | 261 +++++++++++++----- codex-rs/core/src/tools/spec_tests.rs | 26 +- 10 files changed, 427 insertions(+), 103 deletions(-) create mode 100644 codex-rs/core/src/tools/handlers/multi_agents_v2/close_agent.rs diff --git a/codex-rs/core/src/tools/handlers/multi_agents.rs b/codex-rs/core/src/tools/handlers/multi_agents.rs index 0bf7b7bcd3..166dbd287d 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents.rs @@ -6,8 +6,6 @@ //! then optionally layer role-specific config on top. use crate::agent::AgentStatus; -use crate::agent::agent_resolver::resolve_agent_target; -use crate::agent::agent_resolver::resolve_agent_targets; use crate::agent::exceeds_thread_spawn_depth_limit; use crate::codex::Session; use crate::codex::TurnContext; @@ -39,6 +37,27 @@ use serde::Deserialize; use serde::Serialize; use serde_json::Value as JsonValue; +pub(crate) fn parse_agent_id_target(target: &str) -> Result { + ThreadId::from_string(target).map_err(|err| { + FunctionCallError::RespondToModel(format!("invalid agent id {target}: {err:?}")) + }) +} + +pub(crate) fn parse_agent_id_targets( + targets: Vec, +) -> Result, FunctionCallError> { + if targets.is_empty() { + return Err(FunctionCallError::RespondToModel( + "agent ids must be non-empty".to_string(), + )); + } + + targets + .into_iter() + .map(|target| parse_agent_id_target(&target)) + .collect() +} + pub(crate) use close_agent::Handler as CloseAgentHandler; pub(crate) use resume_agent::Handler as ResumeAgentHandler; pub(crate) use send_input::Handler as SendInputHandler; 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 022faa7b76..efc8ec378e 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 @@ -24,7 +24,7 @@ impl ToolHandler for Handler { } = invocation; let arguments = function_arguments(payload)?; let args: CloseAgentArgs = parse_arguments(&arguments)?; - let agent_id = resolve_agent_target(&session, &turn, &args.target).await?; + let agent_id = parse_agent_id_target(&args.target)?; let receiver_agent = session .services .agent_control 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 f649482a2b..9a80a43a22 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 @@ -24,7 +24,7 @@ impl ToolHandler for Handler { } = invocation; let arguments = function_arguments(payload)?; let args: SendInputArgs = parse_arguments(&arguments)?; - let receiver_thread_id = resolve_agent_target(&session, &turn, &args.target).await?; + let receiver_thread_id = parse_agent_id_target(&args.target)?; let input_items = parse_collab_input(args.message, args.items)?; let prompt = input_preview(&input_items); let receiver_agent = session 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 53ab4d35fd..37f166c259 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs @@ -85,7 +85,7 @@ impl ToolHandler for Handler { &turn.session_source, child_depth, role_name, - args.task_name.clone(), + /*task_name*/ None, )?), SpawnAgentOptions { fork_parent_spawn_call_id: args.fork_context.then(|| call_id.clone()), @@ -111,7 +111,7 @@ impl ToolHandler for Handler { } None => None, }; - let (new_agent_path, new_agent_nickname, new_agent_role) = + let (_new_agent_path, new_agent_nickname, new_agent_role) = match (&agent_snapshot, new_agent_metadata) { (Some(snapshot), _) => ( snapshot.session_source.get_agent_path().map(String::from), @@ -134,7 +134,6 @@ impl ToolHandler for Handler { .and_then(|snapshot| snapshot.reasoning_effort) .unwrap_or(args.reasoning_effort.unwrap_or_default()); let nickname = new_agent_nickname.clone(); - let task_name = new_agent_path.clone(); session .send_event( &turn, @@ -161,8 +160,7 @@ impl ToolHandler for Handler { ); Ok(SpawnAgentResult { - agent_id: task_name.is_none().then(|| new_thread_id.to_string()), - task_name, + agent_id: new_thread_id.to_string(), nickname, }) } @@ -172,7 +170,6 @@ impl ToolHandler for Handler { struct SpawnAgentArgs { message: Option, items: Option>, - task_name: Option, agent_type: Option, model: Option, reasoning_effort: Option, @@ -182,8 +179,7 @@ struct SpawnAgentArgs { #[derive(Debug, Serialize)] pub(crate) struct SpawnAgentResult { - agent_id: Option, - task_name: Option, + agent_id: String, nickname: 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 d203e6b398..2fe33f1edd 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents/wait.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents/wait.rs @@ -36,7 +36,7 @@ impl ToolHandler for Handler { } = invocation; let arguments = function_arguments(payload)?; let args: WaitArgs = parse_arguments(&arguments)?; - let receiver_thread_ids = resolve_agent_targets(&session, &turn, args.targets).await?; + let receiver_thread_ids = parse_agent_id_targets(args.targets)?; let mut receiver_agents = Vec::with_capacity(receiver_thread_ids.len()); let mut target_by_thread_id = HashMap::with_capacity(receiver_thread_ids.len()); for receiver_thread_id in &receiver_thread_ids { 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 77ed20dd8e..ab40b20587 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs @@ -22,6 +22,7 @@ use crate::tasks::SessionTask; use crate::tasks::SessionTaskContext; use crate::tools::context::ToolOutput; use crate::tools::handlers::multi_agents_v2::AssignTaskHandler as AssignTaskHandlerV2; +use crate::tools::handlers::multi_agents_v2::CloseAgentHandler as CloseAgentHandlerV2; use crate::tools::handlers::multi_agents_v2::ListAgentsHandler as ListAgentsHandlerV2; use crate::tools::handlers::multi_agents_v2::SendMessageHandler as SendMessageHandlerV2; use crate::tools::handlers::multi_agents_v2::SpawnAgentHandler as SpawnAgentHandlerV2; @@ -294,7 +295,7 @@ async fn spawn_agent_uses_explorer_role_and_preserves_approval_policy() { } #[tokio::test] -async fn spawn_agent_includes_task_name_key_when_not_named() { +async fn spawn_agent_returns_agent_id_without_task_name() { let (mut session, turn) = make_session_and_context().await; let manager = thread_manager(); session.services.agent_control = manager.agent_control(); @@ -315,7 +316,7 @@ async fn spawn_agent_includes_task_name_key_when_not_named() { serde_json::from_str(&content).expect("spawn_agent result should be json"); assert!(result["agent_id"].is_string()); - assert_eq!(result["task_name"], serde_json::Value::Null); + assert!(result.get("task_name").is_none()); assert!(result.get("nickname").is_some()); assert_eq!(success, Some(true)); } @@ -1283,10 +1284,7 @@ async fn send_input_rejects_invalid_id() { let FunctionCallError::RespondToModel(msg) = err else { panic!("expected respond-to-model error"); }; - assert_eq!( - msg, - "agent_name must use only lowercase letters, digits, and underscores" - ); + assert!(msg.starts_with("invalid agent id not-a-uuid:")); } #[tokio::test] @@ -1623,7 +1621,7 @@ async fn wait_agent_rejects_invalid_target() { let FunctionCallError::RespondToModel(msg) = err else { panic!("expected respond-to-model error"); }; - assert_eq!(msg, "live agent path `/root/invalid` not found"); + assert!(msg.starts_with("invalid agent id invalid:")); } #[tokio::test] @@ -1640,7 +1638,7 @@ async fn wait_agent_rejects_empty_targets() { }; assert_eq!( err, - FunctionCallError::RespondToModel("agent targets must be non-empty".to_string()) + FunctionCallError::RespondToModel("agent ids must be non-empty".to_string()) ); } @@ -1979,6 +1977,65 @@ async fn multi_agent_v2_wait_agent_does_not_return_completed_content() { assert_eq!(success, None); } +#[tokio::test] +async fn multi_agent_v2_close_agent_accepts_task_name_target() { + let (mut session, mut turn) = make_session_and_context().await; + let manager = thread_manager(); + let root = manager + .start_thread((*turn.config).clone()) + .await + .expect("root thread should start"); + session.services.agent_control = manager.agent_control(); + session.conversation_id = root.thread_id; + let mut config = (*turn.config).clone(); + config + .features + .enable(Feature::MultiAgentV2) + .expect("test config should allow feature update"); + turn.config = Arc::new(config); + + let session = Arc::new(session); + let turn = Arc::new(turn); + SpawnAgentHandlerV2 + .handle(invocation( + session.clone(), + turn.clone(), + "spawn_agent", + function_payload(json!({ + "message": "inspect this repo", + "task_name": "worker" + })), + )) + .await + .expect("spawn_agent should succeed"); + + let agent_id = session + .services + .agent_control + .resolve_agent_reference(session.conversation_id, &turn.session_source, "worker") + .await + .expect("worker path should resolve"); + + let output = CloseAgentHandlerV2 + .handle(invocation( + session, + turn, + "close_agent", + function_payload(json!({"target": "worker"})), + )) + .await + .expect("close_agent should succeed for v2 task names"); + let (content, success) = expect_text_output(output); + let result: close_agent::CloseAgentResult = + serde_json::from_str(&content).expect("close_agent result should be json"); + assert_ne!(result.previous_status, AgentStatus::NotFound); + assert_eq!(success, Some(true)); + assert_eq!( + manager.agent_control().get_status(agent_id).await, + AgentStatus::NotFound + ); +} + #[tokio::test] async fn close_agent_submits_shutdown_and_returns_previous_status() { let (mut session, turn) = make_session_and_context().await; diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2.rs index 00ee579764..c25f0918ed 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2.rs @@ -23,6 +23,8 @@ use codex_protocol::protocol::CollabAgentInteractionEndEvent; use codex_protocol::protocol::CollabAgentRef; use codex_protocol::protocol::CollabAgentSpawnBeginEvent; use codex_protocol::protocol::CollabAgentSpawnEndEvent; +use codex_protocol::protocol::CollabCloseBeginEvent; +use codex_protocol::protocol::CollabCloseEndEvent; use codex_protocol::protocol::CollabWaitingBeginEvent; use codex_protocol::protocol::CollabWaitingEndEvent; use codex_protocol::user_input::UserInput; @@ -31,12 +33,14 @@ use serde::Serialize; use serde_json::Value as JsonValue; pub(crate) use assign_task::Handler as AssignTaskHandler; +pub(crate) use close_agent::Handler as CloseAgentHandler; pub(crate) use list_agents::Handler as ListAgentsHandler; pub(crate) use send_message::Handler as SendMessageHandler; pub(crate) use spawn::Handler as SpawnAgentHandler; pub(crate) use wait::Handler as WaitAgentHandler; mod assign_task; +mod close_agent; mod list_agents; mod message_tool; mod send_message; diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2/close_agent.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2/close_agent.rs new file mode 100644 index 0000000000..e2b43e6e12 --- /dev/null +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/close_agent.rs @@ -0,0 +1,125 @@ +use super::*; + +pub(crate) struct Handler; + +#[async_trait] +impl ToolHandler for Handler { + type Output = CloseAgentResult; + + fn kind(&self) -> ToolKind { + ToolKind::Function + } + + fn matches_kind(&self, payload: &ToolPayload) -> bool { + matches!(payload, ToolPayload::Function { .. }) + } + + async fn handle(&self, invocation: ToolInvocation) -> Result { + let ToolInvocation { + session, + turn, + payload, + call_id, + .. + } = invocation; + let arguments = function_arguments(payload)?; + let args: CloseAgentArgs = parse_arguments(&arguments)?; + let agent_id = resolve_agent_target(&session, &turn, &args.target).await?; + let receiver_agent = session + .services + .agent_control + .get_agent_metadata(agent_id) + .unwrap_or_default(); + session + .send_event( + &turn, + CollabCloseBeginEvent { + call_id: call_id.clone(), + sender_thread_id: session.conversation_id, + receiver_thread_id: agent_id, + } + .into(), + ) + .await; + let status = match session + .services + .agent_control + .subscribe_status(agent_id) + .await + { + Ok(mut status_rx) => status_rx.borrow_and_update().clone(), + Err(err) => { + let status = session.services.agent_control.get_status(agent_id).await; + session + .send_event( + &turn, + CollabCloseEndEvent { + call_id: call_id.clone(), + sender_thread_id: session.conversation_id, + receiver_thread_id: agent_id, + receiver_agent_nickname: receiver_agent.agent_nickname.clone(), + receiver_agent_role: receiver_agent.agent_role.clone(), + status, + } + .into(), + ) + .await; + return Err(collab_agent_error(agent_id, err)); + } + }; + let result = session + .services + .agent_control + .close_agent(agent_id) + .await + .map_err(|err| collab_agent_error(agent_id, err)) + .map(|_| ()); + session + .send_event( + &turn, + CollabCloseEndEvent { + call_id, + sender_thread_id: session.conversation_id, + receiver_thread_id: agent_id, + receiver_agent_nickname: receiver_agent.agent_nickname, + receiver_agent_role: receiver_agent.agent_role, + status: status.clone(), + } + .into(), + ) + .await; + result?; + + Ok(CloseAgentResult { + previous_status: status, + }) + } +} + +#[derive(Debug, Deserialize)] +struct CloseAgentArgs { + target: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub(crate) struct CloseAgentResult { + pub(crate) previous_status: AgentStatus, +} + +impl ToolOutput for CloseAgentResult { + fn log_preview(&self) -> String { + tool_output_json_text(self, "close_agent") + } + + fn success_for_logging(&self) -> bool { + true + } + + fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem { + tool_output_response_item(call_id, payload, self, Some(true), "close_agent") + } + + fn code_mode_result(&self, _payload: &ToolPayload) -> JsonValue { + tool_output_code_mode_result(self, "close_agent") + } +} diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index 7c8fb0d9b3..82aba1a8cb 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -126,12 +126,25 @@ fn agent_status_output_schema() -> JsonValue { }) } -fn spawn_agent_output_schema(multi_agent_v2: bool) -> JsonValue { - let task_name_description = if multi_agent_v2 { - "Canonical task name for the spawned agent." - } else { - "Canonical task name for the spawned agent when one was assigned." - }; +fn spawn_agent_output_schema_v1() -> JsonValue { + json!({ + "type": "object", + "properties": { + "agent_id": { + "type": "string", + "description": "Thread identifier for the spawned agent." + }, + "nickname": { + "type": ["string", "null"], + "description": "User-facing nickname for the spawned agent when available." + } + }, + "required": ["agent_id", "nickname"], + "additionalProperties": false + }) +} + +fn spawn_agent_output_schema_v2() -> JsonValue { json!({ "type": "object", "properties": { @@ -141,7 +154,7 @@ fn spawn_agent_output_schema(multi_agent_v2: bool) -> JsonValue { }, "task_name": { "type": ["string", "null"], - "description": task_name_description + "description": "Canonical task name for the spawned agent." }, "nickname": { "type": ["string", "null"], @@ -217,7 +230,7 @@ fn wait_output_schema_v1() -> JsonValue { "properties": { "status": { "type": "object", - "description": "Final statuses keyed by canonical task name when available, otherwise by agent id.", + "description": "Final statuses keyed by agent id.", "additionalProperties": agent_status_output_schema() }, "timed_out": { @@ -1133,10 +1146,8 @@ fn create_collab_input_items_schema() -> JsonSchema { } } -fn create_spawn_agent_tool(config: &ToolsConfig) -> ToolSpec { - let available_models_description = spawn_agent_models_description(&config.available_models); - let return_value_description = "Returns the canonical task name when the spawned agent was named, otherwise the agent id, plus the user-facing nickname when available."; - let mut properties = BTreeMap::from([ +fn spawn_agent_common_properties(config: &ToolsConfig) -> BTreeMap { + BTreeMap::from([ ( "message".to_string(), JsonSchema::String { @@ -1182,21 +1193,15 @@ fn create_spawn_agent_tool(config: &ToolsConfig) -> ToolSpec { ), }, ), - ]); - properties.insert( - "task_name".to_string(), - JsonSchema::String { - description: Some( - "Optional task name for the new agent. Use lowercase letters, digits, and underscores." - .to_string(), - ), - }, - ); + ]) +} - ToolSpec::Function(ResponsesApiTool { - name: "spawn_agent".to_string(), - description: format!( - r#" +fn spawn_agent_tool_description( + available_models_description: &str, + return_value_description: &str, +) -> String { + format!( + r#" Only use `spawn_agent` if and only if the user explicitly asks for sub-agents, delegation, or parallel agent work. Requests for depth, thoroughness, research, investigation, or detailed codebase analysis do not count as permission to spawn. Agent-role guidance below only helps choose which agent to use after spawning is already authorized; it never authorizes spawning by itself. @@ -1231,6 +1236,20 @@ fn create_spawn_agent_tool(config: &ToolsConfig) -> ToolSpec { - Split implementation into disjoint codebase slices and spawn multiple agents for them in parallel when the write scopes do not overlap. - Delegate verification only when it can run in parallel with ongoing implementation and is likely to catch a concrete risk before final integration. - The key is to find opportunities to spawn multiple independent subtasks in parallel within the same round, while ensuring each subtask is well-defined, self-contained, and materially advances the main task."# + ) +} + +fn create_spawn_agent_tool_v1(config: &ToolsConfig) -> ToolSpec { + let available_models_description = spawn_agent_models_description(&config.available_models); + let return_value_description = + "Returns the spawned agent id plus the user-facing nickname when available."; + let properties = spawn_agent_common_properties(config); + + ToolSpec::Function(ResponsesApiTool { + name: "spawn_agent".to_string(), + description: spawn_agent_tool_description( + &available_models_description, + return_value_description, ), strict: false, defer_loading: None, @@ -1239,7 +1258,38 @@ fn create_spawn_agent_tool(config: &ToolsConfig) -> ToolSpec { required: None, additional_properties: Some(false.into()), }, - output_schema: Some(spawn_agent_output_schema(config.multi_agent_v2)), + output_schema: Some(spawn_agent_output_schema_v1()), + }) +} + +fn create_spawn_agent_tool_v2(config: &ToolsConfig) -> ToolSpec { + let available_models_description = spawn_agent_models_description(&config.available_models); + let return_value_description = "Returns the canonical task name when the spawned agent was named, otherwise the agent id, plus the user-facing nickname when available."; + let mut properties = spawn_agent_common_properties(config); + properties.insert( + "task_name".to_string(), + JsonSchema::String { + description: Some( + "Optional task name for the new agent. Use lowercase letters, digits, and underscores." + .to_string(), + ), + }, + ); + + ToolSpec::Function(ResponsesApiTool { + name: "spawn_agent".to_string(), + description: spawn_agent_tool_description( + &available_models_description, + return_value_description, + ), + strict: false, + defer_loading: None, + parameters: JsonSchema::Object { + properties, + required: None, + additional_properties: Some(false.into()), + }, + output_schema: Some(spawn_agent_output_schema_v2()), }) } @@ -1401,14 +1451,12 @@ fn create_report_agent_job_result_tool() -> ToolSpec { }) } -fn create_send_input_tool() -> ToolSpec { +fn create_send_input_tool_v1() -> ToolSpec { let properties = BTreeMap::from([ ( "target".to_string(), JsonSchema::String { - description: Some( - "Agent id or canonical task name to message (from spawn_agent).".to_string(), - ), + description: Some("Agent id to message (from spawn_agent).".to_string()), }, ), ( @@ -1546,7 +1594,35 @@ fn create_resume_agent_tool() -> ToolSpec { }) } -fn wait_agent_tool_parameters() -> JsonSchema { +fn wait_agent_tool_parameters_v1() -> JsonSchema { + let mut properties = BTreeMap::new(); + properties.insert( + "targets".to_string(), + JsonSchema::Array { + items: Box::new(JsonSchema::String { description: None }), + description: Some( + "Agent ids to wait on. Pass multiple ids to wait for whichever finishes first." + .to_string(), + ), + }, + ); + properties.insert( + "timeout_ms".to_string(), + JsonSchema::Number { + description: Some(format!( + "Optional timeout in milliseconds. Defaults to {DEFAULT_WAIT_TIMEOUT_MS}, min {MIN_WAIT_TIMEOUT_MS}, max {MAX_WAIT_TIMEOUT_MS}. Prefer longer waits (minutes) to avoid busy polling." + )), + }, + ); + + JsonSchema::Object { + properties, + required: Some(vec!["targets".to_string()]), + additional_properties: Some(false.into()), + } +} + +fn wait_agent_tool_parameters_v2() -> JsonSchema { let mut properties = BTreeMap::new(); properties.insert( "targets".to_string(), @@ -1581,7 +1657,7 @@ fn create_wait_agent_tool_v1() -> ToolSpec { .to_string(), strict: false, defer_loading: None, - parameters: wait_agent_tool_parameters(), + parameters: wait_agent_tool_parameters_v1(), output_schema: Some(wait_output_schema_v1()), }) } @@ -1593,7 +1669,7 @@ fn create_wait_agent_tool_v2() -> ToolSpec { .to_string(), strict: false, defer_loading: None, - parameters: wait_agent_tool_parameters(), + parameters: wait_agent_tool_parameters_v2(), output_schema: Some(wait_output_schema_v2()), }) } @@ -1740,7 +1816,30 @@ fn create_request_permissions_tool() -> ToolSpec { }) } -fn create_close_agent_tool() -> ToolSpec { +fn create_close_agent_tool_v1() -> ToolSpec { + let mut properties = BTreeMap::new(); + properties.insert( + "target".to_string(), + JsonSchema::String { + description: 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, + required: Some(vec!["target".to_string()]), + additional_properties: Some(false.into()), + }, + output_schema: Some(close_agent_output_schema()), + }) +} + +fn create_close_agent_tool_v2() -> ToolSpec { let mut properties = BTreeMap::new(); properties.insert( "target".to_string(), @@ -2581,6 +2680,7 @@ pub(crate) fn build_specs_with_discoverable_tools( use crate::tools::handlers::multi_agents::SpawnAgentHandler; use crate::tools::handlers::multi_agents::WaitAgentHandler; use crate::tools::handlers::multi_agents_v2::AssignTaskHandler as AssignTaskHandlerV2; + use crate::tools::handlers::multi_agents_v2::CloseAgentHandler as CloseAgentHandlerV2; use crate::tools::handlers::multi_agents_v2::ListAgentsHandler as ListAgentsHandlerV2; use crate::tools::handlers::multi_agents_v2::SendMessageHandler as SendMessageHandlerV2; use crate::tools::handlers::multi_agents_v2::SpawnAgentHandler as SpawnAgentHandlerV2; @@ -2924,56 +3024,37 @@ pub(crate) fn build_specs_with_discoverable_tools( builder.register_handler("view_image", view_image_handler); if config.collab_tools { - push_tool_spec( - &mut builder, - create_spawn_agent_tool(config), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - push_tool_spec( - &mut builder, - if config.multi_agent_v2 { - create_send_message_tool() - } else { - create_send_input_tool() - }, - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); if config.multi_agent_v2 { + push_tool_spec( + &mut builder, + create_spawn_agent_tool_v2(config), + /*supports_parallel_tool_calls*/ false, + config.code_mode_enabled, + ); + push_tool_spec( + &mut builder, + create_send_message_tool(), + /*supports_parallel_tool_calls*/ false, + config.code_mode_enabled, + ); push_tool_spec( &mut builder, create_assign_task_tool(), /*supports_parallel_tool_calls*/ false, config.code_mode_enabled, ); - } - if !config.multi_agent_v2 { push_tool_spec( &mut builder, - create_resume_agent_tool(), + create_wait_agent_tool_v2(), + /*supports_parallel_tool_calls*/ false, + config.code_mode_enabled, + ); + push_tool_spec( + &mut builder, + create_close_agent_tool_v2(), /*supports_parallel_tool_calls*/ false, config.code_mode_enabled, ); - builder.register_handler("resume_agent", Arc::new(ResumeAgentHandler)); - } - push_tool_spec( - &mut builder, - if config.multi_agent_v2 { - create_wait_agent_tool_v2() - } else { - create_wait_agent_tool_v1() - }, - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - push_tool_spec( - &mut builder, - create_close_agent_tool(), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - if config.multi_agent_v2 { push_tool_spec( &mut builder, create_list_agents_tool(), @@ -2984,13 +3065,45 @@ pub(crate) fn build_specs_with_discoverable_tools( builder.register_handler("send_message", Arc::new(SendMessageHandlerV2)); builder.register_handler("assign_task", Arc::new(AssignTaskHandlerV2)); builder.register_handler("wait_agent", Arc::new(WaitAgentHandlerV2)); + builder.register_handler("close_agent", Arc::new(CloseAgentHandlerV2)); builder.register_handler("list_agents", Arc::new(ListAgentsHandlerV2)); } else { + push_tool_spec( + &mut builder, + create_spawn_agent_tool_v1(config), + /*supports_parallel_tool_calls*/ false, + config.code_mode_enabled, + ); + push_tool_spec( + &mut builder, + create_send_input_tool_v1(), + /*supports_parallel_tool_calls*/ false, + config.code_mode_enabled, + ); + push_tool_spec( + &mut builder, + create_resume_agent_tool(), + /*supports_parallel_tool_calls*/ false, + config.code_mode_enabled, + ); + builder.register_handler("resume_agent", Arc::new(ResumeAgentHandler)); + push_tool_spec( + &mut builder, + create_wait_agent_tool_v1(), + /*supports_parallel_tool_calls*/ false, + config.code_mode_enabled, + ); + push_tool_spec( + &mut builder, + create_close_agent_tool_v1(), + /*supports_parallel_tool_calls*/ false, + config.code_mode_enabled, + ); builder.register_handler("spawn_agent", Arc::new(SpawnAgentHandler)); builder.register_handler("send_input", Arc::new(SendInputHandler)); builder.register_handler("wait_agent", Arc::new(WaitAgentHandler)); + builder.register_handler("close_agent", Arc::new(CloseAgentHandler)); } - builder.register_handler("close_agent", Arc::new(CloseAgentHandler)); } if config.agent_jobs_tools { diff --git a/codex-rs/core/src/tools/spec_tests.rs b/codex-rs/core/src/tools/spec_tests.rs index 0cd8c2e1f8..bab44764f0 100644 --- a/codex-rs/core/src/tools/spec_tests.rs +++ b/codex-rs/core/src/tools/spec_tests.rs @@ -467,17 +467,27 @@ fn test_full_toolset_specs_for_gpt5_codex_unified_exec_web_search() { search_content_types: None, }, create_view_image_tool(config.can_request_original_image_detail), - create_spawn_agent_tool(&config), - create_send_input_tool(), - if config.multi_agent_v2 { - create_wait_agent_tool_v2() - } else { - create_wait_agent_tool_v1() - }, - create_close_agent_tool(), ] { expected.insert(tool_name(&spec).to_string(), spec); } + let collab_specs = if config.multi_agent_v2 { + vec![ + create_spawn_agent_tool_v2(&config), + create_send_message_tool(), + create_wait_agent_tool_v2(), + create_close_agent_tool_v2(), + ] + } else { + vec![ + create_spawn_agent_tool_v1(&config), + create_send_input_tool_v1(), + create_wait_agent_tool_v1(), + create_close_agent_tool_v1(), + ] + }; + for spec in collab_specs { + expected.insert(tool_name(&spec).to_string(), spec); + } if !config.multi_agent_v2 { let spec = create_resume_agent_tool(); expected.insert(tool_name(&spec).to_string(), spec);