Skip to content

Commit 1222f23

Browse files
committed
feat(core): expose agent tools in namespace
1 parent bb95ec3 commit 1222f23

File tree

6 files changed

+105
-69
lines changed

6 files changed

+105
-69
lines changed

codex-rs/core/src/tools/spec.rs

Lines changed: 59 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ use codex_protocol::protocol::SandboxPolicy;
4141
use codex_protocol::protocol::SessionSource;
4242
use codex_protocol::protocol::SubAgentSource;
4343
use codex_tools::CommandToolOptions;
44+
use codex_tools::FreeformTool;
45+
use codex_tools::FreeformToolFormat;
46+
use codex_tools::ResponsesApiNamespace;
47+
use codex_tools::ResponsesApiNamespaceTool;
4448
use codex_tools::ResponsesApiTool;
4549
use codex_tools::ShellToolOptions;
4650
use codex_tools::SpawnAgentToolOptions;
@@ -86,6 +90,7 @@ use serde::Serialize;
8690
use std::collections::BTreeMap;
8791
use std::collections::HashMap;
8892
use std::path::PathBuf;
93+
use std::sync::Arc;
8994
use std::sync::LazyLock;
9095

9196
pub type JsonSchema = codex_tools::JsonSchema;
@@ -96,6 +101,8 @@ pub(crate) use codex_tools::mcp_call_tool_result_output_schema;
96101
const TOOL_SEARCH_DESCRIPTION_TEMPLATE_SOURCE: &str =
97102
include_str!("../../templates/search_tool/tool_description.md");
98103
const TOOL_SEARCH_DESCRIPTION_TEMPLATE_KEY: &str = "app_descriptions";
104+
const AGENT_TOOLS_NAMESPACE: &str = "agents";
105+
const AGENT_TOOLS_NAMESPACE_DESCRIPTION: &str = "Agent collaboration tools for spawning, messaging, waiting on, listing, and closing subagents.";
99106
static TOOL_SEARCH_DESCRIPTION_TEMPLATE: LazyLock<Template> = LazyLock::new(|| {
100107
Template::parse(TOOL_SEARCH_DESCRIPTION_TEMPLATE_SOURCE)
101108
.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 {
390397
},
391398
),
392399
]);
393-
let mut app_descriptions = BTreeMap::new();
400+
let mut app_descriptions = BTreeMap::from([(
401+
AGENT_TOOLS_NAMESPACE.to_string(),
402+
Some(AGENT_TOOLS_NAMESPACE_DESCRIPTION.to_string()),
403+
)]);
394404
for tool in app_tools.values() {
395405
if tool.server_name != CODEX_APPS_MCP_SERVER_NAME {
396406
continue;
@@ -611,6 +621,30 @@ fn push_tool_spec(
611621
}
612622
}
613623

624+
fn create_agent_tools_namespace(tools: Vec<ToolSpec>) -> ToolSpec {
625+
let tools = tools
626+
.into_iter()
627+
.filter_map(|tool| match tool {
628+
ToolSpec::Function(tool) => Some(ResponsesApiNamespaceTool::Function(tool)),
629+
_ => None,
630+
})
631+
.collect();
632+
633+
ToolSpec::Namespace(ResponsesApiNamespace {
634+
name: AGENT_TOOLS_NAMESPACE.to_string(),
635+
description: AGENT_TOOLS_NAMESPACE_DESCRIPTION.to_string(),
636+
tools,
637+
})
638+
}
639+
640+
fn register_agent_tool_handler<H>(builder: &mut ToolRegistryBuilder, name: &str, handler: Arc<H>)
641+
where
642+
H: crate::tools::registry::ToolHandler + 'static,
643+
{
644+
builder.register_handler(name, handler.clone());
645+
builder.register_handler(tool_handler_key(name, Some(AGENT_TOOLS_NAMESPACE)), handler);
646+
}
647+
614648
/// Builds the tool registry builder while collecting tool specs for later serialization.
615649
#[cfg(test)]
616650
pub(crate) fn build_specs(
@@ -665,8 +699,6 @@ pub(crate) fn build_specs_with_discoverable_tools(
665699
use crate::tools::handlers::multi_agents_v2::SendMessageHandler as SendMessageHandlerV2;
666700
use crate::tools::handlers::multi_agents_v2::SpawnAgentHandler as SpawnAgentHandlerV2;
667701
use crate::tools::handlers::multi_agents_v2::WaitAgentHandler as WaitAgentHandlerV2;
668-
use std::sync::Arc;
669-
670702
let mut builder = ToolRegistryBuilder::new();
671703

672704
let shell_handler = Arc::new(ShellHandler);
@@ -855,9 +887,8 @@ pub(crate) fn build_specs_with_discoverable_tools(
855887
builder.register_handler("request_permissions", request_permissions_handler);
856888
}
857889

858-
if config.search_tool
859-
&& let Some(app_tools) = app_tools
860-
{
890+
if config.search_tool && (app_tools.is_some() || config.collab_tools) {
891+
let app_tools = app_tools.unwrap_or_default();
861892
let search_tool_handler = Arc::new(ToolSearchHandler::new(app_tools.clone()));
862893
push_tool_spec(
863894
&mut builder,
@@ -1001,102 +1032,67 @@ pub(crate) fn build_specs_with_discoverable_tools(
10011032

10021033
if config.collab_tools {
10031034
if config.multi_agent_v2 {
1004-
push_tool_spec(
1005-
&mut builder,
1035+
let agent_tools = vec![
10061036
create_spawn_agent_tool_v2(SpawnAgentToolOptions {
10071037
available_models: &config.available_models,
10081038
agent_type_description: crate::agent::role::spawn_tool_spec::build(
10091039
&config.agent_roles,
10101040
),
10111041
}),
1012-
/*supports_parallel_tool_calls*/ false,
1013-
config.code_mode_enabled,
1014-
);
1015-
push_tool_spec(
1016-
&mut builder,
10171042
create_send_message_tool(),
1018-
/*supports_parallel_tool_calls*/ false,
1019-
config.code_mode_enabled,
1020-
);
1021-
push_tool_spec(
1022-
&mut builder,
10231043
create_assign_task_tool(),
1024-
/*supports_parallel_tool_calls*/ false,
1025-
config.code_mode_enabled,
1026-
);
1027-
push_tool_spec(
1028-
&mut builder,
10291044
create_wait_agent_tool_v2(WaitAgentTimeoutOptions {
10301045
default_timeout_ms: DEFAULT_WAIT_TIMEOUT_MS,
10311046
min_timeout_ms: MIN_WAIT_TIMEOUT_MS,
10321047
max_timeout_ms: MAX_WAIT_TIMEOUT_MS,
10331048
}),
1034-
/*supports_parallel_tool_calls*/ false,
1035-
config.code_mode_enabled,
1036-
);
1049+
create_close_agent_tool_v2(),
1050+
create_list_agents_tool(),
1051+
];
10371052
push_tool_spec(
10381053
&mut builder,
1039-
create_close_agent_tool_v2(),
1054+
create_agent_tools_namespace(agent_tools),
10401055
/*supports_parallel_tool_calls*/ false,
10411056
config.code_mode_enabled,
10421057
);
1043-
push_tool_spec(
1058+
register_agent_tool_handler(&mut builder, "spawn_agent", Arc::new(SpawnAgentHandlerV2));
1059+
register_agent_tool_handler(
10441060
&mut builder,
1045-
create_list_agents_tool(),
1046-
/*supports_parallel_tool_calls*/ false,
1047-
config.code_mode_enabled,
1061+
"send_message",
1062+
Arc::new(SendMessageHandlerV2),
10481063
);
1049-
builder.register_handler("spawn_agent", Arc::new(SpawnAgentHandlerV2));
1050-
builder.register_handler("send_message", Arc::new(SendMessageHandlerV2));
1051-
builder.register_handler("assign_task", Arc::new(AssignTaskHandlerV2));
1052-
builder.register_handler("wait_agent", Arc::new(WaitAgentHandlerV2));
1053-
builder.register_handler("close_agent", Arc::new(CloseAgentHandlerV2));
1054-
builder.register_handler("list_agents", Arc::new(ListAgentsHandlerV2));
1064+
register_agent_tool_handler(&mut builder, "assign_task", Arc::new(AssignTaskHandlerV2));
1065+
register_agent_tool_handler(&mut builder, "wait_agent", Arc::new(WaitAgentHandlerV2));
1066+
register_agent_tool_handler(&mut builder, "close_agent", Arc::new(CloseAgentHandlerV2));
1067+
register_agent_tool_handler(&mut builder, "list_agents", Arc::new(ListAgentsHandlerV2));
10551068
} else {
1056-
push_tool_spec(
1057-
&mut builder,
1069+
let agent_tools = vec![
10581070
create_spawn_agent_tool_v1(SpawnAgentToolOptions {
10591071
available_models: &config.available_models,
10601072
agent_type_description: crate::agent::role::spawn_tool_spec::build(
10611073
&config.agent_roles,
10621074
),
10631075
}),
1064-
/*supports_parallel_tool_calls*/ false,
1065-
config.code_mode_enabled,
1066-
);
1067-
push_tool_spec(
1068-
&mut builder,
10691076
create_send_input_tool_v1(),
1070-
/*supports_parallel_tool_calls*/ false,
1071-
config.code_mode_enabled,
1072-
);
1073-
push_tool_spec(
1074-
&mut builder,
10751077
create_resume_agent_tool(),
1076-
/*supports_parallel_tool_calls*/ false,
1077-
config.code_mode_enabled,
1078-
);
1079-
builder.register_handler("resume_agent", Arc::new(ResumeAgentHandler));
1080-
push_tool_spec(
1081-
&mut builder,
10821078
create_wait_agent_tool_v1(WaitAgentTimeoutOptions {
10831079
default_timeout_ms: DEFAULT_WAIT_TIMEOUT_MS,
10841080
min_timeout_ms: MIN_WAIT_TIMEOUT_MS,
10851081
max_timeout_ms: MAX_WAIT_TIMEOUT_MS,
10861082
}),
1087-
/*supports_parallel_tool_calls*/ false,
1088-
config.code_mode_enabled,
1089-
);
1083+
create_close_agent_tool_v1(),
1084+
];
10901085
push_tool_spec(
10911086
&mut builder,
1092-
create_close_agent_tool_v1(),
1087+
create_agent_tools_namespace(agent_tools),
10931088
/*supports_parallel_tool_calls*/ false,
10941089
config.code_mode_enabled,
10951090
);
1096-
builder.register_handler("spawn_agent", Arc::new(SpawnAgentHandler));
1097-
builder.register_handler("send_input", Arc::new(SendInputHandler));
1098-
builder.register_handler("wait_agent", Arc::new(WaitAgentHandler));
1099-
builder.register_handler("close_agent", Arc::new(CloseAgentHandler));
1091+
register_agent_tool_handler(&mut builder, "spawn_agent", Arc::new(SpawnAgentHandler));
1092+
register_agent_tool_handler(&mut builder, "send_input", Arc::new(SendInputHandler));
1093+
register_agent_tool_handler(&mut builder, "resume_agent", Arc::new(ResumeAgentHandler));
1094+
register_agent_tool_handler(&mut builder, "wait_agent", Arc::new(WaitAgentHandler));
1095+
register_agent_tool_handler(&mut builder, "close_agent", Arc::new(CloseAgentHandler));
11001096
}
11011097
}
11021098

codex-rs/core/src/tools/spec_tests.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,8 @@ fn strip_descriptions_tool(spec: &mut ToolSpec) {
226226
ToolSpec::Function(ResponsesApiTool { parameters, .. }) => {
227227
strip_descriptions_schema(parameters);
228228
}
229-
ToolSpec::Freeform(_)
229+
ToolSpec::Namespace(_)
230+
| ToolSpec::Freeform(_)
230231
| ToolSpec::LocalShell {}
231232
| ToolSpec::ImageGeneration { .. }
232233
| ToolSpec::WebSearch { .. } => {}
@@ -2205,7 +2206,8 @@ fn search_tool_description_handles_no_enabled_apps() {
22052206
panic!("expected tool_search tool");
22062207
};
22072208

2208-
assert!(description.contains("None currently enabled."));
2209+
assert!(description.contains("agents"));
2210+
assert!(description.contains("Agent collaboration tools for spawning, messaging, waiting on, listing, and closing subagents."));
22092211
assert!(!description.contains("{{app_descriptions}}"));
22102212
}
22112213

codex-rs/core/tests/suite/search_tool.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const SEARCH_TOOL_DESCRIPTION_SNIPPETS: [&str; 2] = [
3636
"- Calendar: Plan events and manage your calendar.",
3737
];
3838
const TOOL_SEARCH_TOOL_NAME: &str = "tool_search";
39+
const AGENTS_NAMESPACE_TOOL_NAME: &str = "agents";
3940
const CALENDAR_CREATE_TOOL: &str = "mcp__codex_apps__calendar_create_event";
4041
const CALENDAR_LIST_TOOL: &str = "mcp__codex_apps__calendar_list_events";
4142
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
217218
}
218219

219220
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
220-
async fn search_tool_is_hidden_for_api_key_auth() -> Result<()> {
221+
async fn search_tool_keeps_agents_namespace_for_api_key_auth() -> Result<()> {
221222
skip_if_no_network!(Ok(()));
222223

223224
let server = start_mock_server().await;
@@ -247,8 +248,12 @@ async fn search_tool_is_hidden_for_api_key_auth() -> Result<()> {
247248
let body = mock.single_request().body_json();
248249
let tools = tool_names(&body);
249250
assert!(
250-
!tools.iter().any(|name| name == TOOL_SEARCH_TOOL_NAME),
251-
"tools list should not include {TOOL_SEARCH_TOOL_NAME} for API key auth: {tools:?}"
251+
tools.iter().any(|name| name == TOOL_SEARCH_TOOL_NAME),
252+
"tools list should include {TOOL_SEARCH_TOOL_NAME} for the always-present agents namespace: {tools:?}"
253+
);
254+
assert!(
255+
tools.iter().any(|name| name == AGENTS_NAMESPACE_TOOL_NAME),
256+
"tools list should include the {AGENTS_NAMESPACE_TOOL_NAME} namespace: {tools:?}"
252257
);
253258

254259
Ok(())

codex-rs/tools/src/code_mode.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,8 @@ fn code_mode_tool_definition_for_spec(spec: &ToolSpec) -> Option<CodeModeToolDef
132132
input_schema: None,
133133
output_schema: None,
134134
}),
135-
ToolSpec::LocalShell {}
135+
ToolSpec::Namespace(_)
136+
| ToolSpec::LocalShell {}
136137
| ToolSpec::ImageGeneration { .. }
137138
| ToolSpec::ToolSearch { .. }
138139
| ToolSpec::WebSearch { .. } => None,

codex-rs/tools/src/tool_spec.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::FreeformTool;
22
use crate::JsonSchema;
3+
use crate::ResponsesApiNamespace;
34
use crate::ResponsesApiTool;
45
use codex_protocol::config_types::WebSearchContextSize;
56
use codex_protocol::config_types::WebSearchFilters as ConfigWebSearchFilters;
@@ -15,6 +16,8 @@ use serde_json::Value;
1516
pub enum ToolSpec {
1617
#[serde(rename = "function")]
1718
Function(ResponsesApiTool),
19+
#[serde(rename = "namespace")]
20+
Namespace(ResponsesApiNamespace),
1821
#[serde(rename = "tool_search")]
1922
ToolSearch {
2023
execution: String,
@@ -52,6 +55,7 @@ impl ToolSpec {
5255
pub fn name(&self) -> &str {
5356
match self {
5457
ToolSpec::Function(tool) => tool.name.as_str(),
58+
ToolSpec::Namespace(namespace) => namespace.name.as_str(),
5559
ToolSpec::ToolSearch { .. } => "tool_search",
5660
ToolSpec::LocalShell {} => "local_shell",
5761
ToolSpec::ImageGeneration { .. } => "image_generation",

codex-rs/tools/src/tool_spec_tests.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use crate::AdditionalProperties;
66
use crate::FreeformTool;
77
use crate::FreeformToolFormat;
88
use crate::JsonSchema;
9+
use crate::ResponsesApiNamespace;
910
use crate::ResponsesApiTool;
1011
use crate::create_tools_json_for_responses_api;
1112
use codex_protocol::config_types::WebSearchContextSize;
@@ -47,6 +48,15 @@ fn tool_spec_name_covers_all_variants() {
4748
.name(),
4849
"tool_search"
4950
);
51+
assert_eq!(
52+
ToolSpec::Namespace(ResponsesApiNamespace {
53+
name: "agents".to_string(),
54+
description: "Agent tools".to_string(),
55+
tools: Vec::new(),
56+
})
57+
.name(),
58+
"agents"
59+
);
5060
assert_eq!(ToolSpec::LocalShell {}.name(), "local_shell");
5161
assert_eq!(
5262
ToolSpec::ImageGeneration {
@@ -166,6 +176,24 @@ fn create_tools_json_for_responses_api_includes_top_level_name() {
166176
);
167177
}
168178

179+
#[test]
180+
fn namespace_tool_spec_serializes_expected_wire_shape() {
181+
assert_eq!(
182+
serde_json::to_value(ToolSpec::Namespace(ResponsesApiNamespace {
183+
name: "agents".to_string(),
184+
description: "Agent collaboration tools.".to_string(),
185+
tools: Vec::new(),
186+
}))
187+
.expect("serialize namespace"),
188+
json!({
189+
"type": "namespace",
190+
"name": "agents",
191+
"description": "Agent collaboration tools.",
192+
"tools": []
193+
})
194+
);
195+
}
196+
169197
#[test]
170198
fn web_search_tool_spec_serializes_expected_wire_shape() {
171199
assert_eq!(

0 commit comments

Comments
 (0)