diff --git a/crates/llm-runtime/tests/anthropic_messages_live.rs b/crates/llm-runtime/tests/anthropic_messages_live.rs index ebd84d30..ef73c023 100644 --- a/crates/llm-runtime/tests/anthropic_messages_live.rs +++ b/crates/llm-runtime/tests/anthropic_messages_live.rs @@ -1752,6 +1752,61 @@ async fn anthropic_messages_live_adapter_sees_oversized_image() { assert!(answer.contains("blue"), "expected blue, got {answer:?}"); } +// Advertise the real built-in definitions so provider schema validation runs +// through the same materialization and rejection mapping as a hosted session. +// No tools are executed; each request isolates one potentially invalid schema. +async fn assert_live_builtin_input_schema_accepted(tool_id: &str) { + use harness::CoreAgentLlm as _; + + let blobs = Arc::new(InMemoryBlobStore::new()); + let question = user_entry( + 1, + text_blob(&blobs, "Reply with exactly OK. Do not call any tools.").await, + ); + let mut request = intent_request("live-anthropic-builtin-input-schema", vec![question]); + request.tools = vec![tools::definitions::register( + tool_id, + Default::default(), + harness::ToolParallelism::ParallelSafe, + Default::default(), + )]; + let model = request.model.model.clone(); + let adapter = Arc::new(AnthropicMessagesLlmAdapter::new( + retrying_anthropic_messages_client(live_client()), + blobs, + )); + let runtime = llm_runtime::LlmRuntime::new( + llm_runtime::LlmAdapterRegistry::new() + .with_generation_adapter(ProviderApiKind::AnthropicMessages, adapter), + ); + + let result = runtime + .generate(generation_request(1, request)) + .await + .unwrap_or_else(|error| { + panic!("{model} must accept the {tool_id} input schema: {error:?}") + }); + assert_eq!(result.status, LlmGenerationStatus::Succeeded, "{tool_id}"); +} + +#[tokio::test(flavor = "current_thread")] +#[ignore = "requires ANTHROPIC_API_KEY (costs real money)"] +async fn anthropic_messages_live_accepts_builtin_input_schema_blob_put() { + assert_live_builtin_input_schema_accepted("blob.put").await; +} + +#[tokio::test(flavor = "current_thread")] +#[ignore = "requires ANTHROPIC_API_KEY (costs real money)"] +async fn anthropic_messages_live_accepts_builtin_input_schema_env_write_file() { + assert_live_builtin_input_schema_accepted("env.write_file").await; +} + +#[tokio::test(flavor = "current_thread")] +#[ignore = "requires ANTHROPIC_API_KEY (costs real money)"] +async fn anthropic_messages_live_accepts_builtin_input_schema_vfs_write_file() { + assert_live_builtin_input_schema_accepted("vfs.write_file").await; +} + /// A request the provider refuses crosses the runtime boundary as /// `Rejected`, carrying the provider's own message rather than a wrapped /// runtime error, so the run fails as `request_rejected`. diff --git a/crates/llm-runtime/tests/builtin_catalog_parity.rs b/crates/llm-runtime/tests/builtin_catalog_parity.rs index 9e9b1c01..fb4622fe 100644 --- a/crates/llm-runtime/tests/builtin_catalog_parity.rs +++ b/crates/llm-runtime/tests/builtin_catalog_parity.rs @@ -75,7 +75,7 @@ fn registered(case: &str, api: &ProviderApiKind) -> Vec { registered.tools.into_values().collect() } -async fn fixture(api: ProviderApiKind, case: &str) -> Value { +async fn fixture(api: ProviderApiKind, case: &str, code_mode: bool) -> Value { let blobs = InMemoryBlobStore::new(); let model = ModelSelection { provider_id: if api == ProviderApiKind::AnthropicMessages { @@ -92,9 +92,22 @@ async fn fixture(api: ProviderApiKind, case: &str) -> Value { .into(), api_kind: api.clone(), }; - let tools = registered(case, &api); + let mut tools = registered(case, &api); + let code_mode = code_mode.then(|| { + let presentation = harness::CodeModePresentation { + allowed_tools: tools.iter().map(|tool| tool.name.clone()).collect(), + ..Default::default() + }; + tools.push(tools::definitions::register( + "code.execute", + Default::default(), + harness::ToolParallelism::ParallelSafe, + Default::default(), + )); + presentation + }); let request = LlmRequest { - code_mode: None, + code_mode, model, request_fingerprint: "sha256:catalog-parity".into(), context: ContextSnapshot { @@ -143,11 +156,53 @@ async fn builtin_requests_match_captured_provider_contracts() { for entry in baseline { let api: ProviderApiKind = serde_json::from_value(entry["api"].clone()).expect("API kind"); let case = entry["case"].as_str().expect("case"); - let actual = fixture(api.clone(), case).await; + let actual = fixture(api.clone(), case, false).await; assert_eq!(actual, entry["request"], "{api:?}: {case}"); } } +#[tokio::test(flavor = "current_thread")] +async fn anthropic_builtin_inputs_avoid_top_level_combinators_with_and_without_code_mode() { + for case in [ + "workspace", + "environment", + "canonical", + "one_shot", + "web", + "workflow", + ] { + for code_mode in [false, true] { + let wire = fixture(ProviderApiKind::AnthropicMessages, case, code_mode).await; + let tools = wire["tools"].as_array().expect("advertised tools"); + assert!(tools.iter().any(|tool| tool["name"] == "blob_put")); + for tool in tools { + let Some(schema) = tool.get("input_schema") else { + continue; // Provider-native tools have no custom input schema. + }; + assert_eq!(schema["type"], "object", "{case}: {}", tool["name"]); + for keyword in ["oneOf", "anyOf", "allOf"] { + assert!( + schema.get(keyword).is_none(), + "{case}, code_mode={code_mode}: {} has top-level {keyword}", + tool["name"] + ); + } + if matches!( + tool["name"].as_str(), + Some("blob_put" | "Write" | "VfsWrite" | "write_file" | "vfs_write_file") + ) { + assert!( + tool["description"] + .as_str() + .unwrap() + .contains("exactly one") + ); + } + } + } + } +} + /// Run explicitly after an intentional change to provider-visible tool definitions. #[tokio::test(flavor = "current_thread")] #[ignore = "regenerates the committed provider request fixture"] @@ -156,7 +211,7 @@ async fn regenerate_builtin_provider_contracts() { serde_json::from_str(include_str!("fixtures/builtin_catalogs.json")).expect("baseline"); for entry in &mut baseline { let api = serde_json::from_value(entry["api"].clone()).expect("API kind"); - entry["request"] = fixture(api, entry["case"].as_str().expect("case")).await; + entry["request"] = fixture(api, entry["case"].as_str().expect("case"), false).await; } std::fs::write( concat!( diff --git a/crates/llm-runtime/tests/fixtures/builtin_catalogs.json b/crates/llm-runtime/tests/fixtures/builtin_catalogs.json index 8cb2948d..fdb51e73 100644 --- a/crates/llm-runtime/tests/fixtures/builtin_catalogs.json +++ b/crates/llm-runtime/tests/fixtures/builtin_catalogs.json @@ -92,23 +92,6 @@ "name": "blob_put", "parameters": { "additionalProperties": false, - "oneOf": [ - { - "required": [ - "text" - ] - }, - { - "required": [ - "json" - ] - }, - { - "required": [ - "bytes" - ] - } - ], "properties": { "bytes": { "items": { @@ -475,28 +458,6 @@ "name": "vfs_write_file", "parameters": { "additionalProperties": false, - "oneOf": [ - { - "not": { - "required": [ - "content_ref" - ] - }, - "required": [ - "content" - ] - }, - { - "not": { - "required": [ - "content" - ] - }, - "required": [ - "content_ref" - ] - } - ], "properties": { "content": { "description": "Full UTF-8 file content; mutually exclusive with content_ref.", @@ -676,23 +637,6 @@ "name": "blob_put", "parameters": { "additionalProperties": false, - "oneOf": [ - { - "required": [ - "text" - ] - }, - { - "required": [ - "json" - ] - }, - { - "required": [ - "bytes" - ] - } - ], "properties": { "bytes": { "items": { @@ -1157,28 +1101,6 @@ "name": "write_file", "parameters": { "additionalProperties": false, - "oneOf": [ - { - "not": { - "required": [ - "content_ref" - ] - }, - "required": [ - "content" - ] - }, - { - "not": { - "required": [ - "content" - ] - }, - "required": [ - "content_ref" - ] - } - ], "properties": { "content": { "description": "Full UTF-8 file content; mutually exclusive with content_ref.", @@ -1408,23 +1330,6 @@ "name": "blob_put", "parameters": { "additionalProperties": false, - "oneOf": [ - { - "required": [ - "text" - ] - }, - { - "required": [ - "json" - ] - }, - { - "required": [ - "bytes" - ] - } - ], "properties": { "bytes": { "items": { @@ -1882,28 +1787,6 @@ "name": "write_file", "parameters": { "additionalProperties": false, - "oneOf": [ - { - "not": { - "required": [ - "content_ref" - ] - }, - "required": [ - "content" - ] - }, - { - "not": { - "required": [ - "content" - ] - }, - "required": [ - "content_ref" - ] - } - ], "properties": { "content": { "description": "Full UTF-8 file content; mutually exclusive with content_ref.", @@ -2083,23 +1966,6 @@ "name": "blob_put", "parameters": { "additionalProperties": false, - "oneOf": [ - { - "required": [ - "text" - ] - }, - { - "required": [ - "json" - ] - }, - { - "required": [ - "bytes" - ] - } - ], "properties": { "bytes": { "items": { @@ -2652,28 +2518,6 @@ "name": "write_file", "parameters": { "additionalProperties": false, - "oneOf": [ - { - "not": { - "required": [ - "content_ref" - ] - }, - "required": [ - "content" - ] - }, - { - "not": { - "required": [ - "content" - ] - }, - "required": [ - "content_ref" - ] - } - ], "properties": { "content": { "description": "Full UTF-8 file content; mutually exclusive with content_ref.", @@ -2834,23 +2678,6 @@ "name": "blob_put", "parameters": { "additionalProperties": false, - "oneOf": [ - { - "required": [ - "text" - ] - }, - { - "required": [ - "json" - ] - }, - { - "required": [ - "bytes" - ] - } - ], "properties": { "bytes": { "items": { @@ -3194,23 +3021,6 @@ "name": "blob_put", "parameters": { "additionalProperties": false, - "oneOf": [ - { - "required": [ - "text" - ] - }, - { - "required": [ - "json" - ] - }, - { - "required": [ - "bytes" - ] - } - ], "properties": { "bytes": { "items": { @@ -3848,28 +3658,6 @@ "description": "Writes a complete file using exactly one of content (UTF-8 text) or content_ref (a full blob reference, recorded handle, or descriptor). Reference writes preserve exact binary bytes. Paths are resolved within the configured filesystem scope. Accesses only session-attached VFS workspaces and snapshots; these files are not visible to environment commands.", "input_schema": { "additionalProperties": false, - "oneOf": [ - { - "not": { - "required": [ - "content_ref" - ] - }, - "required": [ - "content" - ] - }, - { - "not": { - "required": [ - "content" - ] - }, - "required": [ - "content_ref" - ] - } - ], "properties": { "content": { "description": "Full UTF-8 file content; mutually exclusive with content_ref.", @@ -4010,23 +3798,6 @@ "description": "Store immutable text, JSON, or bytes (integer array 0..255). The stored content is limited to 1048576 bytes: UTF-8 text bytes, serialized JSON bytes, or raw binary bytes. Request and code-mode argument budgets also apply to the transport representation, including array overhead. Supply exactly one of text, json, or bytes; JSON null is a valid value. Returns a full sha256 content descriptor for later reading or passing to file writers. Deduplicates identical bytes. Does not automatically attach a file or show media; use blob_info(presentation=file) or blob_read(format=media). Optional name/media_type are descriptive metadata.", "input_schema": { "additionalProperties": false, - "oneOf": [ - { - "required": [ - "text" - ] - }, - { - "required": [ - "json" - ] - }, - { - "required": [ - "bytes" - ] - } - ], "properties": { "bytes": { "items": { @@ -4553,28 +4324,6 @@ "description": "Writes a complete file using exactly one of content (UTF-8 text) or content_ref (a full blob reference, recorded handle, or descriptor). Reference writes preserve exact binary bytes. Paths are resolved within the configured filesystem scope. Accesses only the active environment filesystem; it does not read or modify attached VFS files.", "input_schema": { "additionalProperties": false, - "oneOf": [ - { - "not": { - "required": [ - "content_ref" - ] - }, - "required": [ - "content" - ] - }, - { - "not": { - "required": [ - "content" - ] - }, - "required": [ - "content_ref" - ] - } - ], "properties": { "content": { "description": "Full UTF-8 file content; mutually exclusive with content_ref.", @@ -4715,23 +4464,6 @@ "description": "Store immutable text, JSON, or bytes (integer array 0..255). The stored content is limited to 1048576 bytes: UTF-8 text bytes, serialized JSON bytes, or raw binary bytes. Request and code-mode argument budgets also apply to the transport representation, including array overhead. Supply exactly one of text, json, or bytes; JSON null is a valid value. Returns a full sha256 content descriptor for later reading or passing to file writers. Deduplicates identical bytes. Does not automatically attach a file or show media; use blob_info(presentation=file) or blob_read(format=media). Optional name/media_type are descriptive metadata.", "input_schema": { "additionalProperties": false, - "oneOf": [ - { - "required": [ - "text" - ] - }, - { - "required": [ - "json" - ] - }, - { - "required": [ - "bytes" - ] - } - ], "properties": { "bytes": { "items": { @@ -5243,28 +4975,6 @@ "description": "Writes a complete file using exactly one of content (UTF-8 text) or content_ref (a full blob reference, recorded handle, or descriptor). Reference writes preserve exact binary bytes. Paths are resolved within the configured filesystem scope. Accesses only the active environment filesystem; it does not read or modify attached VFS files.", "input_schema": { "additionalProperties": false, - "oneOf": [ - { - "not": { - "required": [ - "content_ref" - ] - }, - "required": [ - "content" - ] - }, - { - "not": { - "required": [ - "content" - ] - }, - "required": [ - "content_ref" - ] - } - ], "properties": { "content": { "description": "Full UTF-8 file content; mutually exclusive with content_ref.", @@ -5405,23 +5115,6 @@ "description": "Store immutable text, JSON, or bytes (integer array 0..255). The stored content is limited to 1048576 bytes: UTF-8 text bytes, serialized JSON bytes, or raw binary bytes. Request and code-mode argument budgets also apply to the transport representation, including array overhead. Supply exactly one of text, json, or bytes; JSON null is a valid value. Returns a full sha256 content descriptor for later reading or passing to file writers. Deduplicates identical bytes. Does not automatically attach a file or show media; use blob_info(presentation=file) or blob_read(format=media). Optional name/media_type are descriptive metadata.", "input_schema": { "additionalProperties": false, - "oneOf": [ - { - "required": [ - "text" - ] - }, - { - "required": [ - "json" - ] - }, - { - "required": [ - "bytes" - ] - } - ], "properties": { "bytes": { "items": { @@ -5712,23 +5405,6 @@ "description": "Store immutable text, JSON, or bytes (integer array 0..255). The stored content is limited to 1048576 bytes: UTF-8 text bytes, serialized JSON bytes, or raw binary bytes. Request and code-mode argument budgets also apply to the transport representation, including array overhead. Supply exactly one of text, json, or bytes; JSON null is a valid value. Returns a full sha256 content descriptor for later reading or passing to file writers. Deduplicates identical bytes. Does not automatically attach a file or show media; use blob_info(presentation=file) or blob_read(format=media). Optional name/media_type are descriptive metadata.", "input_schema": { "additionalProperties": false, - "oneOf": [ - { - "required": [ - "text" - ] - }, - { - "required": [ - "json" - ] - }, - { - "required": [ - "bytes" - ] - } - ], "properties": { "bytes": { "items": { @@ -6256,28 +5932,6 @@ "description": "Write a complete file, creating parent directories when needed. Provide exactly one of content (UTF-8 text) or content_ref (a full blob reference, recorded handle, or content descriptor). References preserve exact binary bytes. Paths are resolved within the configured filesystem scope. Accesses only the active environment filesystem; it does not read or modify attached VFS files.", "input_schema": { "additionalProperties": false, - "oneOf": [ - { - "not": { - "required": [ - "content_ref" - ] - }, - "required": [ - "content" - ] - }, - { - "not": { - "required": [ - "content" - ] - }, - "required": [ - "content_ref" - ] - } - ], "properties": { "content": { "description": "Full UTF-8 file content; mutually exclusive with content_ref.", @@ -6433,23 +6087,6 @@ "description": "Store immutable text, JSON, or bytes (integer array 0..255). The stored content is limited to 1048576 bytes: UTF-8 text bytes, serialized JSON bytes, or raw binary bytes. Request and code-mode argument budgets also apply to the transport representation, including array overhead. Supply exactly one of text, json, or bytes; JSON null is a valid value. Returns a full sha256 content descriptor for later reading or passing to file writers. Deduplicates identical bytes. Does not automatically attach a file or show media; use blob_info(presentation=file) or blob_read(format=media). Optional name/media_type are descriptive metadata.", "input_schema": { "additionalProperties": false, - "oneOf": [ - { - "required": [ - "text" - ] - }, - { - "required": [ - "json" - ] - }, - { - "required": [ - "bytes" - ] - } - ], "properties": { "bytes": { "items": { @@ -6762,23 +6399,6 @@ "description": "Store immutable text, JSON, or bytes (integer array 0..255). The stored content is limited to 1048576 bytes: UTF-8 text bytes, serialized JSON bytes, or raw binary bytes. Request and code-mode argument budgets also apply to the transport representation, including array overhead. Supply exactly one of text, json, or bytes; JSON null is a valid value. Returns a full sha256 content descriptor for later reading or passing to file writers. Deduplicates identical bytes. Does not automatically attach a file or show media; use blob_info(presentation=file) or blob_read(format=media). Optional name/media_type are descriptive metadata.", "input_schema": { "additionalProperties": false, - "oneOf": [ - { - "required": [ - "text" - ] - }, - { - "required": [ - "json" - ] - }, - { - "required": [ - "bytes" - ] - } - ], "properties": { "bytes": { "items": { @@ -7216,23 +6836,6 @@ "name": "blob_put", "parameters": { "additionalProperties": false, - "oneOf": [ - { - "required": [ - "text" - ] - }, - { - "required": [ - "json" - ] - }, - { - "required": [ - "bytes" - ] - } - ], "properties": { "bytes": { "items": { @@ -7596,28 +7199,6 @@ "name": "vfs_write_file", "parameters": { "additionalProperties": false, - "oneOf": [ - { - "not": { - "required": [ - "content_ref" - ] - }, - "required": [ - "content" - ] - }, - { - "not": { - "required": [ - "content" - ] - }, - "required": [ - "content_ref" - ] - } - ], "properties": { "content": { "description": "Full UTF-8 file content; mutually exclusive with content_ref.", @@ -7780,23 +7361,6 @@ "name": "blob_put", "parameters": { "additionalProperties": false, - "oneOf": [ - { - "required": [ - "text" - ] - }, - { - "required": [ - "json" - ] - }, - { - "required": [ - "bytes" - ] - } - ], "properties": { "bytes": { "items": { @@ -8287,28 +7851,6 @@ "name": "write_file", "parameters": { "additionalProperties": false, - "oneOf": [ - { - "not": { - "required": [ - "content_ref" - ] - }, - "required": [ - "content" - ] - }, - { - "not": { - "required": [ - "content" - ] - }, - "required": [ - "content_ref" - ] - } - ], "properties": { "content": { "description": "Full UTF-8 file content; mutually exclusive with content_ref.", @@ -8523,23 +8065,6 @@ "name": "blob_put", "parameters": { "additionalProperties": false, - "oneOf": [ - { - "required": [ - "text" - ] - }, - { - "required": [ - "json" - ] - }, - { - "required": [ - "bytes" - ] - } - ], "properties": { "bytes": { "items": { @@ -9023,28 +8548,6 @@ "name": "write_file", "parameters": { "additionalProperties": false, - "oneOf": [ - { - "not": { - "required": [ - "content_ref" - ] - }, - "required": [ - "content" - ] - }, - { - "not": { - "required": [ - "content" - ] - }, - "required": [ - "content_ref" - ] - } - ], "properties": { "content": { "description": "Full UTF-8 file content; mutually exclusive with content_ref.", @@ -9228,23 +8731,6 @@ "name": "blob_put", "parameters": { "additionalProperties": false, - "oneOf": [ - { - "required": [ - "text" - ] - }, - { - "required": [ - "json" - ] - }, - { - "required": [ - "bytes" - ] - } - ], "properties": { "bytes": { "items": { @@ -9825,28 +9311,6 @@ "name": "write_file", "parameters": { "additionalProperties": false, - "oneOf": [ - { - "not": { - "required": [ - "content_ref" - ] - }, - "required": [ - "content" - ] - }, - { - "not": { - "required": [ - "content" - ] - }, - "required": [ - "content_ref" - ] - } - ], "properties": { "content": { "description": "Full UTF-8 file content; mutually exclusive with content_ref.", @@ -10009,23 +9473,6 @@ "name": "blob_put", "parameters": { "additionalProperties": false, - "oneOf": [ - { - "required": [ - "text" - ] - }, - { - "required": [ - "json" - ] - }, - { - "required": [ - "bytes" - ] - } - ], "properties": { "bytes": { "items": { @@ -10372,23 +9819,6 @@ "name": "blob_put", "parameters": { "additionalProperties": false, - "oneOf": [ - { - "required": [ - "text" - ] - }, - { - "required": [ - "json" - ] - }, - { - "required": [ - "bytes" - ] - } - ], "properties": { "bytes": { "items": { diff --git a/crates/tools/src/blobs.rs b/crates/tools/src/blobs.rs index 5ba76047..99d81e3d 100644 --- a/crates/tools/src/blobs.rs +++ b/crates/tools/src/blobs.rs @@ -90,7 +90,6 @@ impl BlobTool { "name":{"type":"string","minLength":1,"maxLength":1024}, "media_type":{"type":"string","minLength":1,"maxLength":256} }, - "oneOf":[{"required":["text"]},{"required":["json"]},{"required":["bytes"]}], "additionalProperties":false }), crate::definitions::output_schema_for::(), @@ -108,6 +107,8 @@ impl BlobTool { Self::Info => invoke_info(resolver, decode_args(arguments)?).await, Self::Read => invoke_read(resolver, decode_args(arguments)?).await, Self::Put => { + // The provider-facing schema omits top-level combinators for + // Anthropic compatibility; enforce input exclusivity here. let fields = arguments .as_object() .ok_or_else(|| invalid("blob_put requires an object"))?; @@ -616,6 +617,8 @@ mod tests { json!({}), json!({"text":"a","bytes":[1]}), json!({"text":"a","json":null}), + json!({"bytes":[1],"json":null}), + json!({"text":"a","bytes":[1],"json":null}), json!({"text":"a","bytes":null}), json!({"bytes":[256]}), json!({"bytes":[-1]}), @@ -1048,12 +1051,8 @@ mod tests { ] { assert!(validator.is_valid(&input)); } - for input in [ - json!({}), - json!({"text":"a","json":null}), - json!({"bytes":[256]}), - json!({"bytes":null}), - ] { + // Exclusivity is checked at invocation; the schema still checks types. + for input in [json!({"bytes":[256]}), json!({"bytes":null})] { assert!(!validator.is_valid(&input)); } let schema = BlobTool::Read.definition().unwrap().input_schema; diff --git a/crates/tools/src/builtin/canonical.rs b/crates/tools/src/builtin/canonical.rs index 18273461..c3087217 100644 --- a/crates/tools/src/builtin/canonical.rs +++ b/crates/tools/src/builtin/canonical.rs @@ -470,7 +470,9 @@ pub(super) async fn invoke_json( } pub(super) fn write_file_schema(path_field: &'static str) -> Value { - let mut schema = object( + // Anthropic rejects top-level schema combinators. The description guides + // input selection; WriteFileArgs::validate enforces exactly one source. + object( [ (path_field, string("File path to write.")), ( @@ -480,12 +482,7 @@ pub(super) fn write_file_schema(path_field: &'static str) -> Value { ("content_ref", crate::blobs::reference_schema()), ], [path_field], - ); - schema["oneOf"] = json!([ - {"required":["content"],"not":{"required":["content_ref"]}}, - {"required":["content_ref"],"not":{"required":["content"]}} - ]); - schema + ) } fn job_submit_schema() -> Value { diff --git a/crates/tools/src/fs/vfs.rs b/crates/tools/src/fs/vfs.rs index f819236c..5ff3b77c 100644 --- a/crates/tools/src/fs/vfs.rs +++ b/crates/tools/src/fs/vfs.rs @@ -1459,7 +1459,8 @@ mod tests { serde_json::json!({path_key:"/invalid", "content":null, "content_ref":reference}), serde_json::json!({path_key:"/invalid", "content":"text", "content_ref":null}), ] { - assert!(!validator.is_valid(&arguments)); + // Invocation enforces exclusivity even though the advertised + // schema omits provider-incompatible top-level combinators. assert!(matches!( tool.invoke_json( BuiltinToolContext::Vfs {