diff --git a/packages/runtime/src/__tests__/tool-schema-builder.test.ts b/packages/runtime/src/__tests__/tool-schema-builder.test.ts new file mode 100644 index 0000000000..a2928a43a9 --- /dev/null +++ b/packages/runtime/src/__tests__/tool-schema-builder.test.ts @@ -0,0 +1,203 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { z } from 'zod'; + +import { + buildNestableToolSnapshot, + buildToolSchemaPlan, + validateCodeModeToolInput, +} from '../tool-schema-builder.js'; +import { toolSchemaCharsForDiagnostics } from '../request-shape.js'; +import type { MakaTool } from '../tool-runtime.js'; +import type { ToolAvailabilityPlan } from '../tool-availability.js'; + +function tool(name: string, overrides: Partial = {}): MakaTool { + return { + name, + description: `${name} description`, + parameters: z.object({}), + impl: () => ({ ok: true }), + ...overrides, + }; +} + +const invalidTool = tool('invalid'); +const execTool = tool('exec', { parameters: z.object({ code: z.string() }) }); + +function availability(): ToolAvailabilityPlan { + const providerTools = [ + tool('lookup', { parameters: z.object({ id: z.string() }) }), + tool('native_search', { providerTool: { kind: 'openai-web-search' } }), + invalidTool, + ]; + return { + providerTools, + activeTools: ['lookup', 'native_search'], + projectActiveTools: () => ({ activeTools: ['native_search'] }), + currentRepairToolNames: () => ['lookup'], + diagnostics: () => undefined, + }; +} + +test('builds the Code Mode provider surface and schema bindings independently', () => { + const base = availability(); + const baseVisibleChars = toolSchemaCharsForDiagnostics(base.providerTools, base.activeTools); + base.diagnostics = (activeTools, visibleToolSchemaChars) => ({ + mode: 'search', + enabledSourceIds: [], + visibleToolCount: activeTools.length, + fullToolCount: 3, + hiddenToolCount: 1, + visibleToolSchemaChars, + fullToolSchemaChars: baseVisibleChars + 100, + toolSchemaCharReduction: 100, + }); + const result = buildToolSchemaPlan({ + boundTools: base.providerTools.filter((candidate) => candidate !== invalidTool), + availability: base, + requestedToolMode: 'code_mode', + codeModeExecTool: execTool, + }); + + assert.equal(result.toolMode, 'code_mode'); + assert.deepEqual( + result.providerTools.map((candidate) => candidate.name), + ['exec', 'lookup', 'native_search', 'invalid'], + ); + assert.deepEqual(result.availability.activeTools, ['exec', 'lookup', 'native_search']); + assert.deepEqual(result.availability.projectActiveTools?.().activeTools, [ + 'exec', + 'native_search', + ]); + assert.deepEqual(result.availability.currentRepairToolNames(), ['exec', 'lookup']); + assert.deepEqual(result.modelTools.native_search, { + kind: 'provider', + providerTool: { kind: 'openai-web-search' }, + }); + assert.equal(result.modelTools.lookup?.kind, 'function'); + assert.equal(result.modelTools.exec?.kind, 'function'); + + const visibleToolSchemaChars = toolSchemaCharsForDiagnostics( + result.providerTools, + result.availability.activeTools, + ); + const diagnostic = result.availability.diagnostics( + result.availability.activeTools, + visibleToolSchemaChars, + ); + assert.equal(diagnostic?.visibleToolCount, 3); + assert.equal(diagnostic?.fullToolCount, 4); + assert.equal(diagnostic?.visibleToolSchemaChars, visibleToolSchemaChars); + assert.equal( + diagnostic?.fullToolSchemaChars, + baseVisibleChars + 100 + (visibleToolSchemaChars - baseVisibleChars), + ); +}); + +test('keeps the direct availability plan unchanged', () => { + const direct = availability(); + const result = buildToolSchemaPlan({ + boundTools: direct.providerTools, + availability: direct, + requestedToolMode: undefined, + codeModeExecTool: execTool, + }); + + assert.equal(result.toolMode, 'direct'); + assert.strictEqual(result.availability, direct); + assert.deepEqual( + result.providerTools.map((candidate) => candidate.name), + ['lookup', 'native_search', 'invalid'], + ); +}); + +test('rejects invalid modes and a caller-owned Code Mode exec name', () => { + const direct = availability(); + for (const requestedToolMode of ['legacy_mode', null]) { + assert.throws( + () => + buildToolSchemaPlan({ + boundTools: direct.providerTools, + availability: direct, + requestedToolMode, + codeModeExecTool: execTool, + }), + /invalid tool mode/i, + ); + } + assert.throws( + () => + buildToolSchemaPlan({ + boundTools: [tool('exec')], + availability: direct, + requestedToolMode: 'code_mode', + codeModeExecTool: execTool, + }), + /reserved for Code Mode/i, + ); +}); + +test('selects only active function tools that may be nested', () => { + const tools = [ + tool('lookup'), + tool('inactive'), + tool('direct_control', { nesting: 'direct_only' }), + tool('native_search', { providerTool: { kind: 'openai-web-search' } }), + execTool, + invalidTool, + ]; + + assert.deepEqual( + [ + ...buildNestableToolSnapshot( + tools, + tools.map((candidate) => candidate.name), + ).keys(), + ], + ['lookup', 'inactive'], + ); + assert.deepEqual([...buildNestableToolSnapshot(tools, ['lookup']).keys()], ['lookup']); +}); + +test('validates Code Mode inputs through Zod and JSON Schema contracts', async () => { + const zodTool = tool('typed_lookup', { parameters: z.object({ id: z.string() }) }); + const parsed = await validateCodeModeToolInput(zodTool, { id: 'node-1' }); + assert.deepEqual(parsed, { id: 'node-1' }); + await assert.rejects(validateCodeModeToolInput(zodTool, { id: 42 }), /invalid arguments.*id/i); + + const jsonSchemaTool = tool('mcp_lookup', { + parameters: { + $schema: 'https://json-schema.org/draft-07/schema#', + type: 'object', + properties: { id: { type: 'string' } }, + required: ['id'], + additionalProperties: false, + }, + }); + assert.deepEqual(await validateCodeModeToolInput(jsonSchemaTool, { id: 'node-1' }), { + id: 'node-1', + }); + await assert.rejects( + validateCodeModeToolInput(jsonSchemaTool, { id: 42 }), + /invalid arguments.*id/i, + ); +}); diff --git a/packages/runtime/src/ai-sdk-turn.ts b/packages/runtime/src/ai-sdk-turn.ts index c501a66ac7..f74d5c925b 100644 --- a/packages/runtime/src/ai-sdk-turn.ts +++ b/packages/runtime/src/ai-sdk-turn.ts @@ -51,7 +51,6 @@ import type { BackendSendInput } from '@maka/core/backend-types'; import type { RuntimeEvent } from '@maka/core/runtime-event'; import type { SandboxBoundaryResponse } from '@maka/core/sandbox-boundary'; import type { UserQuestionResponse } from '@maka/core/user-question'; -import { DEFAULT_TOOL_MODE, isToolMode, type ToolMode } from '@maka/core/tool-mode'; import { resolveEffectiveOrchestration, type EffectiveOrchestration, @@ -72,9 +71,6 @@ import type { ModelFailureKind, ToolCallPart, } from './model-protocol.js'; -import Ajv, { type AnySchema, type ErrorObject, type ValidateFunction } from 'ajv'; -import Ajv2019 from 'ajv/dist/2019.js'; -import Ajv2020 from 'ajv/dist/2020.js'; import { z } from 'zod'; import { AsyncEventQueue } from './async-queue.js'; @@ -178,6 +174,11 @@ import { isProviderSandboxBoundaryAttempt, repairMakaToolCall, } from './ai-sdk-tool-repair.js'; +import { + buildNestableToolSnapshot, + buildToolSchemaPlan, + validateCodeModeToolInput, +} from './tool-schema-builder.js'; export interface AiSdkSessionState { contextProviderDroppingReported: boolean; @@ -411,166 +412,6 @@ function mergeTextProviderOptions( return merged; } -function projectToolModePlan( - plan: ToolAvailabilityPlan, - toolMode: ToolMode, - execTool: MakaTool, -): ToolAvailabilityPlan { - if (toolMode === 'direct') return plan; - const withExec = (names: readonly string[]): string[] => - [...new Set([...names, execTool.name])].sort((a, b) => a.localeCompare(b)); - const invalid = plan.providerTools.filter((tool) => tool.name === INVALID_TOOL_NAME); - const visible = [ - ...plan.providerTools.filter((tool) => tool.name !== INVALID_TOOL_NAME), - execTool, - ].sort((a, b) => a.name.localeCompare(b.name)); - return { - ...plan, - providerTools: [...visible, ...invalid], - activeTools: withExec(plan.activeTools), - ...(plan.projectActiveTools - ? { - projectActiveTools: (options) => ({ - activeTools: withExec(plan.projectActiveTools?.(options).activeTools ?? []), - }), - } - : {}), - currentRepairToolNames: () => withExec(plan.currentRepairToolNames()), - diagnostics: (activeTools, visibleToolSchemaChars) => { - const baseActive = activeTools.filter((name) => name !== execTool.name); - const baseChars = toolSchemaCharsForDiagnostics(plan.providerTools, baseActive); - const diagnostic = plan.diagnostics(baseActive, baseChars); - if (!diagnostic) return undefined; - const execSchemaChars = Math.max(0, visibleToolSchemaChars - baseChars); - return { - ...diagnostic, - visibleToolCount: (diagnostic.visibleToolCount ?? baseActive.length) + 1, - fullToolCount: - (diagnostic.fullToolCount ?? baseActive.length + (diagnostic.hiddenToolCount ?? 0)) + 1, - visibleToolSchemaChars, - fullToolSchemaChars: - (diagnostic.fullToolSchemaChars ?? - baseChars + (diagnostic.toolSchemaCharReduction ?? 0)) + execSchemaChars, - }; - }, - }; -} - -function nestableToolSnapshot( - providerTools: readonly MakaTool[], - activeToolNames: readonly string[], -): ReadonlyMap { - const active = new Set(activeToolNames); - return new Map( - providerTools - .filter( - (tool) => - active.has(tool.name) && - tool.name !== INVALID_TOOL_NAME && - tool.name !== 'exec' && - tool.providerTool === undefined && - tool.nesting !== 'direct_only', - ) - .map((tool) => [tool.name, tool] as const), - ); -} - -const codeModeJsonSchemaOptions = { - allErrors: true, - strict: false, - validateFormats: false, -} as const; -const codeModeDraft7Validator = new Ajv(codeModeJsonSchemaOptions); -const codeModeDraft2019Validator = new Ajv2019(codeModeJsonSchemaOptions); -const codeModeDraft2020Validator = new Ajv2020(codeModeJsonSchemaOptions); -const codeModeCompiledSchemas = new WeakMap(); - -async function validateCodeModeToolInput(tool: MakaTool, input: unknown): Promise { - const parameters = tool.parameters as { - safeParseAsync?: ( - value: unknown, - ) => Promise<{ success: true; data: unknown } | { success: false; error: unknown }>; - safeParse?: ( - value: unknown, - ) => { success: true; data: unknown } | { success: false; error: unknown }; - validate?: ( - value: unknown, - ) => - | { success: true; value: unknown } - | { success: false; error: unknown } - | Promise<{ success: true; value: unknown } | { success: false; error: unknown }>; - jsonSchema?: unknown; - }; - const parserResult = parameters.safeParseAsync - ? await parameters.safeParseAsync(input) - : parameters.safeParse?.(input); - if (parserResult) { - if (parserResult.success) return parserResult.data; - throw invalidCodeModeToolArguments(tool.name, parserResult.error); - } - - if (parameters.validate) { - const validationResult = await parameters.validate(input); - if (validationResult.success) return validationResult.value; - throw invalidCodeModeToolArguments(tool.name, validationResult.error); - } - - const schema = await parameters.jsonSchema; - const validator = compileCodeModeJsonSchema(schema ?? tool.parameters); - if (!validator || validator(input)) return input; - throw invalidCodeModeToolArguments(tool.name, validator.errors); -} - -function compileCodeModeJsonSchema(schema: unknown): ValidateFunction | undefined { - if (typeof schema === 'boolean') return codeModeDraft2020Validator.compile(schema); - if (typeof schema !== 'object' || schema === null || Array.isArray(schema)) return undefined; - const cached = codeModeCompiledSchemas.get(schema); - if (cached) return cached; - const declaredDialect = (schema as { readonly $schema?: unknown }).$schema; - const dialect = typeof declaredDialect === 'string' ? declaredDialect : ''; - const validator = dialect.includes('draft-07') - ? codeModeDraft7Validator - : dialect.includes('2019-09') - ? codeModeDraft2019Validator - : codeModeDraft2020Validator; - const schemaForCompile = dialect.startsWith('https://json-schema.org/draft-07/schema') - ? { ...schema, $schema: dialect.replace('https://', 'http://') } - : schema; - const compiled = validator.compile(schemaForCompile as AnySchema); - codeModeCompiledSchemas.set(schema, compiled); - return compiled; -} - -function invalidCodeModeToolArguments(toolName: string, error: unknown): Error { - return new Error(`Invalid arguments for tool "${toolName}": ${schemaErrorSummary(error)}`); -} - -function schemaErrorSummary(error: unknown): string { - if (error && typeof error === 'object' && Array.isArray((error as { issues?: unknown }).issues)) { - const issues = (error as { issues: Array<{ path?: unknown; message?: unknown }> }).issues; - return issues - .slice(0, 5) - .map((issue) => { - const path = Array.isArray(issue.path) ? issue.path.join('.') : ''; - const message = typeof issue.message === 'string' ? issue.message : 'invalid value'; - return path ? `${path}: ${message}` : message; - }) - .join('; ') - .slice(0, 1000); - } - if (Array.isArray(error)) { - return (error as ErrorObject[]) - .slice(0, 5) - .map((issue) => { - const path = issue.instancePath || issue.schemaPath; - return `${path || 'input'} ${issue.message ?? 'is invalid'}`; - }) - .join('; ') - .slice(0, 1000); - } - return 'input does not match the declared schema'; -} - function joinPromptFragments(fragments: readonly (string | undefined)[]): string | undefined { const joined = fragments .map((fragment) => fragment?.trim()) @@ -1084,21 +925,20 @@ export class AiSdkTurn { 'agent_output', ]) : new Set(); - const requestedToolMode: unknown = - input.toolMode === undefined ? DEFAULT_TOOL_MODE : input.toolMode; - if (!isToolMode(requestedToolMode)) { - throw new Error(`Invalid tool mode: ${String(requestedToolMode)}`); - } - const toolMode = requestedToolMode; - if (toolMode === 'code_mode' && this.deps.backend.tools.some((tool) => tool.name === 'exec')) { - throw new Error('Tool name "exec" is reserved for Code Mode.'); - } - const plan = projectToolModePlan( - this.deps.toolAvailabilityRuntime.prepare(this.activeTools, requiredOrchestrationTools), + const { toolMode, + availability: plan, + providerTools, + modelTools, + } = buildToolSchemaPlan({ + boundTools: this.deps.backend.tools, + availability: this.deps.toolAvailabilityRuntime.prepare( + this.activeTools, + requiredOrchestrationTools, + ), + requestedToolMode: input.toolMode, codeModeExecTool, - ); - const providerTools = plan.providerTools; + }); let activeToolResultPruneDiagnosticPatch: ActiveToolResultPruneDiagnosticPatch = {}; let midTurnCompactDiagnosticPatch: Partial | undefined; // Tool names the repair path matches a mis-cased call against — follows the @@ -1115,17 +955,6 @@ export class AiSdkTurn { toolRuntime.setGating(plan.gating); } - const modelTools: ModelToolSet = {}; - for (const t of providerTools) { - modelTools[t.name] = t.providerTool - ? { kind: 'provider', providerTool: t.providerTool } - : { - kind: 'function', - description: t.description, - inputSchema: t.parameters, - }; - } - // Resolve the stable Provider envelope before automatic Compaction freezes // its source. The same value is reused by the primary request; Memory does // not resolve or mutate Agent configuration after the checkpoint commits. @@ -1540,7 +1369,7 @@ export class AiSdkTurn { : activeToolsForRequest; this.codeModeTools = toolMode === 'code_mode' - ? nestableToolSnapshot(providerTools, codeModeActiveTools) + ? buildNestableToolSnapshot(providerTools, codeModeActiveTools) : undefined; const requestWatchdog = watchdogState.current; // Read here, beside the messages it describes: `attemptMessages` is diff --git a/packages/runtime/src/tool-schema-builder.ts b/packages/runtime/src/tool-schema-builder.ts new file mode 100644 index 0000000000..01f74d7ad4 --- /dev/null +++ b/packages/runtime/src/tool-schema-builder.ts @@ -0,0 +1,246 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Provider-visible tool schema planning and Code Mode schema validation. + * Tool execution and settlement remain owned by AiSdkTurn and ToolRuntime. + */ + +import { DEFAULT_TOOL_MODE, isToolMode, type ToolMode } from '@maka/core/tool-mode'; +import Ajv, { type AnySchema, type ErrorObject, type ValidateFunction } from 'ajv'; +import Ajv2019 from 'ajv/dist/2019.js'; +import Ajv2020 from 'ajv/dist/2020.js'; + +import type { ModelToolSet } from './model-protocol.js'; +import { toolSchemaCharsForDiagnostics } from './request-shape.js'; +import { INVALID_TOOL_NAME } from './ai-sdk-tool-repair.js'; +import type { MakaTool } from './tool-runtime.js'; +import type { ToolAvailabilityPlan } from './tool-availability.js'; + +export interface ToolSchemaPlan { + toolMode: ToolMode; + availability: ToolAvailabilityPlan; + providerTools: MakaTool[]; + modelTools: ModelToolSet; +} + +export function buildToolSchemaPlan(input: { + boundTools: readonly MakaTool[]; + availability: ToolAvailabilityPlan; + requestedToolMode: unknown; + codeModeExecTool: MakaTool; +}): ToolSchemaPlan { + const requestedToolMode = + input.requestedToolMode === undefined ? DEFAULT_TOOL_MODE : input.requestedToolMode; + if (!isToolMode(requestedToolMode)) { + throw new Error(`Invalid tool mode: ${String(requestedToolMode)}`); + } + if ( + requestedToolMode === 'code_mode' && + input.boundTools.some((tool) => tool.name === input.codeModeExecTool.name) + ) { + throw new Error(`Tool name "${input.codeModeExecTool.name}" is reserved for Code Mode.`); + } + + const availability = projectToolModePlan( + input.availability, + requestedToolMode, + input.codeModeExecTool, + ); + return { + toolMode: requestedToolMode, + availability, + providerTools: availability.providerTools, + modelTools: bindModelTools(availability.providerTools), + }; +} + +export function buildNestableToolSnapshot( + providerTools: readonly MakaTool[], + activeToolNames: readonly string[], +): ReadonlyMap { + const active = new Set(activeToolNames); + return new Map( + providerTools + .filter( + (tool) => + active.has(tool.name) && + tool.name !== INVALID_TOOL_NAME && + tool.name !== 'exec' && + tool.providerTool === undefined && + tool.nesting !== 'direct_only', + ) + .map((tool) => [tool.name, tool] as const), + ); +} + +export async function validateCodeModeToolInput(tool: MakaTool, input: unknown): Promise { + const parameters = tool.parameters as { + safeParseAsync?: ( + value: unknown, + ) => Promise<{ success: true; data: unknown } | { success: false; error: unknown }>; + safeParse?: ( + value: unknown, + ) => { success: true; data: unknown } | { success: false; error: unknown }; + validate?: ( + value: unknown, + ) => + | { success: true; value: unknown } + | { success: false; error: unknown } + | Promise<{ success: true; value: unknown } | { success: false; error: unknown }>; + jsonSchema?: unknown; + }; + const parserResult = parameters.safeParseAsync + ? await parameters.safeParseAsync(input) + : parameters.safeParse?.(input); + if (parserResult) { + if (parserResult.success) return parserResult.data; + throw invalidCodeModeToolArguments(tool.name, parserResult.error); + } + + if (parameters.validate) { + const validationResult = await parameters.validate(input); + if (validationResult.success) return validationResult.value; + throw invalidCodeModeToolArguments(tool.name, validationResult.error); + } + + const schema = await parameters.jsonSchema; + const validator = compileCodeModeJsonSchema(schema ?? tool.parameters); + if (!validator || validator(input)) return input; + throw invalidCodeModeToolArguments(tool.name, validator.errors); +} + +function projectToolModePlan( + plan: ToolAvailabilityPlan, + toolMode: ToolMode, + execTool: MakaTool, +): ToolAvailabilityPlan { + if (toolMode === 'direct') return plan; + const withExec = (names: readonly string[]): string[] => + [...new Set([...names, execTool.name])].sort((a, b) => a.localeCompare(b)); + const invalid = plan.providerTools.filter((tool) => tool.name === INVALID_TOOL_NAME); + const visible = [ + ...plan.providerTools.filter((tool) => tool.name !== INVALID_TOOL_NAME), + execTool, + ].sort((a, b) => a.name.localeCompare(b.name)); + return { + ...plan, + providerTools: [...visible, ...invalid], + activeTools: withExec(plan.activeTools), + ...(plan.projectActiveTools + ? { + projectActiveTools: (options) => ({ + activeTools: withExec(plan.projectActiveTools?.(options).activeTools ?? []), + }), + } + : {}), + currentRepairToolNames: () => withExec(plan.currentRepairToolNames()), + diagnostics: (activeTools, visibleToolSchemaChars) => { + const baseActive = activeTools.filter((name) => name !== execTool.name); + const baseChars = toolSchemaCharsForDiagnostics(plan.providerTools, baseActive); + const diagnostic = plan.diagnostics(baseActive, baseChars); + if (!diagnostic) return undefined; + const execSchemaChars = Math.max(0, visibleToolSchemaChars - baseChars); + return { + ...diagnostic, + visibleToolCount: (diagnostic.visibleToolCount ?? baseActive.length) + 1, + fullToolCount: + (diagnostic.fullToolCount ?? baseActive.length + (diagnostic.hiddenToolCount ?? 0)) + 1, + visibleToolSchemaChars, + fullToolSchemaChars: + (diagnostic.fullToolSchemaChars ?? + baseChars + (diagnostic.toolSchemaCharReduction ?? 0)) + execSchemaChars, + }; + }, + }; +} + +function bindModelTools(providerTools: readonly MakaTool[]): ModelToolSet { + const modelTools: ModelToolSet = {}; + for (const tool of providerTools) { + modelTools[tool.name] = tool.providerTool + ? { kind: 'provider', providerTool: tool.providerTool } + : { + kind: 'function', + description: tool.description, + inputSchema: tool.parameters, + }; + } + return modelTools; +} + +const codeModeJsonSchemaOptions = { + allErrors: true, + strict: false, + validateFormats: false, +} as const; +const codeModeDraft7Validator = new Ajv(codeModeJsonSchemaOptions); +const codeModeDraft2019Validator = new Ajv2019(codeModeJsonSchemaOptions); +const codeModeDraft2020Validator = new Ajv2020(codeModeJsonSchemaOptions); +const codeModeCompiledSchemas = new WeakMap(); + +function compileCodeModeJsonSchema(schema: unknown): ValidateFunction | undefined { + if (typeof schema === 'boolean') return codeModeDraft2020Validator.compile(schema); + if (typeof schema !== 'object' || schema === null || Array.isArray(schema)) return undefined; + const cached = codeModeCompiledSchemas.get(schema); + if (cached) return cached; + const declaredDialect = (schema as { readonly $schema?: unknown }).$schema; + const dialect = typeof declaredDialect === 'string' ? declaredDialect : ''; + const validator = dialect.includes('draft-07') + ? codeModeDraft7Validator + : dialect.includes('2019-09') + ? codeModeDraft2019Validator + : codeModeDraft2020Validator; + const schemaForCompile = dialect.startsWith('https://json-schema.org/draft-07/schema') + ? { ...schema, $schema: dialect.replace('https://', 'http://') } + : schema; + const compiled = validator.compile(schemaForCompile as AnySchema); + codeModeCompiledSchemas.set(schema, compiled); + return compiled; +} + +function invalidCodeModeToolArguments(toolName: string, error: unknown): Error { + return new Error(`Invalid arguments for tool "${toolName}": ${schemaErrorSummary(error)}`); +} + +function schemaErrorSummary(error: unknown): string { + if (error && typeof error === 'object' && Array.isArray((error as { issues?: unknown }).issues)) { + const issues = (error as { issues: Array<{ path?: unknown; message?: unknown }> }).issues; + return issues + .slice(0, 5) + .map((issue) => { + const path = Array.isArray(issue.path) ? issue.path.join('.') : ''; + const message = typeof issue.message === 'string' ? issue.message : 'invalid value'; + return path ? `${path}: ${message}` : message; + }) + .join('; ') + .slice(0, 1000); + } + if (Array.isArray(error)) { + return (error as ErrorObject[]) + .slice(0, 5) + .map((issue) => { + const path = issue.instancePath || issue.schemaPath; + return `${path || 'input'} ${issue.message ?? 'is invalid'}`; + }) + .join('; ') + .slice(0, 1000); + } + return 'input does not match the declared schema'; +}