From 089d4159d3b9e0a2880b403b0ce93ec842816a87 Mon Sep 17 00:00:00 2001 From: tt-a1i <53142663+tt-a1i@users.noreply.github.com> Date: Sat, 20 Jun 2026 09:19:59 +0800 Subject: [PATCH] fix(agent-core): add schema hints to tool arg errors --- .changeset/tool-validation-hints.md | 5 ++ .../agent-core/src/tools/args-validator.ts | 76 +++++++++++++++++-- .../test/tools/input-schema-io.test.ts | 54 +++++++++++++ 3 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 .changeset/tool-validation-hints.md diff --git a/.changeset/tool-validation-hints.md b/.changeset/tool-validation-hints.md new file mode 100644 index 0000000000..ab8c1233c5 --- /dev/null +++ b/.changeset/tool-validation-hints.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Improve tool argument validation errors with schema hints. diff --git a/packages/agent-core/src/tools/args-validator.ts b/packages/agent-core/src/tools/args-validator.ts index ce967ccb0b..b8e67016ee 100644 --- a/packages/agent-core/src/tools/args-validator.ts +++ b/packages/agent-core/src/tools/args-validator.ts @@ -3,13 +3,13 @@ import Ajv2019 from 'ajv/dist/2019'; import Ajv2020 from 'ajv/dist/2020'; import addFormats from 'ajv-formats'; -const DRAFT_07_AJV = new Ajv({ strict: false, allErrors: true }); +const DRAFT_07_AJV = new Ajv({ strict: false, allErrors: true, verbose: true }); addFormats(DRAFT_07_AJV); -const DRAFT_2019_AJV = new Ajv2019({ strict: false, allErrors: true }); +const DRAFT_2019_AJV = new Ajv2019({ strict: false, allErrors: true, verbose: true }); addFormats(DRAFT_2019_AJV); -const DRAFT_2020_AJV = new Ajv2020({ strict: false, allErrors: true }); +const DRAFT_2020_AJV = new Ajv2020({ strict: false, allErrors: true, verbose: true }); addFormats(DRAFT_2020_AJV); const DRAFT_2019_KEYWORDS = new Set([ @@ -61,13 +61,79 @@ export interface JsonObject extends Record {} export type ToolArgsValidator = ValidateFunction; +type SchemaObject = Record; + +function asSchemaObject(value: unknown): SchemaObject | undefined { + if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined; + return value as SchemaObject; +} + +function schemaProperties(error: ErrorObject): SchemaObject | undefined { + const parentSchema = asSchemaObject(error.parentSchema); + if (parentSchema === undefined) return undefined; + return asSchemaObject(parentSchema['properties']); +} + +function validProperties(error: ErrorObject): string | undefined { + const properties = schemaProperties(error); + if (properties === undefined) return undefined; + + const names = Object.keys(properties); + return names.length === 0 ? undefined : names.join(', '); +} + +function formatTypeList(value: unknown): string | undefined { + if (typeof value === 'string') return value; + + if (Array.isArray(value)) { + const types = value.filter((item): item is string => typeof item === 'string'); + return types.length === 0 ? undefined : types.join(' or '); + } + + return undefined; +} + +function expectedType(schema: unknown): string | undefined { + const schemaObject = asSchemaObject(schema); + if (schemaObject === undefined) return undefined; + + const directType = formatTypeList(schemaObject['type']); + if (directType !== undefined) return directType; + + for (const key of ['anyOf', 'oneOf'] as const) { + const branches = schemaObject[key]; + if (!Array.isArray(branches)) continue; + + const branchTypes = new Set(); + for (const branch of branches) { + const branchType = expectedType(branch); + if (branchType !== undefined) branchTypes.add(branchType); + } + if (branchTypes.size > 0) return [...branchTypes].join(' or '); + } + + return undefined; +} + +function expectedPropertyType(error: ErrorObject, property: string): string | undefined { + const properties = schemaProperties(error); + if (properties === undefined) return undefined; + return expectedType(properties[property]); +} + function formatValidationError(error: ErrorObject): string { if (error.keyword === 'required' && 'missingProperty' in error.params) { - return `must have required property '${String(error.params['missingProperty'])}'`; + const property = String(error.params['missingProperty']); + const type = expectedPropertyType(error, property); + const typeHint = type === undefined ? '' : ` (expected ${type})`; + return `must have required property '${property}'${typeHint}`; } if (error.keyword === 'additionalProperties' && 'additionalProperty' in error.params) { - return `must NOT have additional property '${String(error.params['additionalProperty'])}'`; + const property = String(error.params['additionalProperty']); + const properties = validProperties(error); + const propertiesHint = properties === undefined ? '' : `; valid properties: ${properties}`; + return `must NOT have additional property '${property}'${propertiesHint}`; } const path = error.instancePath ? `${error.instancePath} ` : ''; diff --git a/packages/agent-core/test/tools/input-schema-io.test.ts b/packages/agent-core/test/tools/input-schema-io.test.ts index 3be6558598..aa5dcbdeb3 100644 --- a/packages/agent-core/test/tools/input-schema-io.test.ts +++ b/packages/agent-core/test/tools/input-schema-io.test.ts @@ -18,6 +18,8 @@ import { FLAG_DEFINITIONS, FlagResolver } from '../../src/flags'; import { TaskListTool } from '../../src/tools/background/task-list'; import { compileToolArgsValidator, validateToolArgs } from '../../src/tools/args-validator'; import { AskUserQuestionTool } from '../../src/tools/builtin/collaboration/ask-user'; +import { ReadInputSchema } from '../../src/tools/builtin/file/read'; +import { toInputJsonSchema } from '../../src/tools/support/input-schema'; /** Collect every `required` array nested anywhere inside a JSON Schema. */ function collectRequired(schema: unknown, acc: string[] = []): string[] { @@ -96,3 +98,55 @@ describe('builtin tool input JSON Schema', () => { expect(validateToolArgs(validator, { questions: [question] })).not.toBeNull(); }); }); + +describe('tool argument validation errors', () => { + it('lists valid properties for unknown arguments', () => { + const validator = compileToolArgsValidator(toInputJsonSchema(ReadInputSchema)); + + expect(validateToolArgs(validator, { path: '/tmp/a.txt', offset: 10 })).toBe( + "must NOT have additional property 'offset'; valid properties: path, line_offset, n_lines", + ); + }); + + it('includes the expected type for missing required arguments', () => { + const validator = compileToolArgsValidator(toInputJsonSchema(ReadInputSchema)); + + expect(validateToolArgs(validator, { n_lines: 10 })).toBe( + "must have required property 'path' (expected string)", + ); + }); + + it('uses the nested object schema for unknown nested arguments', () => { + const validator = compileToolArgsValidator({ + type: 'object', + properties: { + root: { type: 'string' }, + nested: { + type: 'object', + properties: { + child: { type: 'string' }, + }, + additionalProperties: false, + }, + }, + additionalProperties: false, + }); + + expect(validateToolArgs(validator, { nested: { child: 'ok', root: 'wrong level' } })).toBe( + "must NOT have additional property 'root'; valid properties: child", + ); + }); + + it('keeps the old required message when the expected type is unknown', () => { + const validator = compileToolArgsValidator({ + type: 'object', + properties: { + path: { description: 'A path-like value accepted by the tool.' }, + }, + required: ['path'], + additionalProperties: false, + }); + + expect(validateToolArgs(validator, {})).toBe("must have required property 'path'"); + }); +});