mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Expose explicit dynamic tool namespaces in thread start (#27371)
Stacked on #27365. ## Stack note [#27365](https://github.com/openai/codex/pull/27365) kept `thread/start` unchanged and converted its input in `thread_processor`. This PR updates `thread/start` to accept explicit functions and namespaces directly. Legacy per-tool arrays are still accepted and converted while reading the request. As a result, `thread_processor` can validate and pass the tools through directly, which is why some code added in #27365 is removed here. ## Why `thread/start.dynamicTools` still repeats namespace data on each function even though core now stores explicit namespace groups. The request API should use the same shape so each namespace has one description and one member list. ## What changed - Accept top-level functions and explicit namespace objects in `dynamicTools`. - Continue accepting fully legacy flat arrays, including `exposeToContext`. - Reject arrays that mix legacy and canonical entries. - Reuse the protocol types directly and remove the temporary app-server adapter. - Update validation, docs, the test client, and generated schemas. ## Test plan - `just test -p codex-app-server-protocol` - `just test -p codex-app-server dynamic_tool_call_round_trip_sends_text_content_items_to_model` - `just test -p codex-app-server thread_start_normalizes_legacy_dynamic_tools_into_model_request` - `just test -p codex-app-server thread_start_rejects_mixed_dynamic_tool_formats` - `just test -p codex-app-server thread_start_rejects_hidden_dynamic_tools_without_namespace`
This commit is contained in:
committed by
GitHub
Unverified
parent
42ad752f36
commit
11faf9af94
+95
-24
@@ -635,31 +635,102 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"DynamicToolSpec": {
|
||||
"properties": {
|
||||
"deferLoading": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputSchema": true,
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"namespace": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
"DynamicToolNamespaceTool": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"deferLoading": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputSchema": true,
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"function"
|
||||
],
|
||||
"title": "FunctionDynamicToolNamespaceToolType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"inputSchema",
|
||||
"name",
|
||||
"type"
|
||||
],
|
||||
"title": "FunctionDynamicToolNamespaceTool",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"inputSchema",
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
]
|
||||
},
|
||||
"DynamicToolSpec": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"deferLoading": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputSchema": true,
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"function"
|
||||
],
|
||||
"title": "FunctionDynamicToolSpecType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"inputSchema",
|
||||
"name",
|
||||
"type"
|
||||
],
|
||||
"title": "FunctionDynamicToolSpec",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"tools": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/DynamicToolNamespaceTool"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"namespace"
|
||||
],
|
||||
"title": "NamespaceDynamicToolSpecType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"name",
|
||||
"tools",
|
||||
"type"
|
||||
],
|
||||
"title": "NamespaceDynamicToolSpec",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ExperimentalFeatureEnablementSetParams": {
|
||||
"properties": {
|
||||
|
||||
+95
-24
@@ -8646,31 +8646,102 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"DynamicToolSpec": {
|
||||
"properties": {
|
||||
"deferLoading": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputSchema": true,
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"namespace": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
"DynamicToolNamespaceTool": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"deferLoading": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputSchema": true,
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"function"
|
||||
],
|
||||
"title": "FunctionDynamicToolNamespaceToolType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"inputSchema",
|
||||
"name",
|
||||
"type"
|
||||
],
|
||||
"title": "FunctionDynamicToolNamespaceTool",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"inputSchema",
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
]
|
||||
},
|
||||
"DynamicToolSpec": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"deferLoading": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputSchema": true,
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"function"
|
||||
],
|
||||
"title": "FunctionDynamicToolSpecType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"inputSchema",
|
||||
"name",
|
||||
"type"
|
||||
],
|
||||
"title": "FunctionDynamicToolSpec",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"tools": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/DynamicToolNamespaceTool"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"namespace"
|
||||
],
|
||||
"title": "NamespaceDynamicToolSpecType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"name",
|
||||
"tools",
|
||||
"type"
|
||||
],
|
||||
"title": "NamespaceDynamicToolSpec",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ErrorNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
|
||||
+95
-24
@@ -4959,31 +4959,102 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"DynamicToolSpec": {
|
||||
"properties": {
|
||||
"deferLoading": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputSchema": true,
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"namespace": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
"DynamicToolNamespaceTool": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"deferLoading": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputSchema": true,
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"function"
|
||||
],
|
||||
"title": "FunctionDynamicToolNamespaceToolType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"inputSchema",
|
||||
"name",
|
||||
"type"
|
||||
],
|
||||
"title": "FunctionDynamicToolNamespaceTool",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"inputSchema",
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
]
|
||||
},
|
||||
"DynamicToolSpec": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"deferLoading": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputSchema": true,
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"function"
|
||||
],
|
||||
"title": "FunctionDynamicToolSpecType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"inputSchema",
|
||||
"name",
|
||||
"type"
|
||||
],
|
||||
"title": "FunctionDynamicToolSpec",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"tools": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/DynamicToolNamespaceTool"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"namespace"
|
||||
],
|
||||
"title": "NamespaceDynamicToolSpecType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"name",
|
||||
"tools",
|
||||
"type"
|
||||
],
|
||||
"title": "NamespaceDynamicToolSpec",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ErrorNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
|
||||
+95
-24
@@ -94,31 +94,102 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"DynamicToolSpec": {
|
||||
"properties": {
|
||||
"deferLoading": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputSchema": true,
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"namespace": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
"DynamicToolNamespaceTool": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"deferLoading": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputSchema": true,
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"function"
|
||||
],
|
||||
"title": "FunctionDynamicToolNamespaceToolType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"inputSchema",
|
||||
"name",
|
||||
"type"
|
||||
],
|
||||
"title": "FunctionDynamicToolNamespaceTool",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"inputSchema",
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
]
|
||||
},
|
||||
"DynamicToolSpec": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"deferLoading": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputSchema": true,
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"function"
|
||||
],
|
||||
"title": "FunctionDynamicToolSpecType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"inputSchema",
|
||||
"name",
|
||||
"type"
|
||||
],
|
||||
"title": "FunctionDynamicToolSpec",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"tools": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/DynamicToolNamespaceTool"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"namespace"
|
||||
],
|
||||
"title": "NamespaceDynamicToolSpecType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"name",
|
||||
"tools",
|
||||
"type"
|
||||
],
|
||||
"title": "NamespaceDynamicToolSpec",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Personality": {
|
||||
"enum": [
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { JsonValue } from "../serde_json/JsonValue";
|
||||
|
||||
export type DynamicToolFunctionSpec = { name: string, description: string, inputSchema: JsonValue, deferLoading?: boolean, };
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DynamicToolNamespaceTool } from "./DynamicToolNamespaceTool";
|
||||
|
||||
export type DynamicToolNamespaceSpec = { name: string, description: string, tools: Array<DynamicToolNamespaceTool>, };
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DynamicToolFunctionSpec } from "./DynamicToolFunctionSpec";
|
||||
|
||||
export type DynamicToolNamespaceTool = { "type": "function" } & DynamicToolFunctionSpec;
|
||||
@@ -1,6 +1,7 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { JsonValue } from "../serde_json/JsonValue";
|
||||
import type { DynamicToolFunctionSpec } from "./DynamicToolFunctionSpec";
|
||||
import type { DynamicToolNamespaceSpec } from "./DynamicToolNamespaceSpec";
|
||||
|
||||
export type DynamicToolSpec = { namespace?: string, name: string, description: string, inputSchema: JsonValue, deferLoading?: boolean, };
|
||||
export type DynamicToolSpec = { "type": "function" } & DynamicToolFunctionSpec | { "type": "namespace" } & DynamicToolNamespaceSpec;
|
||||
|
||||
@@ -92,6 +92,9 @@ export type { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputCo
|
||||
export type { DynamicToolCallParams } from "./DynamicToolCallParams";
|
||||
export type { DynamicToolCallResponse } from "./DynamicToolCallResponse";
|
||||
export type { DynamicToolCallStatus } from "./DynamicToolCallStatus";
|
||||
export type { DynamicToolFunctionSpec } from "./DynamicToolFunctionSpec";
|
||||
export type { DynamicToolNamespaceSpec } from "./DynamicToolNamespaceSpec";
|
||||
export type { DynamicToolNamespaceTool } from "./DynamicToolNamespaceTool";
|
||||
export type { DynamicToolSpec } from "./DynamicToolSpec";
|
||||
export type { ErrorNotification } from "./ErrorNotification";
|
||||
export type { ExecPolicyAmendment } from "./ExecPolicyAmendment";
|
||||
|
||||
@@ -3531,56 +3531,6 @@ fn dynamic_tool_response_serializes_text_and_image_content_items() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dynamic_tool_spec_deserializes_defer_loading() {
|
||||
let value = json!({
|
||||
"name": "lookup_ticket",
|
||||
"description": "Fetch a ticket",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"deferLoading": true,
|
||||
});
|
||||
|
||||
let actual: DynamicToolSpec = serde_json::from_value(value).expect("deserialize");
|
||||
|
||||
assert_eq!(
|
||||
actual,
|
||||
DynamicToolSpec {
|
||||
namespace: None,
|
||||
name: "lookup_ticket".to_string(),
|
||||
description: "Fetch a ticket".to_string(),
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "string" }
|
||||
}
|
||||
}),
|
||||
defer_loading: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dynamic_tool_spec_legacy_expose_to_context_inverts_to_defer_loading() {
|
||||
let value = json!({
|
||||
"name": "lookup_ticket",
|
||||
"description": "Fetch a ticket",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
},
|
||||
"exposeToContext": false,
|
||||
});
|
||||
|
||||
let actual: DynamicToolSpec = serde_json::from_value(value).expect("deserialize");
|
||||
|
||||
assert!(actual.defer_loading);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_start_params_preserve_explicit_null_service_tier() {
|
||||
let params: ThreadStartParams =
|
||||
|
||||
@@ -16,6 +16,10 @@ pub use codex_protocol::capabilities::SelectedCapabilityRoot;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
pub use codex_protocol::dynamic_tools::DynamicToolFunctionSpec;
|
||||
pub use codex_protocol::dynamic_tools::DynamicToolNamespaceSpec;
|
||||
pub use codex_protocol::dynamic_tools::DynamicToolNamespaceTool;
|
||||
pub use codex_protocol::dynamic_tools::DynamicToolSpec;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::protocol::ThreadGoalStatus as CoreThreadGoalStatus;
|
||||
@@ -38,55 +42,6 @@ pub enum ThreadStartSource {
|
||||
Clear,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct DynamicToolSpec {
|
||||
#[ts(optional)]
|
||||
pub namespace: Option<String>,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub input_schema: JsonValue,
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub defer_loading: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DynamicToolSpecDe {
|
||||
namespace: Option<String>,
|
||||
name: String,
|
||||
description: String,
|
||||
input_schema: JsonValue,
|
||||
defer_loading: Option<bool>,
|
||||
expose_to_context: Option<bool>,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for DynamicToolSpec {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let DynamicToolSpecDe {
|
||||
namespace,
|
||||
name,
|
||||
description,
|
||||
input_schema,
|
||||
defer_loading,
|
||||
expose_to_context,
|
||||
} = DynamicToolSpecDe::deserialize(deserializer)?;
|
||||
|
||||
Ok(Self {
|
||||
namespace,
|
||||
name,
|
||||
description,
|
||||
input_schema,
|
||||
defer_loading: defer_loading
|
||||
.unwrap_or_else(|| expose_to_context.map(|visible| !visible).unwrap_or(false)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// === Threads, Turns, and Items ===
|
||||
// Thread APIs
|
||||
#[derive(
|
||||
@@ -153,6 +108,10 @@ pub struct ThreadStartParams {
|
||||
#[ts(optional = nullable)]
|
||||
pub environments: Option<Vec<TurnEnvironmentParams>>,
|
||||
#[experimental("thread/start.dynamicTools")]
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "codex_protocol::dynamic_tools::deserialize_dynamic_tool_specs"
|
||||
)]
|
||||
#[ts(optional = nullable)]
|
||||
pub dynamic_tools: Option<Vec<DynamicToolSpec>>,
|
||||
/// Capability roots selected for this thread by the hosting platform.
|
||||
|
||||
@@ -70,6 +70,7 @@ use codex_app_server_protocol::UserInput as V2UserInput;
|
||||
use codex_core::config::Config;
|
||||
use codex_otel::OtelProvider;
|
||||
use codex_otel::current_span_w3c_trace_context;
|
||||
use codex_protocol::dynamic_tools::normalize_dynamic_tool_specs;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::protocol::W3cTraceContext;
|
||||
use codex_utils_cli::CliConfigOverrides;
|
||||
@@ -135,7 +136,7 @@ struct Cli {
|
||||
/// Prefix a filename with '@' to read from a file.
|
||||
///
|
||||
/// Example:
|
||||
/// --dynamic-tools '[{"name":"demo","description":"Demo","inputSchema":{"type":"object"}}]'
|
||||
/// --dynamic-tools '[{"type":"function","name":"demo","description":"Demo","inputSchema":{"type":"object"}}]'
|
||||
/// --dynamic-tools @/path/to/tools.json
|
||||
#[arg(long, value_name = "json-or-@file", global = true)]
|
||||
dynamic_tools: Option<String>,
|
||||
@@ -1373,11 +1374,12 @@ fn parse_dynamic_tools_arg(dynamic_tools: &Option<String>) -> Result<Option<Vec<
|
||||
};
|
||||
|
||||
let value: Value = serde_json::from_str(&raw_json).context("parse dynamic tools JSON")?;
|
||||
let tools = match value {
|
||||
Value::Array(_) => serde_json::from_value(value).context("decode dynamic tools array")?,
|
||||
Value::Object(_) => vec![serde_json::from_value(value).context("decode dynamic tool")?],
|
||||
let values = match value {
|
||||
Value::Array(values) => values,
|
||||
Value::Object(_) => vec![value],
|
||||
_ => bail!("dynamic tools JSON must be an object or array"),
|
||||
};
|
||||
let tools = normalize_dynamic_tool_specs(values).context("decode dynamic tools")?;
|
||||
|
||||
Ok(Some(tools))
|
||||
}
|
||||
|
||||
@@ -271,16 +271,24 @@ Start a fresh thread when you need a new Codex conversation.
|
||||
// Experimental: requires opt-in
|
||||
"dynamicTools": [
|
||||
{
|
||||
"name": "lookup_ticket",
|
||||
"description": "Fetch a ticket by id",
|
||||
"deferLoading": true,
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "string" }
|
||||
},
|
||||
"required": ["id"]
|
||||
}
|
||||
"type": "namespace",
|
||||
"name": "tickets",
|
||||
"description": "Ticket management tools",
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"name": "lookup_ticket",
|
||||
"description": "Fetch a ticket by id",
|
||||
"deferLoading": true,
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "string" }
|
||||
},
|
||||
"required": ["id"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
} }
|
||||
@@ -1502,13 +1510,14 @@ If the session approval policy uses `Granular` with `request_permissions: false`
|
||||
|
||||
`dynamicTools` on `thread/start` and the corresponding `item/tool/call` request/response flow are experimental APIs. To enable them, set `initialize.params.capabilities.experimentalApi = true`.
|
||||
|
||||
Dynamic tool identifiers follow the same constraints as Responses function tools:
|
||||
Each entry in `dynamicTools` is either a top-level function or a namespace containing function tools. Dynamic tool identifiers follow the same constraints as Responses tools:
|
||||
|
||||
- `name` must match `^[a-zA-Z0-9_-]+$` and be between 1 and 128 characters.
|
||||
- `namespace`, when present, must match `^[a-zA-Z0-9_-]+$` and be between 1 and 64 characters.
|
||||
- `namespace` must not collide with reserved Responses runtime namespaces such as `functions`, `multi_tool_use`, `file_search`, `web`, `browser`, `image_gen`, `computer`, `container`, `terminal`, `python`, `python_user_visible`, `api_tool`, `tool_search`, or `submodel_delegator`.
|
||||
- Namespace names must match `^[a-zA-Z0-9_-]+$` and be between 1 and 64 characters.
|
||||
- Namespace descriptions must be at most 1,024 characters.
|
||||
- Namespace names must not collide with reserved Responses runtime namespaces such as `functions`, `multi_tool_use`, `file_search`, `web`, `browser`, `image_gen`, `computer`, `container`, `terminal`, `python`, `python_user_visible`, `api_tool`, `tool_search`, or `submodel_delegator`.
|
||||
|
||||
Each dynamic tool may set `deferLoading`. When omitted, it defaults to `false`. Set it to `true` to keep the tool registered and callable by runtime features such as `code_mode`, while excluding it from the model-facing tool list sent on ordinary turns. When `tool_search` is available, deferred dynamic tools are searchable and can be exposed by a matching search result.
|
||||
Each function may set `deferLoading`. When omitted, it defaults to `false`. Deferred functions must belong to a namespace. Set it to `true` to keep the function registered and callable by runtime features such as `code_mode`, while excluding it from the model-facing tool list sent on ordinary turns. When `tool_search` is available, deferred dynamic tools are searchable and can be exposed by a matching search result.
|
||||
|
||||
When a dynamic tool is invoked during a turn, the server sends an `item/tool/call` JSON-RPC request to the client:
|
||||
|
||||
@@ -1520,6 +1529,7 @@ When a dynamic tool is invoked during a turn, the server sends an `item/tool/cal
|
||||
"threadId": "thr_123",
|
||||
"turnId": "turn_123",
|
||||
"callId": "call_123",
|
||||
"namespace": "tickets",
|
||||
"tool": "lookup_ticket",
|
||||
"arguments": { "id": "ABC-123" }
|
||||
}
|
||||
|
||||
@@ -54,7 +54,9 @@ use codex_app_server_protocol::CommandExecWriteParams;
|
||||
use codex_app_server_protocol::ConfigWarningNotification;
|
||||
use codex_app_server_protocol::ConversationGitInfo;
|
||||
use codex_app_server_protocol::ConversationSummary;
|
||||
use codex_app_server_protocol::DynamicToolSpec as ApiDynamicToolSpec;
|
||||
use codex_app_server_protocol::DynamicToolFunctionSpec;
|
||||
use codex_app_server_protocol::DynamicToolNamespaceTool;
|
||||
use codex_app_server_protocol::DynamicToolSpec;
|
||||
use codex_app_server_protocol::EnvironmentAddParams;
|
||||
use codex_app_server_protocol::EnvironmentAddResponse;
|
||||
use codex_app_server_protocol::ExperimentalFeature as ApiExperimentalFeature;
|
||||
@@ -377,8 +379,6 @@ use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::config_types::TrustLevel;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::dynamic_tools::DynamicToolFunctionSpec;
|
||||
use codex_protocol::dynamic_tools::group_dynamic_tools_by_namespace;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::Result as CodexResult;
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -195,9 +195,10 @@ fn has_model_resume_override(
|
||||
.is_some_and(|overrides| overrides.contains_key("model_reasoning_effort"))
|
||||
}
|
||||
|
||||
fn validate_dynamic_tools(tools: &[ApiDynamicToolSpec]) -> Result<(), String> {
|
||||
fn validate_dynamic_tools(tools: &[DynamicToolSpec]) -> Result<(), String> {
|
||||
const DYNAMIC_TOOL_NAME_MAX_LEN: usize = 128;
|
||||
const DYNAMIC_TOOL_NAMESPACE_MAX_LEN: usize = 64;
|
||||
const DYNAMIC_TOOL_NAMESPACE_DESCRIPTION_MAX_LEN: usize = 1024;
|
||||
const DYNAMIC_TOOL_IDENTIFIER_PATTERN: &str = "^[a-zA-Z0-9_-]+$";
|
||||
const RESERVED_RESPONSES_NAMESPACES: &[&str] = &[
|
||||
"api_tool",
|
||||
@@ -243,8 +244,11 @@ fn validate_dynamic_tools(tools: &[ApiDynamicToolSpec]) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let mut seen = HashSet::new();
|
||||
for tool in tools {
|
||||
fn validate_dynamic_tool<'a>(
|
||||
tool: &'a DynamicToolFunctionSpec,
|
||||
namespace: Option<&str>,
|
||||
seen: &mut HashSet<&'a str>,
|
||||
) -> Result<(), String> {
|
||||
let name = tool.name.trim();
|
||||
if name.is_empty() {
|
||||
return Err("dynamic tool name must not be empty".to_string());
|
||||
@@ -259,37 +263,7 @@ fn validate_dynamic_tools(tools: &[ApiDynamicToolSpec]) -> Result<(), String> {
|
||||
if name == "mcp" || name.starts_with("mcp__") {
|
||||
return Err(format!("dynamic tool name is reserved: {name}"));
|
||||
}
|
||||
let namespace = tool.namespace.as_deref().map(str::trim);
|
||||
if let Some(namespace) = namespace {
|
||||
if namespace.is_empty() {
|
||||
return Err(format!(
|
||||
"dynamic tool namespace must not be empty for {name}"
|
||||
));
|
||||
}
|
||||
if Some(namespace) != tool.namespace.as_deref() {
|
||||
return Err(format!(
|
||||
"dynamic tool namespace has leading/trailing whitespace for {name}: {namespace}",
|
||||
name = escape_identifier_for_error(name),
|
||||
namespace = escape_identifier_for_error(namespace),
|
||||
));
|
||||
}
|
||||
validate_dynamic_tool_identifier(
|
||||
namespace,
|
||||
"dynamic tool namespace",
|
||||
DYNAMIC_TOOL_NAMESPACE_MAX_LEN,
|
||||
)?;
|
||||
if namespace == "mcp" || namespace.starts_with("mcp__") {
|
||||
return Err(format!(
|
||||
"dynamic tool namespace is reserved for {name}: {namespace}"
|
||||
));
|
||||
}
|
||||
if RESERVED_RESPONSES_NAMESPACES.contains(&namespace) {
|
||||
return Err(format!(
|
||||
"dynamic tool namespace collides with a reserved Responses API namespace for {name}: {namespace}",
|
||||
));
|
||||
}
|
||||
}
|
||||
if !seen.insert((namespace, name)) {
|
||||
if !seen.insert(name) {
|
||||
if let Some(namespace) = namespace {
|
||||
return Err(format!(
|
||||
"duplicate dynamic tool name in namespace {namespace}: {name}"
|
||||
@@ -308,6 +282,62 @@ fn validate_dynamic_tools(tools: &[ApiDynamicToolSpec]) -> Result<(), String> {
|
||||
"dynamic tool input schema is not supported for {name}: {err}"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let mut seen_tools = HashSet::new();
|
||||
let mut seen_namespaces = HashSet::new();
|
||||
for spec in tools {
|
||||
match spec {
|
||||
DynamicToolSpec::Function(tool) => {
|
||||
validate_dynamic_tool(tool, /*namespace*/ None, &mut seen_tools)?;
|
||||
}
|
||||
DynamicToolSpec::Namespace(namespace) => {
|
||||
let name = namespace.name.trim();
|
||||
if name.is_empty() {
|
||||
return Err("dynamic tool namespace must not be empty".to_string());
|
||||
}
|
||||
if name != namespace.name {
|
||||
return Err(format!(
|
||||
"dynamic tool namespace has leading/trailing whitespace: {}",
|
||||
escape_identifier_for_error(&namespace.name),
|
||||
));
|
||||
}
|
||||
validate_dynamic_tool_identifier(
|
||||
name,
|
||||
"dynamic tool namespace",
|
||||
DYNAMIC_TOOL_NAMESPACE_MAX_LEN,
|
||||
)?;
|
||||
if namespace.description.chars().count()
|
||||
> DYNAMIC_TOOL_NAMESPACE_DESCRIPTION_MAX_LEN
|
||||
{
|
||||
return Err(format!(
|
||||
"dynamic tool namespace description must be at most {DYNAMIC_TOOL_NAMESPACE_DESCRIPTION_MAX_LEN} characters"
|
||||
));
|
||||
}
|
||||
if name == "mcp" || name.starts_with("mcp__") {
|
||||
return Err(format!("dynamic tool namespace is reserved: {name}"));
|
||||
}
|
||||
if RESERVED_RESPONSES_NAMESPACES.contains(&name) {
|
||||
return Err(format!(
|
||||
"dynamic tool namespace collides with a reserved Responses API namespace: {name}",
|
||||
));
|
||||
}
|
||||
if !seen_namespaces.insert(name) {
|
||||
return Err(format!("duplicate dynamic tool namespace: {name}"));
|
||||
}
|
||||
if namespace.tools.is_empty() {
|
||||
return Err(format!(
|
||||
"dynamic tool namespace must contain at least one tool: {name}"
|
||||
));
|
||||
}
|
||||
let mut seen_namespace_tools = HashSet::new();
|
||||
for tool in &namespace.tools {
|
||||
let DynamicToolNamespaceTool::Function(tool) = tool;
|
||||
validate_dynamic_tool(tool, Some(name), &mut seen_namespace_tools)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -990,7 +1020,7 @@ impl ThreadRequestProcessor {
|
||||
app_server_client_version: Option<String>,
|
||||
config_overrides: Option<HashMap<String, serde_json::Value>>,
|
||||
typesafe_overrides: ConfigOverrides,
|
||||
dynamic_tools: Option<Vec<ApiDynamicToolSpec>>,
|
||||
dynamic_tools: Option<Vec<DynamicToolSpec>>,
|
||||
selected_capability_roots: Vec<SelectedCapabilityRoot>,
|
||||
session_start_source: Option<codex_app_server_protocol::ThreadStartSource>,
|
||||
thread_source: Option<codex_protocol::protocol::ThreadSource>,
|
||||
@@ -1077,29 +1107,17 @@ impl ThreadRequestProcessor {
|
||||
.default_environment_selections(&config.cwd)
|
||||
});
|
||||
let dynamic_tools = dynamic_tools.unwrap_or_default();
|
||||
// Count callable tools before grouping changes the outer list length.
|
||||
let core_dynamic_tool_count = dynamic_tools.len();
|
||||
let core_dynamic_tools = if dynamic_tools.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
if !dynamic_tools.is_empty() {
|
||||
validate_dynamic_tools(&dynamic_tools).map_err(invalid_request)?;
|
||||
// Normalize the flat app-server input into core's function and namespace types.
|
||||
let tools = dynamic_tools
|
||||
.into_iter()
|
||||
.map(|tool| {
|
||||
(
|
||||
tool.namespace,
|
||||
DynamicToolFunctionSpec {
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
input_schema: tool.input_schema,
|
||||
defer_loading: tool.defer_loading,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
group_dynamic_tools_by_namespace(tools)
|
||||
};
|
||||
}
|
||||
// Count callable functions rather than top-level namespace containers.
|
||||
let dynamic_tool_count: usize = dynamic_tools
|
||||
.iter()
|
||||
.map(|tool| match tool {
|
||||
DynamicToolSpec::Function(_) => 1,
|
||||
DynamicToolSpec::Namespace(namespace) => namespace.tools.len(),
|
||||
})
|
||||
.sum();
|
||||
let mut thread_extension_init = ExtensionDataInit::new();
|
||||
if !selected_capability_roots.is_empty() {
|
||||
thread_extension_init.insert(selected_capability_roots);
|
||||
@@ -1123,7 +1141,7 @@ impl ThreadRequestProcessor {
|
||||
},
|
||||
session_source: None,
|
||||
thread_source,
|
||||
dynamic_tools: core_dynamic_tools,
|
||||
dynamic_tools,
|
||||
metrics_service_name: service_name,
|
||||
parent_trace: request_trace,
|
||||
environments,
|
||||
@@ -1132,7 +1150,7 @@ impl ThreadRequestProcessor {
|
||||
.instrument(tracing::info_span!(
|
||||
"app_server.thread_start.create_thread",
|
||||
otel.name = "app_server.thread_start.create_thread",
|
||||
thread_start.dynamic_tool_count = core_dynamic_tool_count,
|
||||
thread_start.dynamic_tool_count = dynamic_tool_count,
|
||||
))
|
||||
.await
|
||||
.map_err(|err| match err {
|
||||
|
||||
@@ -142,45 +142,67 @@ mod thread_processor_behavior_tests {
|
||||
use codex_utils_absolute_path::test_support::PathBufExt;
|
||||
use codex_utils_absolute_path::test_support::test_path_buf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn dynamic_tool(
|
||||
namespace: Option<&str>,
|
||||
name: impl Into<String>,
|
||||
input_schema: Value,
|
||||
defer_loading: bool,
|
||||
) -> DynamicToolSpec {
|
||||
let function = DynamicToolFunctionSpec {
|
||||
name: name.into(),
|
||||
description: "test".to_string(),
|
||||
input_schema,
|
||||
defer_loading,
|
||||
};
|
||||
match namespace {
|
||||
Some(namespace) => {
|
||||
DynamicToolSpec::Namespace(codex_app_server_protocol::DynamicToolNamespaceSpec {
|
||||
name: namespace.to_string(),
|
||||
description: "test namespace".to_string(),
|
||||
tools: vec![DynamicToolNamespaceTool::Function(function)],
|
||||
})
|
||||
}
|
||||
None => DynamicToolSpec::Function(function),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_dynamic_tools_rejects_unsupported_input_schema() {
|
||||
let tools = vec![ApiDynamicToolSpec {
|
||||
namespace: None,
|
||||
name: "my_tool".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({"type": "null"}),
|
||||
defer_loading: false,
|
||||
}];
|
||||
let tools = vec![dynamic_tool(
|
||||
/*namespace*/ None,
|
||||
"my_tool",
|
||||
json!({"type": "null"}),
|
||||
/*defer_loading*/ false,
|
||||
)];
|
||||
let err = validate_dynamic_tools(&tools).expect_err("invalid schema");
|
||||
assert!(err.contains("my_tool"), "unexpected error: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_dynamic_tools_accepts_sanitizable_input_schema() {
|
||||
let tools = vec![ApiDynamicToolSpec {
|
||||
namespace: None,
|
||||
name: "my_tool".to_string(),
|
||||
description: "test".to_string(),
|
||||
let tools = vec![dynamic_tool(
|
||||
/*namespace*/ None,
|
||||
"my_tool",
|
||||
// Missing `type` is common; core sanitizes these to a supported schema.
|
||||
input_schema: json!({"properties": {}}),
|
||||
defer_loading: false,
|
||||
}];
|
||||
json!({"properties": {}}),
|
||||
/*defer_loading*/ false,
|
||||
)];
|
||||
validate_dynamic_tools(&tools).expect("valid schema");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_dynamic_tools_accepts_nullable_field_schema() {
|
||||
let tools = vec![ApiDynamicToolSpec {
|
||||
namespace: None,
|
||||
name: "my_tool".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({
|
||||
let tools = vec![dynamic_tool(
|
||||
/*namespace*/ None,
|
||||
"my_tool",
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": ["string", "null"]}
|
||||
@@ -188,45 +210,57 @@ mod thread_processor_behavior_tests {
|
||||
"required": ["query"],
|
||||
"additionalProperties": false
|
||||
}),
|
||||
defer_loading: false,
|
||||
}];
|
||||
/*defer_loading*/ false,
|
||||
)];
|
||||
validate_dynamic_tools(&tools).expect("valid schema");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_dynamic_tools_accepts_same_name_in_different_namespaces() {
|
||||
let tools = vec![
|
||||
ApiDynamicToolSpec {
|
||||
namespace: Some("codex_app".to_string()),
|
||||
name: "my_tool".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({
|
||||
dynamic_tool(
|
||||
Some("codex_app"),
|
||||
"my_tool",
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}),
|
||||
defer_loading: true,
|
||||
},
|
||||
ApiDynamicToolSpec {
|
||||
namespace: Some("other_app".to_string()),
|
||||
name: "my_tool".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({
|
||||
/*defer_loading*/ true,
|
||||
),
|
||||
dynamic_tool(
|
||||
Some("other_app"),
|
||||
"my_tool",
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}),
|
||||
defer_loading: true,
|
||||
},
|
||||
/*defer_loading*/ true,
|
||||
),
|
||||
];
|
||||
validate_dynamic_tools(&tools).expect("valid schema");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_dynamic_tools_accepts_responses_compatible_identifiers() {
|
||||
let tools = vec![ApiDynamicToolSpec {
|
||||
namespace: Some("Codex-App_2".to_string()),
|
||||
name: "lookup-ticket_2".to_string(),
|
||||
let tools = vec![dynamic_tool(
|
||||
Some("Codex-App_2"),
|
||||
"lookup-ticket_2",
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}),
|
||||
/*defer_loading*/ true,
|
||||
)];
|
||||
validate_dynamic_tools(&tools).expect("valid schema");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_dynamic_tools_rejects_duplicate_name_in_same_namespace() {
|
||||
let function = || DynamicToolFunctionSpec {
|
||||
name: "my_tool".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
@@ -234,36 +268,17 @@ mod thread_processor_behavior_tests {
|
||||
"additionalProperties": false
|
||||
}),
|
||||
defer_loading: true,
|
||||
}];
|
||||
validate_dynamic_tools(&tools).expect("valid schema");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_dynamic_tools_rejects_duplicate_name_in_same_namespace() {
|
||||
let tools = vec![
|
||||
ApiDynamicToolSpec {
|
||||
namespace: Some("codex_app".to_string()),
|
||||
name: "my_tool".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}),
|
||||
defer_loading: true,
|
||||
};
|
||||
let tools = vec![DynamicToolSpec::Namespace(
|
||||
codex_app_server_protocol::DynamicToolNamespaceSpec {
|
||||
name: "codex_app".to_string(),
|
||||
description: "test namespace".to_string(),
|
||||
tools: vec![
|
||||
DynamicToolNamespaceTool::Function(function()),
|
||||
DynamicToolNamespaceTool::Function(function()),
|
||||
],
|
||||
},
|
||||
ApiDynamicToolSpec {
|
||||
namespace: Some("codex_app".to_string()),
|
||||
name: "my_tool".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}),
|
||||
defer_loading: true,
|
||||
},
|
||||
];
|
||||
)];
|
||||
let err = validate_dynamic_tools(&tools).expect_err("duplicate name");
|
||||
assert!(err.contains("codex_app"), "unexpected error: {err}");
|
||||
assert!(err.contains("my_tool"), "unexpected error: {err}");
|
||||
@@ -311,53 +326,48 @@ mod thread_processor_behavior_tests {
|
||||
|
||||
#[test]
|
||||
fn validate_dynamic_tools_rejects_empty_namespace() {
|
||||
let tools = vec![ApiDynamicToolSpec {
|
||||
namespace: Some("".to_string()),
|
||||
name: "my_tool".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({
|
||||
let tools = vec![dynamic_tool(
|
||||
Some(""),
|
||||
"my_tool",
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}),
|
||||
defer_loading: false,
|
||||
}];
|
||||
/*defer_loading*/ false,
|
||||
)];
|
||||
let err = validate_dynamic_tools(&tools).expect_err("empty namespace");
|
||||
assert!(err.contains("my_tool"), "unexpected error: {err}");
|
||||
assert!(err.contains("namespace"), "unexpected error: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_dynamic_tools_rejects_reserved_namespace() {
|
||||
let tools = vec![ApiDynamicToolSpec {
|
||||
namespace: Some("mcp__server__".to_string()),
|
||||
name: "my_tool".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({
|
||||
let tools = vec![dynamic_tool(
|
||||
Some("mcp__server__"),
|
||||
"my_tool",
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}),
|
||||
defer_loading: false,
|
||||
}];
|
||||
/*defer_loading*/ false,
|
||||
)];
|
||||
let err = validate_dynamic_tools(&tools).expect_err("reserved namespace");
|
||||
assert!(err.contains("my_tool"), "unexpected error: {err}");
|
||||
assert!(err.contains("reserved"), "unexpected error: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_dynamic_tools_rejects_name_not_supported_by_responses() {
|
||||
let tools = vec![ApiDynamicToolSpec {
|
||||
namespace: None,
|
||||
name: "lookup.ticket".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({
|
||||
let tools = vec![dynamic_tool(
|
||||
/*namespace*/ None,
|
||||
"lookup.ticket",
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}),
|
||||
defer_loading: false,
|
||||
}];
|
||||
/*defer_loading*/ false,
|
||||
)];
|
||||
let err = validate_dynamic_tools(&tools).expect_err("invalid name");
|
||||
assert!(err.contains("lookup.ticket"), "unexpected error: {err}");
|
||||
assert!(
|
||||
@@ -368,17 +378,16 @@ mod thread_processor_behavior_tests {
|
||||
|
||||
#[test]
|
||||
fn validate_dynamic_tools_rejects_namespace_not_supported_by_responses() {
|
||||
let tools = vec![ApiDynamicToolSpec {
|
||||
namespace: Some("codex.app".to_string()),
|
||||
name: "lookup_ticket".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({
|
||||
let tools = vec![dynamic_tool(
|
||||
Some("codex.app"),
|
||||
"lookup_ticket",
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}),
|
||||
defer_loading: true,
|
||||
}];
|
||||
/*defer_loading*/ true,
|
||||
)];
|
||||
let err = validate_dynamic_tools(&tools).expect_err("invalid namespace");
|
||||
assert!(err.contains("codex.app"), "unexpected error: {err}");
|
||||
assert!(
|
||||
@@ -390,54 +399,59 @@ mod thread_processor_behavior_tests {
|
||||
#[test]
|
||||
fn validate_dynamic_tools_rejects_name_longer_than_responses_limit() {
|
||||
let long_name = "a".repeat(129);
|
||||
let tools = vec![ApiDynamicToolSpec {
|
||||
namespace: None,
|
||||
name: long_name.clone(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({
|
||||
let tools = vec![dynamic_tool(
|
||||
/*namespace*/ None,
|
||||
long_name.clone(),
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}),
|
||||
defer_loading: false,
|
||||
}];
|
||||
/*defer_loading*/ false,
|
||||
)];
|
||||
let err = validate_dynamic_tools(&tools).expect_err("name too long");
|
||||
assert!(err.contains("at most 128"), "unexpected error: {err}");
|
||||
assert!(err.contains(&long_name), "unexpected error: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_dynamic_tools_rejects_namespace_longer_than_responses_limit() {
|
||||
fn validate_dynamic_tools_rejects_namespace_fields_over_limits() {
|
||||
let long_namespace = "a".repeat(65);
|
||||
let tools = vec![ApiDynamicToolSpec {
|
||||
namespace: Some(long_namespace.clone()),
|
||||
name: "lookup_ticket".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({
|
||||
let mut tools = vec![dynamic_tool(
|
||||
Some(&long_namespace),
|
||||
"lookup_ticket",
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}),
|
||||
defer_loading: true,
|
||||
}];
|
||||
/*defer_loading*/ true,
|
||||
)];
|
||||
let err = validate_dynamic_tools(&tools).expect_err("namespace too long");
|
||||
assert!(err.contains("at most 64"), "unexpected error: {err}");
|
||||
assert!(err.contains(&long_namespace), "unexpected error: {err}");
|
||||
|
||||
let DynamicToolSpec::Namespace(namespace) = &mut tools[0] else {
|
||||
unreachable!("expected namespace")
|
||||
};
|
||||
namespace.name = "tickets".to_string();
|
||||
namespace.description = "a".repeat(1025);
|
||||
let err = validate_dynamic_tools(&tools).expect_err("namespace description too long");
|
||||
assert!(err.contains("at most 1024"), "unexpected error: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_dynamic_tools_rejects_reserved_responses_namespace() {
|
||||
let tools = vec![ApiDynamicToolSpec {
|
||||
namespace: Some("functions".to_string()),
|
||||
name: "lookup_ticket".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({
|
||||
let tools = vec![dynamic_tool(
|
||||
Some("functions"),
|
||||
"lookup_ticket",
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}),
|
||||
defer_loading: true,
|
||||
}];
|
||||
/*defer_loading*/ true,
|
||||
)];
|
||||
let err = validate_dynamic_tools(&tools).expect_err("reserved Responses namespace");
|
||||
assert!(err.contains("functions"), "unexpected error: {err}");
|
||||
assert!(err.contains("Responses API"), "unexpected error: {err}");
|
||||
|
||||
@@ -8,6 +8,9 @@ use codex_app_server_protocol::DynamicToolCallOutputContentItem;
|
||||
use codex_app_server_protocol::DynamicToolCallParams;
|
||||
use codex_app_server_protocol::DynamicToolCallResponse;
|
||||
use codex_app_server_protocol::DynamicToolCallStatus;
|
||||
use codex_app_server_protocol::DynamicToolFunctionSpec;
|
||||
use codex_app_server_protocol::DynamicToolNamespaceSpec;
|
||||
use codex_app_server_protocol::DynamicToolNamespaceTool;
|
||||
use codex_app_server_protocol::DynamicToolSpec;
|
||||
use codex_app_server_protocol::ItemCompletedNotification;
|
||||
use codex_app_server_protocol::ItemStartedNotification;
|
||||
@@ -157,80 +160,6 @@ async fn thread_start_normalizes_legacy_dynamic_tools_into_model_request() -> Re
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_start_keeps_hidden_dynamic_tools_out_of_model_requests() -> Result<()> {
|
||||
let responses = vec![create_final_assistant_message_sse_response("Done")?];
|
||||
let server = create_mock_responses_server_sequence_unchecked(responses).await;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri())?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let dynamic_tool = DynamicToolSpec {
|
||||
namespace: Some("codex_app".to_string()),
|
||||
name: "hidden_tool".to_string(),
|
||||
description: "Hidden dynamic tool".to_string(),
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"city": { "type": "string" }
|
||||
},
|
||||
"required": ["city"],
|
||||
"additionalProperties": false,
|
||||
}),
|
||||
defer_loading: true,
|
||||
};
|
||||
|
||||
let thread_req = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
dynamic_tools: Some(vec![dynamic_tool.clone()]),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let thread_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(thread_req)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_resp)?;
|
||||
|
||||
let turn_req = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id,
|
||||
client_user_message_id: None,
|
||||
input: vec![V2UserInput::Text {
|
||||
text: "Hello".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let turn_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(turn_req)),
|
||||
)
|
||||
.await??;
|
||||
let _turn: TurnStartResponse = to_response::<TurnStartResponse>(turn_resp)?;
|
||||
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
let bodies = responses_bodies(&server).await?;
|
||||
assert!(
|
||||
bodies
|
||||
.iter()
|
||||
.all(|body| find_tool(body, &dynamic_tool.name).is_none()),
|
||||
"hidden dynamic tool should not be sent to the model"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_start_rejects_hidden_dynamic_tools_without_namespace() -> Result<()> {
|
||||
let server = MockServer::start().await;
|
||||
@@ -241,8 +170,7 @@ async fn thread_start_rejects_hidden_dynamic_tools_without_namespace() -> Result
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let dynamic_tool = DynamicToolSpec {
|
||||
namespace: None,
|
||||
let dynamic_tool = DynamicToolSpec::Function(DynamicToolFunctionSpec {
|
||||
name: "hidden_tool".to_string(),
|
||||
description: "Hidden dynamic tool".to_string(),
|
||||
input_schema: json!({
|
||||
@@ -251,7 +179,7 @@ async fn thread_start_rejects_hidden_dynamic_tools_without_namespace() -> Result
|
||||
"additionalProperties": false,
|
||||
}),
|
||||
defer_loading: true,
|
||||
};
|
||||
});
|
||||
|
||||
let thread_req = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
@@ -272,7 +200,7 @@ async fn thread_start_rejects_hidden_dynamic_tools_without_namespace() -> Result
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_start_rejects_dynamic_tools_not_supported_by_responses() -> Result<()> {
|
||||
async fn thread_start_rejects_invalid_dynamic_tool_inputs() -> Result<()> {
|
||||
let server = MockServer::start().await;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
@@ -281,32 +209,109 @@ async fn thread_start_rejects_dynamic_tools_not_supported_by_responses() -> Resu
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let dynamic_tool = DynamicToolSpec {
|
||||
namespace: Some("codex.app".to_string()),
|
||||
name: "lookup.ticket".to_string(),
|
||||
description: "Invalid dynamic tool".to_string(),
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false,
|
||||
}),
|
||||
defer_loading: false,
|
||||
};
|
||||
|
||||
let thread_req = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
dynamic_tools: Some(vec![dynamic_tool]),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let error = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(thread_req)),
|
||||
)
|
||||
.await??;
|
||||
assert_eq!(error.error.code, -32600);
|
||||
assert!(error.error.message.contains("Responses API"));
|
||||
assert!(error.error.message.contains("lookup.ticket"));
|
||||
for (dynamic_tools, expected_error) in [
|
||||
(
|
||||
json!([
|
||||
{
|
||||
"type": "function",
|
||||
"name": "canonical_tool",
|
||||
"description": "Canonical tool",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"namespace": "legacy_app",
|
||||
"name": "legacy_tool",
|
||||
"description": "Legacy tool",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
]),
|
||||
"either canonical or legacy format",
|
||||
),
|
||||
(
|
||||
json!([{
|
||||
"type": "namespace",
|
||||
"name": "canonical_namespace",
|
||||
"description": "Canonical namespace",
|
||||
"tools": [{
|
||||
"type": "function",
|
||||
"name": "legacy_visibility_tool",
|
||||
"description": "Uses a legacy visibility field",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
},
|
||||
"exposeToContext": false
|
||||
}]
|
||||
}]),
|
||||
"either canonical or legacy format",
|
||||
),
|
||||
(
|
||||
json!([{
|
||||
"type": "namespace",
|
||||
"name": "empty_namespace",
|
||||
"description": "Contains no tools",
|
||||
"tools": []
|
||||
}]),
|
||||
"must contain at least one tool",
|
||||
),
|
||||
(
|
||||
json!([
|
||||
{
|
||||
"type": "namespace",
|
||||
"name": "duplicate_namespace",
|
||||
"description": "First namespace",
|
||||
"tools": [{
|
||||
"type": "function",
|
||||
"name": "first_tool",
|
||||
"description": "First tool",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
}]
|
||||
},
|
||||
{
|
||||
"type": "namespace",
|
||||
"name": "duplicate_namespace",
|
||||
"description": "Second namespace",
|
||||
"tools": [{
|
||||
"type": "function",
|
||||
"name": "second_tool",
|
||||
"description": "Second tool",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
}]
|
||||
}
|
||||
]),
|
||||
"duplicate dynamic tool namespace",
|
||||
),
|
||||
] {
|
||||
let thread_req = mcp
|
||||
.send_raw_request(
|
||||
"thread/start",
|
||||
Some(json!({ "dynamicTools": dynamic_tools })),
|
||||
)
|
||||
.await?;
|
||||
let error = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(thread_req)),
|
||||
)
|
||||
.await??;
|
||||
assert_eq!(error.error.code, -32600);
|
||||
assert!(
|
||||
error.error.message.contains(expected_error),
|
||||
"unexpected error: {}",
|
||||
error.error.message
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -346,20 +351,41 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let dynamic_tool = DynamicToolSpec {
|
||||
namespace: Some(tool_namespace.to_string()),
|
||||
name: tool_name.to_string(),
|
||||
description: "Demo dynamic tool".to_string(),
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"city": { "type": "string" }
|
||||
},
|
||||
"required": ["city"],
|
||||
"additionalProperties": false,
|
||||
}),
|
||||
defer_loading: false,
|
||||
};
|
||||
let input_schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"city": { "type": "string" }
|
||||
},
|
||||
"required": ["city"],
|
||||
"additionalProperties": false,
|
||||
});
|
||||
let status_schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ticket_id": { "type": "string" }
|
||||
},
|
||||
"required": ["ticket_id"],
|
||||
"additionalProperties": false,
|
||||
});
|
||||
let namespace_description = "Demo namespace tools";
|
||||
let dynamic_tool = DynamicToolSpec::Namespace(DynamicToolNamespaceSpec {
|
||||
name: tool_namespace.to_string(),
|
||||
description: namespace_description.to_string(),
|
||||
tools: vec![
|
||||
DynamicToolNamespaceTool::Function(DynamicToolFunctionSpec {
|
||||
name: tool_name.to_string(),
|
||||
description: "Demo dynamic tool".to_string(),
|
||||
input_schema: input_schema.clone(),
|
||||
defer_loading: false,
|
||||
}),
|
||||
DynamicToolNamespaceTool::Function(DynamicToolFunctionSpec {
|
||||
name: "lookup_status".to_string(),
|
||||
description: "Look up ticket status".to_string(),
|
||||
input_schema: status_schema.clone(),
|
||||
defer_loading: false,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
let thread_req = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
@@ -488,6 +514,32 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res
|
||||
.await??;
|
||||
|
||||
let bodies = responses_bodies(&server).await?;
|
||||
let namespace = find_tool(&bodies[0], tool_namespace)
|
||||
.context("expected explicit dynamic tool namespace in first request")?;
|
||||
assert_eq!(
|
||||
namespace,
|
||||
&json!({
|
||||
"type": "namespace",
|
||||
"name": tool_namespace,
|
||||
"description": namespace_description,
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"name": tool_name,
|
||||
"description": "Demo dynamic tool",
|
||||
"strict": false,
|
||||
"parameters": input_schema,
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "lookup_status",
|
||||
"description": "Look up ticket status",
|
||||
"strict": false,
|
||||
"parameters": status_schema,
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
let payload = bodies
|
||||
.iter()
|
||||
.find_map(|body| function_call_output_payload(body, call_id))
|
||||
@@ -522,8 +574,7 @@ async fn dynamic_tool_call_round_trip_sends_content_items_to_model() -> Result<(
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let dynamic_tool = DynamicToolSpec {
|
||||
namespace: None,
|
||||
let dynamic_tool = DynamicToolSpec::Function(DynamicToolFunctionSpec {
|
||||
name: tool_name.to_string(),
|
||||
description: "Demo dynamic tool".to_string(),
|
||||
input_schema: json!({
|
||||
@@ -535,7 +586,7 @@ async fn dynamic_tool_call_round_trip_sends_content_items_to_model() -> Result<(
|
||||
"additionalProperties": false,
|
||||
}),
|
||||
defer_loading: false,
|
||||
};
|
||||
});
|
||||
|
||||
let thread_req = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
|
||||
@@ -5,6 +5,7 @@ use app_test_support::to_response;
|
||||
use codex_app_server_protocol::DynamicToolCallOutputContentItem;
|
||||
use codex_app_server_protocol::DynamicToolCallParams;
|
||||
use codex_app_server_protocol::DynamicToolCallResponse;
|
||||
use codex_app_server_protocol::DynamicToolFunctionSpec;
|
||||
use codex_app_server_protocol::DynamicToolSpec;
|
||||
use codex_app_server_protocol::ItemStartedNotification;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
@@ -126,8 +127,7 @@ async fn thread_unsubscribe_during_turn_keeps_turn_running() -> Result<()> {
|
||||
let thread_req = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
model: Some("mock-model".to_string()),
|
||||
dynamic_tools: Some(vec![DynamicToolSpec {
|
||||
namespace: None,
|
||||
dynamic_tools: Some(vec![DynamicToolSpec::Function(DynamicToolFunctionSpec {
|
||||
name: tool_name.to_string(),
|
||||
description: "Deterministic wait tool".to_string(),
|
||||
input_schema: json!({
|
||||
@@ -136,7 +136,7 @@ async fn thread_unsubscribe_during_turn_keeps_turn_running() -> Result<()> {
|
||||
"additionalProperties": false,
|
||||
}),
|
||||
defer_loading: false,
|
||||
}]),
|
||||
})]),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -88,15 +88,20 @@ struct LegacyDynamicToolSpec {
|
||||
pub fn normalize_dynamic_tool_specs(
|
||||
values: Vec<JsonValue>,
|
||||
) -> Result<Vec<DynamicToolSpec>, serde_json::Error> {
|
||||
let has_legacy_format = values.iter().any(|value| {
|
||||
let has_legacy_fields = |value: &JsonValue| {
|
||||
value.get("namespace").is_some()
|
||||
|| value.get("exposeToContext").is_some()
|
||||
|| value.get("type").is_none()
|
||||
};
|
||||
let has_legacy_format = values.iter().any(|value| {
|
||||
has_legacy_fields(value)
|
||||
|| value
|
||||
.get("tools")
|
||||
.and_then(JsonValue::as_array)
|
||||
.is_some_and(|tools| tools.iter().any(&has_legacy_fields))
|
||||
});
|
||||
let has_canonical_namespace = values
|
||||
.iter()
|
||||
.any(|value| value.get("type").and_then(JsonValue::as_str) == Some("namespace"));
|
||||
if has_legacy_format && has_canonical_namespace {
|
||||
let has_canonical_format = values.iter().any(|value| value.get("type").is_some());
|
||||
if has_legacy_format && has_canonical_format {
|
||||
return Err(serde_json::Error::custom(
|
||||
"dynamic tools must use either canonical or legacy format consistently",
|
||||
));
|
||||
|
||||
Reference in New Issue
Block a user