From f1373cd7abc479ae89a219e6a3fad6023f66aa64 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Tue, 14 Jul 2026 05:15:17 +0800 Subject: [PATCH 1/3] refactor: drop unused tool schema defaults --- docs/core-data-structures/tools.md | 2 -- packages/core/tools/README.md | 2 +- packages/core/tools/src/schema.ts | 10 ------- packages/core/tools/tests/tools.spec.ts | 37 ++----------------------- 4 files changed, 3 insertions(+), 48 deletions(-) diff --git a/docs/core-data-structures/tools.md b/docs/core-data-structures/tools.md index 36c59aea17..1d59542404 100644 --- a/docs/core-data-structures/tools.md +++ b/docs/core-data-structures/tools.md @@ -57,8 +57,6 @@ interface SchemaProp { description?: string /** Enum of allowed values (strings only). */ enum?: string[] - /** Default value. */ - default?: unknown /** Nested properties for type: 'object'. */ properties?: SchemaSpec /** Items schema for type: 'array'. */ diff --git a/packages/core/tools/README.md b/packages/core/tools/README.md index d57aeba10b..5f06154ec4 100644 --- a/packages/core/tools/README.md +++ b/packages/core/tools/README.md @@ -77,7 +77,7 @@ ctx.tools.register(defineTool({ The helper converts the author-facing `SchemaSpec` (with `required: true` as a per-property boolean) to standard JSON Schema for the wire format and uses the same typed spec for execute/presentation validation. Raw JSON-Schema tool definitions (from MCP servers) are still accepted by the registry directly. -A `defineTool` tool also **validates the model-generated arguments against its `SchemaSpec` before `execute` runs** (`validateArgs`). The model's JSON is untrusted — `InferArgs` is a compile-time claim, not a runtime guarantee — so on a mismatch (missing required key, wrong primitive, bad enum member, nested violation) the tool throws a `ToolArgsError` (`code: 'INVALID_ARGS'`); the registry turns it into an `isError` result whose text lists the violations, which the model sees and self-corrects from. Validation mirrors the JSON Schema conversion exactly: extra keys are allowed, `default` is not applied, and an `object`/`array` prop without `properties`/`items` only type-checks. Raw-registered tools (MCP) are **not** validated by the harness — they validate their own input. +A `defineTool` tool also **validates the model-generated arguments against its `SchemaSpec` before `execute` runs** (`validateArgs`). The model's JSON is untrusted — `InferArgs` is a compile-time claim, not a runtime guarantee — so on a mismatch (missing required key, wrong primitive, bad enum member, nested violation) the tool throws a `ToolArgsError` (`code: 'INVALID_ARGS'`); the registry turns it into an `isError` result whose text lists the violations, which the model sees and self-corrects from. Validation mirrors the JSON Schema conversion exactly: extra keys are allowed, and an `object`/`array` prop without `properties`/`items` only type-checks. Raw-registered tools (MCP) are **not** validated by the harness — they validate their own input. See `defineTool`, `validateArgs`, `ToolArgsError`, `SchemaSpec`, `InferArgs`, and `schemaSpecToJsonSchema` in the public API for details. diff --git a/packages/core/tools/src/schema.ts b/packages/core/tools/src/schema.ts index d3a2d32e18..ad7c28760e 100644 --- a/packages/core/tools/src/schema.ts +++ b/packages/core/tools/src/schema.ts @@ -39,15 +39,6 @@ export interface SchemaProp { description?: string /** Enum of allowed values (strings only). */ enum?: string[] - /** - * Default value, emitted into the JSON Schema only (validation never applies - * it — see the validator note below). - * - * XXX(unused-default): no tool definition in the repo sets `default`; it rides - * into the wire schema for a model that no tool surfaces it to. Drop the field - * and its converter line unless a real tool needs a model-visible default. - */ - default?: unknown /** Nested properties for type: 'object'. */ properties?: SchemaSpec /** Items schema for type: 'array'. */ @@ -123,7 +114,6 @@ function propToJsonSchema(prop: SchemaProp): { schema: Record; const result: Record = { type: prop.type } if (prop.description) result.description = prop.description if (prop.enum) result.enum = prop.enum - if (prop.default !== undefined) result.default = prop.default const required = prop.required === true diff --git a/packages/core/tools/tests/tools.spec.ts b/packages/core/tools/tests/tools.spec.ts index a2dff84287..2bca8e05fc 100644 --- a/packages/core/tools/tests/tools.spec.ts +++ b/packages/core/tools/tests/tools.spec.ts @@ -904,17 +904,6 @@ describe('schema DSL edge cases', () => { }) }) - it('emits default value in JSON Schema property', () => { - const spec = { - limit: { type: 'number', default: 25 }, - } satisfies SchemaSpec - const jsonSchema = schemaSpecToJsonSchema(spec) - expect(jsonSchema.properties['limit']).toMatchObject({ - type: 'number', - default: 25, - }) - }) - it('handles array items without nested properties (plain type array)', () => { const spec = { tags: { type: 'array', items: { type: 'string' } }, @@ -926,28 +915,12 @@ describe('schema DSL edge cases', () => { }) }) - it('handles enum and default together in one property', () => { - const spec = { - level: { type: 'string', enum: ['low', 'high'], default: 'low' }, - } satisfies SchemaSpec - const jsonSchema = schemaSpecToJsonSchema(spec) - expect(jsonSchema.properties['level']).toMatchObject({ - type: 'string', - enum: ['low', 'high'], - default: 'low', - }) - }) - - it('omits description, enum, default keys when not specified', () => { + it('emits only the type when optional fields are omitted', () => { const spec = { bare: { type: 'string' }, } satisfies SchemaSpec const jsonSchema = schemaSpecToJsonSchema(spec) - const prop = jsonSchema.properties['bare'] as Record - expect(prop).toEqual({ type: 'string' }) - expect('description' in prop).toBe(false) - expect('enum' in prop).toBe(false) - expect('default' in prop).toBe(false) + expect(jsonSchema.properties['bare']).toEqual({ type: 'string' }) }) it('handles array with no items (items omitted)', () => { @@ -1137,12 +1110,6 @@ describe('validateArgs (the runtime-validation RFC, part 1)', () => { expect(validateArgs(spec, { path: '/tmp', extra: 1 })).toEqual([]) }) - it('does not apply defaults (validation only)', () => { - const spec = { limit: { type: 'number', default: 25 } } satisfies SchemaSpec - // absent optional is valid, and validation does not synthesize the default - expect(validateArgs(spec, {})).toEqual([]) - }) - it('type-checks primitives', () => { const spec = { s: { type: 'string' }, From 863116daaf406d3fc6485be819bcba0fc3d275f1 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Tue, 14 Jul 2026 05:23:17 +0800 Subject: [PATCH 2/3] fix: retain dynamic tool schema defaults --- docs/core-data-structures/tools.md | 2 + .../cordis/tool-cordis/tests/mount.spec.ts | 6 ++- packages/core/tools/README.md | 2 +- packages/core/tools/src/schema.ts | 6 +++ packages/core/tools/tests/tools.spec.ts | 37 ++++++++++++++++++- 5 files changed, 49 insertions(+), 4 deletions(-) diff --git a/docs/core-data-structures/tools.md b/docs/core-data-structures/tools.md index 1d59542404..36c59aea17 100644 --- a/docs/core-data-structures/tools.md +++ b/docs/core-data-structures/tools.md @@ -57,6 +57,8 @@ interface SchemaProp { description?: string /** Enum of allowed values (strings only). */ enum?: string[] + /** Default value. */ + default?: unknown /** Nested properties for type: 'object'. */ properties?: SchemaSpec /** Items schema for type: 'array'. */ diff --git a/packages/cordis/tool-cordis/tests/mount.spec.ts b/packages/cordis/tool-cordis/tests/mount.spec.ts index fc29eeb2a0..bf2e57c13e 100644 --- a/packages/cordis/tool-cordis/tests/mount.spec.ts +++ b/packages/cordis/tool-cordis/tests/mount.spec.ts @@ -185,9 +185,13 @@ describe('cordis_mount', () => { // The registered schema is canonical JSON Schema derived from the DSL: // the required array survived, integer became number, extra is optional. const schema = ctx.tools.schemas().find(s => s.name === 'json_schema_tool')! - const parameters = schema.parameters as { properties: Record; required?: string[] } + const parameters = schema.parameters as { + properties: Record + required?: string[] + } expect(parameters.required).toEqual(['text']) expect(parameters.properties.count!.type).toBe('number') + expect(parameters.properties.count!.default).toBe(1) expect(parameters.properties.mode!.enum).toEqual(['fast', 'slow']) // Arg validation enforces the normalized spec: text required, extra not. expect((await call(ctx, 'json_schema_tool', { count: 2 })).isError).toBe(true) diff --git a/packages/core/tools/README.md b/packages/core/tools/README.md index 5f06154ec4..d57aeba10b 100644 --- a/packages/core/tools/README.md +++ b/packages/core/tools/README.md @@ -77,7 +77,7 @@ ctx.tools.register(defineTool({ The helper converts the author-facing `SchemaSpec` (with `required: true` as a per-property boolean) to standard JSON Schema for the wire format and uses the same typed spec for execute/presentation validation. Raw JSON-Schema tool definitions (from MCP servers) are still accepted by the registry directly. -A `defineTool` tool also **validates the model-generated arguments against its `SchemaSpec` before `execute` runs** (`validateArgs`). The model's JSON is untrusted — `InferArgs` is a compile-time claim, not a runtime guarantee — so on a mismatch (missing required key, wrong primitive, bad enum member, nested violation) the tool throws a `ToolArgsError` (`code: 'INVALID_ARGS'`); the registry turns it into an `isError` result whose text lists the violations, which the model sees and self-corrects from. Validation mirrors the JSON Schema conversion exactly: extra keys are allowed, and an `object`/`array` prop without `properties`/`items` only type-checks. Raw-registered tools (MCP) are **not** validated by the harness — they validate their own input. +A `defineTool` tool also **validates the model-generated arguments against its `SchemaSpec` before `execute` runs** (`validateArgs`). The model's JSON is untrusted — `InferArgs` is a compile-time claim, not a runtime guarantee — so on a mismatch (missing required key, wrong primitive, bad enum member, nested violation) the tool throws a `ToolArgsError` (`code: 'INVALID_ARGS'`); the registry turns it into an `isError` result whose text lists the violations, which the model sees and self-corrects from. Validation mirrors the JSON Schema conversion exactly: extra keys are allowed, `default` is not applied, and an `object`/`array` prop without `properties`/`items` only type-checks. Raw-registered tools (MCP) are **not** validated by the harness — they validate their own input. See `defineTool`, `validateArgs`, `ToolArgsError`, `SchemaSpec`, `InferArgs`, and `schemaSpecToJsonSchema` in the public API for details. diff --git a/packages/core/tools/src/schema.ts b/packages/core/tools/src/schema.ts index ad7c28760e..e912252b18 100644 --- a/packages/core/tools/src/schema.ts +++ b/packages/core/tools/src/schema.ts @@ -39,6 +39,11 @@ export interface SchemaProp { description?: string /** Enum of allowed values (strings only). */ enum?: string[] + /** + * Model-visible JSON Schema default annotation. Validation does not apply it; + * dynamic tool mounts may supply it even though first-party definitions do not. + */ + default?: unknown /** Nested properties for type: 'object'. */ properties?: SchemaSpec /** Items schema for type: 'array'. */ @@ -114,6 +119,7 @@ function propToJsonSchema(prop: SchemaProp): { schema: Record; const result: Record = { type: prop.type } if (prop.description) result.description = prop.description if (prop.enum) result.enum = prop.enum + if (prop.default !== undefined) result.default = prop.default const required = prop.required === true diff --git a/packages/core/tools/tests/tools.spec.ts b/packages/core/tools/tests/tools.spec.ts index 2bca8e05fc..a2dff84287 100644 --- a/packages/core/tools/tests/tools.spec.ts +++ b/packages/core/tools/tests/tools.spec.ts @@ -904,6 +904,17 @@ describe('schema DSL edge cases', () => { }) }) + it('emits default value in JSON Schema property', () => { + const spec = { + limit: { type: 'number', default: 25 }, + } satisfies SchemaSpec + const jsonSchema = schemaSpecToJsonSchema(spec) + expect(jsonSchema.properties['limit']).toMatchObject({ + type: 'number', + default: 25, + }) + }) + it('handles array items without nested properties (plain type array)', () => { const spec = { tags: { type: 'array', items: { type: 'string' } }, @@ -915,12 +926,28 @@ describe('schema DSL edge cases', () => { }) }) - it('emits only the type when optional fields are omitted', () => { + it('handles enum and default together in one property', () => { + const spec = { + level: { type: 'string', enum: ['low', 'high'], default: 'low' }, + } satisfies SchemaSpec + const jsonSchema = schemaSpecToJsonSchema(spec) + expect(jsonSchema.properties['level']).toMatchObject({ + type: 'string', + enum: ['low', 'high'], + default: 'low', + }) + }) + + it('omits description, enum, default keys when not specified', () => { const spec = { bare: { type: 'string' }, } satisfies SchemaSpec const jsonSchema = schemaSpecToJsonSchema(spec) - expect(jsonSchema.properties['bare']).toEqual({ type: 'string' }) + const prop = jsonSchema.properties['bare'] as Record + expect(prop).toEqual({ type: 'string' }) + expect('description' in prop).toBe(false) + expect('enum' in prop).toBe(false) + expect('default' in prop).toBe(false) }) it('handles array with no items (items omitted)', () => { @@ -1110,6 +1137,12 @@ describe('validateArgs (the runtime-validation RFC, part 1)', () => { expect(validateArgs(spec, { path: '/tmp', extra: 1 })).toEqual([]) }) + it('does not apply defaults (validation only)', () => { + const spec = { limit: { type: 'number', default: 25 } } satisfies SchemaSpec + // absent optional is valid, and validation does not synthesize the default + expect(validateArgs(spec, {})).toEqual([]) + }) + it('type-checks primitives', () => { const spec = { s: { type: 'string' }, From 60c4d523d1a868d6766b828a2e5a0771f7c42205 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Thu, 16 Jul 2026 00:59:24 +0800 Subject: [PATCH 3/3] docs(tools): align schema default contract --- packages/core/tools/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/tools/README.md b/packages/core/tools/README.md index 0f7974e0f9..00bbb67ff5 100644 --- a/packages/core/tools/README.md +++ b/packages/core/tools/README.md @@ -142,7 +142,7 @@ The available tools: - **Native tool calls execute sequentially** — `ToolDefinition` carries no concurrency-safety metadata; adding it (and parallel execution in the loop) waits on the deferred tool-shapes review (`TODO(review)`). - **`tools/pre-execute` deliberately cannot rewrite `exec.arguments`** — logged and rendered args would desync from what ran; the rewrite design is [a proposed RFC](../../../docs/rfc/proposed/feature/2026-06-30-pre-tool-input-rewrite.md). -- **`defineTool`'s schema DSL is a deliberate subset** — string/number/boolean/object/array with string-only `enum`; `validateArgs` tolerates extra keys and never applies `default` (`XXX(unused-default)` flags removing that field); raw-registered JSON-Schema tools validate their own input. +- **`defineTool`'s schema DSL is a deliberate subset** — string/number/boolean/object/array with string-only `enum`; `validateArgs` tolerates extra keys and preserves `default` as a model-visible JSON Schema annotation without applying it during validation; dynamic Cordis mounts may supply defaults even though first-party definitions do not, while raw-registered JSON-Schema tools validate their own input. - **`timeoutMs` on a definition is declarative only** — the registry never enforces deadlines; enforcement requires the `@deepseek-ai/dsh-timeout-policy` wrapper. - **Code Mode is TypeScript-only and the presentation mode is service-wide** — `mode: code`/`both` rejects prompt assembly unless `ctx.codeRuntime.language === 'typescript'`; scoped restrictions/shadows still choose each agent's visible bindings, but one tool cannot be native-only while another is code-only. - **Code Mode bindings return text only** — non-text content blocks in a sub-call result collapse to `[ content]` placeholders.