feat(core): expose agent tools in namespace

This commit is contained in:
Friel
2026-03-29 17:56:12 +00:00
parent bb95ec3ec6
commit 1222f23d0c
6 changed files with 107 additions and 71 deletions

View File

@@ -41,6 +41,10 @@ use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource;
use codex_tools::CommandToolOptions;
use codex_tools::FreeformTool;
use codex_tools::FreeformToolFormat;
use codex_tools::ResponsesApiNamespace;
use codex_tools::ResponsesApiNamespaceTool;
use codex_tools::ResponsesApiTool;
use codex_tools::ShellToolOptions;
use codex_tools::SpawnAgentToolOptions;
@@ -86,6 +90,7 @@ use serde::Serialize;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::LazyLock;
pub type JsonSchema = codex_tools::JsonSchema;
@@ -96,6 +101,8 @@ pub(crate) use codex_tools::mcp_call_tool_result_output_schema;
const TOOL_SEARCH_DESCRIPTION_TEMPLATE_SOURCE: &str =
include_str!("../../templates/search_tool/tool_description.md");
const TOOL_SEARCH_DESCRIPTION_TEMPLATE_KEY: &str = "app_descriptions";
const AGENT_TOOLS_NAMESPACE: &str = "agents";
const AGENT_TOOLS_NAMESPACE_DESCRIPTION: &str = "Agent collaboration tools for spawning, messaging, waiting on, listing, and closing subagents.";
static TOOL_SEARCH_DESCRIPTION_TEMPLATE: LazyLock<Template> = LazyLock::new(|| {
Template::parse(TOOL_SEARCH_DESCRIPTION_TEMPLATE_SOURCE)
.unwrap_or_else(|err| panic!("tool_search description template must parse: {err}"))
@@ -390,7 +397,10 @@ fn create_tool_search_tool(app_tools: &HashMap<String, ToolInfo>) -> ToolSpec {
},
),
]);
let mut app_descriptions = BTreeMap::new();
let mut app_descriptions = BTreeMap::from([(
AGENT_TOOLS_NAMESPACE.to_string(),
Some(AGENT_TOOLS_NAMESPACE_DESCRIPTION.to_string()),
)]);
for tool in app_tools.values() {
if tool.server_name != CODEX_APPS_MCP_SERVER_NAME {
continue;
@@ -611,6 +621,30 @@ fn push_tool_spec(
}
}
fn create_agent_tools_namespace(tools: Vec<ToolSpec>) -> ToolSpec {
let tools = tools
.into_iter()
.filter_map(|tool| match tool {
ToolSpec::Function(tool) => Some(ResponsesApiNamespaceTool::Function(tool)),
_ => None,
})
.collect();
ToolSpec::Namespace(ResponsesApiNamespace {
name: AGENT_TOOLS_NAMESPACE.to_string(),
description: AGENT_TOOLS_NAMESPACE_DESCRIPTION.to_string(),
tools,
})
}
fn register_agent_tool_handler<H>(builder: &mut ToolRegistryBuilder, name: &str, handler: Arc<H>)
where
H: crate::tools::registry::ToolHandler + 'static,
{
builder.register_handler(name, handler.clone());
builder.register_handler(tool_handler_key(name, Some(AGENT_TOOLS_NAMESPACE)), handler);
}
/// Builds the tool registry builder while collecting tool specs for later serialization.
#[cfg(test)]
pub(crate) fn build_specs(
@@ -665,8 +699,6 @@ pub(crate) fn build_specs_with_discoverable_tools(
use crate::tools::handlers::multi_agents_v2::SendMessageHandler as SendMessageHandlerV2;
use crate::tools::handlers::multi_agents_v2::SpawnAgentHandler as SpawnAgentHandlerV2;
use crate::tools::handlers::multi_agents_v2::WaitAgentHandler as WaitAgentHandlerV2;
use std::sync::Arc;
let mut builder = ToolRegistryBuilder::new();
let shell_handler = Arc::new(ShellHandler);
@@ -855,9 +887,8 @@ pub(crate) fn build_specs_with_discoverable_tools(
builder.register_handler("request_permissions", request_permissions_handler);
}
if config.search_tool
&& let Some(app_tools) = app_tools
{
if config.search_tool && (app_tools.is_some() || config.collab_tools) {
let app_tools = app_tools.unwrap_or_default();
let search_tool_handler = Arc::new(ToolSearchHandler::new(app_tools.clone()));
push_tool_spec(
&mut builder,
@@ -1001,102 +1032,67 @@ pub(crate) fn build_specs_with_discoverable_tools(
if config.collab_tools {
if config.multi_agent_v2 {
push_tool_spec(
&mut builder,
let agent_tools = vec![
create_spawn_agent_tool_v2(SpawnAgentToolOptions {
available_models: &config.available_models,
agent_type_description: crate::agent::role::spawn_tool_spec::build(
&config.agent_roles,
),
}),
/*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,
);
push_tool_spec(
&mut builder,
create_wait_agent_tool_v2(WaitAgentTimeoutOptions {
default_timeout_ms: DEFAULT_WAIT_TIMEOUT_MS,
min_timeout_ms: MIN_WAIT_TIMEOUT_MS,
max_timeout_ms: MAX_WAIT_TIMEOUT_MS,
}),
/*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,
);
push_tool_spec(
&mut builder,
create_list_agents_tool(),
];
push_tool_spec(
&mut builder,
create_agent_tools_namespace(agent_tools),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
builder.register_handler("spawn_agent", Arc::new(SpawnAgentHandlerV2));
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(
register_agent_tool_handler(&mut builder, "spawn_agent", Arc::new(SpawnAgentHandlerV2));
register_agent_tool_handler(
&mut builder,
"send_message",
Arc::new(SendMessageHandlerV2),
);
register_agent_tool_handler(&mut builder, "assign_task", Arc::new(AssignTaskHandlerV2));
register_agent_tool_handler(&mut builder, "wait_agent", Arc::new(WaitAgentHandlerV2));
register_agent_tool_handler(&mut builder, "close_agent", Arc::new(CloseAgentHandlerV2));
register_agent_tool_handler(&mut builder, "list_agents", Arc::new(ListAgentsHandlerV2));
} else {
let agent_tools = vec![
create_spawn_agent_tool_v1(SpawnAgentToolOptions {
available_models: &config.available_models,
agent_type_description: crate::agent::role::spawn_tool_spec::build(
&config.agent_roles,
),
}),
/*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(WaitAgentTimeoutOptions {
default_timeout_ms: DEFAULT_WAIT_TIMEOUT_MS,
min_timeout_ms: MIN_WAIT_TIMEOUT_MS,
max_timeout_ms: MAX_WAIT_TIMEOUT_MS,
}),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
create_close_agent_tool_v1(),
];
push_tool_spec(
&mut builder,
create_close_agent_tool_v1(),
create_agent_tools_namespace(agent_tools),
/*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));
register_agent_tool_handler(&mut builder, "spawn_agent", Arc::new(SpawnAgentHandler));
register_agent_tool_handler(&mut builder, "send_input", Arc::new(SendInputHandler));
register_agent_tool_handler(&mut builder, "resume_agent", Arc::new(ResumeAgentHandler));
register_agent_tool_handler(&mut builder, "wait_agent", Arc::new(WaitAgentHandler));
register_agent_tool_handler(&mut builder, "close_agent", Arc::new(CloseAgentHandler));
}
}

View File

@@ -226,7 +226,8 @@ fn strip_descriptions_tool(spec: &mut ToolSpec) {
ToolSpec::Function(ResponsesApiTool { parameters, .. }) => {
strip_descriptions_schema(parameters);
}
ToolSpec::Freeform(_)
ToolSpec::Namespace(_)
| ToolSpec::Freeform(_)
| ToolSpec::LocalShell {}
| ToolSpec::ImageGeneration { .. }
| ToolSpec::WebSearch { .. } => {}
@@ -2205,7 +2206,8 @@ fn search_tool_description_handles_no_enabled_apps() {
panic!("expected tool_search tool");
};
assert!(description.contains("None currently enabled."));
assert!(description.contains("agents"));
assert!(description.contains("Agent collaboration tools for spawning, messaging, waiting on, listing, and closing subagents."));
assert!(!description.contains("{{app_descriptions}}"));
}

View File

@@ -36,6 +36,7 @@ const SEARCH_TOOL_DESCRIPTION_SNIPPETS: [&str; 2] = [
"- Calendar: Plan events and manage your calendar.",
];
const TOOL_SEARCH_TOOL_NAME: &str = "tool_search";
const AGENTS_NAMESPACE_TOOL_NAME: &str = "agents";
const CALENDAR_CREATE_TOOL: &str = "mcp__codex_apps__calendar_create_event";
const CALENDAR_LIST_TOOL: &str = "mcp__codex_apps__calendar_list_events";
const SEARCH_CALENDAR_NAMESPACE: &str = "mcp__codex_apps__calendar";
@@ -217,7 +218,7 @@ async fn tool_search_disabled_by_default_exposes_apps_tools_directly() -> Result
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn search_tool_is_hidden_for_api_key_auth() -> Result<()> {
async fn search_tool_keeps_agents_namespace_for_api_key_auth() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
@@ -247,8 +248,12 @@ async fn search_tool_is_hidden_for_api_key_auth() -> Result<()> {
let body = mock.single_request().body_json();
let tools = tool_names(&body);
assert!(
!tools.iter().any(|name| name == TOOL_SEARCH_TOOL_NAME),
"tools list should not include {TOOL_SEARCH_TOOL_NAME} for API key auth: {tools:?}"
tools.iter().any(|name| name == TOOL_SEARCH_TOOL_NAME),
"tools list should include {TOOL_SEARCH_TOOL_NAME} for the always-present agents namespace: {tools:?}"
);
assert!(
tools.iter().any(|name| name == AGENTS_NAMESPACE_TOOL_NAME),
"tools list should include the {AGENTS_NAMESPACE_TOOL_NAME} namespace: {tools:?}"
);
Ok(())

View File

@@ -132,7 +132,8 @@ fn code_mode_tool_definition_for_spec(spec: &ToolSpec) -> Option<CodeModeToolDef
input_schema: None,
output_schema: None,
}),
ToolSpec::LocalShell {}
ToolSpec::Namespace(_)
| ToolSpec::LocalShell {}
| ToolSpec::ImageGeneration { .. }
| ToolSpec::ToolSearch { .. }
| ToolSpec::WebSearch { .. } => None,

View File

@@ -1,5 +1,6 @@
use crate::FreeformTool;
use crate::JsonSchema;
use crate::ResponsesApiNamespace;
use crate::ResponsesApiTool;
use codex_protocol::config_types::WebSearchContextSize;
use codex_protocol::config_types::WebSearchFilters as ConfigWebSearchFilters;
@@ -15,6 +16,8 @@ use serde_json::Value;
pub enum ToolSpec {
#[serde(rename = "function")]
Function(ResponsesApiTool),
#[serde(rename = "namespace")]
Namespace(ResponsesApiNamespace),
#[serde(rename = "tool_search")]
ToolSearch {
execution: String,
@@ -52,6 +55,7 @@ impl ToolSpec {
pub fn name(&self) -> &str {
match self {
ToolSpec::Function(tool) => tool.name.as_str(),
ToolSpec::Namespace(namespace) => namespace.name.as_str(),
ToolSpec::ToolSearch { .. } => "tool_search",
ToolSpec::LocalShell {} => "local_shell",
ToolSpec::ImageGeneration { .. } => "image_generation",

View File

@@ -6,6 +6,7 @@ use crate::AdditionalProperties;
use crate::FreeformTool;
use crate::FreeformToolFormat;
use crate::JsonSchema;
use crate::ResponsesApiNamespace;
use crate::ResponsesApiTool;
use crate::create_tools_json_for_responses_api;
use codex_protocol::config_types::WebSearchContextSize;
@@ -47,6 +48,15 @@ fn tool_spec_name_covers_all_variants() {
.name(),
"tool_search"
);
assert_eq!(
ToolSpec::Namespace(ResponsesApiNamespace {
name: "agents".to_string(),
description: "Agent tools".to_string(),
tools: Vec::new(),
})
.name(),
"agents"
);
assert_eq!(ToolSpec::LocalShell {}.name(), "local_shell");
assert_eq!(
ToolSpec::ImageGeneration {
@@ -166,6 +176,24 @@ fn create_tools_json_for_responses_api_includes_top_level_name() {
);
}
#[test]
fn namespace_tool_spec_serializes_expected_wire_shape() {
assert_eq!(
serde_json::to_value(ToolSpec::Namespace(ResponsesApiNamespace {
name: "agents".to_string(),
description: "Agent collaboration tools.".to_string(),
tools: Vec::new(),
}))
.expect("serialize namespace"),
json!({
"type": "namespace",
"name": "agents",
"description": "Agent collaboration tools.",
"tools": []
})
);
}
#[test]
fn web_search_tool_spec_serializes_expected_wire_shape() {
assert_eq!(