Render namespace description for tools (#16879)

This commit is contained in:
Vivian Fang
2026-04-08 02:39:40 -07:00
committed by GitHub
Unverified
parent 9091999c83
commit d47b755aa2
19 changed files with 323 additions and 54 deletions
+103 -12
View File
@@ -1,6 +1,7 @@
use serde::Deserialize;
use serde::Serialize;
use serde_json::Value as JsonValue;
use std::collections::BTreeMap;
use crate::PUBLIC_TOOL_NAME;
@@ -57,6 +58,12 @@ pub struct ToolDefinition {
pub output_schema: Option<JsonValue>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ToolNamespaceDescription {
pub name: String,
pub description: String,
}
#[derive(Debug, Default, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
struct CodeModeExecPragma {
@@ -163,6 +170,7 @@ pub fn is_code_mode_nested_tool(tool_name: &str) -> bool {
pub fn build_exec_tool_description(
enabled_tools: &[(String, String)],
namespace_descriptions: &BTreeMap<String, ToolNamespaceDescription>,
code_mode_only: bool,
) -> String {
if !code_mode_only {
@@ -175,17 +183,38 @@ pub fn build_exec_tool_description(
];
if !enabled_tools.is_empty() {
let nested_tool_reference = enabled_tools
.iter()
.map(|(name, nested_description)| {
let global_name = normalize_code_mode_identifier(name);
format!(
"### `{global_name}` (`{name}`)\n{}",
nested_description.trim()
)
})
.collect::<Vec<_>>()
.join("\n\n");
let mut current_namespace: Option<&str> = None;
let mut nested_tool_sections = Vec::with_capacity(enabled_tools.len());
for (name, nested_description) in enabled_tools {
let next_namespace = namespace_descriptions
.get(name)
.map(|namespace_description| namespace_description.name.as_str());
if next_namespace != current_namespace {
if let Some(namespace_description) = namespace_descriptions.get(name) {
let namespace_description_text = namespace_description.description.trim();
if !namespace_description_text.is_empty() {
nested_tool_sections.push(format!(
"## {}\n{namespace_description_text}",
namespace_description.name
));
}
}
current_namespace = next_namespace;
}
let global_name = normalize_code_mode_identifier(name);
let nested_description = nested_description.trim();
if nested_description.is_empty() {
nested_tool_sections.push(format!("### `{global_name}` (`{name}`)"));
} else {
nested_tool_sections.push(format!(
"### `{global_name}` (`{name}`)\n{nested_description}"
));
}
}
let nested_tool_reference = nested_tool_sections.join("\n\n");
sections.push(nested_tool_reference);
}
@@ -524,12 +553,14 @@ mod tests {
use super::CodeModeToolKind;
use super::ParsedExecSource;
use super::ToolDefinition;
use super::ToolNamespaceDescription;
use super::augment_tool_definition;
use super::build_exec_tool_description;
use super::normalize_code_mode_identifier;
use super::parse_exec_source;
use pretty_assertions::assert_eq;
use serde_json::json;
use std::collections::BTreeMap;
#[test]
fn parse_exec_source_without_pragma() {
@@ -646,6 +677,7 @@ mod tests {
fn code_mode_only_description_includes_nested_tools() {
let description = build_exec_tool_description(
&[("foo".to_string(), "bar".to_string())],
&BTreeMap::new(),
/*code_mode_only*/ true,
);
assert!(description.contains("### `foo` (`foo`)"));
@@ -653,8 +685,67 @@ mod tests {
#[test]
fn exec_description_mentions_timeout_helpers() {
let description = build_exec_tool_description(&[], /*code_mode_only*/ false);
let description =
build_exec_tool_description(&[], &BTreeMap::new(), /*code_mode_only*/ false);
assert!(description.contains("`setTimeout(callback: () => void, delayMs?: number)`"));
assert!(description.contains("`clearTimeout(timeoutId?: number)`"));
}
#[test]
fn code_mode_only_description_groups_namespace_instructions_once() {
let namespace_descriptions = BTreeMap::from([
(
"mcp__sample__alpha".to_string(),
ToolNamespaceDescription {
name: "mcp__sample".to_string(),
description: "Shared namespace guidance.".to_string(),
},
),
(
"mcp__sample__beta".to_string(),
ToolNamespaceDescription {
name: "mcp__sample".to_string(),
description: "Shared namespace guidance.".to_string(),
},
),
]);
let description = build_exec_tool_description(
&[
("mcp__sample__alpha".to_string(), "First tool".to_string()),
("mcp__sample__beta".to_string(), "Second tool".to_string()),
],
&namespace_descriptions,
/*code_mode_only*/ true,
);
assert_eq!(description.matches("## mcp__sample").count(), 1);
assert!(description.contains(
r#"## mcp__sample
Shared namespace guidance.
### `mcp__sample__alpha` (`mcp__sample__alpha`)
First tool
### `mcp__sample__beta` (`mcp__sample__beta`)
Second tool"#
));
}
#[test]
fn code_mode_only_description_omits_empty_namespace_sections() {
let namespace_descriptions = BTreeMap::from([(
"mcp__sample__alpha".to_string(),
ToolNamespaceDescription {
name: "mcp__sample".to_string(),
description: String::new(),
},
)]);
let description = build_exec_tool_description(
&[("mcp__sample__alpha".to_string(), "First tool".to_string())],
&namespace_descriptions,
/*code_mode_only*/ true,
);
assert!(!description.contains("## mcp__sample"));
assert!(description.contains("### `mcp__sample__alpha` (`mcp__sample__alpha`)"));
}
}
+1
View File
@@ -6,6 +6,7 @@ mod service;
pub use description::CODE_MODE_PRAGMA_PREFIX;
pub use description::CodeModeToolKind;
pub use description::ToolDefinition;
pub use description::ToolNamespaceDescription;
pub use description::append_code_mode_sample;
pub use description::augment_tool_definition;
pub use description::build_exec_tool_description;
@@ -186,6 +186,8 @@ pub struct ToolInfo {
pub server_name: String,
pub tool_name: String,
pub tool_namespace: String,
#[serde(default)]
pub server_instructions: Option<String>,
pub tool: Tool,
pub connector_id: Option<String>,
pub connector_name: Option<String>,
@@ -356,6 +358,7 @@ struct ManagedClient {
tools: Vec<ToolInfo>,
tool_filter: ToolFilter,
tool_timeout: Option<Duration>,
server_instructions: Option<String>,
server_supports_sandbox_state_capability: bool,
codex_apps_tools_cache_context: Option<CodexAppsToolsCacheContext>,
}
@@ -842,6 +845,7 @@ impl McpConnectionManager {
CODEX_APPS_MCP_SERVER_NAME,
&managed_client.client,
managed_client.tool_timeout,
managed_client.server_instructions.as_deref(),
)
.await
.with_context(|| {
@@ -1374,9 +1378,14 @@ async fn start_server_task(
let list_start = Instant::now();
let fetch_start = Instant::now();
let tools = list_tools_for_client_uncached(&server_name, &client, startup_timeout)
.await
.map_err(StartupOutcomeError::from)?;
let tools = list_tools_for_client_uncached(
&server_name,
&client,
startup_timeout,
initialize_result.instructions.as_deref(),
)
.await
.map_err(StartupOutcomeError::from)?;
emit_duration(
MCP_TOOLS_FETCH_UNCACHED_DURATION_METRIC,
fetch_start.elapsed(),
@@ -1407,6 +1416,7 @@ async fn start_server_task(
tools,
tool_timeout: Some(tool_timeout),
tool_filter,
server_instructions: initialize_result.instructions,
server_supports_sandbox_state_capability,
codex_apps_tools_cache_context,
};
@@ -1587,6 +1597,7 @@ async fn list_tools_for_client_uncached(
server_name: &str,
client: &Arc<RmcpClient>,
timeout: Option<Duration>,
server_instructions: Option<&str>,
) -> Result<Vec<ToolInfo>> {
let resp = client
.list_tools_with_connector_ids(/*params*/ None, timeout)
@@ -1617,6 +1628,7 @@ async fn list_tools_for_client_uncached(
server_name: server_name.to_owned(),
tool_name,
tool_namespace,
server_instructions: server_instructions.map(str::to_string),
tool: tool_def,
connector_id: tool.connector_id,
connector_name,
@@ -15,6 +15,7 @@ fn create_test_tool(server_name: &str, tool_name: &str) -> ToolInfo {
} else {
server_name.to_string()
},
server_instructions: None,
tool: Tool {
name: tool_name.to_string().into(),
title: None,
+8 -6
View File
@@ -6888,16 +6888,18 @@ pub(crate) async fn built_tools(
} else {
app_tools
};
let mcp_tool_router_inputs =
has_mcp_servers.then(|| crate::tools::router::map_mcp_tool_infos(&mcp_tools));
Ok(Arc::new(ToolRouter::from_config(
&turn_context.tools_config,
ToolRouterParams {
mcp_tools: has_mcp_servers.then(|| {
mcp_tools
.into_iter()
.map(|(name, tool)| (name, tool.tool))
.collect()
}),
mcp_tools: mcp_tool_router_inputs
.as_ref()
.map(|inputs| inputs.mcp_tools.clone()),
tool_namespaces: mcp_tool_router_inputs
.as_ref()
.map(|inputs| inputs.tool_namespaces.clone()),
app_tools,
discoverable_tools,
dynamic_tools: turn_context.dynamic_tools.as_slice(),
+11 -12
View File
@@ -121,7 +121,7 @@ mod guardian_tests;
struct InstructionsTestCase {
slug: &'static str,
expects_apply_patch_instructions: bool,
expects_apply_patch_description: bool,
}
fn user_message(text: &str) -> ResponseItem {
@@ -305,6 +305,7 @@ fn test_tool_runtime(session: Arc<Session>, turn_context: Arc<TurnContext>) -> T
&turn_context.tools_config,
crate::tools::router::ToolRouterParams {
mcp_tools: None,
tool_namespaces: None,
app_tools: None,
discoverable_tools: None,
dynamic_tools: turn_context.dynamic_tools.as_slice(),
@@ -413,6 +414,7 @@ fn make_mcp_tool(
server_name: server_name.to_string(),
tool_name: tool_name.to_string(),
tool_namespace,
server_instructions: None,
tool: Tool {
name: tool_name.to_string().into(),
title: None,
@@ -695,19 +697,19 @@ async fn get_base_instructions_no_user_content() {
let test_cases = vec![
InstructionsTestCase {
slug: "gpt-5",
expects_apply_patch_instructions: false,
expects_apply_patch_description: false,
},
InstructionsTestCase {
slug: "gpt-5.1",
expects_apply_patch_instructions: false,
expects_apply_patch_description: false,
},
InstructionsTestCase {
slug: "gpt-5.1-codex",
expects_apply_patch_instructions: false,
expects_apply_patch_description: false,
},
InstructionsTestCase {
slug: "gpt-5.1-codex-max",
expects_apply_patch_instructions: false,
expects_apply_patch_description: false,
},
];
@@ -716,7 +718,7 @@ async fn get_base_instructions_no_user_content() {
for test_case in test_cases {
let model_info = model_info_for_slug(test_case.slug, &config);
if test_case.expects_apply_patch_instructions {
if test_case.expects_apply_patch_description {
assert_eq!(
model_info.base_instructions.as_str(),
prompt_with_apply_patch_instructions
@@ -5292,15 +5294,12 @@ async fn fatal_tool_error_stops_turn_and_reports_error() {
.await
};
let app_tools = Some(tools.clone());
let mcp_tool_router_inputs = crate::tools::router::map_mcp_tool_infos(&tools);
let router = ToolRouter::from_config(
&turn_context.tools_config,
crate::tools::router::ToolRouterParams {
mcp_tools: Some(
tools
.into_iter()
.map(|(name, tool)| (name, tool.tool))
.collect(),
),
mcp_tools: Some(mcp_tool_router_inputs.mcp_tools),
tool_namespaces: Some(mcp_tool_router_inputs.tool_namespaces),
app_tools,
discoverable_tools: None,
dynamic_tools: turn_context.dynamic_tools.as_slice(),
+3
View File
@@ -112,6 +112,7 @@ fn codex_app_tool(
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
tool_name: tool_name.to_string(),
tool_namespace,
server_instructions: None,
tool: test_tool_definition(tool_name),
connector_id: Some(connector_id.to_string()),
connector_name: connector_name.map(ToOwned::to_owned),
@@ -190,6 +191,7 @@ fn accessible_connectors_from_mcp_tools_carries_plugin_display_names() {
server_name: "sample".to_string(),
tool_name: "echo".to_string(),
tool_namespace: "sample".to_string(),
server_instructions: None,
tool: test_tool_definition("echo"),
connector_id: None,
connector_name: None,
@@ -314,6 +316,7 @@ fn accessible_connectors_from_mcp_tools_preserves_description() {
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
tool_name: "calendar_create_event".to_string(),
tool_namespace: "mcp__codex_apps__calendar".to_string(),
server_instructions: None,
tool: Tool {
name: "calendar_create_event".to_string().into(),
title: None,
+4 -5
View File
@@ -257,15 +257,14 @@ async fn build_nested_router(exec: &ExecContext) -> ToolRouter {
.read()
.await
.list_all_tools()
.await
.into_iter()
.map(|(name, tool_info)| (name, tool_info.tool))
.collect();
.await;
let mcp_tool_router_inputs = crate::tools::router::map_mcp_tool_infos(&mcp_tools);
ToolRouter::from_config(
&nested_tools_config,
ToolRouterParams {
mcp_tools: Some(mcp_tools),
mcp_tools: Some(mcp_tool_router_inputs.mcp_tools),
tool_namespaces: Some(mcp_tool_router_inputs.tool_namespaces),
app_tools: None,
discoverable_tools: None,
dynamic_tools: exec.turn.dynamic_tools.as_slice(),
+3 -6
View File
@@ -1561,16 +1561,13 @@ impl JsReplManager {
.await
.list_all_tools()
.await;
let mcp_tool_router_inputs = crate::tools::router::map_mcp_tool_infos(&mcp_tools);
let router = ToolRouter::from_config(
&exec.turn.tools_config,
crate::tools::router::ToolRouterParams {
mcp_tools: Some(
mcp_tools
.into_iter()
.map(|(name, tool)| (name, tool.tool))
.collect(),
),
mcp_tools: Some(mcp_tool_router_inputs.mcp_tools),
tool_namespaces: Some(mcp_tool_router_inputs.tool_namespaces),
app_tools: None,
discoverable_tools: None,
dynamic_tools: exec.turn.dynamic_tools.as_slice(),
+30
View File
@@ -16,6 +16,7 @@ use codex_protocol::models::SearchToolCallParams;
use codex_protocol::models::ShellToolCallParams;
use codex_tools::ConfiguredToolSpec;
use codex_tools::DiscoverableTool;
use codex_tools::ToolNamespace;
use codex_tools::ToolSpec;
use codex_tools::ToolsConfig;
use rmcp::model::Tool;
@@ -41,15 +42,43 @@ pub struct ToolRouter {
pub(crate) struct ToolRouterParams<'a> {
pub(crate) mcp_tools: Option<HashMap<String, Tool>>,
pub(crate) tool_namespaces: Option<HashMap<String, ToolNamespace>>,
pub(crate) app_tools: Option<HashMap<String, ToolInfo>>,
pub(crate) discoverable_tools: Option<Vec<DiscoverableTool>>,
pub(crate) dynamic_tools: &'a [DynamicToolSpec],
}
pub(crate) struct McpToolRouterInputs {
pub(crate) mcp_tools: HashMap<String, Tool>,
pub(crate) tool_namespaces: HashMap<String, ToolNamespace>,
}
pub(crate) fn map_mcp_tool_infos(mcp_tools: &HashMap<String, ToolInfo>) -> McpToolRouterInputs {
McpToolRouterInputs {
mcp_tools: mcp_tools
.iter()
.map(|(name, tool)| (name.clone(), tool.tool.clone()))
.collect(),
tool_namespaces: mcp_tools
.iter()
.map(|(name, tool)| {
(
name.clone(),
ToolNamespace {
name: tool.tool_namespace.clone(),
description: tool.server_instructions.clone(),
},
)
})
.collect(),
}
}
impl ToolRouter {
pub fn from_config(config: &ToolsConfig, params: ToolRouterParams<'_>) -> Self {
let ToolRouterParams {
mcp_tools,
tool_namespaces,
app_tools,
discoverable_tools,
dynamic_tools,
@@ -58,6 +87,7 @@ impl ToolRouter {
config,
mcp_tools,
app_tools,
tool_namespaces,
discoverable_tools,
dynamic_tools,
);
+2
View File
@@ -35,6 +35,7 @@ async fn js_repl_tools_only_blocks_direct_tool_calls() -> anyhow::Result<()> {
.map(|(name, tool)| (name, tool.tool))
.collect(),
),
tool_namespaces: None,
app_tools,
discoverable_tools: None,
dynamic_tools: turn.dynamic_tools.as_slice(),
@@ -93,6 +94,7 @@ async fn js_repl_tools_only_allows_js_repl_source_calls() -> anyhow::Result<()>
.map(|(name, tool)| (name, tool.tool))
.collect(),
),
tool_namespaces: None,
app_tools,
discoverable_tools: None,
dynamic_tools: turn.dynamic_tools.as_slice(),
+3
View File
@@ -10,6 +10,7 @@ use codex_mcp::ToolInfo;
use codex_protocol::dynamic_tools::DynamicToolSpec;
use codex_tools::DiscoverableTool;
use codex_tools::ToolHandlerKind;
use codex_tools::ToolNamespace;
use codex_tools::ToolRegistryPlanAppTool;
use codex_tools::ToolRegistryPlanParams;
use codex_tools::ToolUserShellType;
@@ -33,6 +34,7 @@ pub(crate) fn build_specs_with_discoverable_tools(
config: &ToolsConfig,
mcp_tools: Option<HashMap<String, rmcp::model::Tool>>,
app_tools: Option<HashMap<String, ToolInfo>>,
tool_namespaces: Option<HashMap<String, ToolNamespace>>,
discoverable_tools: Option<Vec<DiscoverableTool>>,
dynamic_tools: &[DynamicToolSpec],
) -> ToolRegistryBuilder {
@@ -86,6 +88,7 @@ pub(crate) fn build_specs_with_discoverable_tools(
config,
ToolRegistryPlanParams {
mcp_tools: mcp_tools.as_ref(),
tool_namespaces: tool_namespaces.as_ref(),
app_tools: app_tool_sources.as_deref(),
discoverable_tools: discoverable_tools.as_deref(),
dynamic_tools,
+6
View File
@@ -181,6 +181,7 @@ fn build_specs(
config,
mcp_tools,
app_tools,
/*tool_namespaces*/ None,
/*discoverable_tools*/ None,
dynamic_tools,
)
@@ -261,6 +262,7 @@ fn assert_model_tools(
&tools_config,
ToolRouterParams {
mcp_tools: None,
tool_namespaces: None,
app_tools: None,
discoverable_tools: None,
dynamic_tools: &[],
@@ -628,6 +630,7 @@ fn tool_suggest_requires_apps_and_plugins_features() {
&tools_config,
/*mcp_tools*/ None,
/*app_tools*/ None,
/*tool_namespaces*/ None,
discoverable_tools.clone(),
&[],
)
@@ -701,6 +704,7 @@ fn search_tool_description_falls_back_to_connector_name_without_description() {
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
tool_name: "_create_event".to_string(),
tool_namespace: "mcp__codex_apps__calendar".to_string(),
server_instructions: None,
tool: mcp_tool(
"calendar_create_event",
"Create calendar event",
@@ -751,6 +755,7 @@ fn search_tool_registers_namespaced_app_tool_aliases() {
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
tool_name: "_create_event".to_string(),
tool_namespace: "mcp__codex_apps__calendar".to_string(),
server_instructions: None,
tool: mcp_tool(
"calendar-create-event",
"Create calendar event",
@@ -768,6 +773,7 @@ fn search_tool_registers_namespaced_app_tool_aliases() {
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
tool_name: "_list_events".to_string(),
tool_namespace: "mcp__codex_apps__calendar".to_string(),
server_instructions: None,
tool: mcp_tool(
"calendar-list-events",
"List calendar events",
+2
View File
@@ -96,6 +96,7 @@ pub fn create_wait_tool() -> ToolSpec {
pub fn create_code_mode_tool(
enabled_tools: &[(String, String)],
namespace_descriptions: &BTreeMap<String, codex_code_mode::ToolNamespaceDescription>,
code_mode_only_enabled: bool,
) -> ToolSpec {
const CODE_MODE_FREEFORM_GRAMMAR: &str = r#"
@@ -112,6 +113,7 @@ SOURCE: /[\s\S]+/
name: codex_code_mode::PUBLIC_TOOL_NAME.to_string(),
description: codex_code_mode::build_exec_tool_description(
enabled_tools,
namespace_descriptions,
code_mode_only_enabled,
),
format: FreeformToolFormat {
+32 -7
View File
@@ -20,10 +20,14 @@ fn augment_tool_spec_for_code_mode_augments_function_tools() {
description: "Look up an order".to_string(),
strict: false,
defer_loading: Some(true),
parameters: JsonSchema::object(BTreeMap::from([(
parameters: JsonSchema::object(
BTreeMap::from([(
"order_id".to_string(),
JsonSchema::string(/*description*/ None),
)]), Some(vec!["order_id".to_string()]), Some(AdditionalProperties::Boolean(false))),
)]),
Some(vec!["order_id".to_string()]),
Some(AdditionalProperties::Boolean(false))
),
output_schema: Some(json!({
"type": "object",
"properties": {
@@ -34,13 +38,23 @@ fn augment_tool_spec_for_code_mode_augments_function_tools() {
})),
ToolSpec::Function(ResponsesApiTool {
name: "lookup_order".to_string(),
description: "Look up an order\n\nexec tool declaration:\n```ts\ndeclare const tools: { lookup_order(args: { order_id: string; }): Promise<{ ok: boolean; }>; };\n```".to_string(),
description: r#"Look up an order
exec tool declaration:
```ts
declare const tools: { lookup_order(args: { order_id: string; }): Promise<{ ok: boolean; }>; };
```"#
.to_string(),
strict: false,
defer_loading: Some(true),
parameters: JsonSchema::object(BTreeMap::from([(
parameters: JsonSchema::object(
BTreeMap::from([(
"order_id".to_string(),
JsonSchema::string(/*description*/ None),
)]), Some(vec!["order_id".to_string()]), Some(AdditionalProperties::Boolean(false))),
)]),
Some(vec!["order_id".to_string()]),
Some(AdditionalProperties::Boolean(false))
),
output_schema: Some(json!({
"type": "object",
"properties": {
@@ -92,7 +106,13 @@ fn tool_spec_to_code_mode_tool_definition_returns_augmented_nested_tools() {
tool_spec_to_code_mode_tool_definition(&spec),
Some(codex_code_mode::ToolDefinition {
name: "apply_patch".to_string(),
description: "Apply a patch\n\nexec tool declaration:\n```ts\ndeclare const tools: { apply_patch(input: string): Promise<unknown>; };\n```".to_string(),
description: r#"Apply a patch
exec tool declaration:
```ts
declare const tools: { apply_patch(input: string): Promise<unknown>; };
```"#
.to_string(),
kind: codex_code_mode::CodeModeToolKind::Freeform,
input_schema: None,
output_schema: None,
@@ -165,11 +185,16 @@ fn create_code_mode_tool_matches_expected_spec() {
let enabled_tools = vec![("update_plan".to_string(), "Update the plan".to_string())];
assert_eq!(
create_code_mode_tool(&enabled_tools, /*code_mode_only_enabled*/ true),
create_code_mode_tool(
&enabled_tools,
&BTreeMap::new(),
/*code_mode_only_enabled*/ true,
),
ToolSpec::Freeform(FreeformTool {
name: codex_code_mode::PUBLIC_TOOL_NAME.to_string(),
description: codex_code_mode::build_exec_tool_description(
&enabled_tools,
&BTreeMap::new(),
/*code_mode_only*/ true
),
format: FreeformToolFormat {
+1
View File
@@ -114,6 +114,7 @@ pub use tool_discovery::filter_tool_suggest_discoverable_tools_for_client;
pub use tool_registry_plan::build_tool_registry_plan;
pub use tool_registry_plan_types::ToolHandlerKind;
pub use tool_registry_plan_types::ToolHandlerSpec;
pub use tool_registry_plan_types::ToolNamespace;
pub use tool_registry_plan_types::ToolRegistryPlan;
pub use tool_registry_plan_types::ToolRegistryPlanAppTool;
pub use tool_registry_plan_types::ToolRegistryPlanParams;
+59 -2
View File
@@ -61,6 +61,7 @@ use crate::tool_registry_plan_types::agent_type_description;
use codex_protocol::openai_models::ApplyPatchToolType;
use codex_protocol::openai_models::ConfigShellToolType;
use rmcp::model::Tool as McpTool;
use std::collections::BTreeMap;
pub fn build_tool_registry_plan(
config: &ToolsConfig,
@@ -70,6 +71,20 @@ pub fn build_tool_registry_plan(
let exec_permission_approvals_enabled = config.exec_permission_approvals_enabled;
if config.code_mode_enabled {
let namespace_descriptions = params
.tool_namespaces
.into_iter()
.flatten()
.map(|(name, detail)| {
(
name.clone(),
codex_code_mode::ToolNamespaceDescription {
name: detail.name.clone(),
description: detail.description.clone().unwrap_or_default(),
},
)
})
.collect::<BTreeMap<_, _>>();
let nested_config = config.for_code_mode_nested_tools();
let nested_plan = build_tool_registry_plan(
&nested_config,
@@ -78,7 +93,7 @@ pub fn build_tool_registry_plan(
..params
},
);
let enabled_tools = collect_code_mode_tool_definitions(
let mut enabled_tools = collect_code_mode_tool_definitions(
nested_plan
.specs
.iter()
@@ -87,8 +102,15 @@ pub fn build_tool_registry_plan(
.into_iter()
.map(|tool| (tool.name, tool.description))
.collect::<Vec<_>>();
enabled_tools.sort_by(|(left_name, _), (right_name, _)| {
compare_code_mode_tool_names(left_name, right_name, &namespace_descriptions)
});
plan.push_spec(
create_code_mode_tool(&enabled_tools, config.code_mode_only_enabled),
create_code_mode_tool(
&enabled_tools,
&namespace_descriptions,
config.code_mode_only_enabled,
),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
@@ -494,6 +516,41 @@ pub fn build_tool_registry_plan(
plan
}
fn compare_code_mode_tool_names(
left_name: &str,
right_name: &str,
namespace_descriptions: &BTreeMap<String, codex_code_mode::ToolNamespaceDescription>,
) -> std::cmp::Ordering {
let left_namespace = code_mode_namespace_name(left_name, namespace_descriptions);
let right_namespace = code_mode_namespace_name(right_name, namespace_descriptions);
left_namespace
.cmp(&right_namespace)
.then_with(|| {
code_mode_function_name(left_name, left_namespace)
.cmp(code_mode_function_name(right_name, right_namespace))
})
.then_with(|| left_name.cmp(right_name))
}
fn code_mode_namespace_name<'a>(
name: &str,
namespace_descriptions: &'a BTreeMap<String, codex_code_mode::ToolNamespaceDescription>,
) -> Option<&'a str> {
namespace_descriptions
.get(name)
.map(|namespace_description| namespace_description.name.as_str())
}
fn code_mode_function_name<'a>(name: &'a str, namespace: Option<&str>) -> &'a str {
namespace
.and_then(|namespace| {
name.strip_prefix(namespace)
.and_then(|suffix| suffix.strip_prefix("__"))
})
.unwrap_or(name)
}
#[cfg(test)]
#[path = "tool_registry_plan_tests.rs"]
mod tests;
+32 -1
View File
@@ -11,6 +11,7 @@ use crate::ResponsesApiTool;
use crate::ResponsesApiWebSearchFilters;
use crate::ResponsesApiWebSearchUserLocation;
use crate::ToolHandlerSpec;
use crate::ToolNamespace;
use crate::ToolRegistryPlanAppTool;
use crate::ToolsConfigParams;
use crate::WaitAgentTimeoutOptions;
@@ -1332,6 +1333,7 @@ fn tool_suggest_is_not_registered_without_feature_flag() {
&tools_config,
/*mcp_tools*/ None,
/*app_tools*/ None,
/*tool_namespaces*/ None,
Some(vec![discoverable_connector(
"connector_2128aebfecb84f64a069897515042a44",
"Google Calendar",
@@ -1371,6 +1373,7 @@ fn tool_suggest_can_be_registered_without_search_tool() {
&tools_config,
/*mcp_tools*/ None,
/*app_tools*/ None,
/*tool_namespaces*/ None,
Some(vec![discoverable_connector(
"connector_2128aebfecb84f64a069897515042a44",
"Google Calendar",
@@ -1438,6 +1441,7 @@ fn tool_suggest_description_lists_discoverable_tools() {
&tools_config,
/*mcp_tools*/ None,
/*app_tools*/ None,
/*tool_namespaces*/ None,
Some(discoverable_tools),
&[],
);
@@ -1501,6 +1505,7 @@ fn code_mode_augments_mcp_tool_descriptions_with_namespaced_sample() {
let model_info = model_info();
let mut features = Features::with_defaults();
features.enable(Feature::CodeMode);
features.enable(Feature::CodeModeOnly);
features.enable(Feature::UnifiedExec);
let available_models = Vec::new();
let tools_config = ToolsConfig::new(&ToolsConfigParams {
@@ -1542,7 +1547,12 @@ fn code_mode_augments_mcp_tool_descriptions_with_namespaced_sample() {
assert_eq!(
description,
"Echo text\n\nexec tool declaration:\n```ts\ndeclare const tools: { mcp__sample__echo(args: { message: string; }): Promise<{ _meta?: unknown; content: Array<unknown>; isError?: boolean; structuredContent?: unknown; }>; };\n```"
r#"Echo text
exec tool declaration:
```ts
declare const tools: { mcp__sample__echo(args: { message: string; }): Promise<{ _meta?: unknown; content: Array<unknown>; isError?: boolean; structuredContent?: unknown; }>; };
```"#
);
}
@@ -1789,6 +1799,7 @@ fn build_specs<'a>(
config,
mcp_tools,
app_tools,
/*tool_namespaces*/ None,
/*discoverable_tools*/ None,
dynamic_tools,
)
@@ -1798,6 +1809,25 @@ fn build_specs_with_discoverable_tools<'a>(
config: &ToolsConfig,
mcp_tools: Option<HashMap<String, rmcp::model::Tool>>,
app_tools: Option<Vec<ToolRegistryPlanAppTool<'a>>>,
tool_namespaces: Option<HashMap<String, ToolNamespace>>,
discoverable_tools: Option<Vec<DiscoverableTool>>,
dynamic_tools: &[DynamicToolSpec],
) -> (Vec<ConfiguredToolSpec>, Vec<ToolHandlerSpec>) {
build_specs_with_optional_tool_namespaces(
config,
mcp_tools,
tool_namespaces,
app_tools,
discoverable_tools,
dynamic_tools,
)
}
fn build_specs_with_optional_tool_namespaces<'a>(
config: &ToolsConfig,
mcp_tools: Option<HashMap<String, rmcp::model::Tool>>,
tool_namespaces: Option<HashMap<String, ToolNamespace>>,
app_tools: Option<Vec<ToolRegistryPlanAppTool<'a>>>,
discoverable_tools: Option<Vec<DiscoverableTool>>,
dynamic_tools: &[DynamicToolSpec],
) -> (Vec<ConfiguredToolSpec>, Vec<ToolHandlerSpec>) {
@@ -1805,6 +1835,7 @@ fn build_specs_with_discoverable_tools<'a>(
config,
ToolRegistryPlanParams {
mcp_tools: mcp_tools.as_ref(),
tool_namespaces: tool_namespaces.as_ref(),
app_tools: app_tools.as_deref(),
discoverable_tools: discoverable_tools.as_deref(),
dynamic_tools,
@@ -58,6 +58,7 @@ pub struct ToolRegistryPlan {
#[derive(Debug, Clone, Copy)]
pub struct ToolRegistryPlanParams<'a> {
pub mcp_tools: Option<&'a HashMap<String, McpTool>>,
pub tool_namespaces: Option<&'a HashMap<String, ToolNamespace>>,
pub app_tools: Option<&'a [ToolRegistryPlanAppTool<'a>]>,
pub discoverable_tools: Option<&'a [DiscoverableTool]>,
pub dynamic_tools: &'a [DynamicToolSpec],
@@ -66,6 +67,12 @@ pub struct ToolRegistryPlanParams<'a> {
pub codex_apps_mcp_server_name: &'a str,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToolNamespace {
pub name: String,
pub description: Option<String>,
}
#[derive(Debug, Clone, Copy)]
pub struct ToolRegistryPlanAppTool<'a> {
pub tool_name: &'a str,