@caupulican/pi-ai 0.81.11 → 0.81.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-registry.d.ts.map +1 -1
- package/dist/api-registry.js +17 -1
- package/dist/api-registry.js.map +1 -1
- package/dist/env-api-keys.d.ts.map +1 -1
- package/dist/env-api-keys.js +27 -16
- package/dist/env-api-keys.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/models.generated.d.ts +120 -0
- package/dist/models.generated.d.ts.map +1 -1
- package/dist/models.generated.js +124 -4
- package/dist/models.generated.js.map +1 -1
- package/dist/providers/amazon-bedrock.d.ts.map +1 -1
- package/dist/providers/amazon-bedrock.js +39 -6
- package/dist/providers/amazon-bedrock.js.map +1 -1
- package/dist/providers/google-shared.d.ts +13 -4
- package/dist/providers/google-shared.d.ts.map +1 -1
- package/dist/providers/google-shared.js +132 -11
- package/dist/providers/google-shared.js.map +1 -1
- package/dist/providers/google-vertex.d.ts.map +1 -1
- package/dist/providers/google-vertex.js +16 -98
- package/dist/providers/google-vertex.js.map +1 -1
- package/dist/providers/google.d.ts.map +1 -1
- package/dist/providers/google.js +16 -116
- package/dist/providers/google.js.map +1 -1
- package/dist/providers/openai-completions.d.ts +2 -1
- package/dist/providers/openai-completions.d.ts.map +1 -1
- package/dist/providers/openai-completions.js +56 -22
- package/dist/providers/openai-completions.js.map +1 -1
- package/dist/providers/openai-responses-shared.d.ts.map +1 -1
- package/dist/providers/openai-responses-shared.js +34 -15
- package/dist/providers/openai-responses-shared.js.map +1 -1
- package/dist/providers/transform-messages.d.ts.map +1 -1
- package/dist/providers/transform-messages.js +25 -6
- package/dist/providers/transform-messages.js.map +1 -1
- package/dist/stream.d.ts +2 -1
- package/dist/stream.d.ts.map +1 -1
- package/dist/stream.js +79 -2
- package/dist/stream.js.map +1 -1
- package/dist/types.d.ts +29 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/json-parse.d.ts +5 -0
- package/dist/utils/json-parse.d.ts.map +1 -1
- package/dist/utils/json-parse.js +9 -13
- package/dist/utils/json-parse.js.map +1 -1
- package/dist/utils/tool-names.d.ts +7 -0
- package/dist/utils/tool-names.d.ts.map +1 -0
- package/dist/utils/tool-names.js +46 -0
- package/dist/utils/tool-names.js.map +1 -0
- package/dist/utils/tool-repair/analyzer.d.ts +45 -0
- package/dist/utils/tool-repair/analyzer.d.ts.map +1 -0
- package/dist/utils/tool-repair/analyzer.js +255 -0
- package/dist/utils/tool-repair/analyzer.js.map +1 -0
- package/dist/utils/tool-repair/registry.d.ts +32 -0
- package/dist/utils/tool-repair/registry.d.ts.map +1 -0
- package/dist/utils/tool-repair/registry.js +129 -0
- package/dist/utils/tool-repair/registry.js.map +1 -0
- package/dist/utils/tool-repair/repairer.d.ts +16 -0
- package/dist/utils/tool-repair/repairer.d.ts.map +1 -0
- package/dist/utils/tool-repair/repairer.js +254 -0
- package/dist/utils/tool-repair/repairer.js.map +1 -0
- package/dist/utils/tool-repair/replay.d.ts +35 -0
- package/dist/utils/tool-repair/replay.d.ts.map +1 -0
- package/dist/utils/tool-repair/replay.js +200 -0
- package/dist/utils/tool-repair/replay.js.map +1 -0
- package/dist/utils/tool-repair/text-protocol.d.ts +16 -0
- package/dist/utils/tool-repair/text-protocol.d.ts.map +1 -0
- package/dist/utils/tool-repair/text-protocol.js +198 -0
- package/dist/utils/tool-repair/text-protocol.js.map +1 -0
- package/dist/utils/validation.d.ts +47 -4
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +300 -217
- package/dist/utils/validation.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/utils/validation.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AA0QlD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,QAAQ,GAAG,GAAG,CAMvE;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,GAAG,GAAG,CAgCzE","sourcesContent":["import { Compile } from \"typebox/compile\";\nimport type { TLocalizedValidationError } from \"typebox/error\";\nimport { Value } from \"typebox/value\";\nimport type { Tool, ToolCall } from \"../types.ts\";\n\nconst validatorCache = new WeakMap<object, ReturnType<typeof Compile>>();\nconst TYPEBOX_KIND = Symbol.for(\"TypeBox.Kind\");\n\ninterface JsonSchemaObject {\n\ttype?: string | string[];\n\tproperties?: Record<string, JsonSchemaObject>;\n\titems?: JsonSchemaObject | JsonSchemaObject[];\n\tadditionalProperties?: boolean | JsonSchemaObject;\n\tallOf?: JsonSchemaObject[];\n\tanyOf?: JsonSchemaObject[];\n\toneOf?: JsonSchemaObject[];\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null;\n}\n\nfunction isJsonSchemaObject(value: unknown): value is JsonSchemaObject {\n\treturn isRecord(value);\n}\n\nfunction hasTypeBoxMetadata(schema: unknown): boolean {\n\treturn isRecord(schema) && Object.getOwnPropertySymbols(schema).includes(TYPEBOX_KIND);\n}\n\nfunction getSchemaTypes(schema: JsonSchemaObject): string[] {\n\tif (typeof schema.type === \"string\") {\n\t\treturn [schema.type];\n\t}\n\tif (Array.isArray(schema.type)) {\n\t\treturn schema.type.filter((type): type is string => typeof type === \"string\");\n\t}\n\treturn [];\n}\n\nfunction matchesJsonType(value: unknown, type: string): boolean {\n\tswitch (type) {\n\t\tcase \"number\":\n\t\t\treturn typeof value === \"number\";\n\t\tcase \"integer\":\n\t\t\treturn typeof value === \"number\" && Number.isInteger(value);\n\t\tcase \"boolean\":\n\t\t\treturn typeof value === \"boolean\";\n\t\tcase \"string\":\n\t\t\treturn typeof value === \"string\";\n\t\tcase \"null\":\n\t\t\treturn value === null;\n\t\tcase \"array\":\n\t\t\treturn Array.isArray(value);\n\t\tcase \"object\":\n\t\t\treturn isRecord(value) && !Array.isArray(value);\n\t\tdefault:\n\t\t\treturn false;\n\t}\n}\n\nfunction isValidatorSchema(value: unknown): value is Tool[\"parameters\"] {\n\treturn isRecord(value);\n}\n\nfunction getSubSchemaValidator(schema: JsonSchemaObject): ReturnType<typeof Compile> | undefined {\n\tif (!isValidatorSchema(schema)) {\n\t\treturn undefined;\n\t}\n\ttry {\n\t\treturn getValidator(schema);\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction coercePrimitiveByType(value: unknown, type: string): unknown {\n\tswitch (type) {\n\t\tcase \"number\": {\n\t\t\tif (value === null) {\n\t\t\t\treturn 0;\n\t\t\t}\n\t\t\tif (typeof value === \"string\" && value.trim() !== \"\") {\n\t\t\t\tconst parsed = Number(value);\n\t\t\t\tif (Number.isFinite(parsed)) {\n\t\t\t\t\treturn parsed;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (typeof value === \"boolean\") {\n\t\t\t\treturn value ? 1 : 0;\n\t\t\t}\n\t\t\treturn value;\n\t\t}\n\t\tcase \"integer\": {\n\t\t\tif (value === null) {\n\t\t\t\treturn 0;\n\t\t\t}\n\t\t\tif (typeof value === \"string\" && value.trim() !== \"\") {\n\t\t\t\tconst parsed = Number(value);\n\t\t\t\tif (Number.isInteger(parsed)) {\n\t\t\t\t\treturn parsed;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (typeof value === \"boolean\") {\n\t\t\t\treturn value ? 1 : 0;\n\t\t\t}\n\t\t\treturn value;\n\t\t}\n\t\tcase \"boolean\": {\n\t\t\tif (value === null) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tif (typeof value === \"string\") {\n\t\t\t\tif (value === \"true\") {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\tif (value === \"false\") {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (typeof value === \"number\") {\n\t\t\t\tif (value === 1) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\tif (value === 0) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn value;\n\t\t}\n\t\tcase \"string\": {\n\t\t\tif (value === null) {\n\t\t\t\treturn \"\";\n\t\t\t}\n\t\t\tif (typeof value === \"number\" || typeof value === \"boolean\") {\n\t\t\t\treturn String(value);\n\t\t\t}\n\t\t\treturn value;\n\t\t}\n\t\tcase \"null\": {\n\t\t\tif (value === \"\" || value === 0 || value === false) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\treturn value;\n\t\t}\n\t\tdefault:\n\t\t\treturn value;\n\t}\n}\n\nfunction applySchemaObjectCoercion(value: Record<string, unknown>, schema: JsonSchemaObject): void {\n\tconst properties = schema.properties;\n\tconst definedKeys = new Set<string>(properties ? Object.keys(properties) : []);\n\n\tif (properties) {\n\t\tfor (const [key, propertySchema] of Object.entries(properties)) {\n\t\t\tif (!(key in value)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tvalue[key] = coerceWithJsonSchema(value[key], propertySchema);\n\t\t}\n\t}\n\n\tif (schema.additionalProperties && isJsonSchemaObject(schema.additionalProperties)) {\n\t\tfor (const [key, propertyValue] of Object.entries(value)) {\n\t\t\tif (definedKeys.has(key)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tvalue[key] = coerceWithJsonSchema(propertyValue, schema.additionalProperties);\n\t\t}\n\t}\n}\n\nfunction applySchemaArrayCoercion(value: unknown[], schema: JsonSchemaObject): void {\n\tif (Array.isArray(schema.items)) {\n\t\tfor (let index = 0; index < value.length; index++) {\n\t\t\tconst itemSchema = schema.items[index];\n\t\t\tif (!itemSchema) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tvalue[index] = coerceWithJsonSchema(value[index], itemSchema);\n\t\t}\n\t\treturn;\n\t}\n\n\tif (isJsonSchemaObject(schema.items)) {\n\t\tfor (let index = 0; index < value.length; index++) {\n\t\t\tvalue[index] = coerceWithJsonSchema(value[index], schema.items);\n\t\t}\n\t}\n}\n\nfunction coerceWithUnionSchema(value: unknown, schemas: JsonSchemaObject[]): unknown {\n\tfor (const schema of schemas) {\n\t\tconst candidate = structuredClone(value);\n\t\tconst coerced = coerceWithJsonSchema(candidate, schema);\n\t\tconst validator = getSubSchemaValidator(schema);\n\t\tif (validator?.Check(coerced)) {\n\t\t\treturn coerced;\n\t\t}\n\t}\n\treturn value;\n}\n\nfunction coerceWithJsonSchema(value: unknown, schema: JsonSchemaObject): unknown {\n\tlet nextValue = value;\n\n\tif (Array.isArray(schema.allOf)) {\n\t\tfor (const nested of schema.allOf) {\n\t\t\tnextValue = coerceWithJsonSchema(nextValue, nested);\n\t\t}\n\t}\n\n\tif (Array.isArray(schema.anyOf)) {\n\t\tnextValue = coerceWithUnionSchema(nextValue, schema.anyOf);\n\t}\n\n\tif (Array.isArray(schema.oneOf)) {\n\t\tnextValue = coerceWithUnionSchema(nextValue, schema.oneOf);\n\t}\n\n\tconst schemaTypes = getSchemaTypes(schema);\n\tconst matchesUnionMember =\n\t\tschemaTypes.length > 1 && schemaTypes.some((schemaType) => matchesJsonType(nextValue, schemaType));\n\tif (schemaTypes.length > 0 && !matchesUnionMember) {\n\t\tfor (const schemaType of schemaTypes) {\n\t\t\tconst candidate = coercePrimitiveByType(nextValue, schemaType);\n\t\t\tif (candidate !== nextValue) {\n\t\t\t\tnextValue = candidate;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (schemaTypes.includes(\"object\") && isRecord(nextValue) && !Array.isArray(nextValue)) {\n\t\tapplySchemaObjectCoercion(nextValue, schema);\n\t}\n\n\tif (schemaTypes.includes(\"array\") && Array.isArray(nextValue)) {\n\t\tapplySchemaArrayCoercion(nextValue, schema);\n\t}\n\n\treturn nextValue;\n}\n\nfunction getValidator(schema: Tool[\"parameters\"]): ReturnType<typeof Compile> {\n\tconst key = schema as object;\n\tconst cached = validatorCache.get(key);\n\tif (cached) {\n\t\treturn cached;\n\t}\n\tconst validator = Compile(schema);\n\tvalidatorCache.set(key, validator);\n\treturn validator;\n}\n\nfunction formatValidationPath(error: TLocalizedValidationError): string {\n\tif (error.keyword === \"required\") {\n\t\tconst requiredProperties = (error.params as { requiredProperties?: string[] }).requiredProperties;\n\t\tconst requiredProperty = requiredProperties?.[0];\n\t\tif (requiredProperty) {\n\t\t\tconst basePath = error.instancePath.replace(/^\\//, \"\").replace(/\\//g, \".\");\n\t\t\treturn basePath ? `${basePath}.${requiredProperty}` : requiredProperty;\n\t\t}\n\t}\n\tconst path = error.instancePath.replace(/^\\//, \"\").replace(/\\//g, \".\");\n\treturn path || \"root\";\n}\n\n/**\n * Finds a tool by name and validates the tool call arguments against its TypeBox schema\n * @param tools Array of tool definitions\n * @param toolCall The tool call from the LLM\n * @returns The validated arguments\n * @throws Error if tool is not found or validation fails\n */\nexport function validateToolCall(tools: Tool[], toolCall: ToolCall): any {\n\tconst tool = tools.find((t) => t.name === toolCall.name);\n\tif (!tool) {\n\t\tthrow new Error(`Tool \"${toolCall.name}\" not found`);\n\t}\n\treturn validateToolArguments(tool, toolCall);\n}\n\n/**\n * Validates tool call arguments against the tool's TypeBox schema\n * @param tool The tool definition with TypeBox schema\n * @param toolCall The tool call from the LLM\n * @returns The validated (and potentially coerced) arguments\n * @throws Error with formatted message if validation fails\n */\nexport function validateToolArguments(tool: Tool, toolCall: ToolCall): any {\n\tconst args = structuredClone(toolCall.arguments);\n\tValue.Convert(tool.parameters, args);\n\n\tconst validator = getValidator(tool.parameters);\n\tif (!hasTypeBoxMetadata(tool.parameters) && isJsonSchemaObject(tool.parameters)) {\n\t\tconst coerced = coerceWithJsonSchema(args, tool.parameters);\n\t\tif (coerced !== args) {\n\t\t\tif (isRecord(args) && isRecord(coerced)) {\n\t\t\t\tfor (const key of Object.keys(args)) {\n\t\t\t\t\tdelete args[key];\n\t\t\t\t}\n\t\t\t\tObject.assign(args, coerced);\n\t\t\t} else {\n\t\t\t\treturn validator.Check(coerced) ? coerced : args;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (validator.Check(args)) {\n\t\treturn args;\n\t}\n\n\tconst errors =\n\t\tvalidator\n\t\t\t.Errors(args)\n\t\t\t.map((error) => ` - ${formatValidationPath(error)}: ${error.message}`)\n\t\t\t.join(\"\\n\") || \"Unknown validation error\";\n\n\tconst errorMessage = `Validation failed for tool \"${toolCall.name}\":\\n${errors}\\n\\nReceived arguments:\\n${JSON.stringify(toolCall.arguments, null, 2)}`;\n\n\tthrow new Error(errorMessage);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/utils/validation.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAElD,OAAO,EAEN,KAAK,yBAAyB,EAC9B,KAAK,kBAAkB,EACvB,MAAM,2BAA2B,CAAC;AAQnC,MAAM,MAAM,6BAA6B,GAAG,OAAO,GAAG,UAAU,GAAG,SAAS,CAAC;AAC7E,MAAM,MAAM,sBAAsB,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAC9D,MAAM,MAAM,4BAA4B,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAC;AAE9E,MAAM,WAAW,6BAA6B;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,oCAAoC;IACpD,OAAO,EAAE,6BAA6B,CAAC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,yBAAyB,EAAE,CAAC;IAC1C,cAAc,EAAE,kBAAkB,EAAE,CAAC;IACrC,YAAY,CAAC,EAAE,6BAA6B,EAAE,CAAC;IAC/C,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,MAAM,EAAE,sBAAsB,CAAC;IAC/B,gBAAgB,EAAE,4BAA4B,CAAC;CAC/C;AAED,MAAM,WAAW,6BAA6B;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,oCAAoC,KAAK,IAAI,CAAC;IAClE,aAAa,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,qBAAa,2BAA4B,SAAQ,KAAK;IACrD,SAAgB,QAAQ,EAAE,MAAM,CAAC;IACjC,SAAgB,SAAS,EAAE,MAAM,CAAC;IAClC,SAAgB,UAAU,EAAE,MAAM,CAAC;IAEnC,YAAY,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,EAMhG;CACD;AAqTD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC/B,KAAK,EAAE,IAAI,EAAE,EACb,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,6BAA6B,GACrC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAMzB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CACpC,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,6BAA6B,GACrC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA0DzB","sourcesContent":["import { Compile } from \"typebox/compile\";\nimport type { TLocalizedValidationError } from \"typebox/error\";\nimport type { Tool, ToolCall } from \"../types.ts\";\nimport { analyzeToolArgumentErrors } from \"./tool-repair/analyzer.ts\";\nimport {\n\tformatToolRepairNote,\n\ttype ToolRepairFailureModeName,\n\ttype ToolRepairModeName,\n} from \"./tool-repair/registry.ts\";\nimport { repairToolArguments } from \"./tool-repair/repairer.ts\";\n\nconst validatorCache = new WeakMap<object, ReturnType<typeof Compile>>();\nconst EXPECTED_FRAGMENT_MAX_LENGTH = 320;\nconst RECEIVED_VALUE_MAX_LENGTH = 200;\nconst MISSING_VALUE = Symbol(\"missing\");\n\nexport type ToolArgumentValidationOutcome = \"clean\" | \"repaired\" | \"bounced\";\nexport type ToolArgumentTeachState = \"none\" | \"note\" | \"rule\";\nexport type ToolArgumentExecutionOutcome = \"not_run\" | \"succeeded\" | \"failed\";\n\nexport interface ToolArgumentFailureShapeEntry {\n\tpath: string;\n\texpectedType: string;\n\treceivedType: string;\n\tkeyword?: string;\n}\n\nexport interface ToolArgumentValidationTelemetryEvent {\n\toutcome: ToolArgumentValidationOutcome;\n\tprovider?: string;\n\tmodel?: string;\n\ttool: string;\n\tfailureModes: ToolRepairFailureModeName[];\n\trepairsApplied: ToolRepairModeName[];\n\tfailureShape?: ToolArgumentFailureShapeEntry[];\n\terrorKeywords?: string[];\n\ttaught: ToolArgumentTeachState;\n\texecutionOutcome: ToolArgumentExecutionOutcome;\n}\n\nexport interface ToolArgumentValidationOptions {\n\tmodel?: string;\n\tprovider?: string;\n\ttelemetry?: (event: ToolArgumentValidationTelemetryEvent) => void;\n\trepairEnabled?: boolean;\n}\n\nexport class ToolArgumentValidationError extends Error {\n\tpublic readonly toolName: string;\n\tpublic readonly signature: string;\n\tpublic readonly enrichment: string;\n\n\tconstructor(message: string, options: { toolName: string; signature: string; enrichment: string }) {\n\t\tsuper(message);\n\t\tthis.name = \"ToolArgumentValidationError\";\n\t\tthis.toolName = options.toolName;\n\t\tthis.signature = options.signature;\n\t\tthis.enrichment = options.enrichment;\n\t}\n}\n\nfunction emitToolArgumentValidationTelemetry(\n\toptions: ToolArgumentValidationOptions | undefined,\n\tevent: Omit<ToolArgumentValidationTelemetryEvent, \"model\" | \"provider\" | \"taught\" | \"executionOutcome\">,\n): void {\n\ttry {\n\t\toptions?.telemetry?.({\n\t\t\t...event,\n\t\t\tmodel: options.model,\n\t\t\tprovider: options.provider,\n\t\t\ttaught: \"none\",\n\t\t\texecutionOutcome: \"not_run\",\n\t\t});\n\t} catch {\n\t\t// Telemetry is observe-only; never fail validation because a sink failed.\n\t}\n}\n\nfunction uniqueRepairModes(modes: Iterable<ToolRepairModeName>): ToolRepairModeName[] {\n\treturn [...new Set(modes)];\n}\n\nfunction uniqueFailureModes(modes: Iterable<ToolRepairModeName>): ToolRepairFailureModeName[] {\n\tconst uniqueModes = uniqueRepairModes(modes);\n\treturn uniqueModes.length > 0 ? uniqueModes : [\"other\"];\n}\n\nfunction getValidator(schema: Tool[\"parameters\"]): ReturnType<typeof Compile> {\n\tconst key = schema as object;\n\tconst cached = validatorCache.get(key);\n\tif (cached) {\n\t\treturn cached;\n\t}\n\tconst validator = Compile(schema);\n\tvalidatorCache.set(key, validator);\n\treturn validator;\n}\n\nfunction formatValidationPath(error: TLocalizedValidationError): string {\n\tif (error.keyword === \"required\") {\n\t\tconst requiredProperties = (error.params as { requiredProperties?: string[] }).requiredProperties;\n\t\tconst requiredProperty = requiredProperties?.[0];\n\t\tif (requiredProperty) {\n\t\t\tconst basePath = error.instancePath.replace(/^\\//, \"\").replace(/\\//g, \".\");\n\t\t\treturn basePath ? `${basePath}.${requiredProperty}` : requiredProperty;\n\t\t}\n\t}\n\tconst path = error.instancePath.replace(/^\\//, \"\").replace(/\\//g, \".\");\n\treturn path || \"root\";\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> | undefined {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value)\n\t\t? (value as Record<string, unknown>)\n\t\t: undefined;\n}\n\nfunction truncateText(text: string, maxLength: number): string {\n\tif (text.length <= maxLength) {\n\t\treturn text;\n\t}\n\treturn `${text.slice(0, maxLength)}...[truncated]`;\n}\n\nfunction formatCompactJson(value: unknown, maxLength: number): string {\n\tif (value === MISSING_VALUE) {\n\t\treturn \"<missing>\";\n\t}\n\tif (value === undefined) {\n\t\treturn \"undefined\";\n\t}\n\treturn truncateText(JSON.stringify(value), maxLength);\n}\n\nfunction validationPathSegments(error: TLocalizedValidationError): string[] {\n\tconst path = formatValidationPath(error);\n\treturn path === \"root\" ? [] : path.split(\".\");\n}\n\nfunction schemaAtPath(schema: unknown, pathSegments: readonly string[]): unknown {\n\tlet current: unknown = schema;\n\tfor (const segment of pathSegments) {\n\t\tconst record = asRecord(current);\n\t\tif (!record) {\n\t\t\treturn current;\n\t\t}\n\n\t\tconst properties = asRecord(record.properties);\n\t\tif (properties && segment in properties) {\n\t\t\tcurrent = properties[segment];\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (record.items !== undefined) {\n\t\t\tcurrent = record.items;\n\t\t\tcontinue;\n\t\t}\n\n\t\treturn current;\n\t}\n\treturn current;\n}\n\nfunction receivedValueAtPath(args: unknown, pathSegments: readonly string[]): unknown {\n\tlet current: unknown = args;\n\tfor (const segment of pathSegments) {\n\t\tconst record = asRecord(current);\n\t\tif (record) {\n\t\t\tif (!(segment in record)) {\n\t\t\t\treturn MISSING_VALUE;\n\t\t\t}\n\t\t\tcurrent = record[segment];\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (Array.isArray(current)) {\n\t\t\tconst index = Number(segment);\n\t\t\tif (!Number.isInteger(index) || index < 0 || index >= current.length) {\n\t\t\t\treturn MISSING_VALUE;\n\t\t\t}\n\t\t\tcurrent = current[index];\n\t\t\tcontinue;\n\t\t}\n\n\t\treturn MISSING_VALUE;\n\t}\n\treturn current;\n}\n\nfunction receivedTypeOf(value: unknown): string {\n\tif (value === MISSING_VALUE) return \"missing\";\n\tif (value === null) return \"null\";\n\tif (Array.isArray(value)) return \"array\";\n\treturn typeof value;\n}\n\nfunction expectedTypeOf(schema: unknown): string {\n\tconst record = asRecord(schema);\n\tif (!record) return \"unknown\";\n\tif (Array.isArray(record.type)) return record.type.filter((type) => typeof type === \"string\").join(\"|\") || \"unknown\";\n\tif (typeof record.type === \"string\") return record.type;\n\tif (literalValues(record)) return \"enum\";\n\tif (record.properties !== undefined) return \"object\";\n\tif (record.items !== undefined) return \"array\";\n\treturn \"unknown\";\n}\n\nfunction formatFailureShape(\n\terrors: readonly TLocalizedValidationError[],\n\targs: unknown,\n\tschema: unknown,\n): ToolArgumentFailureShapeEntry[] {\n\tconst seen = new Set<string>();\n\tconst shape: ToolArgumentFailureShapeEntry[] = [];\n\tfor (const error of errors) {\n\t\tconst path = formatValidationPath(error);\n\t\tconst pathSegments = path === \"root\" ? [] : path.split(\".\");\n\t\tconst expectedSchema = schemaAtPath(schema, pathSegments);\n\t\tconst value = receivedValueAtPath(args, pathSegments);\n\t\tconst entry = {\n\t\t\tpath,\n\t\t\texpectedType: expectedTypeOf(expectedSchema),\n\t\t\treceivedType: receivedTypeOf(value),\n\t\t\tkeyword: error.keyword,\n\t\t};\n\t\tconst key = `${entry.path}\\0${entry.expectedType}\\0${entry.receivedType}\\0${entry.keyword}`;\n\t\tif (seen.has(key)) continue;\n\t\tseen.add(key);\n\t\tshape.push(entry);\n\t}\n\treturn shape;\n}\n\nfunction errorKeywords(errors: readonly TLocalizedValidationError[]): string[] {\n\treturn [...new Set(errors.map((error) => error.keyword))].sort();\n}\n\nfunction literalValues(schema: unknown): unknown[] | undefined {\n\tconst record = asRecord(schema);\n\tif (!record) {\n\t\treturn undefined;\n\t}\n\tif (Array.isArray(record.enum)) {\n\t\treturn record.enum;\n\t}\n\tif (record.const !== undefined) {\n\t\treturn [record.const];\n\t}\n\tconst anyOf = Array.isArray(record.anyOf) ? record.anyOf : Array.isArray(record.oneOf) ? record.oneOf : undefined;\n\tif (anyOf) {\n\t\tconst values: unknown[] = [];\n\t\tfor (const option of anyOf) {\n\t\t\tconst optionRecord = asRecord(option);\n\t\t\tif (!optionRecord || optionRecord.const === undefined) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\tvalues.push(optionRecord.const);\n\t\t}\n\t\treturn values;\n\t}\n\treturn undefined;\n}\n\nfunction compactSchemaFragment(schema: unknown): Record<string, unknown> {\n\tconst record = asRecord(schema);\n\tif (!record) {\n\t\treturn {};\n\t}\n\n\tconst fragment: Record<string, unknown> = {};\n\tconst values = literalValues(record);\n\tif (values) {\n\t\tfragment.enum = values;\n\t\treturn fragment;\n\t}\n\n\tfor (const key of [\"type\", \"required\", \"minimum\", \"maximum\", \"minLength\", \"maxLength\", \"format\"] as const) {\n\t\tif (record[key] !== undefined) {\n\t\t\tfragment[key] = record[key];\n\t\t}\n\t}\n\n\tconst properties = asRecord(record.properties);\n\tif (properties) {\n\t\tfragment.properties = Object.keys(properties);\n\t}\n\tif (record.items !== undefined) {\n\t\tfragment.items = compactSchemaFragment(record.items);\n\t}\n\treturn fragment;\n}\n\nfunction minimalExample(schema: unknown): unknown {\n\tconst record = asRecord(schema);\n\tif (!record) {\n\t\treturn undefined;\n\t}\n\tif (record.default !== undefined) {\n\t\treturn record.default;\n\t}\n\tconst values = literalValues(record);\n\tif (values?.length) {\n\t\treturn values[0];\n\t}\n\n\tconst type = Array.isArray(record.type) ? record.type[0] : record.type;\n\tswitch (type) {\n\t\tcase \"string\":\n\t\t\treturn \"\";\n\t\tcase \"number\":\n\t\t\treturn typeof record.minimum === \"number\" ? record.minimum : 0;\n\t\tcase \"integer\":\n\t\t\treturn typeof record.minimum === \"number\" ? Math.ceil(record.minimum) : 0;\n\t\tcase \"boolean\":\n\t\t\treturn true;\n\t\tcase \"array\":\n\t\t\treturn [];\n\t\tcase \"object\": {\n\t\t\tconst properties = asRecord(record.properties) ?? {};\n\t\t\tconst required = Array.isArray(record.required)\n\t\t\t\t? record.required.filter((key) => typeof key === \"string\")\n\t\t\t\t: [];\n\t\t\treturn Object.fromEntries(required.map((key) => [key, minimalExample(properties[key]) ?? null]));\n\t\t}\n\t\tcase \"null\":\n\t\t\treturn null;\n\t\tdefault:\n\t\t\treturn undefined;\n\t}\n}\n\nfunction formatValidationErrors(errors: readonly TLocalizedValidationError[], args: unknown, schema: unknown): string {\n\treturn (\n\t\terrors\n\t\t\t.map((error) => {\n\t\t\t\tconst path = formatValidationPath(error);\n\t\t\t\tconst pathSegments = validationPathSegments(error);\n\t\t\t\tconst expectedSchema = schemaAtPath(schema, pathSegments);\n\t\t\t\tconst expectedFragment = formatCompactJson(\n\t\t\t\t\tcompactSchemaFragment(expectedSchema),\n\t\t\t\t\tEXPECTED_FRAGMENT_MAX_LENGTH,\n\t\t\t\t);\n\t\t\t\tconst example = minimalExample(expectedSchema);\n\t\t\t\tconst received = formatCompactJson(receivedValueAtPath(args, pathSegments), RECEIVED_VALUE_MAX_LENGTH);\n\t\t\t\tconst exampleText =\n\t\t\t\t\texample === undefined ? \"\" : `; Example: ${formatCompactJson(example, RECEIVED_VALUE_MAX_LENGTH)}`;\n\t\t\t\treturn ` - ${path}: ${error.message}; Expected schema: ${expectedFragment}${exampleText}; Received: ${received}`;\n\t\t\t})\n\t\t\t.join(\"\\n\") || \"Unknown validation error\"\n\t);\n}\n\nfunction validationFailureSignature(errors: readonly TLocalizedValidationError[]): string {\n\treturn JSON.stringify(\n\t\terrors.map((error) => ({\n\t\t\tpath: formatValidationPath(error),\n\t\t\tkeyword: error.keyword,\n\t\t\tmessage: error.message,\n\t\t})),\n\t);\n}\n\nfunction formatValidationEnrichment(tool: Tool): string {\n\tconst example = minimalExample(tool.parameters);\n\tconst exampleText = example === undefined ? \"\" : `\\nValid example:\\n${formatCompactJson(example, 2000)}`;\n\treturn `Full tool schema:\\n${formatCompactJson(tool.parameters, 4000)}${exampleText}`;\n}\n\n/**\n * Finds a tool by name and validates the tool call arguments against its TypeBox schema\n * @param tools Array of tool definitions\n * @param toolCall The tool call from the LLM\n * @returns The validated arguments\n * @throws Error if tool is not found or validation fails\n */\nexport function validateToolCall(\n\ttools: Tool[],\n\ttoolCall: ToolCall,\n\toptions?: ToolArgumentValidationOptions,\n): Record<string, unknown> {\n\tconst tool = tools.find((t) => t.name === toolCall.name);\n\tif (!tool) {\n\t\tthrow new Error(`Tool \"${toolCall.name}\" not found`);\n\t}\n\treturn validateToolArguments(tool, toolCall, options);\n}\n\n/**\n * Validates tool call arguments against the tool's TypeBox schema.\n *\n * The hot path is validate-first and allocation-free: valid arguments return the exact\n * argument object emitted by the model. Only invalid calls enter the deterministic\n * repair layer, which applies named, guard-checked shape repairs on a clone.\n *\n * @param tool The tool definition with TypeBox schema\n * @param toolCall The tool call from the LLM\n * @returns The validated (and potentially repaired) arguments\n * @throws Error with formatted message if validation fails\n */\nexport function validateToolArguments(\n\ttool: Tool,\n\ttoolCall: ToolCall,\n\toptions?: ToolArgumentValidationOptions,\n): Record<string, unknown> {\n\tconst args = toolCall.arguments;\n\tconst validator = getValidator(tool.parameters);\n\n\tif (validator.Check(args)) {\n\t\temitToolArgumentValidationTelemetry(options, {\n\t\t\toutcome: \"clean\",\n\t\t\ttool: toolCall.name,\n\t\t\tfailureModes: [],\n\t\t\trepairsApplied: [],\n\t\t});\n\t\treturn args;\n\t}\n\n\tconst validationErrors = [...validator.Errors(args)] as TLocalizedValidationError[];\n\tconst repairIssues = analyzeToolArgumentErrors(toolCall.name, tool.parameters, args, validationErrors);\n\tconst failureModes = uniqueFailureModes(repairIssues.flatMap((issue) => issue.modes));\n\tconst repaired =\n\t\toptions?.repairEnabled === false\n\t\t\t? undefined\n\t\t\t: repairToolArguments(toolCall.name, tool.parameters, args, validationErrors, (candidate) =>\n\t\t\t\t\tvalidator.Check(candidate),\n\t\t\t\t);\n\tif (repaired) {\n\t\tconst repairsApplied = uniqueRepairModes(repaired.repairsApplied);\n\t\ttoolCall.repairNotes = repaired.repairs.map(\n\t\t\t(repair) =>\n\t\t\t\t`[harness] ${repair.name}: ${formatToolRepairNote(repair.name, repair.path)}; executed with repaired arguments.`,\n\t\t);\n\t\temitToolArgumentValidationTelemetry(options, {\n\t\t\toutcome: \"repaired\",\n\t\t\ttool: toolCall.name,\n\t\t\tfailureModes,\n\t\t\trepairsApplied,\n\t\t});\n\t\treturn repaired.args;\n\t}\n\n\temitToolArgumentValidationTelemetry(options, {\n\t\toutcome: \"bounced\",\n\t\ttool: toolCall.name,\n\t\tfailureModes,\n\t\trepairsApplied: [],\n\t\tfailureShape: formatFailureShape(validationErrors, toolCall.arguments, tool.parameters),\n\t\terrorKeywords: errorKeywords(validationErrors),\n\t});\n\n\tconst errorMessage = `Validation failed for tool \"${toolCall.name}\":\\n${formatValidationErrors(\n\t\tvalidationErrors,\n\t\ttoolCall.arguments,\n\t\ttool.parameters,\n\t)}\\n\\nReceived arguments:\\n${truncateText(JSON.stringify(toolCall.arguments, null, 2), 2000)}`;\n\n\tthrow new ToolArgumentValidationError(errorMessage, {\n\t\ttoolName: toolCall.name,\n\t\tsignature: validationFailureSignature(validationErrors),\n\t\tenrichment: formatValidationEnrichment(tool),\n\t});\n}\n"]}
|
package/dist/utils/validation.js
CHANGED
|
@@ -1,233 +1,295 @@
|
|
|
1
1
|
import { Compile } from "typebox/compile";
|
|
2
|
-
import {
|
|
2
|
+
import { analyzeToolArgumentErrors } from "./tool-repair/analyzer.js";
|
|
3
|
+
import { formatToolRepairNote, } from "./tool-repair/registry.js";
|
|
4
|
+
import { repairToolArguments } from "./tool-repair/repairer.js";
|
|
3
5
|
const validatorCache = new WeakMap();
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
const EXPECTED_FRAGMENT_MAX_LENGTH = 320;
|
|
7
|
+
const RECEIVED_VALUE_MAX_LENGTH = 200;
|
|
8
|
+
const MISSING_VALUE = Symbol("missing");
|
|
9
|
+
export class ToolArgumentValidationError extends Error {
|
|
10
|
+
toolName;
|
|
11
|
+
signature;
|
|
12
|
+
enrichment;
|
|
13
|
+
constructor(message, options) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "ToolArgumentValidationError";
|
|
16
|
+
this.toolName = options.toolName;
|
|
17
|
+
this.signature = options.signature;
|
|
18
|
+
this.enrichment = options.enrichment;
|
|
19
|
+
}
|
|
7
20
|
}
|
|
8
|
-
function
|
|
9
|
-
|
|
21
|
+
function emitToolArgumentValidationTelemetry(options, event) {
|
|
22
|
+
try {
|
|
23
|
+
options?.telemetry?.({
|
|
24
|
+
...event,
|
|
25
|
+
model: options.model,
|
|
26
|
+
provider: options.provider,
|
|
27
|
+
taught: "none",
|
|
28
|
+
executionOutcome: "not_run",
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Telemetry is observe-only; never fail validation because a sink failed.
|
|
33
|
+
}
|
|
10
34
|
}
|
|
11
|
-
function
|
|
12
|
-
return
|
|
35
|
+
function uniqueRepairModes(modes) {
|
|
36
|
+
return [...new Set(modes)];
|
|
13
37
|
}
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
38
|
+
function uniqueFailureModes(modes) {
|
|
39
|
+
const uniqueModes = uniqueRepairModes(modes);
|
|
40
|
+
return uniqueModes.length > 0 ? uniqueModes : ["other"];
|
|
41
|
+
}
|
|
42
|
+
function getValidator(schema) {
|
|
43
|
+
const key = schema;
|
|
44
|
+
const cached = validatorCache.get(key);
|
|
45
|
+
if (cached) {
|
|
46
|
+
return cached;
|
|
20
47
|
}
|
|
21
|
-
|
|
48
|
+
const validator = Compile(schema);
|
|
49
|
+
validatorCache.set(key, validator);
|
|
50
|
+
return validator;
|
|
22
51
|
}
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
case "string":
|
|
32
|
-
return typeof value === "string";
|
|
33
|
-
case "null":
|
|
34
|
-
return value === null;
|
|
35
|
-
case "array":
|
|
36
|
-
return Array.isArray(value);
|
|
37
|
-
case "object":
|
|
38
|
-
return isRecord(value) && !Array.isArray(value);
|
|
39
|
-
default:
|
|
40
|
-
return false;
|
|
52
|
+
function formatValidationPath(error) {
|
|
53
|
+
if (error.keyword === "required") {
|
|
54
|
+
const requiredProperties = error.params.requiredProperties;
|
|
55
|
+
const requiredProperty = requiredProperties?.[0];
|
|
56
|
+
if (requiredProperty) {
|
|
57
|
+
const basePath = error.instancePath.replace(/^\//, "").replace(/\//g, ".");
|
|
58
|
+
return basePath ? `${basePath}.${requiredProperty}` : requiredProperty;
|
|
59
|
+
}
|
|
41
60
|
}
|
|
61
|
+
const path = error.instancePath.replace(/^\//, "").replace(/\//g, ".");
|
|
62
|
+
return path || "root";
|
|
42
63
|
}
|
|
43
|
-
function
|
|
44
|
-
return
|
|
64
|
+
function asRecord(value) {
|
|
65
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
66
|
+
? value
|
|
67
|
+
: undefined;
|
|
45
68
|
}
|
|
46
|
-
function
|
|
47
|
-
if (
|
|
48
|
-
return
|
|
69
|
+
function truncateText(text, maxLength) {
|
|
70
|
+
if (text.length <= maxLength) {
|
|
71
|
+
return text;
|
|
49
72
|
}
|
|
50
|
-
|
|
51
|
-
|
|
73
|
+
return `${text.slice(0, maxLength)}...[truncated]`;
|
|
74
|
+
}
|
|
75
|
+
function formatCompactJson(value, maxLength) {
|
|
76
|
+
if (value === MISSING_VALUE) {
|
|
77
|
+
return "<missing>";
|
|
52
78
|
}
|
|
53
|
-
|
|
54
|
-
return undefined;
|
|
79
|
+
if (value === undefined) {
|
|
80
|
+
return "undefined";
|
|
55
81
|
}
|
|
82
|
+
return truncateText(JSON.stringify(value), maxLength);
|
|
56
83
|
}
|
|
57
|
-
function
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
if (typeof value === "boolean") {
|
|
70
|
-
return value ? 1 : 0;
|
|
71
|
-
}
|
|
72
|
-
return value;
|
|
73
|
-
}
|
|
74
|
-
case "integer": {
|
|
75
|
-
if (value === null) {
|
|
76
|
-
return 0;
|
|
77
|
-
}
|
|
78
|
-
if (typeof value === "string" && value.trim() !== "") {
|
|
79
|
-
const parsed = Number(value);
|
|
80
|
-
if (Number.isInteger(parsed)) {
|
|
81
|
-
return parsed;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
if (typeof value === "boolean") {
|
|
85
|
-
return value ? 1 : 0;
|
|
86
|
-
}
|
|
87
|
-
return value;
|
|
88
|
-
}
|
|
89
|
-
case "boolean": {
|
|
90
|
-
if (value === null) {
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
93
|
-
if (typeof value === "string") {
|
|
94
|
-
if (value === "true") {
|
|
95
|
-
return true;
|
|
96
|
-
}
|
|
97
|
-
if (value === "false") {
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
if (typeof value === "number") {
|
|
102
|
-
if (value === 1) {
|
|
103
|
-
return true;
|
|
104
|
-
}
|
|
105
|
-
if (value === 0) {
|
|
106
|
-
return false;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return value;
|
|
84
|
+
function validationPathSegments(error) {
|
|
85
|
+
const path = formatValidationPath(error);
|
|
86
|
+
return path === "root" ? [] : path.split(".");
|
|
87
|
+
}
|
|
88
|
+
function schemaAtPath(schema, pathSegments) {
|
|
89
|
+
let current = schema;
|
|
90
|
+
for (const segment of pathSegments) {
|
|
91
|
+
const record = asRecord(current);
|
|
92
|
+
if (!record) {
|
|
93
|
+
return current;
|
|
110
94
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (typeof value === "number" || typeof value === "boolean") {
|
|
116
|
-
return String(value);
|
|
117
|
-
}
|
|
118
|
-
return value;
|
|
95
|
+
const properties = asRecord(record.properties);
|
|
96
|
+
if (properties && segment in properties) {
|
|
97
|
+
current = properties[segment];
|
|
98
|
+
continue;
|
|
119
99
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
return value;
|
|
100
|
+
if (record.items !== undefined) {
|
|
101
|
+
current = record.items;
|
|
102
|
+
continue;
|
|
125
103
|
}
|
|
126
|
-
|
|
127
|
-
return value;
|
|
104
|
+
return current;
|
|
128
105
|
}
|
|
106
|
+
return current;
|
|
129
107
|
}
|
|
130
|
-
function
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (!(
|
|
136
|
-
|
|
108
|
+
function receivedValueAtPath(args, pathSegments) {
|
|
109
|
+
let current = args;
|
|
110
|
+
for (const segment of pathSegments) {
|
|
111
|
+
const record = asRecord(current);
|
|
112
|
+
if (record) {
|
|
113
|
+
if (!(segment in record)) {
|
|
114
|
+
return MISSING_VALUE;
|
|
137
115
|
}
|
|
138
|
-
|
|
116
|
+
current = record[segment];
|
|
117
|
+
continue;
|
|
139
118
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
continue;
|
|
119
|
+
if (Array.isArray(current)) {
|
|
120
|
+
const index = Number(segment);
|
|
121
|
+
if (!Number.isInteger(index) || index < 0 || index >= current.length) {
|
|
122
|
+
return MISSING_VALUE;
|
|
145
123
|
}
|
|
146
|
-
|
|
124
|
+
current = current[index];
|
|
125
|
+
continue;
|
|
147
126
|
}
|
|
127
|
+
return MISSING_VALUE;
|
|
148
128
|
}
|
|
129
|
+
return current;
|
|
149
130
|
}
|
|
150
|
-
function
|
|
151
|
-
if (
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
if (isJsonSchemaObject(schema.items)) {
|
|
162
|
-
for (let index = 0; index < value.length; index++) {
|
|
163
|
-
value[index] = coerceWithJsonSchema(value[index], schema.items);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
131
|
+
function receivedTypeOf(value) {
|
|
132
|
+
if (value === MISSING_VALUE)
|
|
133
|
+
return "missing";
|
|
134
|
+
if (value === null)
|
|
135
|
+
return "null";
|
|
136
|
+
if (Array.isArray(value))
|
|
137
|
+
return "array";
|
|
138
|
+
return typeof value;
|
|
166
139
|
}
|
|
167
|
-
function
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
140
|
+
function expectedTypeOf(schema) {
|
|
141
|
+
const record = asRecord(schema);
|
|
142
|
+
if (!record)
|
|
143
|
+
return "unknown";
|
|
144
|
+
if (Array.isArray(record.type))
|
|
145
|
+
return record.type.filter((type) => typeof type === "string").join("|") || "unknown";
|
|
146
|
+
if (typeof record.type === "string")
|
|
147
|
+
return record.type;
|
|
148
|
+
if (literalValues(record))
|
|
149
|
+
return "enum";
|
|
150
|
+
if (record.properties !== undefined)
|
|
151
|
+
return "object";
|
|
152
|
+
if (record.items !== undefined)
|
|
153
|
+
return "array";
|
|
154
|
+
return "unknown";
|
|
155
|
+
}
|
|
156
|
+
function formatFailureShape(errors, args, schema) {
|
|
157
|
+
const seen = new Set();
|
|
158
|
+
const shape = [];
|
|
159
|
+
for (const error of errors) {
|
|
160
|
+
const path = formatValidationPath(error);
|
|
161
|
+
const pathSegments = path === "root" ? [] : path.split(".");
|
|
162
|
+
const expectedSchema = schemaAtPath(schema, pathSegments);
|
|
163
|
+
const value = receivedValueAtPath(args, pathSegments);
|
|
164
|
+
const entry = {
|
|
165
|
+
path,
|
|
166
|
+
expectedType: expectedTypeOf(expectedSchema),
|
|
167
|
+
receivedType: receivedTypeOf(value),
|
|
168
|
+
keyword: error.keyword,
|
|
169
|
+
};
|
|
170
|
+
const key = `${entry.path}\0${entry.expectedType}\0${entry.receivedType}\0${entry.keyword}`;
|
|
171
|
+
if (seen.has(key))
|
|
172
|
+
continue;
|
|
173
|
+
seen.add(key);
|
|
174
|
+
shape.push(entry);
|
|
175
175
|
}
|
|
176
|
-
return
|
|
176
|
+
return shape;
|
|
177
177
|
}
|
|
178
|
-
function
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
178
|
+
function errorKeywords(errors) {
|
|
179
|
+
return [...new Set(errors.map((error) => error.keyword))].sort();
|
|
180
|
+
}
|
|
181
|
+
function literalValues(schema) {
|
|
182
|
+
const record = asRecord(schema);
|
|
183
|
+
if (!record) {
|
|
184
|
+
return undefined;
|
|
184
185
|
}
|
|
185
|
-
if (Array.isArray(
|
|
186
|
-
|
|
186
|
+
if (Array.isArray(record.enum)) {
|
|
187
|
+
return record.enum;
|
|
187
188
|
}
|
|
188
|
-
if (
|
|
189
|
-
|
|
189
|
+
if (record.const !== undefined) {
|
|
190
|
+
return [record.const];
|
|
190
191
|
}
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
for (const
|
|
195
|
-
const
|
|
196
|
-
if (
|
|
197
|
-
|
|
198
|
-
break;
|
|
192
|
+
const anyOf = Array.isArray(record.anyOf) ? record.anyOf : Array.isArray(record.oneOf) ? record.oneOf : undefined;
|
|
193
|
+
if (anyOf) {
|
|
194
|
+
const values = [];
|
|
195
|
+
for (const option of anyOf) {
|
|
196
|
+
const optionRecord = asRecord(option);
|
|
197
|
+
if (!optionRecord || optionRecord.const === undefined) {
|
|
198
|
+
return undefined;
|
|
199
199
|
}
|
|
200
|
+
values.push(optionRecord.const);
|
|
200
201
|
}
|
|
202
|
+
return values;
|
|
201
203
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
function compactSchemaFragment(schema) {
|
|
207
|
+
const record = asRecord(schema);
|
|
208
|
+
if (!record) {
|
|
209
|
+
return {};
|
|
204
210
|
}
|
|
205
|
-
|
|
206
|
-
|
|
211
|
+
const fragment = {};
|
|
212
|
+
const values = literalValues(record);
|
|
213
|
+
if (values) {
|
|
214
|
+
fragment.enum = values;
|
|
215
|
+
return fragment;
|
|
207
216
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const cached = validatorCache.get(key);
|
|
213
|
-
if (cached) {
|
|
214
|
-
return cached;
|
|
217
|
+
for (const key of ["type", "required", "minimum", "maximum", "minLength", "maxLength", "format"]) {
|
|
218
|
+
if (record[key] !== undefined) {
|
|
219
|
+
fragment[key] = record[key];
|
|
220
|
+
}
|
|
215
221
|
}
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
222
|
+
const properties = asRecord(record.properties);
|
|
223
|
+
if (properties) {
|
|
224
|
+
fragment.properties = Object.keys(properties);
|
|
225
|
+
}
|
|
226
|
+
if (record.items !== undefined) {
|
|
227
|
+
fragment.items = compactSchemaFragment(record.items);
|
|
228
|
+
}
|
|
229
|
+
return fragment;
|
|
219
230
|
}
|
|
220
|
-
function
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
231
|
+
function minimalExample(schema) {
|
|
232
|
+
const record = asRecord(schema);
|
|
233
|
+
if (!record) {
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
if (record.default !== undefined) {
|
|
237
|
+
return record.default;
|
|
238
|
+
}
|
|
239
|
+
const values = literalValues(record);
|
|
240
|
+
if (values?.length) {
|
|
241
|
+
return values[0];
|
|
242
|
+
}
|
|
243
|
+
const type = Array.isArray(record.type) ? record.type[0] : record.type;
|
|
244
|
+
switch (type) {
|
|
245
|
+
case "string":
|
|
246
|
+
return "";
|
|
247
|
+
case "number":
|
|
248
|
+
return typeof record.minimum === "number" ? record.minimum : 0;
|
|
249
|
+
case "integer":
|
|
250
|
+
return typeof record.minimum === "number" ? Math.ceil(record.minimum) : 0;
|
|
251
|
+
case "boolean":
|
|
252
|
+
return true;
|
|
253
|
+
case "array":
|
|
254
|
+
return [];
|
|
255
|
+
case "object": {
|
|
256
|
+
const properties = asRecord(record.properties) ?? {};
|
|
257
|
+
const required = Array.isArray(record.required)
|
|
258
|
+
? record.required.filter((key) => typeof key === "string")
|
|
259
|
+
: [];
|
|
260
|
+
return Object.fromEntries(required.map((key) => [key, minimalExample(properties[key]) ?? null]));
|
|
227
261
|
}
|
|
262
|
+
case "null":
|
|
263
|
+
return null;
|
|
264
|
+
default:
|
|
265
|
+
return undefined;
|
|
228
266
|
}
|
|
229
|
-
|
|
230
|
-
|
|
267
|
+
}
|
|
268
|
+
function formatValidationErrors(errors, args, schema) {
|
|
269
|
+
return (errors
|
|
270
|
+
.map((error) => {
|
|
271
|
+
const path = formatValidationPath(error);
|
|
272
|
+
const pathSegments = validationPathSegments(error);
|
|
273
|
+
const expectedSchema = schemaAtPath(schema, pathSegments);
|
|
274
|
+
const expectedFragment = formatCompactJson(compactSchemaFragment(expectedSchema), EXPECTED_FRAGMENT_MAX_LENGTH);
|
|
275
|
+
const example = minimalExample(expectedSchema);
|
|
276
|
+
const received = formatCompactJson(receivedValueAtPath(args, pathSegments), RECEIVED_VALUE_MAX_LENGTH);
|
|
277
|
+
const exampleText = example === undefined ? "" : `; Example: ${formatCompactJson(example, RECEIVED_VALUE_MAX_LENGTH)}`;
|
|
278
|
+
return ` - ${path}: ${error.message}; Expected schema: ${expectedFragment}${exampleText}; Received: ${received}`;
|
|
279
|
+
})
|
|
280
|
+
.join("\n") || "Unknown validation error");
|
|
281
|
+
}
|
|
282
|
+
function validationFailureSignature(errors) {
|
|
283
|
+
return JSON.stringify(errors.map((error) => ({
|
|
284
|
+
path: formatValidationPath(error),
|
|
285
|
+
keyword: error.keyword,
|
|
286
|
+
message: error.message,
|
|
287
|
+
})));
|
|
288
|
+
}
|
|
289
|
+
function formatValidationEnrichment(tool) {
|
|
290
|
+
const example = minimalExample(tool.parameters);
|
|
291
|
+
const exampleText = example === undefined ? "" : `\nValid example:\n${formatCompactJson(example, 2000)}`;
|
|
292
|
+
return `Full tool schema:\n${formatCompactJson(tool.parameters, 4000)}${exampleText}`;
|
|
231
293
|
}
|
|
232
294
|
/**
|
|
233
295
|
* Finds a tool by name and validates the tool call arguments against its TypeBox schema
|
|
@@ -236,46 +298,67 @@ function formatValidationPath(error) {
|
|
|
236
298
|
* @returns The validated arguments
|
|
237
299
|
* @throws Error if tool is not found or validation fails
|
|
238
300
|
*/
|
|
239
|
-
export function validateToolCall(tools, toolCall) {
|
|
301
|
+
export function validateToolCall(tools, toolCall, options) {
|
|
240
302
|
const tool = tools.find((t) => t.name === toolCall.name);
|
|
241
303
|
if (!tool) {
|
|
242
304
|
throw new Error(`Tool "${toolCall.name}" not found`);
|
|
243
305
|
}
|
|
244
|
-
return validateToolArguments(tool, toolCall);
|
|
306
|
+
return validateToolArguments(tool, toolCall, options);
|
|
245
307
|
}
|
|
246
308
|
/**
|
|
247
|
-
* Validates tool call arguments against the tool's TypeBox schema
|
|
309
|
+
* Validates tool call arguments against the tool's TypeBox schema.
|
|
310
|
+
*
|
|
311
|
+
* The hot path is validate-first and allocation-free: valid arguments return the exact
|
|
312
|
+
* argument object emitted by the model. Only invalid calls enter the deterministic
|
|
313
|
+
* repair layer, which applies named, guard-checked shape repairs on a clone.
|
|
314
|
+
*
|
|
248
315
|
* @param tool The tool definition with TypeBox schema
|
|
249
316
|
* @param toolCall The tool call from the LLM
|
|
250
|
-
* @returns The validated (and potentially
|
|
317
|
+
* @returns The validated (and potentially repaired) arguments
|
|
251
318
|
* @throws Error with formatted message if validation fails
|
|
252
319
|
*/
|
|
253
|
-
export function validateToolArguments(tool, toolCall) {
|
|
254
|
-
const args =
|
|
255
|
-
Value.Convert(tool.parameters, args);
|
|
320
|
+
export function validateToolArguments(tool, toolCall, options) {
|
|
321
|
+
const args = toolCall.arguments;
|
|
256
322
|
const validator = getValidator(tool.parameters);
|
|
257
|
-
if (!hasTypeBoxMetadata(tool.parameters) && isJsonSchemaObject(tool.parameters)) {
|
|
258
|
-
const coerced = coerceWithJsonSchema(args, tool.parameters);
|
|
259
|
-
if (coerced !== args) {
|
|
260
|
-
if (isRecord(args) && isRecord(coerced)) {
|
|
261
|
-
for (const key of Object.keys(args)) {
|
|
262
|
-
delete args[key];
|
|
263
|
-
}
|
|
264
|
-
Object.assign(args, coerced);
|
|
265
|
-
}
|
|
266
|
-
else {
|
|
267
|
-
return validator.Check(coerced) ? coerced : args;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
323
|
if (validator.Check(args)) {
|
|
324
|
+
emitToolArgumentValidationTelemetry(options, {
|
|
325
|
+
outcome: "clean",
|
|
326
|
+
tool: toolCall.name,
|
|
327
|
+
failureModes: [],
|
|
328
|
+
repairsApplied: [],
|
|
329
|
+
});
|
|
272
330
|
return args;
|
|
273
331
|
}
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
332
|
+
const validationErrors = [...validator.Errors(args)];
|
|
333
|
+
const repairIssues = analyzeToolArgumentErrors(toolCall.name, tool.parameters, args, validationErrors);
|
|
334
|
+
const failureModes = uniqueFailureModes(repairIssues.flatMap((issue) => issue.modes));
|
|
335
|
+
const repaired = options?.repairEnabled === false
|
|
336
|
+
? undefined
|
|
337
|
+
: repairToolArguments(toolCall.name, tool.parameters, args, validationErrors, (candidate) => validator.Check(candidate));
|
|
338
|
+
if (repaired) {
|
|
339
|
+
const repairsApplied = uniqueRepairModes(repaired.repairsApplied);
|
|
340
|
+
toolCall.repairNotes = repaired.repairs.map((repair) => `[harness] ${repair.name}: ${formatToolRepairNote(repair.name, repair.path)}; executed with repaired arguments.`);
|
|
341
|
+
emitToolArgumentValidationTelemetry(options, {
|
|
342
|
+
outcome: "repaired",
|
|
343
|
+
tool: toolCall.name,
|
|
344
|
+
failureModes,
|
|
345
|
+
repairsApplied,
|
|
346
|
+
});
|
|
347
|
+
return repaired.args;
|
|
348
|
+
}
|
|
349
|
+
emitToolArgumentValidationTelemetry(options, {
|
|
350
|
+
outcome: "bounced",
|
|
351
|
+
tool: toolCall.name,
|
|
352
|
+
failureModes,
|
|
353
|
+
repairsApplied: [],
|
|
354
|
+
failureShape: formatFailureShape(validationErrors, toolCall.arguments, tool.parameters),
|
|
355
|
+
errorKeywords: errorKeywords(validationErrors),
|
|
356
|
+
});
|
|
357
|
+
const errorMessage = `Validation failed for tool "${toolCall.name}":\n${formatValidationErrors(validationErrors, toolCall.arguments, tool.parameters)}\n\nReceived arguments:\n${truncateText(JSON.stringify(toolCall.arguments, null, 2), 2000)}`;
|
|
358
|
+
throw new ToolArgumentValidationError(errorMessage, {
|
|
359
|
+
toolName: toolCall.name,
|
|
360
|
+
signature: validationFailureSignature(validationErrors),
|
|
361
|
+
enrichment: formatValidationEnrichment(tool),
|
|
362
|
+
});
|
|
280
363
|
}
|
|
281
364
|
//# sourceMappingURL=validation.js.map
|