Fix schema-DSL findings from the second Codex review

InferArgs now produces genuinely optional keys: required/optional
properties are split at the key level (RequiredKeys + mapped `?`), so
{ limit: { type: 'number' } } infers as { limit?: number } and callers
can omit it — previously the key stayed required with `| undefined`.
Array item inference recurses (arrays of objects infer their element
shape instead of Record<string, unknown>), matching the generated
JSON Schema.

Tool execution error reporting handles non-Error throws again:
`throw { message: 'denied' }` reports the message instead of
"[object Object]" (errorMessage helper).

The new schema tests now actually typecheck: schema literals use
`satisfies SchemaSpec` (the standalone-literal widening made
schemaSpecToJsonSchema reject the suite's own examples), and the
ToolSchema probe cast goes through unknown. Tests-and-examples
typechecking is now part of `yarn typecheck` via the new
tsconfig.typecheck.json (resolves vendor packages by their built
declarations, so vendor's relaxed-strictness source stays out of
scope) — vitest never typechecks, so this gate is what catches such
breakage. +4 regression tests (typed omission, array-of-objects
inference both type- and runtime-level, non-Error throw message).
This commit is contained in:
Tianyi Cui
2026-06-11 13:46:01 +08:00
parent 7f024a1a9d
commit ef45ca823a
5 changed files with 161 additions and 30 deletions

View File

@@ -70,6 +70,21 @@ export interface ToolExecutionResult {
isError: boolean
}
/**
* Best-effort human-readable message from an arbitrary thrown value: Error
* instances use `.message`; non-Error objects with a string `message`
* property (e.g. `throw { message: 'denied' }`) use it too; everything else
* is stringified.
*/
function errorMessage(error: unknown): string {
if (error instanceof Error) return error.message
if (typeof error === 'object' && error !== null
&& 'message' in error && typeof error.message === 'string') {
return error.message
}
return String(error)
}
/**
* Tool registry (`ctx.tools`): tool plugins register definitions; the agent
* loop executes calls through the `tools/execute` waterfall. The registry
@@ -138,10 +153,9 @@ export class ToolRegistry extends Service {
const content = await tool.execute(exec.arguments, exec)
return { callId: exec.callId, content, isError: false }
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error)
return {
callId: exec.callId,
content: [{ type: 'text', text: `Error: ${message}` }],
content: [{ type: 'text', text: `Error: ${errorMessage(error)}` }],
isError: true,
}
}

View File

@@ -66,34 +66,41 @@ type TypeOf<T extends SchemaType> =
T extends 'array' ? unknown[] :
never
/** Flatten an intersection into one object type for readable hovers. */
type Simplify<T> = { [K in keyof T]: T[K] } & {}
/** Keys of `S` whose prop is marked `required: true`. */
type RequiredKeys<S extends SchemaSpec> =
{ [K in keyof S]: S[K] extends { required: true } ? K : never }[keyof S]
/**
* Infer the TS type of a single {@link SchemaProp}.
* - `required: true` → required (non-optional)
* - absent required → optional
* - `properties` on 'object' → recurse
* The VALUE type of one {@link SchemaProp} — optionality is handled at the
* key level by {@link InferArgs}, never here.
* - `properties` on 'object' → recurse into the nested SchemaSpec
* - `items` on 'array' → recurse into the item prop (arrays of objects work)
* - otherwise → the primitive for `type`
*/
type InferProp<P extends SchemaProp> =
P extends { type: 'object'; properties: infer Sub extends SchemaSpec } ?
// Nested objects with their own SchemaSpec — infer their shape
(P extends { required: true } ? InferArgs<Sub> : InferArgs<Sub> | undefined) :
P extends { type: 'array'; items: infer Item extends SchemaProp } ?
// Arrays: infer item type
(P extends { required: true } ? TypeOf<Item['type']>[] : TypeOf<Item['type']>[] | undefined) :
// Primitive types
(P extends { required: true } ? TypeOf<P['type']> : TypeOf<P['type']> | undefined)
type InferPropValue<P extends SchemaProp> =
P extends { type: 'object'; properties: infer Sub extends SchemaSpec } ? InferArgs<Sub> :
P extends { type: 'array'; items: infer Item extends SchemaProp } ? InferPropValue<Item>[] :
TypeOf<P['type']>
/**
* Infer the TS argument type for a complete {@link SchemaSpec}.
*
* Properties marked `required: true` are required keys; all others are
* genuinely optional keys (`?`), so callers may omit them entirely.
*
* Example:
* ```ts
* type Args = InferArgs<{ path: { type: 'string'; required: true }; limit: { type: 'number' } }>
* // → { path: string; limit?: number }
* ```
*/
export type InferArgs<S extends SchemaSpec> = {
[K in keyof S]: InferProp<S[K]>
}
export type InferArgs<S extends SchemaSpec> = Simplify<
& { [K in RequiredKeys<S>]: InferPropValue<S[K]> }
& { [K in Exclude<keyof S, RequiredKeys<S>>]?: InferPropValue<S[K]> }
>
// ---------------------------------------------------------------------------
// Runtime conversion: SchemaSpec → JSON Schema