diff --git a/codex-rs/core/src/tools/handlers/multi_agents_spec_tests.rs b/codex-rs/core/src/tools/handlers/multi_agents_spec_tests.rs index ad685919f9..d74729ef21 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_spec_tests.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_spec_tests.rs @@ -64,8 +64,8 @@ fn spawn_agent_tool_v2_requires_task_name() { ); assert!(!properties.contains_key("service_tier")); assert_eq!( - parameters.required.as_ref(), - Some(&vec!["task_name".to_string(), "message".to_string()]) + parameters.required.as_deref(), + Some(["task_name".to_string(), "message".to_string()].as_slice()) ); assert_eq!( output_schema.expect("spawn_agent output schema").to_value()["required"], @@ -274,8 +274,8 @@ fn send_message_tool_requires_message_and_has_no_output_schema() { Some("Relative or canonical task name to message (from spawn_agent).") ); assert_eq!( - parameters.required.as_ref(), - Some(&vec!["target".to_string(), "message".to_string()]) + parameters.required.as_deref(), + Some(["target".to_string(), "message".to_string()].as_slice()) ); assert_eq!(output_schema, None); } @@ -315,8 +315,8 @@ fn followup_task_tool_requires_message_and_has_no_output_schema() { ); assert!(!properties.contains_key("items")); assert_eq!( - parameters.required.as_ref(), - Some(&vec!["target".to_string(), "message".to_string()]) + parameters.required.as_deref(), + Some(["target".to_string(), "message".to_string()].as_slice()) ); assert_eq!(output_schema, None); } diff --git a/codex-rs/core/src/tools/handlers/shell_spec.rs b/codex-rs/core/src/tools/handlers/shell_spec.rs index ca48b21220..264a214f62 100644 --- a/codex-rs/core/src/tools/handlers/shell_spec.rs +++ b/codex-rs/core/src/tools/handlers/shell_spec.rs @@ -262,7 +262,7 @@ fn create_approval_parameters( let mut additional_permissions = permission_profile_schema(); additional_permissions.description = Some( "Sandboxed filesystem or network access for this command; only with `sandbox_permissions: \"with_additional_permissions\"`." - .to_string(), + .into(), ); properties.insert("additional_permissions".to_string(), additional_permissions); } @@ -279,7 +279,7 @@ fn permission_profile_schema() -> JsonSchema { /*required*/ None, Some(false.into()), ); - schema.description = Some("Filesystem or network access request.".to_string()); + schema.description = Some("Filesystem or network access request.".into()); schema } @@ -294,7 +294,7 @@ fn network_permissions_schema() -> JsonSchema { /*required*/ None, Some(false.into()), ); - schema.description = Some("Network access request.".to_string()); + schema.description = Some("Network access request.".into()); schema } @@ -325,7 +325,7 @@ fn file_system_permissions_schema() -> JsonSchema { /*required*/ None, Some(false.into()), ); - schema.description = Some("Filesystem access request.".to_string()); + schema.description = Some("Filesystem access request.".into()); schema } diff --git a/codex-rs/core/src/tools/multi_agent_tool.rs b/codex-rs/core/src/tools/multi_agent_tool.rs index 098e433646..0e44fd92b2 100644 --- a/codex-rs/core/src/tools/multi_agent_tool.rs +++ b/codex-rs/core/src/tools/multi_agent_tool.rs @@ -30,7 +30,7 @@ pub(super) fn multi_agent_v2_handler( && let Some(properties) = tool.parameters.properties { // Argument transport requires these markers even without server encryption config. - for (name, schema) in properties { + for (name, schema) in *properties { if schema.encrypted == Some(true) { let property = parameters .properties diff --git a/codex-rs/tools/src/json_schema/types.rs b/codex-rs/tools/src/json_schema/types.rs index 0792816fd3..f7868bff04 100644 --- a/codex-rs/tools/src/json_schema/types.rs +++ b/codex-rs/tools/src/json_schema/types.rs @@ -29,49 +29,52 @@ pub enum JsonSchemaPrimitiveType { #[serde(untagged)] pub enum JsonSchemaType { Single(JsonSchemaPrimitiveType), - Multiple(Vec), + Multiple(Box<[JsonSchemaPrimitiveType]>), } /// Generic JSON-Schema subset needed for our tool definitions. +/// +/// Stored strings and sequences do not grow, so they do not retain spare capacity. +/// Optional maps are boxed because most schema nodes do not contain them. #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] pub struct JsonSchema { /// Explicit ordinary MCP server input schema limit; never sent as JSON Schema. #[serde(skip)] pub mcp_input_schema_max_bytes: Option, #[serde(rename = "$ref", skip_serializing_if = "Option::is_none")] - pub schema_ref: Option, + pub schema_ref: Option>, #[serde(rename = "type", skip_serializing_if = "Option::is_none")] pub schema_type: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, + pub description: Option>, /// Responses-only marker for reviewed encrypted tool parameters. #[serde(skip_serializing_if = "Option::is_none")] pub encrypted: Option, #[serde(rename = "enum", skip_serializing_if = "Option::is_none")] - pub enum_values: Option>, + pub enum_values: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub items: Option>, #[serde(rename = "minItems", skip_serializing_if = "Option::is_none")] pub min_items: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub properties: Option>, + pub properties: Option>>, #[serde(skip_serializing_if = "Option::is_none")] - pub required: Option>, + pub required: Option>, #[serde( rename = "additionalProperties", skip_serializing_if = "Option::is_none" )] pub additional_properties: Option, #[serde(rename = "anyOf", skip_serializing_if = "Option::is_none")] - pub any_of: Option>, + pub any_of: Option>, #[serde(rename = "oneOf", skip_serializing_if = "Option::is_none")] - pub one_of: Option>, + pub one_of: Option>, #[serde(rename = "allOf", skip_serializing_if = "Option::is_none")] - pub all_of: Option>, + pub all_of: Option>, #[serde(rename = "$defs", skip_serializing_if = "Option::is_none")] - pub defs: Option>, + pub defs: Option>>, #[serde(skip_serializing_if = "Option::is_none")] - pub definitions: Option>, + pub definitions: Option>>, } impl JsonSchema { @@ -79,31 +82,31 @@ impl JsonSchema { fn typed(schema_type: JsonSchemaPrimitiveType, description: Option) -> Self { Self { schema_type: Some(JsonSchemaType::Single(schema_type)), - description, + description: description.map(String::into_boxed_str), ..Default::default() } } pub fn any_of(variants: Vec, description: Option) -> Self { Self { - description, - any_of: Some(variants), + description: description.map(String::into_boxed_str), + any_of: Some(variants.into_boxed_slice()), ..Default::default() } } pub fn one_of(variants: Vec, description: Option) -> Self { Self { - description, - one_of: Some(variants), + description: description.map(String::into_boxed_str), + one_of: Some(variants.into_boxed_slice()), ..Default::default() } } pub fn all_of(variants: Vec, description: Option) -> Self { Self { - description, - all_of: Some(variants), + description: description.map(String::into_boxed_str), + all_of: Some(variants.into_boxed_slice()), ..Default::default() } } @@ -136,8 +139,8 @@ impl JsonSchema { pub fn string_enum(values: Vec, description: Option) -> Self { Self { schema_type: Some(JsonSchemaType::Single(JsonSchemaPrimitiveType::String)), - description, - enum_values: Some(values), + description: description.map(String::into_boxed_str), + enum_values: Some(values.into_boxed_slice()), ..Default::default() } } @@ -145,7 +148,7 @@ impl JsonSchema { pub fn array(items: JsonSchema, description: Option) -> Self { Self { schema_type: Some(JsonSchemaType::Single(JsonSchemaPrimitiveType::Array)), - description, + description: description.map(String::into_boxed_str), items: Some(Box::new(items)), ..Default::default() } @@ -158,8 +161,8 @@ impl JsonSchema { ) -> Self { Self { schema_type: Some(JsonSchemaType::Single(JsonSchemaPrimitiveType::Object)), - properties: Some(properties), - required, + properties: Some(Box::new(properties)), + required: required.map(Vec::into_boxed_slice), additional_properties, ..Default::default() } diff --git a/codex-rs/tools/src/json_schema_tests.rs b/codex-rs/tools/src/json_schema_tests.rs index 449631475d..768ae2deee 100644 --- a/codex-rs/tools/src/json_schema_tests.rs +++ b/codex-rs/tools/src/json_schema_tests.rs @@ -479,11 +479,11 @@ fn parse_tool_input_schema_fills_default_properties_for_nullable_object_union() assert_eq!( schema, JsonSchema { - schema_type: Some(JsonSchemaType::Multiple(vec![ + schema_type: Some(JsonSchemaType::Multiple(Box::new([ JsonSchemaPrimitiveType::Object, JsonSchemaPrimitiveType::Null, - ])), - properties: Some(BTreeMap::new()), + ]))), + properties: Some(Box::default()), ..Default::default() } ); @@ -507,10 +507,10 @@ fn parse_tool_input_schema_fills_default_items_for_nullable_array_union() { assert_eq!( schema, JsonSchema { - schema_type: Some(JsonSchemaType::Multiple(vec![ + schema_type: Some(JsonSchemaType::Multiple(Box::new([ JsonSchemaPrimitiveType::Array, JsonSchemaPrimitiveType::Null, - ])), + ]))), items: Some(Box::new(JsonSchema::string(/*description*/ None))), ..Default::default() } @@ -650,11 +650,11 @@ fn parse_tool_input_schema_preserves_nested_nullable_type_union() { BTreeMap::from([( "nickname".to_string(), JsonSchema { - schema_type: Some(JsonSchemaType::Multiple(vec![ + schema_type: Some(JsonSchemaType::Multiple(Box::new([ JsonSchemaPrimitiveType::String, JsonSchemaPrimitiveType::Null, - ])), - description: Some("Optional nickname".to_string()), + ]))), + description: Some("Optional nickname".into()), ..Default::default() }, )]), @@ -837,11 +837,11 @@ fn parse_tool_input_schema_preserves_type_unions_without_rewriting_to_any_of() { assert_eq!( schema, JsonSchema { - schema_type: Some(JsonSchemaType::Multiple(vec![ + schema_type: Some(JsonSchemaType::Multiple(Box::new([ JsonSchemaPrimitiveType::String, JsonSchemaPrimitiveType::Null, - ])), - description: Some("optional string".to_string()), + ]))), + description: Some("optional string".into()), ..Default::default() } ); @@ -868,16 +868,16 @@ fn parse_tool_input_schema_preserves_explicit_enum_type_union() { assert_eq!( schema, JsonSchema { - schema_type: Some(JsonSchemaType::Multiple(vec![ + schema_type: Some(JsonSchemaType::Multiple(Box::new([ JsonSchemaPrimitiveType::String, JsonSchemaPrimitiveType::Null, - ])), - description: Some("optional response length".to_string()), - enum_values: Some(vec![ + ]))), + description: Some("optional response length".into()), + enum_values: Some(Box::new([ serde_json::json!("short"), serde_json::json!("medium"), serde_json::json!("long"), - ]), + ])), ..Default::default() } ); @@ -1382,14 +1382,14 @@ fn parse_tool_input_schema_preserves_refs_and_prunes_unreachable_defs() { schema, JsonSchema { schema_type: Some(JsonSchemaType::Single(JsonSchemaPrimitiveType::Object)), - properties: Some(BTreeMap::from([( + properties: Some(Box::new(BTreeMap::from([( "user".to_string(), JsonSchema { - schema_ref: Some("#/$defs/User".to_string()), + schema_ref: Some("#/$defs/User".into()), ..Default::default() }, - )])), - defs: Some(BTreeMap::from([( + )]))), + defs: Some(Box::new(BTreeMap::from([( "User".to_string(), JsonSchema::object( BTreeMap::from([( @@ -1399,7 +1399,7 @@ fn parse_tool_input_schema_preserves_refs_and_prunes_unreachable_defs() { /*required*/ None, /*additional_properties*/ None, ), - )])), + )]))), ..Default::default() } ); @@ -1437,17 +1437,17 @@ fn parse_tool_input_schema_preserves_refs_from_properties_named_def_tables() { schema, JsonSchema { schema_type: Some(JsonSchemaType::Single(JsonSchemaPrimitiveType::Object)), - properties: Some(BTreeMap::from([( + properties: Some(Box::new(BTreeMap::from([( "$defs".to_string(), JsonSchema { - schema_ref: Some("#/$defs/User".to_string()), + schema_ref: Some("#/$defs/User".into()), ..Default::default() }, - )])), - defs: Some(BTreeMap::from([( + )]))), + defs: Some(Box::new(BTreeMap::from([( "User".to_string(), JsonSchema::string(/*description*/ None), - )])), + )]))), ..Default::default() } ); @@ -1578,27 +1578,27 @@ fn parse_tool_input_schema_handles_cyclic_local_refs() { schema, JsonSchema { schema_type: Some(JsonSchemaType::Single(JsonSchemaPrimitiveType::Object)), - properties: Some(BTreeMap::from([( + properties: Some(Box::new(BTreeMap::from([( "node".to_string(), JsonSchema { - schema_ref: Some("#/$defs/Node".to_string()), + schema_ref: Some("#/$defs/Node".into()), ..Default::default() }, - )])), - defs: Some(BTreeMap::from([( + )]))), + defs: Some(Box::new(BTreeMap::from([( "Node".to_string(), JsonSchema::object( BTreeMap::from([( "next".to_string(), JsonSchema { - schema_ref: Some("#/$defs/Node".to_string()), + schema_ref: Some("#/$defs/Node".into()), ..Default::default() }, )]), /*required*/ None, /*additional_properties*/ None, ), - )])), + )]))), ..Default::default() } ); @@ -1647,14 +1647,14 @@ fn parse_tool_input_schema_preserves_legacy_definitions() { schema, JsonSchema { schema_type: Some(JsonSchemaType::Single(JsonSchemaPrimitiveType::Object)), - properties: Some(BTreeMap::from([( + properties: Some(Box::new(BTreeMap::from([( "user".to_string(), JsonSchema { - schema_ref: Some("#/definitions/User".to_string()), + schema_ref: Some("#/definitions/User".into()), ..Default::default() }, - )])), - definitions: Some(BTreeMap::from([ + )]))), + definitions: Some(Box::new(BTreeMap::from([ ( "Profile".to_string(), JsonSchema::object( @@ -1672,7 +1672,7 @@ fn parse_tool_input_schema_preserves_legacy_definitions() { BTreeMap::from([( "profile".to_string(), JsonSchema { - schema_ref: Some("#/definitions/Profile".to_string()), + schema_ref: Some("#/definitions/Profile".into()), ..Default::default() }, )]), @@ -1680,7 +1680,7 @@ fn parse_tool_input_schema_preserves_legacy_definitions() { /*additional_properties*/ None, ), ), - ])), + ]))), ..Default::default() } ); @@ -1718,22 +1718,22 @@ fn parse_tool_input_schema_preserves_unresolved_and_external_refs() { schema, JsonSchema { schema_type: Some(JsonSchemaType::Single(JsonSchemaPrimitiveType::Object)), - properties: Some(BTreeMap::from([ + properties: Some(Box::new(BTreeMap::from([ ( "missing".to_string(), JsonSchema { - schema_ref: Some("#/$defs/Missing".to_string()), + schema_ref: Some("#/$defs/Missing".into()), ..Default::default() }, ), ( "remote".to_string(), JsonSchema { - schema_ref: Some("https://example.com/schema.json".to_string()), + schema_ref: Some("https://example.com/schema.json".into()), ..Default::default() }, ), - ])), + ]))), ..Default::default() } ); @@ -1778,14 +1778,14 @@ fn parse_tool_input_schema_preserves_nested_defs_ref_parent() { schema, JsonSchema { schema_type: Some(JsonSchemaType::Single(JsonSchemaPrimitiveType::Object)), - properties: Some(BTreeMap::from([( + properties: Some(Box::new(BTreeMap::from([( "name".to_string(), JsonSchema { - schema_ref: Some("#/$defs/User/properties/name".to_string()), + schema_ref: Some("#/$defs/User/properties/name".into()), ..Default::default() }, - )])), - defs: Some(BTreeMap::from([( + )]))), + defs: Some(Box::new(BTreeMap::from([( "User".to_string(), JsonSchema::object( BTreeMap::from([( @@ -1795,7 +1795,7 @@ fn parse_tool_input_schema_preserves_nested_defs_ref_parent() { /*required*/ None, /*additional_properties*/ None, ), - )])), + )]))), ..Default::default() } ); @@ -1840,23 +1840,23 @@ fn parse_tool_input_schema_preserves_percent_encoded_definition_refs() { schema, JsonSchema { schema_type: Some(JsonSchemaType::Single(JsonSchemaPrimitiveType::Object)), - properties: Some(BTreeMap::from([ + properties: Some(Box::new(BTreeMap::from([ ( "profile".to_string(), JsonSchema { - schema_ref: Some("#/%24defs/Profile%7E0Name".to_string()), + schema_ref: Some("#/%24defs/Profile%7E0Name".into()), ..Default::default() }, ), ( "user".to_string(), JsonSchema { - schema_ref: Some("#/$defs/User%20Name".to_string()), + schema_ref: Some("#/$defs/User%20Name".into()), ..Default::default() }, ), - ])), - defs: Some(BTreeMap::from([ + ]))), + defs: Some(Box::new(BTreeMap::from([ ( "Profile~Name".to_string(), JsonSchema::string(/*description*/ None), @@ -1865,7 +1865,7 @@ fn parse_tool_input_schema_preserves_percent_encoded_definition_refs() { "User Name".to_string(), JsonSchema::string(/*description*/ None), ), - ])), + ]))), ..Default::default() } ); @@ -1896,13 +1896,13 @@ fn parse_tool_input_schema_drops_malformed_definition_tables() { schema, JsonSchema { schema_type: Some(JsonSchemaType::Single(JsonSchemaPrimitiveType::Object)), - properties: Some(BTreeMap::from([( + properties: Some(Box::new(BTreeMap::from([( "user".to_string(), JsonSchema { - schema_ref: Some("#/$defs/User".to_string()), + schema_ref: Some("#/$defs/User".into()), ..Default::default() }, - )])), + )]))), ..Default::default() } ); diff --git a/codex-rs/tools/src/tool_search.rs b/codex-rs/tools/src/tool_search.rs index 829910ea8f..c25daad32f 100644 --- a/codex-rs/tools/src/tool_search.rs +++ b/codex-rs/tools/src/tool_search.rs @@ -177,9 +177,9 @@ fn append_function_search_text(tool: &ResponsesApiTool, parts: &mut Vec) fn append_schema_search_text(schema: &JsonSchema, parts: &mut Vec) { if let Some(description) = &schema.description { - push_search_part(parts, description.clone()); + push_search_part(parts, description.to_string()); } - if let Some(properties) = &schema.properties { + if let Some(properties) = schema.properties.as_deref() { for (name, schema) in properties { push_search_part(parts, name.clone()); append_schema_search_text(schema, parts); diff --git a/codex-rs/tools/src/tool_search_tests.rs b/codex-rs/tools/src/tool_search_tests.rs index 9e34967e59..3d654aa8e6 100644 --- a/codex-rs/tools/src/tool_search_tests.rs +++ b/codex-rs/tools/src/tool_search_tests.rs @@ -180,7 +180,7 @@ fn default_search_text_uses_model_visible_namespace_metadata_once() { /*required*/ None, /*additional_properties*/ None, ); - schedule_schema.description = Some("Schedule settings.".to_string()); + schedule_schema.description = Some("Schedule settings.".into()); let mut parameters = JsonSchema::object( BTreeMap::from([ ( @@ -192,7 +192,7 @@ fn default_search_text_uses_model_visible_namespace_metadata_once() { /*required*/ None, /*additional_properties*/ None, ); - parameters.description = Some("Automation options.".to_string()); + parameters.description = Some("Automation options.".into()); let spec = ToolSpec::Namespace(crate::ResponsesApiNamespace { name: "codex_app".to_string(), description: "Manage Codex automations.".to_string(),