Rename multi-agent v2 close_agent to interrupt_agent (#26994)

## Why

`close_agent` is the wrong model-facing name for the v2 operation after
the residency changes. V2 agents remain reusable by task name, and
residency/unloading owns capacity management; the exposed tool should
describe the action it actually performs: interrupt the target agent's
current turn without making the agent unavailable for future messages or
follow-up tasks.

## What changed

- Rename the multi-agent v2 tool from `close_agent` to
`interrupt_agent`.
- Keep the v1 `close_agent` surface unchanged.
- Update the v2 handler to send `Op::Interrupt`, keep interrupted agents
registered, and reject root/self targets with interrupt-specific errors.
- Route interrupt delivery through the existing dead-thread cleanup path
so stale resident entries do not keep consuming capacity.
- Update tool planning and handler tests for the new v2 surface and
semantics.

## Verification

Added focused coverage in:

- `core/src/tools/spec_plan_tests.rs`
- `core/src/tools/handlers/multi_agents_tests.rs`
This commit is contained in:
jif
2026-06-08 14:46:35 +02:00
committed by GitHub
Unverified
parent 743f5aad38
commit 8d415050fc
8 changed files with 239 additions and 130 deletions
+12 -1
View File
@@ -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<String> {
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<AgentMetadata> {
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,
@@ -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()]
}
},
@@ -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<String>,
}
#[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 output = CloseAgentHandlerV2
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(
session,
turn,
"close_agent",
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 = InterruptAgentHandler
.handle(invocation(
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::<ThreadId>::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::<ThreadId>::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()
)
);
@@ -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;
@@ -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<ToolInvocation> 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<Box<dyn crate::tools::context::ToolOutput>, 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<CloseAgentResult, FunctionCallError> {
) -> Result<InterruptAgentResult, FunctionCallError> {
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")
}
}
@@ -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
+2 -2
View File
@@ -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(
+7 -5
View File
@@ -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",