diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index c5846fbe19..67d43f1453 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2632,6 +2632,7 @@ dependencies = [ name = "codex-tools" version = "0.0.0" dependencies = [ + "codex-protocol", "pretty_assertions", "rmcp", "serde", diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index ea85eaacdd..42dbbff98c 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -46,8 +46,8 @@ use codex_protocol::openai_models::WebSearchToolType; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; +use codex_tools::parse_dynamic_tool; use codex_tools::parse_mcp_tool; -pub use codex_tools::parse_tool_input_schema; use codex_utils_absolute_path::AbsolutePathBuf; use serde::Deserialize; use serde::Serialize; @@ -2397,14 +2397,14 @@ pub(crate) fn mcp_tool_to_deferred_openai_tool( fn dynamic_tool_to_openai_tool( tool: &DynamicToolSpec, ) -> Result { - let input_schema = parse_tool_input_schema(&tool.input_schema)?; + let parsed_tool = parse_dynamic_tool(tool)?; Ok(ResponsesApiTool { name: tool.name.clone(), - description: tool.description.clone(), + description: parsed_tool.description, strict: false, - defer_loading: None, - parameters: input_schema, + defer_loading: parsed_tool.defer_loading.then_some(true), + parameters: parsed_tool.input_schema, output_schema: None, }) } diff --git a/codex-rs/core/src/tools/spec_tests.rs b/codex-rs/core/src/tools/spec_tests.rs index 5b9016ec6a..cd0ffd6f6e 100644 --- a/codex-rs/core/src/tools/spec_tests.rs +++ b/codex-rs/core/src/tools/spec_tests.rs @@ -8,6 +8,7 @@ use crate::tools::ToolRouter; use crate::tools::registry::ConfiguredToolSpec; use crate::tools::router::ToolRouterParams; use codex_app_server_protocol::AppInfo; +use codex_protocol::dynamic_tools::DynamicToolSpec; use codex_protocol::openai_models::InputModality; use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelsResponse; @@ -126,6 +127,27 @@ fn deferred_responses_api_tool_serializes_with_defer_loading() { ); } +#[test] +fn dynamic_tool_preserves_defer_loading() { + let tool = DynamicToolSpec { + name: "lookup_order".to_string(), + description: "Look up an order".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "order_id": {"type": "string"} + }, + "required": ["order_id"], + "additionalProperties": false, + }), + defer_loading: true, + }; + + let openai_tool = dynamic_tool_to_openai_tool(&tool).expect("convert dynamic tool"); + + assert_eq!(openai_tool.defer_loading, Some(true)); +} + fn tool_name(tool: &ToolSpec) -> &str { match tool { ToolSpec::Function(ResponsesApiTool { name, .. }) => name, diff --git a/codex-rs/tools/Cargo.toml b/codex-rs/tools/Cargo.toml index d695336fae..1dc9b1c2fa 100644 --- a/codex-rs/tools/Cargo.toml +++ b/codex-rs/tools/Cargo.toml @@ -8,6 +8,7 @@ version.workspace = true workspace = true [dependencies] +codex-protocol = { workspace = true } rmcp = { workspace = true, default-features = false, features = [ "base64", "macros", diff --git a/codex-rs/tools/README.md b/codex-rs/tools/README.md index b8780fe7b1..b6f4539ffd 100644 --- a/codex-rs/tools/README.md +++ b/codex-rs/tools/README.md @@ -10,6 +10,8 @@ schema primitives that no longer need to live in `core/src/tools/spec.rs`: - `JsonSchema` - `AdditionalProperties` - `parse_tool_input_schema()` +- `ParsedDynamicTool` +- `parse_dynamic_tool()` - `ParsedMcpTool` - `parse_mcp_tool()` - `mcp_call_tool_result_output_schema()` diff --git a/codex-rs/tools/src/dynamic_tool.rs b/codex-rs/tools/src/dynamic_tool.rs new file mode 100644 index 0000000000..8be6a934a6 --- /dev/null +++ b/codex-rs/tools/src/dynamic_tool.rs @@ -0,0 +1,24 @@ +use crate::JsonSchema; +use crate::parse_tool_input_schema; +use codex_protocol::dynamic_tools::DynamicToolSpec; + +/// Parsed dynamic tool metadata and schemas that can be adapted into a +/// higher-level tool spec by downstream crates. +#[derive(Debug, PartialEq)] +pub struct ParsedDynamicTool { + pub description: String, + pub input_schema: JsonSchema, + pub defer_loading: bool, +} + +pub fn parse_dynamic_tool(tool: &DynamicToolSpec) -> Result { + Ok(ParsedDynamicTool { + description: tool.description.clone(), + input_schema: parse_tool_input_schema(&tool.input_schema)?, + defer_loading: tool.defer_loading, + }) +} + +#[cfg(test)] +#[path = "dynamic_tool_tests.rs"] +mod tests; diff --git a/codex-rs/tools/src/dynamic_tool_tests.rs b/codex-rs/tools/src/dynamic_tool_tests.rs new file mode 100644 index 0000000000..67bb76cc5e --- /dev/null +++ b/codex-rs/tools/src/dynamic_tool_tests.rs @@ -0,0 +1,66 @@ +use super::ParsedDynamicTool; +use super::parse_dynamic_tool; +use crate::JsonSchema; +use codex_protocol::dynamic_tools::DynamicToolSpec; +use pretty_assertions::assert_eq; +use std::collections::BTreeMap; + +#[test] +fn parse_dynamic_tool_sanitizes_input_schema() { + let tool = DynamicToolSpec { + name: "lookup_ticket".to_string(), + description: "Fetch a ticket".to_string(), + input_schema: serde_json::json!({ + "properties": { + "id": { + "description": "Ticket identifier" + } + } + }), + defer_loading: false, + }; + + assert_eq!( + parse_dynamic_tool(&tool).expect("parse dynamic tool"), + ParsedDynamicTool { + description: "Fetch a ticket".to_string(), + input_schema: JsonSchema::Object { + properties: BTreeMap::from([( + "id".to_string(), + JsonSchema::String { + description: Some("Ticket identifier".to_string()), + }, + )]), + required: None, + additional_properties: None, + }, + defer_loading: false, + } + ); +} + +#[test] +fn parse_dynamic_tool_preserves_defer_loading() { + let tool = DynamicToolSpec { + name: "lookup_ticket".to_string(), + description: "Fetch a ticket".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": {} + }), + defer_loading: true, + }; + + assert_eq!( + parse_dynamic_tool(&tool).expect("parse dynamic tool"), + ParsedDynamicTool { + description: "Fetch a ticket".to_string(), + input_schema: JsonSchema::Object { + properties: BTreeMap::new(), + required: None, + additional_properties: None, + }, + defer_loading: true, + } + ); +} diff --git a/codex-rs/tools/src/lib.rs b/codex-rs/tools/src/lib.rs index e719d3e23f..2698e83f4f 100644 --- a/codex-rs/tools/src/lib.rs +++ b/codex-rs/tools/src/lib.rs @@ -1,8 +1,11 @@ //! Shared tool-schema parsing primitives that can live outside `codex-core`. +mod dynamic_tool; mod json_schema; mod mcp_tool; +pub use dynamic_tool::ParsedDynamicTool; +pub use dynamic_tool::parse_dynamic_tool; pub use json_schema::AdditionalProperties; pub use json_schema::JsonSchema; pub use json_schema::parse_tool_input_schema;