From 2f9beaccb897de72f762966b71307da28298b72b Mon Sep 17 00:00:00 2001 From: jif-oai Date: Mon, 8 Jun 2026 13:25:01 +0100 Subject: [PATCH] feat: close to interrupt --- codex-rs/core/src/agent/control.rs | 13 +- .../src/tools/handlers/multi_agents_spec.rs | 20 +- .../src/tools/handlers/multi_agents_tests.rs | 229 +++++++++++++----- .../src/tools/handlers/multi_agents_v2.rs | 4 +- .../{close_agent.rs => interrupt_agent.rs} | 77 +++--- .../handlers/multi_agents_v2/message_tool.rs | 8 +- codex-rs/core/src/tools/spec_plan.rs | 4 +- codex-rs/core/src/tools/spec_plan_tests.rs | 12 +- 8 files changed, 238 insertions(+), 129 deletions(-) rename codex-rs/core/src/tools/handlers/multi_agents_v2/{close_agent.rs => interrupt_agent.rs} (56%) diff --git a/codex-rs/core/src/agent/control.rs b/codex-rs/core/src/agent/control.rs index 888f9cdf94f..53a50e8e1f0 100644 --- a/codex-rs/core/src/agent/control.rs +++ b/codex-rs/core/src/agent/control.rs @@ -189,7 +189,12 @@ impl AgentControl { /// Interrupt the current task for an existing agent thread. pub(crate) async fn interrupt_agent(&self, agent_id: ThreadId) -> CodexResult { let state = self.upgrade()?; - state.send_op(agent_id, Op::Interrupt).await + self.handle_thread_request_result( + agent_id, + &state, + state.send_op(agent_id, Op::Interrupt).await, + ) + .await } async fn handle_thread_request_result( @@ -232,6 +237,12 @@ impl AgentControl { self.state.agent_metadata_for_thread(agent_id) } + pub(crate) fn ensure_agent_known(&self, agent_id: ThreadId) -> CodexResult { + self.state + .agent_metadata_for_thread(agent_id) + .ok_or(CodexErr::ThreadNotFound(agent_id)) + } + pub(crate) async fn list_live_agent_subtree_thread_ids( &self, agent_id: ThreadId, 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 1dcb55f436f..8455b130360 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_spec.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_spec.rs @@ -302,26 +302,30 @@ pub fn create_close_agent_tool_v1() -> ToolSpec { strict: false, defer_loading: None, parameters: JsonSchema::object(properties, Some(vec!["target".to_string()]), Some(false.into())), - output_schema: Some(close_agent_output_schema()), + output_schema: Some(agent_previous_status_output_schema( + "The agent status observed before shutdown was requested.", + )), })], }) } -pub fn create_close_agent_tool_v2() -> ToolSpec { +pub fn create_interrupt_agent_tool_v2() -> ToolSpec { let properties = BTreeMap::from([( "target".to_string(), JsonSchema::string(Some( - "Agent id or canonical task name to close (from spawn_agent).".to_string(), + "Agent id or canonical task name to interrupt (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. Completed agents remain open and count toward the concurrency limit until closed. Don't keep agents open for too long if they are not needed anymore.".to_string(), + name: "interrupt_agent".to_string(), + description: "Interrupt an agent's current turn, if any, and return its previous status. The agent remains available for messages and follow-up tasks.".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()), + output_schema: Some(agent_previous_status_output_schema( + "The agent status observed before the interrupt request was handled.", + )), }) } @@ -501,12 +505,12 @@ fn wait_output_schema_v2() -> Value { }) } -fn close_agent_output_schema() -> Value { +fn agent_previous_status_output_schema(previous_status_description: &str) -> Value { json!({ "type": "object", "properties": { "previous_status": { - "description": "The agent status observed before shutdown was requested.", + "description": previous_status_description, "allOf": [agent_status_output_schema()] } }, 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 67ccf36f916..4f514e23a7d 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs @@ -9,8 +9,8 @@ use crate::session::tests::make_session_and_context; use crate::session_prefix::format_subagent_notification_message; use crate::thread_manager::thread_store_from_config; use crate::tools::context::ToolOutput; -use crate::tools::handlers::multi_agents_v2::CloseAgentHandler as CloseAgentHandlerV2; use crate::tools::handlers::multi_agents_v2::FollowupTaskHandler as FollowupTaskHandlerV2; +use crate::tools::handlers::multi_agents_v2::InterruptAgentHandler; 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; @@ -178,6 +178,11 @@ struct ListedAgentResult { last_task_message: Option, } +#[derive(Debug, Deserialize)] +struct InterruptAgentResult { + previous_status: AgentStatus, +} + #[tokio::test] async fn handler_rejects_non_function_payloads() { let (session, turn) = make_session_and_context().await; @@ -1744,6 +1749,82 @@ async fn multi_agent_v2_list_agents_omits_closed_agents() { ); } +#[tokio::test] +async fn multi_agent_v2_list_agents_keeps_interrupted_resident_agents() { + 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.thread_id = root.thread_id; + let mut config = (*turn.config).clone(); + let _ = config.features.enable(Feature::MultiAgentV2); + set_turn_config(&mut turn, config); + + let session = Arc::new(session); + let turn = Arc::new(turn); + let spawn_output = SpawnAgentHandlerV2::default() + .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 _ = expect_text_output(spawn_output); + + let agent_id = session + .services + .agent_control + .resolve_agent_reference(session.thread_id, &turn.session_source, "worker") + .await + .expect("worker path should resolve"); + let agent_path = session + .services + .agent_control + .get_agent_metadata(agent_id) + .expect("worker metadata should exist") + .agent_path + .expect("worker path should exist"); + let interrupt_output = InterruptAgentHandler + .handle(invocation( + session.clone(), + turn.clone(), + "interrupt_agent", + function_payload(json!({"target": "worker"})), + )) + .await + .expect("interrupt_agent should succeed"); + let _ = expect_text_output(interrupt_output); + + let output = ListAgentsHandlerV2 + .handle(invocation( + session, + turn, + "list_agents", + function_payload(json!({})), + )) + .await + .expect("list_agents should succeed"); + let (content, _) = expect_text_output(output); + let result: ListAgentsResult = + serde_json::from_str(&content).expect("list_agents result should be json"); + + assert_eq!(result.agents.len(), 2); + assert_eq!(result.agents[0].agent_name, "/root"); + assert_eq!( + result.agents[0].last_task_message.as_deref(), + Some("Main thread") + ); + assert_eq!(result.agents[1].agent_name, agent_path.as_str()); +} + #[tokio::test] async fn multi_agent_v2_send_message_rejects_legacy_items_field() { let (mut session, mut turn) = make_session_and_context().await; @@ -3699,7 +3780,7 @@ async fn multi_agent_v2_wait_agent_does_not_return_completed_content() { } #[tokio::test] -async fn multi_agent_v2_close_agent_accepts_task_name_target() { +async fn multi_agent_v2_interrupt_agent_accepts_task_name_target() { let (mut session, mut turn) = make_session_and_context().await; let manager = thread_manager(); let root = manager @@ -3736,29 +3817,77 @@ async fn multi_agent_v2_close_agent_accepts_task_name_target() { .resolve_agent_reference(session.thread_id, &turn.session_source, "worker") .await .expect("worker path should resolve"); + let worker_thread = manager + .get_thread(agent_id) + .await + .expect("worker thread should be resident"); + let worker_session = worker_thread.codex.session.clone(); + SpawnAgentHandlerV2::default() + .handle(invocation( + worker_session.clone(), + worker_session.new_default_turn().await, + "spawn_agent", + function_payload(json!({ + "message": "inspect a child task", + "task_name": "child" + })), + )) + .await + .expect("child spawn should succeed"); + let child_id = session + .services + .agent_control + .resolve_agent_reference(session.thread_id, &turn.session_source, "worker/child") + .await + .expect("child path should resolve"); - let output = CloseAgentHandlerV2 + let output = InterruptAgentHandler .handle(invocation( - session, - turn, - "close_agent", + session.clone(), + turn.clone(), + "interrupt_agent", function_payload(json!({"target": "worker"})), )) .await - .expect("close_agent should succeed for v2 task names"); + .expect("interrupt_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"); + let result: InterruptAgentResult = + serde_json::from_str(&content).expect("interrupt_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 + session + .services + .agent_control + .resolve_agent_reference(session.thread_id, &turn.session_source, "worker") + .await + .expect("worker path should remain resolvable"), + agent_id + ); + manager + .get_thread(agent_id) + .await + .expect("worker should remain resident"); + manager + .get_thread(child_id) + .await + .expect("child should remain resident"); + let ops = manager.captured_ops(); + assert!( + ops.iter() + .any(|(thread_id, op)| *thread_id == agent_id && matches!(op, Op::Interrupt)) + ); + assert!(!ops.iter().any(|(thread_id, op)| { + (*thread_id == agent_id || *thread_id == child_id) && matches!(op, Op::Shutdown) + })); + assert!( + !ops.iter() + .any(|(thread_id, op)| *thread_id == child_id && matches!(op, Op::Interrupt)) ); } #[tokio::test] -async fn multi_agent_v2_close_agent_reaps_stale_task_name_target() { +async fn multi_agent_v2_interrupt_agent_accepts_unloaded_task_name_target() { let (mut session, mut turn) = make_session_and_context().await; let mut config = (*turn.config).clone(); config.multi_agent_v2.max_concurrent_threads_per_session = 2; @@ -3819,18 +3948,18 @@ async fn multi_agent_v2_close_agent_reaps_stale_task_name_target() { .expect("removed worker thread should still accept shutdown"); stale_thread.wait_until_terminated().await; - let output = CloseAgentHandlerV2 + let output = InterruptAgentHandler .handle(invocation( session.clone(), turn.clone(), - "close_agent", + "interrupt_agent", function_payload(json!({"target": "worker"})), )) .await - .expect("close_agent should reap stale v2 task names"); + .expect("interrupt_agent should accept unloaded 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"); + let result: InterruptAgentResult = + serde_json::from_str(&content).expect("interrupt_agent result should be json"); assert_eq!(result.previous_status, AgentStatus::NotFound); assert_eq!(success, Some(true)); @@ -3841,7 +3970,7 @@ async fn multi_agent_v2_close_agent_reaps_stale_task_name_target() { ) .await .expect("open children should load"); - assert_eq!(open_children, Vec::::new()); + assert_eq!(open_children, vec![agent_id]); let closed_children = state_db .list_thread_spawn_children_with_status( root.thread_id, @@ -3849,36 +3978,26 @@ async fn multi_agent_v2_close_agent_reaps_stale_task_name_target() { ) .await .expect("closed children should load"); - assert_eq!(closed_children, vec![agent_id]); + assert_eq!(closed_children, Vec::::new()); - SpawnAgentHandlerV2::default() + let output = ListAgentsHandlerV2 .handle(invocation( session.clone(), turn.clone(), - "spawn_agent", - function_payload(json!({ - "message": "inspect this repo again", - "task_name": "replacement" - })), + "list_agents", + function_payload(json!({})), )) .await - .expect("spawn_agent should succeed after stale close releases the slot"); - let replacement_id = session - .services - .agent_control - .resolve_agent_reference(session.thread_id, &turn.session_source, "replacement") - .await - .expect("replacement path should resolve"); - let _ = session - .services - .agent_control - .shutdown_live_agent(replacement_id) - .await - .expect("replacement should shut down"); + .expect("list_agents should succeed"); + let (content, _) = expect_text_output(output); + let result: ListAgentsResult = + serde_json::from_str(&content).expect("list_agents result should be json"); + assert_eq!(result.agents.len(), 1); + assert_eq!(result.agents[0].agent_name, "/root"); } #[tokio::test] -async fn multi_agent_v2_close_agent_rejects_root_target_and_id() { +async fn multi_agent_v2_interrupt_agent_rejects_root_target_and_id() { let (mut session, mut turn) = make_session_and_context().await; let manager = thread_manager(); let root = manager @@ -3896,31 +4015,31 @@ async fn multi_agent_v2_close_agent_rejects_root_target_and_id() { let session = Arc::new(session); let turn = Arc::new(turn); - let root_path_error = CloseAgentHandlerV2 + let root_path_error = InterruptAgentHandler .handle(invocation( session.clone(), turn.clone(), - "close_agent", + "interrupt_agent", function_payload(json!({"target": "/root"})), )) .await .err() - .expect("close_agent should reject the root path"); + .expect("interrupt_agent should reject the root path"); assert_eq!( root_path_error, FunctionCallError::RespondToModel("root is not a spawned agent".to_string()) ); - let root_id_error = CloseAgentHandlerV2 + let root_id_error = InterruptAgentHandler .handle(invocation( session, turn, - "close_agent", + "interrupt_agent", function_payload(json!({"target": root.thread_id.to_string()})), )) .await .err() - .expect("close_agent should reject the root thread id"); + .expect("interrupt_agent should reject the root thread id"); assert_eq!( root_id_error, FunctionCallError::RespondToModel("root is not a spawned agent".to_string()) @@ -3928,7 +4047,7 @@ async fn multi_agent_v2_close_agent_rejects_root_target_and_id() { } #[tokio::test] -async fn multi_agent_v2_close_agent_rejects_self_target_by_id() { +async fn multi_agent_v2_interrupt_agent_rejects_self_target_by_id() { let (mut session, mut turn) = make_session_and_context().await; let manager = thread_manager(); let mut config = (*turn.config).clone(); @@ -3976,27 +4095,27 @@ async fn multi_agent_v2_close_agent_rejects_self_target_by_id() { agent_role: None, }); - let err = CloseAgentHandlerV2 + let err = InterruptAgentHandler .handle(invocation( Arc::new(session), Arc::new(turn), - "close_agent", + "interrupt_agent", function_payload(json!({"target": child_thread_id.to_string()})), )) .await .err() - .expect("close_agent should reject self-target by id"); + .expect("interrupt_agent should reject self-target by id"); assert_eq!( err, FunctionCallError::RespondToModel( - "an agent cannot close itself; return your result and let the parent close you if needed" + "an agent cannot interrupt itself; return your result and let the parent interrupt you if needed" .to_string() ) ); } #[tokio::test] -async fn multi_agent_v2_close_agent_rejects_self_target_by_task_name() { +async fn multi_agent_v2_interrupt_agent_rejects_self_target_by_task_name() { let (mut session, mut turn) = make_session_and_context().await; let manager = thread_manager(); let mut config = (*turn.config).clone(); @@ -4044,20 +4163,20 @@ async fn multi_agent_v2_close_agent_rejects_self_target_by_task_name() { agent_role: None, }); - let err = CloseAgentHandlerV2 + let err = InterruptAgentHandler .handle(invocation( Arc::new(session), Arc::new(turn), - "close_agent", + "interrupt_agent", function_payload(json!({"target": child_path.to_string()})), )) .await .err() - .expect("close_agent should reject self-target by task name"); + .expect("interrupt_agent should reject self-target by task name"); assert_eq!( err, FunctionCallError::RespondToModel( - "an agent cannot close itself; return your result and let the parent close you if needed" + "an agent cannot interrupt itself; return your result and let the parent interrupt you if needed" .to_string() ) ); 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 8bb207e3e19..6b5bd9f241a 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2.rs @@ -29,15 +29,15 @@ use serde::Deserialize; use serde::Serialize; use serde_json::Value as JsonValue; -pub(crate) use close_agent::Handler as CloseAgentHandler; pub(crate) use followup_task::Handler as FollowupTaskHandler; +pub(crate) use interrupt_agent::Handler as InterruptAgentHandler; 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 close_agent; mod followup_task; +mod interrupt_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/interrupt_agent.rs similarity index 56% rename from codex-rs/core/src/tools/handlers/multi_agents_v2/close_agent.rs rename to codex-rs/core/src/tools/handlers/multi_agents_v2/interrupt_agent.rs index 7b3f8d776e2..907367348b1 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2/close_agent.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/interrupt_agent.rs @@ -1,5 +1,5 @@ use super::*; -use crate::tools::handlers::multi_agents_spec::create_close_agent_tool_v2; +use crate::tools::handlers::multi_agents_spec::create_interrupt_agent_tool_v2; use crate::turn_timing::now_unix_timestamp_ms; use codex_protocol::error::CodexErr; use codex_tools::ToolSpec; @@ -9,24 +9,26 @@ pub(crate) struct Handler; #[async_trait::async_trait] impl ToolExecutor for Handler { fn tool_name(&self) -> ToolName { - ToolName::plain("close_agent") + ToolName::plain("interrupt_agent") } fn spec(&self) -> ToolSpec { - create_close_agent_tool_v2() + create_interrupt_agent_tool_v2() } async fn handle( &self, invocation: ToolInvocation, ) -> Result, FunctionCallError> { - handle_close_agent(invocation).await.map(boxed_tool_output) + handle_interrupt_agent(invocation) + .await + .map(boxed_tool_output) } } -async fn handle_close_agent( +async fn handle_interrupt_agent( invocation: ToolInvocation, -) -> Result { +) -> Result { let ToolInvocation { session, turn, @@ -35,11 +37,13 @@ async fn handle_close_agent( .. } = invocation; let arguments = function_arguments(payload)?; - let args: CloseAgentArgs = parse_arguments(&arguments)?; + let args: InterruptAgentArgs = 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); - let known_agent = receiver_agent.is_some(); - let receiver_agent = receiver_agent.unwrap_or_default(); + let receiver_agent = session + .services + .agent_control + .ensure_agent_known(agent_id) + .map_err(|err| collab_agent_error(agent_id, err))?; if receiver_agent .agent_path .as_ref() @@ -51,7 +55,7 @@ async fn handle_close_agent( } if agent_id == session.thread_id { return Err(FunctionCallError::RespondToModel( - "an agent cannot close itself; return your result and let the parent close you if needed" + "an agent cannot interrupt itself; return your result and let the parent interrupt you if needed" .to_string(), )); } @@ -67,43 +71,16 @@ async fn handle_close_agent( .into(), ) .await; - let status = match session + let status = session.services.agent_control.get_status(agent_id).await; + let result = match session .services .agent_control - .subscribe_status(agent_id) + .interrupt_agent(agent_id) .await { - Ok(mut status_rx) => status_rx.borrow_and_update().clone(), - Err(CodexErr::ThreadNotFound(_)) if known_agent => { - session.services.agent_control.get_status(agent_id).await - } - Err(err) => { - let status = session.services.agent_control.get_status(agent_id).await; - session - .send_event( - &turn, - CollabCloseEndEvent { - call_id: call_id.clone(), - completed_at_ms: now_unix_timestamp_ms(), - sender_thread_id: session.thread_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)); - } + Ok(_) | Err(CodexErr::ThreadNotFound(_)) | Err(CodexErr::InternalAgentDied) => Ok(()), + Err(err) => 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, @@ -121,7 +98,7 @@ async fn handle_close_agent( .await; result?; - Ok(CloseAgentResult { + Ok(InterruptAgentResult { previous_status: status, }) } @@ -134,18 +111,18 @@ impl CoreToolRuntime for Handler { #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] -struct CloseAgentArgs { +struct InterruptAgentArgs { target: String, } #[derive(Debug, Deserialize, Serialize)] -pub(crate) struct CloseAgentResult { +pub(crate) struct InterruptAgentResult { pub(crate) previous_status: AgentStatus, } -impl ToolOutput for CloseAgentResult { +impl ToolOutput for InterruptAgentResult { fn log_preview(&self) -> String { - tool_output_json_text(self, "close_agent") + tool_output_json_text(self, "interrupt_agent") } fn success_for_logging(&self) -> bool { @@ -153,10 +130,10 @@ impl ToolOutput for CloseAgentResult { } fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem { - tool_output_response_item(call_id, payload, self, Some(true), "close_agent") + tool_output_response_item(call_id, payload, self, Some(true), "interrupt_agent") } fn code_mode_result(&self, _payload: &ToolPayload) -> JsonValue { - tool_output_code_mode_result(self, "close_agent") + tool_output_code_mode_result(self, "interrupt_agent") } } diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs index 528fa55264c..07fd9d70ee0 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs @@ -74,12 +74,8 @@ pub(crate) async fn handle_message_string_tool( let receiver_agent = session .services .agent_control - .get_agent_metadata(receiver_thread_id) - .ok_or_else(|| { - FunctionCallError::RespondToModel(format!( - "agent with id {receiver_thread_id} not found" - )) - })?; + .ensure_agent_known(receiver_thread_id) + .map_err(|err| collab_agent_error(receiver_thread_id, err))?; if mode == MessageDeliveryMode::TriggerTurn && receiver_agent .agent_path diff --git a/codex-rs/core/src/tools/spec_plan.rs b/codex-rs/core/src/tools/spec_plan.rs index b1a01cd7aad..fed62a0ee10 100644 --- a/codex-rs/core/src/tools/spec_plan.rs +++ b/codex-rs/core/src/tools/spec_plan.rs @@ -37,8 +37,8 @@ use crate::tools::handlers::multi_agents_common::MAX_WAIT_TIMEOUT_MS; use crate::tools::handlers::multi_agents_common::MIN_WAIT_TIMEOUT_MS; use crate::tools::handlers::multi_agents_spec::SpawnAgentToolOptions; use crate::tools::handlers::multi_agents_spec::WaitAgentTimeoutOptions; -use crate::tools::handlers::multi_agents_v2::CloseAgentHandler as CloseAgentHandlerV2; use crate::tools::handlers::multi_agents_v2::FollowupTaskHandler as FollowupTaskHandlerV2; +use crate::tools::handlers::multi_agents_v2::InterruptAgentHandler; 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; @@ -742,7 +742,7 @@ fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mu exposure, )); planned_tools.add_arc(override_tool_exposure( - multi_agent_v2_handler(CloseAgentHandlerV2, tool_namespace), + multi_agent_v2_handler(InterruptAgentHandler, tool_namespace), exposure, )); planned_tools.add_arc(override_tool_exposure( diff --git a/codex-rs/core/src/tools/spec_plan_tests.rs b/codex-rs/core/src/tools/spec_plan_tests.rs index 5fe811b19e8..f96f41f0f25 100644 --- a/codex-rs/core/src/tools/spec_plan_tests.rs +++ b/codex-rs/core/src/tools/spec_plan_tests.rs @@ -959,6 +959,7 @@ async fn multi_agent_feature_selects_one_agent_tool_family() { "resume_agent", "wait_agent", "close_agent", + "interrupt_agent", "send_message", "followup_task", "assign_task", @@ -1011,10 +1012,10 @@ async fn multi_agent_feature_selects_one_agent_tool_family() { "send_message", "followup_task", "wait_agent", - "close_agent", + "interrupt_agent", "list_agents", ]); - v2.assert_visible_lacks(&["send_input", "resume_agent", "assign_task"]); + v2.assert_visible_lacks(&["send_input", "resume_agent", "assign_task", "close_agent"]); let spawn_agent_description = match v2.visible_spec("spawn_agent") { ToolSpec::Function(tool) => tool.description.as_str(), other => panic!("expected spawn_agent function spec, got {other:?}"), @@ -1096,6 +1097,7 @@ async fn v1_multi_agent_tools_defer_when_tool_search_available() { "resume_agent", "wait_agent", "close_agent", + "interrupt_agent", ]); for tool_name in [ "spawn_agent", @@ -1154,7 +1156,7 @@ async fn multi_agent_v2_can_use_configured_tool_namespace() { "send_message", "followup_task", "wait_agent", - "close_agent", + "interrupt_agent", "list_agents", ] { namespaced.assert_visible_lacks(&[tool_name]); @@ -1244,7 +1246,7 @@ async fn code_mode_only_can_expose_namespaced_multi_agent_v2_as_normal_tools() { "send_message", "followup_task", "wait_agent", - "close_agent", + "interrupt_agent", "list_agents", ] { assert!( @@ -1317,7 +1319,7 @@ async fn hosted_tools_follow_provider_auth_model_and_config_gates() { "send_message", "followup_task", "wait_agent", - "close_agent", + "interrupt_agent", "list_agents", // Hosted Responses tools. "web_search",