Merge refreshed canonical outputs into typed Code Mode results

# Conflicts:
#	docs/module-graph.md
#	examples/acp-agent/tests/snapshots/advanced-toolchain/system-prompt.expected.md
#	examples/acp-agent/tests/snapshots/both-mode-turn/system-prompt.expected.md
#	examples/acp-agent/tests/snapshots/code-mode-turn/system-prompt.expected.md
#	examples/acp-agent/tests/snapshots/code-mode-workspace-context/session.jsonl
#	examples/acp-agent/tests/snapshots/code-mode-workspace-context/system-prompt.expected.md
#	packages/core/tools/src/ts-types.ts
This commit is contained in:
Tianyi Cui
2026-07-22 21:49:35 +08:00
397 changed files with 16516 additions and 2988 deletions

View File

@@ -87,7 +87,7 @@ ctx.tools.register(defineTool({
}))
```
The unified schema DSL uses `ParameterSchemaSpec` for the implicit open parameter object and `ValueSchemaSpec` for any JSON-value root. It supports `string`, `number`, `integer`, `boolean`, `null`, `array`, `object`, author-only `json`, and exact-one `oneOf`; scalar `enum`/`const` values are type-correct. Every explicit DSL object declares `additionalProperties: true | false`, while the implicit parameter root and raw JSON Schema keep the standard open default.
The unified schema DSL uses `ParameterSchemaSpec` for the implicit open parameter object and `ValueSchemaSpec` for any JSON-value root. It supports `string`, `number`, `integer`, `boolean`, `null`, `array`, `object`, author-only `json`, and exact-one `oneOf`; scalar `enum`/`const` values are type-correct. Every explicit DSL object declares `additionalProperties: true | false`, while the implicit parameter root and raw JSON Schema keep the standard open default. Compilation, validation, registry detachment, and schema-to-TypeScript rendering use explicit work stacks, so valid deep schemas are memory-bounded rather than call-stack-bounded.
A `defineTool` definition validates model arguments before execution and turns missing required values, wrong primitives, invalid enum members, and nested violations into `ToolArgsError` (`INVALID_ARGS`) for the normal error-result path. It also infers the body return and pure output projectors from `output.schema`; the registry snapshots and validates the returned lossless JSON before presentation. The implicit parameter root is open; an explicit object accepts extra keys only with `additionalProperties: true`, and a closed object with no declared properties accepts only `{}`. Raw JSON Schema objects remain open unless they explicitly set `additionalProperties: false`. Defaults are not applied; open objects without `properties` and arrays without `items` receive only a container type check. Raw-registered tools own input validation but still declare and receive registry-enforced output.

View File

@@ -873,10 +873,14 @@ export class ToolRegistry extends Service {
/** Project one definition onto the model-facing schema fields. */
private schemaOf(definition: ToolDefinition, detachParameters: boolean): ToolSchema {
const { name, description, parameters } = definition
const detached = detachParameters ? snapshotJsonValue(parameters) : parameters
if (detached === undefined) {
throw new Error(`tool "${name}" parameters must be lossless JSON before schema projection`)
}
return {
name,
description,
parameters: detachParameters ? structuredClone(parameters) : parameters,
parameters: detached,
}
}

View File

@@ -116,18 +116,70 @@ function scalarMatches(type: JsonSchemaScalarType, value: unknown): value is Jso
}
}
/** Collect every violation for one raw schema node. */
function checkSchemaNode(node: unknown, path: string, violations: string[], seen: Set<object>): void {
if (!isPlainJsonRecord(node)) {
violations.push(`${path} must be a schema object`)
return
/** Deferred work for the stack-safe raw-schema walk. */
type SchemaWalkTask =
| { kind: 'enter'; node: unknown; path: string }
| { kind: 'leave'; node: object }
| { kind: 'one-of-tail'; node: Record<string, unknown>; path: string }
| { kind: 'object-tail'; node: Record<string, unknown>; path: string; properties: unknown }
/** Keywords that are invalid beside `oneOf`. */
const ONE_OF_SIBLING_KEYWORDS = ['properties', 'required', 'additionalProperties', 'items', 'enum', 'const'] as const
/** Validate object-only fields after its property schemas have been visited. */
function checkObjectSchemaTail(
node: Record<string, unknown>,
path: string,
properties: unknown,
violations: string[],
): void {
const required = node.required
if (Object.hasOwn(node, 'required')) {
if (!Array.isArray(required) || required.some(entry => typeof entry !== 'string')) {
violations.push(`${path}.required must be an array of strings`)
} else {
const declared = isPlainJsonRecord(properties) ? properties : {}
for (const key of required as string[]) {
if (!Object.hasOwn(declared, key)) violations.push(`${path}.required names "${key}" which is not in properties`)
}
}
}
if (seen.has(node)) {
violations.push(`${path} is circular`)
return
if (Object.hasOwn(node, 'additionalProperties') && typeof node.additionalProperties !== 'boolean') {
violations.push(`${path}.additionalProperties must be a boolean`)
}
seen.add(node)
try {
}
/** Collect every violation for one raw schema tree without using the JavaScript call stack. */
function checkSchemaNode(root: unknown, rootPath: string, violations: string[], seen: Set<object>): void {
const tasks: SchemaWalkTask[] = [{ kind: 'enter', node: root, path: rootPath }]
for (let task = tasks.pop(); task !== undefined; task = tasks.pop()) {
if (task.kind === 'leave') {
seen.delete(task.node)
continue
}
if (task.kind === 'one-of-tail') {
for (const key of ONE_OF_SIBLING_KEYWORDS) {
if (Object.hasOwn(task.node, key)) violations.push(`${task.path}.${key} is not supported beside oneOf`)
}
continue
}
if (task.kind === 'object-tail') {
checkObjectSchemaTail(task.node, task.path, task.properties, violations)
continue
}
const { node, path } = task
if (!isPlainJsonRecord(node)) {
violations.push(`${path} must be a schema object`)
continue
}
if (seen.has(node)) {
violations.push(`${path} is circular`)
continue
}
seen.add(node)
tasks.push({ kind: 'leave', node })
for (const key of Object.keys(node)) {
if (CONSTRAINT_KEYWORDS.has(key)) continue
if (ANNOTATION_KEYWORDS.has(key)) {
@@ -151,28 +203,26 @@ function checkSchemaNode(node: unknown, path: string, violations: string[], seen
const hasOneOf = Object.hasOwn(node, 'oneOf')
if (hasType && hasOneOf) {
violations.push(`${path} cannot declare both type and oneOf`)
return
continue
}
if (!hasType && !hasOneOf) {
for (const key of ['properties', 'required', 'additionalProperties', 'items', 'enum', 'const']) {
for (const key of ONE_OF_SIBLING_KEYWORDS) {
if (Object.hasOwn(node, key)) violations.push(`${path}.${key} requires type or oneOf`)
}
return
continue
}
if (hasOneOf) {
const oneOf = node.oneOf
tasks.push({ kind: 'one-of-tail', node, path })
if (!Array.isArray(oneOf) || oneOf.length < 2) {
violations.push(`${path}.oneOf must be an array of at least two schemas`)
} else {
for (let index = 0; index < oneOf.length; index++) {
checkSchemaNode(oneOf[index], `${path}.oneOf[${index}]`, violations, seen)
for (let index = oneOf.length - 1; index >= 0; index--) {
tasks.push({ kind: 'enter', node: oneOf[index], path: `${path}.oneOf[${index}]` })
}
}
for (const key of ['properties', 'required', 'additionalProperties', 'items', 'enum', 'const']) {
if (Object.hasOwn(node, key)) violations.push(`${path}.${key} is not supported beside oneOf`)
}
return
continue
}
const type = node.type
@@ -180,7 +230,7 @@ function checkSchemaNode(node: unknown, path: string, violations: string[], seen
violations.push(Array.isArray(type)
? `${path}.type must be a single type string (type arrays are not supported)`
: `${path}.type must be one of ${SCHEMA_TYPES.join('/')}`)
return
continue
}
const schemaType = type as JsonSchemaType
const allowedFor: Record<string, JsonSchemaType[]> = {
@@ -200,33 +250,24 @@ function checkSchemaNode(node: unknown, path: string, violations: string[], seen
switch (schemaType) {
case 'object': {
const properties = node.properties
tasks.push({ kind: 'object-tail', node, path, properties })
if (Object.hasOwn(node, 'properties')) {
if (!isPlainJsonRecord(properties)) {
violations.push(`${path}.properties must be an object of schemas`)
} else {
for (const [key, child] of Object.entries(properties)) {
checkSchemaNode(child, `${path}.properties.${key}`, violations, seen)
const entries = Object.entries(properties)
for (let index = entries.length - 1; index >= 0; index--) {
const entry = entries[index]
/* v8 ignore next -- the loop is bounded by the captured entry count. */
if (entry === undefined) continue
tasks.push({ kind: 'enter', node: entry[1], path: `${path}.properties.${entry[0]}` })
}
}
}
const required = node.required
if (Object.hasOwn(node, 'required')) {
if (!Array.isArray(required) || required.some(entry => typeof entry !== 'string')) {
violations.push(`${path}.required must be an array of strings`)
} else {
const declared = isPlainJsonRecord(properties) ? properties : {}
for (const key of required as string[]) {
if (!Object.hasOwn(declared, key)) violations.push(`${path}.required names "${key}" which is not in properties`)
}
}
}
if (Object.hasOwn(node, 'additionalProperties') && typeof node.additionalProperties !== 'boolean') {
violations.push(`${path}.additionalProperties must be a boolean`)
}
break
}
case 'array': {
if (Object.hasOwn(node, 'items')) checkSchemaNode(node.items, `${path}.items`, violations, seen)
if (Object.hasOwn(node, 'items')) tasks.push({ kind: 'enter', node: node.items, path: `${path}.items` })
break
}
case 'string':
@@ -238,10 +279,8 @@ function checkSchemaNode(node: unknown, path: string, violations: string[], seen
const enumValid = Array.isArray(allowed)
&& allowed.length > 0
&& allowed.every(entry => scalarMatches(schemaType, entry))
if (Object.hasOwn(node, 'enum')) {
if (!enumValid) {
violations.push(`${path}.enum must be a non-empty array of ${schemaType} values`)
}
if (Object.hasOwn(node, 'enum') && !enumValid) {
violations.push(`${path}.enum must be a non-empty array of ${schemaType} values`)
}
const constValid = scalarMatches(schemaType, node.const)
if (Object.hasOwn(node, 'const')) {
@@ -256,8 +295,6 @@ function checkSchemaNode(node: unknown, path: string, violations: string[], seen
/* v8 ignore next -- schemaType was narrowed from the closed SCHEMA_TYPES table above. */
default: assertNever(schemaType, 'JsonSchemaType')
}
} finally {
seen.delete(node)
}
}
@@ -308,81 +345,57 @@ function propertyPath(path: string, key: string): string {
return path === '' ? key : `${path}.${key}`
}
/** Contain hostile getters/proxies so validation remains total for arbitrary values. */
function checkValue(node: JsonSchemaNode, value: unknown, path: string): string[] {
if (node.type !== undefined && !(SCHEMA_TYPES as readonly unknown[]).includes(node.type)) {
return checkValueUnchecked(node, value, path)
}
try {
return checkValueUnchecked(node, value, path)
} catch {
return [`"${diagnosticPath(path)}" must be a lossless JSON value`]
/** One child evaluation deferred by a container or exact-one union frame. */
interface ValueChild {
readonly node: JsonSchemaNode
readonly value: unknown
readonly path: string
}
/** Explicit call frame for stack-safe schema-value validation. */
interface ValueFrame {
readonly node: JsonSchemaNode
readonly value: unknown
readonly path: string
catches: boolean
phase: 'start' | 'children'
kind?: 'oneOf' | 'object' | 'array'
children: ValueChild[]
childIndex: number
violations: string[]
tailViolations: string[]
matches: number
}
/** The generic exception-containment diagnostic owned by one valid schema node. */
function losslessValueViolation(path: string): string[] {
return [`"${diagnosticPath(path)}" must be a lossless JSON value`]
}
/** Append diagnostics without spreading a potentially wide child result as call arguments. */
function appendViolations(target: string[], source: readonly string[]): void {
for (const violation of source) target.push(violation)
}
/** Initialize one validation frame with empty aggregation state. */
function valueFrame(node: JsonSchemaNode, value: unknown, path: string): ValueFrame {
return {
node,
value,
path,
catches: false,
phase: 'start',
children: [],
childIndex: 0,
violations: [],
tailViolations: [],
matches: 0,
}
}
/** Collect value violations for one trusted schema node after the exception boundary. */
function checkValueUnchecked(node: JsonSchemaNode, value: unknown, path: string): string[] {
if (node.oneOf !== undefined) {
const matches = node.oneOf.filter(branch => checkValue(branch, value, path).length === 0).length
return matches === 1 ? [] : [`"${diagnosticPath(path)}" must match exactly one oneOf branch (matched ${matches})`]
}
if (node.type === undefined) {
return safelyIsJsonValue(value) ? [] : [`"${diagnosticPath(path)}" must be a lossless JSON value`]
}
switch (node.type) {
case 'object': {
if (!isPlainJsonRecord(value)) return [`"${diagnosticPath(path)}" must be an object`]
const violations: string[] = []
const properties = node.properties ?? {}
for (const key of node.required ?? []) {
if (!Object.hasOwn(value, key) || value[key] === undefined) violations.push(`missing required property "${propertyPath(path, key)}"`)
}
for (const [key, child] of Object.entries(properties)) {
if (!Object.hasOwn(value, key) || value[key] === undefined) continue
violations.push(...checkValue(child, value[key], propertyPath(path, key)))
}
if (node.additionalProperties === false) {
for (const key of Object.keys(value)) {
if (!Object.hasOwn(properties, key)) violations.push(`"${propertyPath(path, key)}" is not a declared property (additionalProperties: false)`)
}
}
if (violations.length > 0) return violations
return safelyIsJsonValue(value) ? [] : [`"${diagnosticPath(path)}" must be a lossless JSON object`]
}
case 'array': {
if (!Array.isArray(value)) return [`"${diagnosticPath(path)}" must be an array`]
const items = node.items
const violations = items === undefined
? []
: value.flatMap((entry, index) => checkValue(items, entry, `${path}[${index}]`))
if (violations.length > 0) return violations
return safelyIsJsonValue(value) ? [] : [`"${diagnosticPath(path)}" must be a dense lossless JSON array`]
}
case 'string': {
if (typeof value !== 'string') return [`"${diagnosticPath(path)}" must be a string`]
break
}
case 'number': {
if (typeof value !== 'number') return [`"${diagnosticPath(path)}" must be a number`]
if (!isJsonNumber(value)) return [`"${diagnosticPath(path)}" must be a finite JSON number`]
break
}
case 'integer': {
if (!isJsonNumber(value) || !Number.isInteger(value)) return [`"${diagnosticPath(path)}" must be an integer`]
break
}
case 'boolean': {
if (typeof value !== 'boolean') return [`"${diagnosticPath(path)}" must be a boolean`]
break
}
case 'null': {
if (value !== null) return [`"${diagnosticPath(path)}" must be null`]
break
}
default: return assertNever(node.type, 'JsonSchemaType')
}
if (node.enum !== undefined && !node.enum.includes(value)) {
/** Validate one scalar node after its primitive type check. */
function checkScalarValue(node: JsonSchemaNode, value: unknown, path: string): string[] {
if (node.enum !== undefined && !node.enum.includes(value as JsonSchemaScalar)) {
return [`"${diagnosticPath(path)}" must be one of ${JSON.stringify(node.enum)}`]
}
if (Object.hasOwn(node, 'const') && value !== node.const) {
@@ -391,6 +404,165 @@ function checkValueUnchecked(node: JsonSchemaNode, value: unknown, path: string)
return []
}
/** Validate one trusted schema/value pair with explicit frames rather than recursive calls. */
function checkValue(schema: JsonSchemaNode, value: unknown, path: string): string[] {
const frames: ValueFrame[] = [valueFrame(schema, value, path)]
let rootResult: string[] | undefined
const receive = (result: string[]): void => {
const parent = frames.at(-1)
if (parent === undefined) {
rootResult = result
return
}
if (parent.kind === 'oneOf') {
if (result.length === 0) parent.matches++
} else {
appendViolations(parent.violations, result)
}
}
const finish = (result: string[]): void => {
frames.pop()
receive(result)
}
while (frames.length > 0) {
const frame = frames.at(-1)
/* v8 ignore next -- the loop condition guarantees a current frame. */
if (frame === undefined) break
try {
if (frame.phase === 'children') {
if (frame.childIndex < frame.children.length) {
const child = frame.children[frame.childIndex]
/* v8 ignore next -- childIndex is bounded by children.length. */
if (child === undefined) throw new Error('missing schema-value child frame')
frame.childIndex++
frames.push(valueFrame(child.node, child.value, child.path))
continue
}
if (frame.kind === 'oneOf') {
finish(frame.matches === 1 ? [] : [`"${diagnosticPath(frame.path)}" must match exactly one oneOf branch (matched ${frame.matches})`])
continue
}
appendViolations(frame.violations, frame.tailViolations)
if (frame.violations.length > 0) {
finish(frame.violations)
} else if (frame.kind === 'object') {
finish(safelyIsJsonValue(frame.value) ? [] : [`"${diagnosticPath(frame.path)}" must be a lossless JSON object`])
} else {
finish(safelyIsJsonValue(frame.value) ? [] : [`"${diagnosticPath(frame.path)}" must be a dense lossless JSON array`])
}
continue
}
const nodeType = frame.node.type
frame.catches = !(nodeType !== undefined && !(SCHEMA_TYPES as readonly unknown[]).includes(nodeType))
const oneOf = frame.node.oneOf
if (oneOf !== undefined) {
frame.kind = 'oneOf'
frame.children = Array.from(oneOf, branch => ({ node: branch, value: frame.value, path: frame.path }))
frame.childIndex = 0
frame.matches = 0
frame.phase = 'children'
continue
}
if (nodeType === undefined) {
finish(safelyIsJsonValue(frame.value) ? [] : losslessValueViolation(frame.path))
continue
}
switch (nodeType) {
case 'object': {
if (!isPlainJsonRecord(frame.value)) {
finish([`"${diagnosticPath(frame.path)}" must be an object`])
break
}
const properties = frame.node.properties ?? {}
const violations: string[] = []
for (const key of frame.node.required ?? []) {
if (!Object.hasOwn(frame.value, key) || frame.value[key] === undefined) {
violations.push(`missing required property "${propertyPath(frame.path, key)}"`)
}
}
const children: ValueChild[] = []
for (const [key, child] of Object.entries(properties)) {
if (!Object.hasOwn(frame.value, key) || frame.value[key] === undefined) continue
children.push({ node: child, value: frame.value[key], path: propertyPath(frame.path, key) })
}
const tailViolations: string[] = []
if (frame.node.additionalProperties === false) {
for (const key of Object.keys(frame.value)) {
if (!Object.hasOwn(properties, key)) {
tailViolations.push(`"${propertyPath(frame.path, key)}" is not a declared property (additionalProperties: false)`)
}
}
}
frame.kind = 'object'
frame.children = children
frame.childIndex = 0
frame.violations = violations
frame.tailViolations = tailViolations
frame.phase = 'children'
break
}
case 'array': {
if (!Array.isArray(frame.value)) {
finish([`"${diagnosticPath(frame.path)}" must be an array`])
break
}
const items = frame.node.items
const children = items === undefined
? []
: frame.value.flatMap((entry, index): ValueChild[] => [{ node: items, value: entry, path: `${frame.path}[${index}]` }])
frame.kind = 'array'
frame.children = children
frame.childIndex = 0
frame.violations = []
frame.phase = 'children'
break
}
case 'string':
finish(typeof frame.value === 'string'
? checkScalarValue(frame.node, frame.value, frame.path)
: [`"${diagnosticPath(frame.path)}" must be a string`])
break
case 'number':
finish(typeof frame.value !== 'number'
? [`"${diagnosticPath(frame.path)}" must be a number`]
: !isJsonNumber(frame.value)
? [`"${diagnosticPath(frame.path)}" must be a finite JSON number`]
: checkScalarValue(frame.node, frame.value, frame.path))
break
case 'integer':
finish(!isJsonNumber(frame.value) || !Number.isInteger(frame.value)
? [`"${diagnosticPath(frame.path)}" must be an integer`]
: checkScalarValue(frame.node, frame.value, frame.path))
break
case 'boolean':
finish(typeof frame.value === 'boolean'
? checkScalarValue(frame.node, frame.value, frame.path)
: [`"${diagnosticPath(frame.path)}" must be a boolean`])
break
case 'null':
finish(frame.value === null
? checkScalarValue(frame.node, frame.value, frame.path)
: [`"${diagnosticPath(frame.path)}" must be null`])
break
default:
finish(assertNever(nodeType, 'JsonSchemaType'))
}
} catch (error) {
let failed = frames.pop()
while (failed !== undefined && !failed.catches) failed = frames.pop()
if (failed === undefined) throw error
receive(losslessValueViolation(failed.path))
}
}
/* v8 ignore next -- every root frame finishes or throws. */
return rootResult ?? losslessValueViolation(path)
}
/**
* Validate a candidate value against an asserted raw schema. The function is
* total for arbitrary values and returns path-qualified violations.

View File

@@ -186,66 +186,172 @@ function assertAuthorKeys(source: Record<string, unknown>, path: string, allowed
}
}
/** Compile one implicit property map, collecting per-property requiredness. */
function compilePropertyMap(
input: unknown,
path: string,
seen: Set<object>,
): { properties: Record<string, JsonSchemaNode>; required?: string[] } {
if (!isPlainJsonRecord(input)) authorError(`${path} must be an object of value schemas`)
if (seen.has(input)) authorError(`${path} is circular`)
seen.add(input)
try {
const properties: Record<string, JsonSchemaNode> = {}
const required: string[] = []
for (const [key, property] of Object.entries(input)) {
if (!isPlainJsonRecord(property)) authorError(`${path}.${key} must be a value schema object`)
if (Object.hasOwn(property, 'required') && property.required !== true) {
authorError(`${path}.${key}.required must be true when present`)
}
Object.defineProperty(properties, key, {
value: compileValueSchema(property, `${path}.${key}`, seen, true),
/** Compiled form of one implicit property map. */
interface CompiledPropertyMap {
properties: Record<string, JsonSchemaNode>
required?: string[]
}
/** Mutable holder used only while an iterative compilation root is unresolved. */
interface CompileRoot<T> {
value?: T
}
/** Where one compiled value node is installed. */
type NodeDestination =
| { kind: 'root'; holder: CompileRoot<JsonSchemaNode> }
| { kind: 'property'; target: Record<string, JsonSchemaNode>; key: string }
| { kind: 'item'; target: JsonSchemaNode }
| { kind: 'one-of'; target: JsonSchemaNode[]; index: number }
/** Where one compiled property map is installed. */
type PropertyMapDestination =
| { kind: 'root'; holder: CompileRoot<CompiledPropertyMap> }
| { kind: 'object'; target: JsonSchemaNode }
/** Deferred work for stack-safe author-schema compilation. */
type CompileTask =
| { kind: 'value'; input: unknown; path: string; allowRequired: boolean; destination: NodeDestination }
| { kind: 'property-map'; input: unknown; path: string; destination: PropertyMapDestination }
| {
kind: 'property'
property: unknown
path: string
key: string
properties: Record<string, JsonSchemaNode>
required: string[]
}
| {
kind: 'property-map-tail'
compiled: CompiledPropertyMap
required: string[]
destination: PropertyMapDestination
}
| { kind: 'leave'; input: object }
/** Install a compiled node without giving `__proto__` assignment semantics. */
function assignCompiledNode(destination: NodeDestination, node: JsonSchemaNode): void {
switch (destination.kind) {
case 'root':
destination.holder.value = node
break
case 'property':
Object.defineProperty(destination.target, destination.key, {
value: node,
enumerable: true,
configurable: true,
writable: true,
})
if (property.required === true) required.push(key)
}
return required.length > 0 ? { properties, required } : { properties }
} finally {
seen.delete(input)
break
case 'item':
destination.target.items = node
break
case 'one-of':
destination.target[destination.index] = node
break
}
}
/** Compile one author node without applying any consumer root restriction. */
function compileValueSchema(
input: unknown,
path: string,
seen: Set<object>,
allowRequired = false,
): JsonSchemaNode {
if (!isPlainJsonRecord(input)) authorError(`${path} must be a value schema object`)
if (seen.has(input)) authorError(`${path} is circular`)
seen.add(input)
try {
const authorKeys = [...ANNOTATION_KEYS, ...(allowRequired ? ['required'] : [])]
/** Install a compiled property map at its root or containing object node. */
function assignCompiledPropertyMap(destination: PropertyMapDestination, compiled: CompiledPropertyMap): void {
if (destination.kind === 'root') {
destination.holder.value = compiled
} else {
destination.target.properties = compiled.properties
}
}
/** Execute an author-schema compilation task graph without recursive descent. */
function runSchemaCompiler(initial: CompileTask): void {
const seen = new Set<object>()
const tasks: CompileTask[] = [initial]
for (let task = tasks.pop(); task !== undefined; task = tasks.pop()) {
if (task.kind === 'leave') {
seen.delete(task.input)
continue
}
if (task.kind === 'property-map-tail') {
if (task.required.length > 0) {
task.compiled.required = task.required
if (task.destination.kind === 'object') task.destination.target.required = task.required
}
continue
}
if (task.kind === 'property') {
if (!isPlainJsonRecord(task.property)) authorError(`${task.path} must be a value schema object`)
if (Object.hasOwn(task.property, 'required') && task.property.required !== true) {
authorError(`${task.path}.required must be true when present`)
}
if (task.property.required === true) task.required.push(task.key)
tasks.push({
kind: 'value',
input: task.property,
path: task.path,
allowRequired: true,
destination: { kind: 'property', target: task.properties, key: task.key },
})
continue
}
if (task.kind === 'property-map') {
if (!isPlainJsonRecord(task.input)) authorError(`${task.path} must be an object of value schemas`)
if (seen.has(task.input)) authorError(`${task.path} is circular`)
seen.add(task.input)
const compiled: CompiledPropertyMap = { properties: {} }
const required: string[] = []
assignCompiledPropertyMap(task.destination, compiled)
tasks.push({ kind: 'leave', input: task.input })
tasks.push({ kind: 'property-map-tail', compiled, required, destination: task.destination })
const entries = Object.entries(task.input)
for (let index = entries.length - 1; index >= 0; index--) {
const entry = entries[index]
/* v8 ignore next -- the loop is bounded by the captured entry count. */
if (entry === undefined) continue
tasks.push({
kind: 'property',
property: entry[1],
path: `${task.path}.${entry[0]}`,
key: entry[0],
properties: compiled.properties,
required,
})
}
continue
}
const { input, path } = task
if (!isPlainJsonRecord(input)) authorError(`${path} must be a value schema object`)
if (seen.has(input)) authorError(`${path} is circular`)
seen.add(input)
const authorKeys = [...ANNOTATION_KEYS, ...(task.allowRequired ? ['required'] : [])]
const node: JsonSchemaNode = {}
assignCompiledNode(task.destination, node)
tasks.push({ kind: 'leave', input })
if (Object.hasOwn(input, 'oneOf')) {
assertAuthorKeys(input, path, [...authorKeys, 'oneOf', 'type'])
if (Object.hasOwn(input, 'type')) authorError(`${path} cannot declare both type and oneOf`)
if (!Array.isArray(input.oneOf)) authorError(`${path}.oneOf must be an array of at least two value schemas`)
node.oneOf = input.oneOf.map((branch, index) => compileValueSchema(branch, `${path}.oneOf[${index}]`, seen))
const branches: JsonSchemaNode[] = []
node.oneOf = branches
copyAnnotations(input, node)
return node
for (let index = input.oneOf.length - 1; index >= 0; index--) {
tasks.push({
kind: 'value',
input: input.oneOf[index],
path: `${path}.oneOf[${index}]`,
allowRequired: false,
destination: { kind: 'one-of', target: branches, index },
})
}
continue
}
switch (input.type) {
case 'json':
assertAuthorKeys(input, path, [...authorKeys, 'type'])
copyAnnotations(input, node)
return node
case 'object': {
break
case 'object':
assertAuthorKeys(input, path, [...authorKeys, 'type', 'properties', 'additionalProperties'])
if (!Object.hasOwn(input, 'additionalProperties') || typeof input.additionalProperties !== 'boolean') {
authorError(`${path}.additionalProperties must be explicitly true or false`)
@@ -254,18 +360,28 @@ function compileValueSchema(
copyAnnotations(input, node)
node.additionalProperties = input.additionalProperties
if (Object.hasOwn(input, 'properties')) {
const compiled = compilePropertyMap(input.properties, `${path}.properties`, seen)
node.properties = compiled.properties
if (compiled.required !== undefined) node.required = compiled.required
tasks.push({
kind: 'property-map',
input: input.properties,
path: `${path}.properties`,
destination: { kind: 'object', target: node },
})
}
return node
}
break
case 'array':
assertAuthorKeys(input, path, [...authorKeys, 'type', 'items'])
node.type = 'array'
copyAnnotations(input, node)
if (Object.hasOwn(input, 'items')) node.items = compileValueSchema(input.items, `${path}.items`, seen)
return node
if (Object.hasOwn(input, 'items')) {
tasks.push({
kind: 'value',
input: input.items,
path: `${path}.items`,
allowRequired: false,
destination: { kind: 'item', target: node },
})
}
break
case 'string':
case 'number':
case 'integer':
@@ -280,15 +396,29 @@ function compileValueSchema(
: input.enum as JsonSchemaScalar[]
}
if (Object.hasOwn(input, 'const')) node.const = input.const as JsonSchemaScalar
return node
break
default:
return authorError(`${path}.type must be string/number/integer/boolean/null/array/object/json, or use oneOf`)
authorError(`${path}.type must be string/number/integer/boolean/null/array/object/json, or use oneOf`)
}
} finally {
seen.delete(input)
}
}
/** Compile one implicit property map, collecting per-property requiredness. */
function compilePropertyMap(input: unknown, path: string): CompiledPropertyMap {
const holder: CompileRoot<CompiledPropertyMap> = {}
runSchemaCompiler({ kind: 'property-map', input, path, destination: { kind: 'root', holder } })
/* v8 ignore next -- the root task assigns before scheduling any descendants. */
return holder.value ?? authorError(`${path} did not compile`)
}
/** Compile one author node without applying any consumer root restriction. */
function compileValueSchema(input: unknown, path: string): JsonSchemaNode {
const holder: CompileRoot<JsonSchemaNode> = {}
runSchemaCompiler({ kind: 'value', input, path, allowRequired: false, destination: { kind: 'root', holder } })
/* v8 ignore next -- the root task assigns before scheduling any descendants. */
return holder.value ?? authorError(`${path} did not compile`)
}
/**
* Compile one author-facing value schema to the enforced raw JSON Schema
* subset. The author-only `json` node becomes an annotation-only schema.
@@ -296,7 +426,7 @@ function compileValueSchema(
* @returns The asserted raw schema projection.
*/
export function valueSchemaSpecToJsonSchema(spec: ValueSchemaSpec): JsonSchemaNode {
const schema = compileValueSchema(spec, 'schema', new Set())
const schema = compileValueSchema(spec, 'schema')
assertSupportedJsonSchema(schema)
return schema
}
@@ -307,7 +437,7 @@ export function valueSchemaSpecToJsonSchema(spec: ValueSchemaSpec): JsonSchemaNo
* @returns An object-rooted raw schema with no implicit-root openness override.
*/
export function parameterSchemaSpecToJsonSchema(spec: ParameterSchemaSpec): ParameterJsonSchema {
const compiled = compilePropertyMap(spec, 'parameters', new Set())
const compiled = compilePropertyMap(spec, 'parameters')
const schema: ParameterJsonSchema = {
type: 'object',
properties: compiled.properties,

View File

@@ -9,7 +9,6 @@
import type { ToolSchema } from '@deepseek-ai/dsh-llm'
import { assertSupportedJsonSchema } from './json-schema.ts'
import type { JsonSchemaNode, JsonSchemaScalar } from './json-schema.ts'
/** Internal Code Mode projection: the model-facing schema plus the canonical output schema. */
export interface ToolSdkSchema extends ToolSchema {
/** Validated canonical value returned by the tool binding. */
@@ -53,9 +52,181 @@ function renderConstrainedScalar(node: Record<string, unknown>, type: string): s
return broad
}
/** Parenthesize a union or object intersection before applying `[]`. */
function arrayItem(type: string): string {
return type.includes('|') || type.includes('&') ? `(${type})[]` : `${type}[]`
/** A composable type document that can be flattened without recursive string concatenation. */
interface TypeDocument {
readonly parts: readonly (string | TypeDocument)[]
readonly containsUnionOrIntersection: boolean
}
/** Build one document from captured parts while retaining the legacy array-parenthesization test. */
function typeDocumentFrom(parts: readonly (string | TypeDocument)[]): TypeDocument {
return {
parts,
containsUnionOrIntersection: parts.some(part => typeof part === 'string'
? part.includes('|') || part.includes('&')
: part.containsUnionOrIntersection),
}
}
/** Build a small document without an intermediate array at each call site. */
function typeDocument(...parts: (string | TypeDocument)[]): TypeDocument {
return typeDocumentFrom(parts)
}
/** Flatten a nested document with an explicit work stack. */
function flattenTypeDocument(document: TypeDocument): string {
const chunks: string[] = []
const tasks: (string | TypeDocument)[] = [document]
for (let task = tasks.pop(); task !== undefined; task = tasks.pop()) {
if (typeof task === 'string') {
chunks.push(task)
continue
}
for (let index = task.parts.length - 1; index >= 0; index--) {
const part = task.parts[index]
/* v8 ignore next -- the loop is bounded by the captured part count. */
if (part !== undefined) tasks.push(part)
}
}
return chunks.join('')
}
/** One explicit call frame for stack-safe schema-to-TypeScript rendering. */
interface SchemaRenderFrame {
readonly node: JsonSchemaNode
readonly indent: number
phase: 'start' | 'children'
kind?: 'oneOf' | 'array' | 'object'
children: { node: JsonSchemaNode; indent: number }[]
childIndex: number
childDocuments: TypeDocument[]
entries: [string, JsonSchemaNode][]
}
/** Initialize one schema-render frame with empty aggregation state. */
function schemaRenderFrame(node: JsonSchemaNode, indent: number): SchemaRenderFrame {
return { node, indent, phase: 'start', children: [], childIndex: 0, childDocuments: [], entries: [] }
}
/** Render an already asserted schema to a composable document. */
function renderSupportedSchema(schema: JsonSchemaNode, indent: number): TypeDocument {
const frames: SchemaRenderFrame[] = [schemaRenderFrame(schema, indent)]
let rootDocument: TypeDocument | undefined
const finish = (document: TypeDocument): void => {
frames.pop()
const parent = frames.at(-1)
if (parent === undefined) rootDocument = document
else parent.childDocuments.push(document)
}
while (frames.length > 0) {
const frame = frames.at(-1)
/* v8 ignore next -- the loop condition guarantees a current frame. */
if (frame === undefined) break
if (frame.phase === 'children') {
if (frame.childIndex < frame.children.length) {
const child = frame.children[frame.childIndex]
/* v8 ignore next -- childIndex is bounded by children.length. */
if (child === undefined) throw new Error('missing schema render child')
frame.childIndex++
frames.push(schemaRenderFrame(child.node, child.indent))
continue
}
if (frame.kind === 'oneOf') {
const parts: (string | TypeDocument)[] = []
for (let index = 0; index < frame.childDocuments.length; index++) {
if (index > 0) parts.push(' | ')
const child = frame.childDocuments[index]
/* v8 ignore next -- child documents correspond one-to-one with children. */
if (child !== undefined) parts.push(child)
}
finish(typeDocumentFrom(parts))
continue
}
if (frame.kind === 'array') {
const child = frame.childDocuments[0]
/* v8 ignore next -- array frames always schedule exactly one child. */
if (child === undefined) throw new Error('missing array item type')
finish(child.containsUnionOrIntersection
? typeDocument('(', child, ')[]')
: typeDocument(child, '[]'))
continue
}
const required = new Set(frame.node.required)
const parts: (string | TypeDocument)[] = ['{']
for (let index = 0; index < frame.entries.length; index++) {
const entry = frame.entries[index]
const child = frame.childDocuments[index]
/* v8 ignore next -- object entries and child documents have the same length. */
if (entry === undefined || child === undefined) throw new Error('missing object property type')
const [name, prop] = entry
for (const line of docLines(prop.description, frame.indent + 1)) parts.push('\n', line)
parts.push('\n', `${pad(frame.indent + 1)}${renderKey(name)}${required.has(name) ? '' : '?'}: `, child, ';')
}
parts.push('\n', `${pad(frame.indent)}}`)
const declared = typeDocumentFrom(parts)
finish(frame.node.additionalProperties === false
? declared
: typeDocument(declared, ' & Record<string, JsonValue>'))
continue
}
const node = frame.node
if (node.oneOf !== undefined) {
frame.kind = 'oneOf'
frame.children = Array.from(node.oneOf, child => ({ node: child, indent: frame.indent }))
frame.childIndex = 0
frame.childDocuments = []
frame.phase = 'children'
continue
}
if (node.type === undefined) {
finish(typeDocument('JsonValue'))
continue
}
switch (node.type) {
case 'string':
case 'number':
case 'integer':
case 'boolean':
case 'null':
finish(typeDocument(renderConstrainedScalar(node as Record<string, unknown>, node.type)))
break
case 'array':
if (node.items === undefined) {
finish(typeDocument('JsonValue[]'))
} else {
frame.kind = 'array'
frame.children = [{ node: node.items, indent: frame.indent }]
frame.childIndex = 0
frame.childDocuments = []
frame.phase = 'children'
}
break
case 'object': {
const open = node.additionalProperties !== false
const entries = Object.entries(node.properties ?? {})
if (entries.length === 0) {
finish(typeDocument(open ? 'Record<string, JsonValue>' : 'Record<string, never>'))
} else {
frame.kind = 'object'
frame.entries = entries
frame.children = entries.map(([, child]) => ({ node: child, indent: frame.indent + 1 }))
frame.childIndex = 0
frame.childDocuments = []
frame.phase = 'children'
}
break
}
/* v8 ignore next -- assertSupportedJsonSchema narrowed this closed type union. */
default:
finish(typeDocument('unknown'))
}
}
/* v8 ignore next -- every root frame produces one document. */
return rootDocument ?? typeDocument('unknown')
}
/**
@@ -69,43 +240,10 @@ function arrayItem(type: string): string {
export function jsonSchemaToTs(schema: unknown, indent = 0): string {
try {
assertSupportedJsonSchema(schema)
return flattenTypeDocument(renderSupportedSchema(schema, indent))
} catch {
return 'unknown'
}
const node = schema as Record<string, unknown>
if (Object.hasOwn(node, 'oneOf')) {
return (node.oneOf as unknown[]).map(branch => jsonSchemaToTs(branch, indent)).join(' | ')
}
if (!Object.hasOwn(node, 'type')) return 'JsonValue'
switch (node.type) {
case 'string': return renderConstrainedScalar(node, 'string')
case 'number': return renderConstrainedScalar(node, 'number')
case 'integer': return renderConstrainedScalar(node, 'integer')
case 'boolean': return renderConstrainedScalar(node, 'boolean')
case 'null': return renderConstrainedScalar(node, 'null')
case 'array': {
return arrayItem(Object.hasOwn(node, 'items') ? jsonSchemaToTs(node.items, indent) : 'JsonValue')
}
case 'object': {
const properties = node.properties
const open = node.additionalProperties !== false
if (properties === undefined) return open ? 'Record<string, JsonValue>' : 'Record<string, never>'
const entries = Object.entries(properties as Record<string, unknown>)
if (entries.length === 0) return open ? 'Record<string, JsonValue>' : 'Record<string, never>'
const required = new Set(node.required as string[] | undefined)
const lines: string[] = ['{']
for (const [name, prop] of entries) {
const description = (prop as Record<string, unknown>).description
lines.push(...docLines(description, indent + 1))
lines.push(`${pad(indent + 1)}${renderKey(name)}${required.has(name) ? '' : '?'}: ${jsonSchemaToTs(prop, indent + 1)};`)
}
lines.push(`${pad(indent)}}`)
const declared = lines.join('\n')
return open ? `${declared} & Record<string, JsonValue>` : declared
}
/* v8 ignore next -- assertSupportedJsonSchema narrowed this closed type union. */
default: return 'unknown'
}
}
/** The fixed model-facing usage contract rendered above the declarations (see the Code Mode Agent Note's "What the model sees"). */

View File

@@ -23,7 +23,7 @@ describe('gen-tool-catalog collectToolCatalog', () => {
it('boots every shipped tool package and harvests its model-facing schemas', async () => {
const catalog = await collectToolCatalog()
const names = catalog.flatMap(entry => entry.schemas.map(s => s.name)).sort()
expect(names).toEqual(['ask_user_question', 'bash', 'cordis_inspect', 'cordis_mount', 'cordis_unmount', 'create_goal', 'edit', 'get_goal', 'glob', 'grep', 'lsp', 'ralph', 'read', 'run_code', 'skill', 'subagent', 'task_kill', 'task_list', 'task_output', 'todo_write', 'update_goal', 'web_fetch', 'web_search', 'workflow', 'write'])
expect(names).toEqual(['ask_user_question', 'bash', 'cordis_inspect', 'cordis_mount', 'cordis_unmount', 'create_goal', 'edit', 'exit_plan_mode', 'get_goal', 'glob', 'grep', 'lsp', 'ralph', 'read', 'run_code', 'skill', 'subagent', 'task_kill', 'task_list', 'task_output', 'todo_write', 'update_goal', 'web_fetch', 'web_search', 'workflow', 'write'])
// Every tool carries a JSON-Schema `parameters` object (what the model sees).
for (const entry of catalog) {
for (const schema of entry.schemas) {

View File

@@ -214,6 +214,14 @@ describe('the enforced raw JSON Schema subset', () => {
.toEqual(['schema.properties.at must be a schema object'])
})
it('asserts deeply nested raw unions without using the JavaScript call stack', () => {
const depth = 5_000
let schema: JsonSchemaNode = { type: 'string' }
for (let index = 0; index < depth; index++) schema = { oneOf: [schema, { type: 'null' }] }
expect(() => { assertSupportedJsonSchema(schema) }).not.toThrow()
})
it('uses own-property semantics for required declarations', () => {
expect(violationsOf({ type: 'object', properties: {}, required: ['toString'] }))
.toEqual(['schema.required names "toString" which is not in properties'])
@@ -322,6 +330,17 @@ describe('validateJsonSchemaValue', () => {
expect(validateJsonSchemaValue(overlap, 1.5)).toEqual([])
})
it('validates deeply nested exact-one unions without using the JavaScript call stack', () => {
const depth = 5_000
let schema: JsonSchemaNode = { type: 'string' }
for (let index = 0; index < depth; index++) schema = { oneOf: [schema, { type: 'null' }] }
assertSupportedJsonSchema(schema)
expect(validateJsonSchemaValue(schema, 'leaf')).toEqual([])
expect(validateJsonSchemaValue(schema, 42))
.toEqual(['"value" must match exactly one oneOf branch (matched 0)'])
})
it('an unconstrained schema accepts only lossless JSON values', () => {
const anyJson = asserted({})
for (const value of [null, true, 1, 'x', [1], { x: null }]) {

View File

@@ -92,6 +92,23 @@ describe('the unified author schema DSL', () => {
expect(() => parameterSchemaSpecToJsonSchema(properties as ParameterSchemaSpec)).toThrow(/circular/)
})
it('compiles deeply nested author unions without using the JavaScript call stack', () => {
const depth = 5_000
let spec: unknown = { type: 'string' }
for (let index = 0; index < depth; index++) spec = { oneOf: [spec, { type: 'null' }] }
const compiled = valueSchemaSpecToJsonSchema(spec as ValueSchemaSpec)
let cursor = compiled
let layers = 0
while (cursor.oneOf !== undefined) {
cursor = cursor.oneOf[0]!
layers++
}
expect(layers).toBe(depth)
expect(cursor).toEqual({ type: 'string' })
})
it('preserves a property literally named __proto__ as schema data', () => {
const properties = Object.create(null) as ParameterSchemaSpec
properties.__proto__ = { type: 'string', required: true }

View File

@@ -8,7 +8,7 @@ import ToolRegistry, {
defineContentToolFixture, defineTool, JsonSchemaError, parameterSchemaSpecToJsonSchema, validateArgs, ToolArgsError, ToolNotFoundError,
TOOL_ABORTED, TOOL_ABORTED_BEFORE_DISPATCH,
type InferArgs, type JsonValue, type ParameterSchemaSpec, type PreToolDecision, type PostToolDecision,
type ToolDefinition, type ToolDispatchExecution, type ToolExecutionResult, type ToolExecutionToken,
type JsonSchemaNode, type ToolDefinition, type ToolDispatchExecution, type ToolExecutionResult, type ToolExecutionToken,
} from '@deepseek-ai/dsh-tools'
const testToolSignal = new AbortController().signal
@@ -1687,6 +1687,41 @@ describe('ToolRegistry', () => {
}])
})
it('schemas() snapshots deeply nested parameters without using structured-clone recursion', async () => {
const ctx = await setup()
const depth = 5_000
let nested: JsonSchemaNode = { type: 'string' }
for (let index = 0; index < depth; index++) nested = { oneOf: [nested, { type: 'null' }] }
ctx.tools.register({
...echoTool,
name: 'deep-schema',
parameters: { type: 'object', properties: { nested } },
})
const projected = ctx.tools.schemas()[0]!.parameters as JsonSchemaNode
let cursor = projected.properties!.nested!
let layers = 0
while (cursor.oneOf !== undefined) {
cursor = cursor.oneOf[0]!
layers++
}
expect(layers).toBe(depth)
expect(cursor).toEqual({ type: 'string' })
})
it('rejects schema projection when a raw registration is not lossless JSON', async () => {
const ctx = await setup()
ctx.tools.register({
...echoTool,
name: 'lossy-schema',
parameters: { type: 'object', default: Number.NaN },
})
expect(() => ctx.tools.schemas())
.toThrow('tool "lossy-schema" parameters must be lossless JSON before schema projection')
})
it('rejects a non-positive or non-finite registration timeout', async () => {
const ctx = await setup()
expect(() => ctx.tools.register({ ...echoTool, name: 'zero-timeout', timeoutMs: 0 }))

View File

@@ -93,6 +93,17 @@ describe('jsonSchemaToTs', () => {
expect(rendered).not.toContain('tool-*/ over')
expect(rendered).toContain(String.raw`tool-*\/ over`)
})
it('renders deeply nested unions without using the JavaScript call stack', () => {
const depth = 5_000
let schema: unknown = { type: 'string' }
for (let index = 0; index < depth; index++) schema = { oneOf: [schema, { type: 'null' }] }
const rendered = jsonSchemaToTs(schema)
expect(rendered.startsWith('string | null')).toBe(true)
expect(rendered.length).toBe('string'.length + depth * ' | null'.length)
})
})
describe('renderToolsSdk', () => {