Compare commits

...

1 Commits

Author SHA1 Message Date
Sayan Sisodiya
104a3f71a6 Introduce canonical tool definitions 2026-04-20 15:23:35 -07:00
10 changed files with 221 additions and 47 deletions

View File

@@ -11,6 +11,10 @@ schema and Responses API tool primitives that no longer need to live in
- `JsonSchema`
- `AdditionalProperties`
- `ToolDefinition`
- `ToolLoadingPolicy`
- `ToolExecution`
- `ToolPresentation`
- `ToolSearchMetadata`
- `ToolSpec`
- `ConfiguredToolSpec`
- `ResponsesApiTool`
@@ -32,6 +36,8 @@ schema and Responses API tool primitives that no longer need to live in
- `parse_tool_input_schema()`
- `parse_dynamic_tool()`
- `parse_mcp_tool()`
- `dynamic_tool_to_tool_definition()`
- `mcp_tool_to_tool_definition()`
- `create_tools_json_for_responses_api()`
- `mcp_call_tool_result_output_schema()`
- `tool_definition_to_responses_api_tool()`

View File

@@ -1,8 +1,13 @@
use crate::ToolDefinition;
use crate::ToolExecution;
use crate::ToolLoadingPolicy;
use crate::ToolName;
use crate::parse_tool_input_schema;
use codex_protocol::dynamic_tools::DynamicToolSpec;
pub fn parse_dynamic_tool(tool: &DynamicToolSpec) -> Result<ToolDefinition, serde_json::Error> {
pub fn dynamic_tool_to_tool_definition(
tool: &DynamicToolSpec,
) -> Result<ToolDefinition, serde_json::Error> {
let DynamicToolSpec {
name,
description,
@@ -10,14 +15,28 @@ pub fn parse_dynamic_tool(tool: &DynamicToolSpec) -> Result<ToolDefinition, serd
defer_loading,
} = tool;
Ok(ToolDefinition {
name: name.clone(),
name: ToolName::plain(name.clone()),
description: description.clone(),
input_schema: parse_tool_input_schema(input_schema)?,
output_schema: None,
defer_loading: *defer_loading,
loading: if *defer_loading {
ToolLoadingPolicy::Deferred
} else {
ToolLoadingPolicy::Eager
},
execution: ToolExecution::Dynamic,
presentation: None,
search: None,
supports_parallel_tool_calls: false,
})
}
// TODO(tool-definition-unification): migrate remaining callers to
// `dynamic_tool_to_tool_definition` and remove this compatibility wrapper.
pub fn parse_dynamic_tool(tool: &DynamicToolSpec) -> Result<ToolDefinition, serde_json::Error> {
dynamic_tool_to_tool_definition(tool)
}
#[cfg(test)]
#[path = "dynamic_tool_tests.rs"]
mod tests;

View File

@@ -1,6 +1,9 @@
use super::parse_dynamic_tool;
use crate::JsonSchema;
use crate::ToolDefinition;
use crate::ToolExecution;
use crate::ToolLoadingPolicy;
use crate::ToolName;
use codex_protocol::dynamic_tools::DynamicToolSpec;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;
@@ -23,7 +26,7 @@ fn parse_dynamic_tool_sanitizes_input_schema() {
assert_eq!(
parse_dynamic_tool(&tool).expect("parse dynamic tool"),
ToolDefinition {
name: "lookup_ticket".to_string(),
name: ToolName::plain("lookup_ticket"),
description: "Fetch a ticket".to_string(),
input_schema: JsonSchema::object(
BTreeMap::from([(
@@ -34,7 +37,11 @@ fn parse_dynamic_tool_sanitizes_input_schema() {
/*additional_properties*/ None
),
output_schema: None,
defer_loading: false,
loading: ToolLoadingPolicy::Eager,
execution: ToolExecution::Dynamic,
presentation: None,
search: None,
supports_parallel_tool_calls: false,
}
);
}
@@ -54,7 +61,7 @@ fn parse_dynamic_tool_preserves_defer_loading() {
assert_eq!(
parse_dynamic_tool(&tool).expect("parse dynamic tool"),
ToolDefinition {
name: "lookup_ticket".to_string(),
name: ToolName::plain("lookup_ticket"),
description: "Fetch a ticket".to_string(),
input_schema: JsonSchema::object(
BTreeMap::new(),
@@ -62,7 +69,11 @@ fn parse_dynamic_tool_preserves_defer_loading() {
/*additional_properties*/ None
),
output_schema: None,
defer_loading: true,
loading: ToolLoadingPolicy::Deferred,
execution: ToolExecution::Dynamic,
presentation: None,
search: None,
supports_parallel_tool_calls: false,
}
);
}

View File

@@ -50,6 +50,7 @@ pub use code_mode::create_code_mode_tool;
pub use code_mode::create_wait_tool;
pub use code_mode::tool_spec_to_code_mode_tool_definition;
pub use codex_protocol::ToolName;
pub use dynamic_tool::dynamic_tool_to_tool_definition;
pub use dynamic_tool::parse_dynamic_tool;
pub use image_detail::can_request_original_image_detail;
pub use image_detail::normalize_output_image_detail;
@@ -73,6 +74,7 @@ pub use mcp_resource_tool::create_list_mcp_resource_templates_tool;
pub use mcp_resource_tool::create_list_mcp_resources_tool;
pub use mcp_resource_tool::create_read_mcp_resource_tool;
pub use mcp_tool::mcp_call_tool_result_output_schema;
pub use mcp_tool::mcp_tool_to_tool_definition;
pub use mcp_tool::parse_mcp_tool;
pub use plan_tool::create_update_plan_tool;
pub use request_user_input_tool::REQUEST_USER_INPUT_TOOL_NAME;
@@ -98,6 +100,10 @@ pub use tool_config::ToolsConfigParams;
pub use tool_config::UnifiedExecShellMode;
pub use tool_config::ZshForkConfig;
pub use tool_definition::ToolDefinition;
pub use tool_definition::ToolExecution;
pub use tool_definition::ToolLoadingPolicy;
pub use tool_definition::ToolPresentation;
pub use tool_definition::ToolSearchMetadata;
pub use tool_discovery::DiscoverablePluginInfo;
pub use tool_discovery::DiscoverableTool;
pub use tool_discovery::DiscoverableToolAction;

View File

@@ -1,9 +1,18 @@
use crate::ToolDefinition;
use crate::ToolExecution;
use crate::ToolLoadingPolicy;
use crate::ToolName;
use crate::parse_tool_input_schema;
use serde_json::Value as JsonValue;
use serde_json::json;
pub fn parse_mcp_tool(tool: &rmcp::model::Tool) -> Result<ToolDefinition, serde_json::Error> {
// TODO(tool-definition-unification): remove this incomplete raw MCP adapter
// once callers can use a full `ToolInfo -> ToolDefinition` adapter that also
// populates MCP presentation/search metadata.
pub fn mcp_tool_to_tool_definition(
tool_name: &ToolName,
tool: &rmcp::model::Tool,
) -> Result<ToolDefinition, serde_json::Error> {
let mut serialized_input_schema = serde_json::Value::Object(tool.input_schema.as_ref().clone());
// OpenAI models mandate the "properties" field in the schema. Some MCP
@@ -26,16 +35,26 @@ pub fn parse_mcp_tool(tool: &rmcp::model::Tool) -> Result<ToolDefinition, serde_
.unwrap_or_else(|| JsonValue::Object(serde_json::Map::new()));
Ok(ToolDefinition {
name: tool.name.to_string(),
name: tool_name.clone(),
description: tool.description.clone().map(Into::into).unwrap_or_default(),
input_schema,
output_schema: Some(mcp_call_tool_result_output_schema(
structured_content_schema,
)),
defer_loading: false,
loading: ToolLoadingPolicy::Eager,
execution: ToolExecution::Mcp,
presentation: None,
search: None,
supports_parallel_tool_calls: false,
})
}
// TODO(tool-definition-unification): remove this compatibility wrapper once
// callers can use the full `ToolInfo -> ToolDefinition` adapter.
pub fn parse_mcp_tool(tool: &rmcp::model::Tool) -> Result<ToolDefinition, serde_json::Error> {
mcp_tool_to_tool_definition(&ToolName::plain(tool.name.to_string()), tool)
}
pub fn mcp_call_tool_result_output_schema(structured_content_schema: JsonValue) -> JsonValue {
json!({
"type": "object",

View File

@@ -1,7 +1,11 @@
use super::mcp_call_tool_result_output_schema;
use super::mcp_tool_to_tool_definition;
use super::parse_mcp_tool;
use crate::JsonSchema;
use crate::ToolDefinition;
use crate::ToolExecution;
use crate::ToolLoadingPolicy;
use crate::ToolName;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;
@@ -19,6 +23,40 @@ fn mcp_tool(name: &str, description: &str, input_schema: serde_json::Value) -> r
}
}
#[test]
fn mcp_tool_to_tool_definition_uses_canonical_tool_name() {
let tool = mcp_tool(
"raw_lookup",
"Look up an order",
serde_json::json!({
"type": "object"
}),
);
assert_eq!(
mcp_tool_to_tool_definition(
&ToolName::namespaced("mcp__orders__", "lookup_order"),
&tool,
)
.expect("convert MCP tool"),
ToolDefinition {
name: ToolName::namespaced("mcp__orders__", "lookup_order"),
description: "Look up an order".to_string(),
input_schema: JsonSchema::object(
BTreeMap::new(),
/*required*/ None,
/*additional_properties*/ None
),
output_schema: Some(mcp_call_tool_result_output_schema(serde_json::json!({}))),
loading: ToolLoadingPolicy::Eager,
execution: ToolExecution::Mcp,
presentation: None,
search: None,
supports_parallel_tool_calls: false,
}
);
}
#[test]
fn parse_mcp_tool_inserts_empty_properties() {
let tool = mcp_tool(
@@ -32,7 +70,7 @@ fn parse_mcp_tool_inserts_empty_properties() {
assert_eq!(
parse_mcp_tool(&tool).expect("parse MCP tool"),
ToolDefinition {
name: "no_props".to_string(),
name: ToolName::plain("no_props"),
description: "No properties".to_string(),
input_schema: JsonSchema::object(
BTreeMap::new(),
@@ -40,7 +78,11 @@ fn parse_mcp_tool_inserts_empty_properties() {
/*additional_properties*/ None
),
output_schema: Some(mcp_call_tool_result_output_schema(serde_json::json!({}))),
defer_loading: false,
loading: ToolLoadingPolicy::Eager,
execution: ToolExecution::Mcp,
presentation: None,
search: None,
supports_parallel_tool_calls: false,
}
);
}
@@ -70,7 +112,7 @@ fn parse_mcp_tool_preserves_top_level_output_schema() {
assert_eq!(
parse_mcp_tool(&tool).expect("parse MCP tool"),
ToolDefinition {
name: "with_output".to_string(),
name: ToolName::plain("with_output"),
description: "Has output schema".to_string(),
input_schema: JsonSchema::object(
BTreeMap::new(),
@@ -87,7 +129,11 @@ fn parse_mcp_tool_preserves_top_level_output_schema() {
},
"required": ["result"]
}))),
defer_loading: false,
loading: ToolLoadingPolicy::Eager,
execution: ToolExecution::Mcp,
presentation: None,
search: None,
supports_parallel_tool_calls: false,
}
);
}
@@ -110,7 +156,7 @@ fn parse_mcp_tool_preserves_output_schema_without_inferred_type() {
assert_eq!(
parse_mcp_tool(&tool).expect("parse MCP tool"),
ToolDefinition {
name: "with_enum_output".to_string(),
name: ToolName::plain("with_enum_output"),
description: "Has enum output schema".to_string(),
input_schema: JsonSchema::object(
BTreeMap::new(),
@@ -120,7 +166,11 @@ fn parse_mcp_tool_preserves_output_schema_without_inferred_type() {
output_schema: Some(mcp_call_tool_result_output_schema(serde_json::json!({
"enum": ["ok", "error"]
}))),
defer_loading: false,
loading: ToolLoadingPolicy::Eager,
execution: ToolExecution::Mcp,
presentation: None,
search: None,
supports_parallel_tool_calls: false,
}
);
}

View File

@@ -1,8 +1,8 @@
use crate::JsonSchema;
use crate::ToolDefinition;
use crate::ToolName;
use crate::parse_dynamic_tool;
use crate::parse_mcp_tool;
use crate::dynamic_tool_to_tool_definition;
use crate::mcp_tool_to_tool_definition;
use codex_protocol::dynamic_tools::DynamicToolSpec;
use serde::Deserialize;
use serde::Serialize;
@@ -69,9 +69,9 @@ pub enum ResponsesApiNamespaceTool {
pub fn dynamic_tool_to_responses_api_tool(
tool: &DynamicToolSpec,
) -> Result<ResponsesApiTool, serde_json::Error> {
Ok(tool_definition_to_responses_api_tool(parse_dynamic_tool(
tool,
)?))
Ok(tool_definition_to_responses_api_tool(
&dynamic_tool_to_tool_definition(tool)?,
))
}
pub fn mcp_tool_to_responses_api_tool(
@@ -79,7 +79,7 @@ pub fn mcp_tool_to_responses_api_tool(
tool: &rmcp::model::Tool,
) -> Result<ResponsesApiTool, serde_json::Error> {
Ok(tool_definition_to_responses_api_tool(
parse_mcp_tool(tool)?.renamed(tool_name.name.clone()),
&mcp_tool_to_tool_definition(tool_name, tool)?,
))
}
@@ -88,20 +88,22 @@ pub fn mcp_tool_to_deferred_responses_api_tool(
tool: &rmcp::model::Tool,
) -> Result<ResponsesApiTool, serde_json::Error> {
Ok(tool_definition_to_responses_api_tool(
parse_mcp_tool(tool)?
.renamed(tool_name.name.clone())
.into_deferred(),
&mcp_tool_to_tool_definition(tool_name, tool)?.into_deferred(),
))
}
pub fn tool_definition_to_responses_api_tool(tool_definition: ToolDefinition) -> ResponsesApiTool {
/// Converts the leaf function shape of a canonical tool definition.
///
/// If the tool is namespaced, callers are still responsible for wrapping the
/// returned function in a Responses API namespace tool.
pub fn tool_definition_to_responses_api_tool(tool_definition: &ToolDefinition) -> ResponsesApiTool {
ResponsesApiTool {
name: tool_definition.name,
description: tool_definition.description,
name: tool_definition.name.name.clone(),
description: tool_definition.description.clone(),
strict: false,
defer_loading: tool_definition.defer_loading.then_some(true),
parameters: tool_definition.input_schema,
output_schema: tool_definition.output_schema,
defer_loading: tool_definition.defer_loading().then_some(true),
parameters: tool_definition.input_schema.clone(),
output_schema: tool_definition.output_schema.clone(),
}
}

View File

@@ -7,6 +7,8 @@ use super::mcp_tool_to_deferred_responses_api_tool;
use super::tool_definition_to_responses_api_tool;
use crate::JsonSchema;
use crate::ToolDefinition;
use crate::ToolExecution;
use crate::ToolLoadingPolicy;
use crate::ToolName;
use codex_protocol::dynamic_tools::DynamicToolSpec;
use pretty_assertions::assert_eq;
@@ -16,8 +18,8 @@ use std::collections::BTreeMap;
#[test]
fn tool_definition_to_responses_api_tool_omits_false_defer_loading() {
assert_eq!(
tool_definition_to_responses_api_tool(ToolDefinition {
name: "lookup_order".to_string(),
tool_definition_to_responses_api_tool(&ToolDefinition {
name: ToolName::plain("lookup_order"),
description: "Look up an order".to_string(),
input_schema: JsonSchema::object(
BTreeMap::from([(
@@ -28,7 +30,11 @@ fn tool_definition_to_responses_api_tool_omits_false_defer_loading() {
Some(false.into())
),
output_schema: Some(json!({"type": "object"})),
defer_loading: false,
loading: ToolLoadingPolicy::Eager,
execution: ToolExecution::Dynamic,
presentation: None,
search: None,
supports_parallel_tool_calls: false,
}),
ResponsesApiTool {
name: "lookup_order".to_string(),

View File

@@ -1,28 +1,76 @@
use crate::JsonSchema;
use crate::ToolName;
use serde_json::Value as JsonValue;
/// Tool metadata and schemas that downstream crates can adapt into higher-level
/// tool specs.
#[derive(Debug, PartialEq)]
/// Canonical metadata for JSON-schema function tools.
///
/// This intentionally models function-like tools only. If freeform tools need
/// the same registry/search/code-mode lifecycle later, this can grow a
/// function-vs-freeform input enum without changing the conversion boundary.
#[derive(Debug, Clone, PartialEq)]
pub struct ToolDefinition {
pub name: String,
pub name: ToolName,
pub description: String,
pub input_schema: JsonSchema,
pub output_schema: Option<JsonValue>,
pub defer_loading: bool,
pub loading: ToolLoadingPolicy,
pub execution: ToolExecution,
pub presentation: Option<ToolPresentation>,
pub search: Option<ToolSearchMetadata>,
pub supports_parallel_tool_calls: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolLoadingPolicy {
Eager,
Deferred,
}
impl ToolLoadingPolicy {
pub fn is_deferred(self) -> bool {
matches!(self, Self::Deferred)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolExecution {
/// Tool execution handled by an in-process Codex handler.
Builtin,
/// Tool registered dynamically by the caller for the current thread.
Dynamic,
/// Tool routed through the MCP connection manager.
Mcp,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToolPresentation {
pub namespace_display_name: Option<String>,
pub namespace_description: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToolSearchMetadata {
pub source_name: String,
pub source_description: Option<String>,
pub extra_terms: Vec<String>,
pub limit_bucket: Option<String>,
}
impl ToolDefinition {
pub fn renamed(mut self, name: String) -> Self {
self.name = name;
pub fn renamed(mut self, name: impl Into<ToolName>) -> Self {
self.name = name.into();
self
}
pub fn into_deferred(mut self) -> Self {
self.output_schema = None;
self.defer_loading = true;
self.loading = ToolLoadingPolicy::Deferred;
self
}
pub fn defer_loading(&self) -> bool {
self.loading.is_deferred()
}
}
#[cfg(test)]

View File

@@ -1,11 +1,14 @@
use super::ToolDefinition;
use crate::JsonSchema;
use crate::ToolExecution;
use crate::ToolLoadingPolicy;
use crate::ToolName;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;
fn tool_definition() -> ToolDefinition {
ToolDefinition {
name: "lookup_order".to_string(),
name: ToolName::plain("lookup_order"),
description: "Look up an order".to_string(),
input_schema: JsonSchema::object(
BTreeMap::new(),
@@ -15,16 +18,20 @@ fn tool_definition() -> ToolDefinition {
output_schema: Some(serde_json::json!({
"type": "object",
})),
defer_loading: false,
loading: ToolLoadingPolicy::Eager,
execution: ToolExecution::Dynamic,
presentation: None,
search: None,
supports_parallel_tool_calls: false,
}
}
#[test]
fn renamed_overrides_name_only() {
assert_eq!(
tool_definition().renamed("mcp__orders__lookup_order".to_string()),
tool_definition().renamed(ToolName::namespaced("mcp__orders__", "lookup_order")),
ToolDefinition {
name: "mcp__orders__lookup_order".to_string(),
name: ToolName::namespaced("mcp__orders__", "lookup_order"),
..tool_definition()
}
);
@@ -36,7 +43,7 @@ fn into_deferred_drops_output_schema_and_sets_defer_loading() {
tool_definition().into_deferred(),
ToolDefinition {
output_schema: None,
defer_loading: true,
loading: ToolLoadingPolicy::Deferred,
..tool_definition()
}
);