Defer v1 multi-agent tools behind tool search (#23144)

Summary: defer v1 multi-agent tools when tool_search and namespace tools
are available; keep concise searchable descriptions and move the v1
usage guidance into developer instructions; add targeted coverage.
Testing: not run per request; ran just fmt.
This commit is contained in:
jif-oai
2026-05-19 15:04:35 +02:00
committed by GitHub
parent 80fdd4688f
commit b3ae3de405
14 changed files with 273 additions and 66 deletions

View File

@@ -1,6 +1,5 @@
---
source: core/src/session/tests.rs
assertion_line: 1619
expression: snapshot
---
Scenario: First request after fork when startup preserves the parent baseline, the fork changes approval policy, and the first forked turn enters plan mode.

View File

@@ -18,6 +18,7 @@ pub(crate) use crate::tools::handlers::multi_agents_common::*;
use crate::tools::handlers::parse_arguments;
use crate::tools::registry::CoreToolRuntime;
use crate::tools::registry::ToolExecutor;
use crate::tools::tool_search_entry::ToolSearchInfo;
use codex_protocol::ThreadId;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::openai_models::ReasoningEffort;
@@ -34,10 +35,14 @@ use codex_protocol::protocol::CollabWaitingBeginEvent;
use codex_protocol::protocol::CollabWaitingEndEvent;
use codex_protocol::user_input::UserInput;
use codex_tools::ToolName;
use codex_tools::ToolSearchSourceInfo;
use serde::Deserialize;
use serde::Serialize;
use serde_json::Value as JsonValue;
const MULTI_AGENT_TOOL_SEARCH_SOURCE_NAME: &str = "Multi-agent tools";
const MULTI_AGENT_TOOL_SEARCH_SOURCE_DESCRIPTION: &str = "Spawn and manage sub-agents.";
pub(crate) fn parse_agent_id_target(target: &str) -> Result<ThreadId, FunctionCallError> {
ThreadId::from_string(target).map_err(|err| {
FunctionCallError::RespondToModel(format!("invalid agent id {target}: {err:?}"))
@@ -59,6 +64,20 @@ pub(crate) fn parse_agent_id_targets(
.collect()
}
fn multi_agent_tool_search_info(
search_text: &str,
spec: codex_tools::ToolSpec,
) -> Option<ToolSearchInfo> {
ToolSearchInfo::from_spec(
search_text.to_string(),
spec,
Some(ToolSearchSourceInfo {
name: MULTI_AGENT_TOOL_SEARCH_SOURCE_NAME.to_string(),
description: Some(MULTI_AGENT_TOOL_SEARCH_SOURCE_DESCRIPTION.to_string()),
}),
)
}
pub(crate) use close_agent::Handler as CloseAgentHandler;
pub(crate) use resume_agent::Handler as ResumeAgentHandler;
pub(crate) use send_input::Handler as SendInputHandler;

View File

@@ -107,6 +107,13 @@ async fn handle_close_agent(
}
impl CoreToolRuntime for Handler {
fn search_info(&self) -> Option<ToolSearchInfo> {
multi_agent_tool_search_info(
"close_agent close shutdown stop agent subagent thread status target",
self.spec()?,
)
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}

View File

@@ -135,6 +135,13 @@ async fn handle_resume_agent(
}
impl CoreToolRuntime for Handler {
fn search_info(&self) -> Option<ToolSearchInfo> {
multi_agent_tool_search_info(
"resume_agent resume reopen closed agent subagent thread id target",
self.spec()?,
)
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}

View File

@@ -91,6 +91,13 @@ impl ToolExecutor<ToolInvocation> for Handler {
}
impl CoreToolRuntime for Handler {
fn search_info(&self) -> Option<ToolSearchInfo> {
multi_agent_tool_search_info(
"send_input send message existing agent subagent follow up interrupt redirect queue target",
self.spec()?,
)
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}

View File

@@ -200,6 +200,13 @@ async fn handle_spawn_agent(
}
impl CoreToolRuntime for Handler {
fn search_info(&self) -> Option<ToolSearchInfo> {
multi_agent_tool_search_info(
"spawn_agent spawn agent subagent sub-agent delegate delegation parallel work worker explorer no-apps fork model reasoning",
self.spec()?,
)
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}

View File

@@ -203,6 +203,13 @@ impl ToolExecutor<ToolInvocation> for Handler {
}
impl CoreToolRuntime for Handler {
fn search_info(&self) -> Option<ToolSearchInfo> {
multi_agent_tool_search_info(
"wait_agent wait agent subagent status final result complete timeout targets",
self.spec()?,
)
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}

View File

@@ -610,23 +610,35 @@ fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mu
} else {
let agent_type_description =
agent_type_description(turn_context, context.default_agent_type_description);
planned_tools.add_runtime(SpawnAgentHandler::new(SpawnAgentToolOptions {
available_models: turn_context.available_models.clone(),
agent_type_description,
hide_agent_type_model_reasoning: turn_context
.config
.multi_agent_v2
.hide_spawn_agent_metadata,
include_usage_hint: turn_context.config.multi_agent_v2.usage_hint_enabled,
usage_hint_text: turn_context.config.multi_agent_v2.usage_hint_text.clone(),
max_concurrent_threads_per_session: max_concurrent_threads_per_session(
turn_context,
),
}));
planned_tools.add_runtime(SendInputHandler);
planned_tools.add_runtime(ResumeAgentHandler);
planned_tools.add_runtime(WaitAgentHandler::new(context.wait_agent_timeouts));
planned_tools.add_runtime(CloseAgentHandler);
let exposure =
if search_tool_enabled(turn_context) && namespace_tools_enabled(turn_context) {
ToolExposure::Deferred
} else {
ToolExposure::Direct
};
planned_tools.add_runtime_arc(multi_agent_v1_handler(
SpawnAgentHandler::new(SpawnAgentToolOptions {
available_models: turn_context.available_models.clone(),
agent_type_description,
hide_agent_type_model_reasoning: turn_context
.config
.multi_agent_v2
.hide_spawn_agent_metadata,
include_usage_hint: turn_context.config.multi_agent_v2.usage_hint_enabled,
usage_hint_text: turn_context.config.multi_agent_v2.usage_hint_text.clone(),
max_concurrent_threads_per_session: max_concurrent_threads_per_session(
turn_context,
),
}),
exposure,
));
planned_tools.add_runtime_arc(multi_agent_v1_handler(SendInputHandler, exposure));
planned_tools.add_runtime_arc(multi_agent_v1_handler(ResumeAgentHandler, exposure));
planned_tools.add_runtime_arc(multi_agent_v1_handler(
WaitAgentHandler::new(context.wait_agent_timeouts),
exposure,
));
planned_tools.add_runtime_arc(multi_agent_v1_handler(CloseAgentHandler, exposure));
}
}
@@ -757,6 +769,13 @@ fn append_extension_tool_executors(
}
}
fn multi_agent_v1_handler(
handler: impl CoreToolRuntime + 'static,
exposure: ToolExposure,
) -> Arc<dyn CoreToolRuntime> {
override_tool_exposure(Arc::new(handler), exposure)
}
fn multi_agent_v2_handler(
handler: impl CoreToolRuntime + 'static,
exposure: ToolExposure,

View File

@@ -470,6 +470,7 @@ async fn mcp_and_tool_search_follow_direct_and_deferred_tool_exposure() {
missing_model_capability.assert_visible_lacks(&["tool_search"]);
let missing_deferred_tools = probe(|turn| {
set_feature(turn, Feature::Collab, /*enabled*/ false);
turn.model_info.supports_search_tool = true;
})
.await;
@@ -653,6 +654,39 @@ async fn multi_agent_feature_selects_one_agent_tool_family() {
);
}
#[tokio::test]
async fn v1_multi_agent_tools_defer_when_tool_search_available() {
let plan = probe(|turn| {
turn.model_info.supports_search_tool = true;
set_feature(turn, Feature::Collab, /*enabled*/ true);
set_feature(turn, Feature::MultiAgentV2, /*enabled*/ false);
})
.await;
plan.assert_visible_contains(&["tool_search"]);
plan.assert_visible_lacks(&[
"spawn_agent",
"send_input",
"resume_agent",
"wait_agent",
"close_agent",
]);
for tool_name in [
"spawn_agent",
"send_input",
"resume_agent",
"wait_agent",
"close_agent",
] {
plan.assert_registered_contains(&[tool_name]);
assert_eq!(plan.exposure(tool_name), ToolExposure::Deferred);
}
let ToolSpec::ToolSearch { description, .. } = plan.visible_spec("tool_search") else {
panic!("expected visible tool_search spec");
};
assert!(description.contains("- Multi-agent tools: Spawn and manage sub-agents."));
}
#[tokio::test]
async fn multi_agent_v2_can_use_configured_tool_namespace() {
let namespaced = probe(|turn| {