返回源码地图

packages/core/tools/src/schema.ts

main snapshot · da00f7f5358f · 正文引用章节 08 / 08;完整原文可核对,不声称全文件人工逐行审计

完整原文供逐行核对;页面收录不代表每行都经过人工语义审核。MIT 许可见 许可证。

1/** Unified JSON-value schema DSL, inference, compilation, and typed tool helper. @module dsh-tools/schema */
2
3import { HarnessError } from '@deepseek-ai/dsh-llm'
4import type { ContentBlock } from '@deepseek-ai/dsh-llm'
5import type { JsonValue } from '@deepseek-ai/dsh-util-values'
6import type { ToolDefinition, ToolExecution, ToolExecutionResult, ToolRunContext, ToolResult } from './index.ts'
7import { assertSupportedJsonSchema, isJsonSchemaRecord, isPlainJsonArray, JsonSchemaError, validateJsonSchemaValue } from './json-schema.ts'
8import type { JsonSchemaNode, JsonSchemaScalar, ObjectJsonSchema } from './json-schema.ts'
9import type { ToolCallView, ToolResultView } from './presentation.ts'
10
11/** Annotation keywords shared by every author-facing schema node. */
12export interface ValueSchemaAnnotations {
13 /** Human-readable description projected into JSON Schema and generated types. */
14 description?: string
15 /** Human-readable title projected into JSON Schema. */
16 title?: string
17 /** Non-validating default annotation; it must be lossless JSON data. */
18 default?: JsonValue
19 /** Non-validating examples annotation; it must be lossless JSON data. */
20 examples?: JsonValue
21}
22
23/** String value schema with type-correct literal constraints. */
24export interface StringValueSchemaSpec extends ValueSchemaAnnotations {
25 type: 'string'
26 enum?: readonly string[]
27 const?: string
28}
29
30/** Finite JSON-number schema with type-correct literal constraints. */
31export interface NumberValueSchemaSpec extends ValueSchemaAnnotations {
32 type: 'number'
33 enum?: readonly number[]
34 const?: number
35}
36
37/** Integer schema with type-correct literal constraints. */
38export interface IntegerValueSchemaSpec extends ValueSchemaAnnotations {
39 type: 'integer'
40 enum?: readonly number[]
41 const?: number
42}
43
44/** Boolean value schema with type-correct literal constraints. */
45export interface BooleanValueSchemaSpec extends ValueSchemaAnnotations {
46 type: 'boolean'
47 enum?: readonly boolean[]
48 const?: boolean
49}
50
51/** Null value schema with type-correct literal constraints. */
52export interface NullValueSchemaSpec extends ValueSchemaAnnotations {
53 type: 'null'
54 enum?: readonly null[]
55 const?: null
56}
57
58/** Array value schema; omitted `items` accepts any lossless JSON item. */
59export interface ArrayValueSchemaSpec extends ValueSchemaAnnotations {
60 type: 'array'
61 items?: ValueSchemaSpec
62}
63
64/**
65 * Explicit object value schema. Openness is mandatory so a nested or output
66 * object never acquires an accidental JSON Schema default.
67 */
68export interface ObjectValueSchemaSpec extends ValueSchemaAnnotations {
69 type: 'object'
70 properties?: ParameterSchemaSpec
71 additionalProperties: boolean
72}
73
74/** Author-only unconstrained lossless JSON node. */
75export interface JsonValueSchemaSpec extends ValueSchemaAnnotations {
76 type: 'json'
77}
78
79/** Exact-one union schema; at least two branches are required. */
80export interface OneOfValueSchemaSpec extends ValueSchemaAnnotations {
81 oneOf: readonly [ValueSchemaSpec, ValueSchemaSpec, ...ValueSchemaSpec[]]
82}
83
84/** One author-facing schema for any lossless JSON value root. */
85export type ValueSchemaSpec =
86 | StringValueSchemaSpec
87 | NumberValueSchemaSpec
88 | IntegerValueSchemaSpec
89 | BooleanValueSchemaSpec
90 | NullValueSchemaSpec
91 | ArrayValueSchemaSpec
92 | ObjectValueSchemaSpec
93 | JsonValueSchemaSpec
94 | OneOfValueSchemaSpec
95
96/** One implicit parameter-root property, optionally required. */
97export type ParameterPropertySpec = ValueSchemaSpec & { required?: true }
98
99/**
100 * Tool parameter schema. The map itself is an implicit open object root;
101 * requiredness remains a per-property `required: true` annotation.
102 */
103export type ParameterSchemaSpec = {
104 [key: string]: ParameterPropertySpec
105 [key: symbol]: never
106}
107
108/** Raw JSON Schema projection of the implicit parameter object. */
109export interface ParameterJsonSchema extends ObjectJsonSchema {
110 properties: Record<string, JsonSchemaNode>
111}
112
113/** Flatten an intersection into one object type for readable hovers. */
114type Simplify<T> = { [K in keyof T]: T[K] } & {}
115
116/** String keys of one property map; runtime compilation rejects symbol keys. */
117type StringKeyOf<S> = Extract<keyof S, string>
118
119/** Keys of a property map marked `required: true`. */
120type RequiredKeys<S> = {
121 [K in StringKeyOf<S>]: S[K] extends { required: true } ? K : never
122}[StringKeyOf<S>]
123
124/** Infer the declared value of one parameter property without key optionality. */
125type InferProperty<P, Depth extends unknown[]> = InferValueAt<P, Depth>
126
127/** Infer an implicit property map into required and optional object keys. */
128type InferProperties<S, Depth extends unknown[]> = Simplify<
129 & { [K in RequiredKeys<S>]: InferProperty<S[K], Depth> }
130 & { [K in Exclude<StringKeyOf<S>, RequiredKeys<S>>]?: InferProperty<S[K], Depth> }
131>
132
133/** Infer an explicit object node, including its declared openness. */
134type InferObject<S extends { additionalProperties: boolean }, Depth extends unknown[]> =
135 S extends { properties: infer P }
136 ? S['additionalProperties'] extends true
137 ? InferProperties<P, Depth> & Record<string, JsonValue>
138 : InferProperties<P, Depth>
139 : S['additionalProperties'] extends true
140 ? Record<string, JsonValue>
141 : Record<string, never>
142
143/** Infer a scalar node's literal constraint before its broad primitive type. */
144type InferScalar<S, Fallback> =
145 S extends { const: infer C } ? C :
146 S extends { enum: readonly (infer E)[] } ? E :
147 Fallback
148
149/** Add one schema-container level to bounded compile-time inference. */
150type NextInferenceDepth<Depth extends unknown[]> = [unknown, ...Depth]
151
152/** Infer one node without recursively checking it against the full author union. */
153type InferValueAt<S, Depth extends unknown[]> =
154 Depth['length'] extends 16 ? JsonValue :
155 S extends { type: 'string' } ? InferScalar<S, string> :
156 S extends { type: 'number' | 'integer' } ? InferScalar<S, number> :
157 S extends { type: 'boolean' } ? InferScalar<S, boolean> :
158 S extends { type: 'null' } ? null :
159 S extends { type: 'array' }
160 ? S extends { items: infer I } ? InferValueAt<I, NextInferenceDepth<Depth>>[] : JsonValue[]
161 : S extends { type: 'object'; additionalProperties: boolean }
162 ? InferObject<S, NextInferenceDepth<Depth>>
163 : S extends { type: 'json' } ? JsonValue :
164 S extends { oneOf: readonly unknown[] }
165 ? InferValueAt<S['oneOf'][number], NextInferenceDepth<Depth>>
166 : never
167
168/**
169 * Infer the TypeScript value accepted by an author-facing value schema. Exact
170 * inference is bounded to 16 container levels, then falls back to `JsonValue`.
171 */
172export type InferValue<S> = InferValueAt<S, []>
173
174/** Infer the TypeScript argument object for an implicit parameter schema. */
175export type InferArgs<S> = InferProperties<S, []>
176
177const ANNOTATION_KEYS = ['description', 'title', 'default', 'examples'] as const
178
179/** Throw one author-schema violation through the shared schema error type. */
180function authorError(message: string): never {
181 throw new JsonSchemaError([message])
182}
183
184/** Copy own annotation fields for validation by the raw-schema boundary. */
185function copyAnnotations(source: Record<string, unknown>, target: JsonSchemaNode): void {
186 if (Object.hasOwn(source, 'description')) target.description = source.description as string
187 if (Object.hasOwn(source, 'title')) target.title = source.title as string
188 if (Object.hasOwn(source, 'default')) target.default = source.default as JsonValue
189 if (Object.hasOwn(source, 'examples')) target.examples = source.examples as JsonValue
190}
191
192/** Reject author-only keys outside one node's declared vocabulary. */
193function assertAuthorKeys(source: Record<string, unknown>, path: string, allowed: readonly string[]): void {
194 for (const key of Object.keys(source)) {
195 if (!allowed.includes(key)) authorError(`${path}.${key} is not supported by the value schema DSL`)
196 }
197}
198
199/** Compiled form of one implicit property map. */
200interface CompiledPropertyMap {
201 properties: Record<string, JsonSchemaNode>
202 required?: string[]
203}
204
205/** Mutable holder used only while an iterative compilation root is unresolved. */
206interface CompileRoot<T> {
207 value?: T
208}
209
210/** Where one compiled value node is installed. */
211type NodeDestination =
212 | { kind: 'root'; holder: CompileRoot<JsonSchemaNode> }
213 | { kind: 'property'; target: Record<string, JsonSchemaNode>; key: string }
214 | { kind: 'item'; target: JsonSchemaNode }
215 | { kind: 'one-of'; target: JsonSchemaNode[]; index: number }
216
217/** Where one compiled property map is installed. */
218type PropertyMapDestination =
219 | { kind: 'root'; holder: CompileRoot<CompiledPropertyMap> }
220 | { kind: 'object'; target: JsonSchemaNode }
221
222/** Deferred work for stack-safe author-schema compilation. */
223type CompileTask =
224 | { kind: 'value'; input: unknown; path: string; allowRequired: boolean; destination: NodeDestination }
225 | { kind: 'property-map'; input: unknown; path: string; destination: PropertyMapDestination }
226 | {
227 kind: 'property'
228 property: unknown
229 path: string
230 key: string
231 properties: Record<string, JsonSchemaNode>
232 required: string[]
233 }
234 | {
235 kind: 'property-map-tail'
236 compiled: CompiledPropertyMap
237 required: string[]
238 destination: PropertyMapDestination
239 }
240 | { kind: 'leave'; input: object }
241
242/** Install a compiled node without giving `__proto__` assignment semantics. */
243function assignCompiledNode(destination: NodeDestination, node: JsonSchemaNode): void {
244 switch (destination.kind) {
245 case 'root':
246 destination.holder.value = node
247 break
248 case 'property':
249 Object.defineProperty(destination.target, destination.key, {
250 value: node,
251 enumerable: true,
252 configurable: true,
253 writable: true,
254 })
255 break
256 case 'item':
257 destination.target.items = node
258 break
259 case 'one-of':
260 destination.target[destination.index] = node
261 break
262 }
263}
264
265/** Install a compiled property map at its root or containing object node. */
266function assignCompiledPropertyMap(destination: PropertyMapDestination, compiled: CompiledPropertyMap): void {
267 if (destination.kind === 'root') {
268 destination.holder.value = compiled
269 } else {
270 destination.target.properties = compiled.properties
271 }
272}
273
274/** Execute an author-schema compilation task graph without recursive descent. */
275function runSchemaCompiler(initial: CompileTask): void {
276 const seen = new Set<object>()
277 const tasks: CompileTask[] = [initial]
278 for (let task = tasks.pop(); task !== undefined; task = tasks.pop()) {
279 if (task.kind === 'leave') {
280 seen.delete(task.input)
281 continue
282 }
283 if (task.kind === 'property-map-tail') {
284 if (task.required.length > 0) {
285 task.compiled.required = task.required
286 if (task.destination.kind === 'object') task.destination.target.required = task.required
287 }
288 continue
289 }
290 if (task.kind === 'property') {
291 if (!isJsonSchemaRecord(task.property)) authorError(`${task.path} must be a value schema object`)
292 if (Object.hasOwn(task.property, 'required') && task.property.required !== true) {
293 authorError(`${task.path}.required must be true when present`)
294 }
295 if (Object.hasOwn(task.property, 'required') && task.property.required === true) task.required.push(task.key)
296 tasks.push({
297 kind: 'value',
298 input: task.property,
299 path: task.path,
300 allowRequired: true,
301 destination: { kind: 'property', target: task.properties, key: task.key },
302 })
303 continue
304 }
305 if (task.kind === 'property-map') {
306 if (!isJsonSchemaRecord(task.input)) authorError(`${task.path} must be an object of value schemas`)
307 if (seen.has(task.input)) authorError(`${task.path} is circular`)
308 seen.add(task.input)
309 const compiled: CompiledPropertyMap = { properties: {} }
310 const required: string[] = []
311 assignCompiledPropertyMap(task.destination, compiled)
312 tasks.push({ kind: 'leave', input: task.input })
313 tasks.push({ kind: 'property-map-tail', compiled, required, destination: task.destination })
314 const entries = Object.entries(task.input)
315 for (let index = entries.length - 1; index >= 0; index--) {
316 const entry = entries[index]
317 /* v8 ignore next -- the loop is bounded by the captured entry count. */
318 if (entry === undefined) continue
319 tasks.push({
320 kind: 'property',
321 property: entry[1],
322 path: `${task.path}.${entry[0]}`,
323 key: entry[0],
324 properties: compiled.properties,
325 required,
326 })
327 }
328 continue
329 }
330
331 const { input, path } = task
332 if (!isJsonSchemaRecord(input)) authorError(`${path} must be a value schema object`)
333 if (seen.has(input)) authorError(`${path} is circular`)
334 seen.add(input)
335 const authorKeys = [...ANNOTATION_KEYS, ...(task.allowRequired ? ['required'] : [])]
336 const node: JsonSchemaNode = {}
337 assignCompiledNode(task.destination, node)
338 tasks.push({ kind: 'leave', input })
339
340 if (Object.hasOwn(input, 'oneOf')) {
341 assertAuthorKeys(input, path, [...authorKeys, 'oneOf', 'type'])
342 if (Object.hasOwn(input, 'type')) authorError(`${path} cannot declare both type and oneOf`)
343 if (!isPlainJsonArray(input.oneOf)) authorError(`${path}.oneOf must be an array of at least two value schemas`)
344 const branches: JsonSchemaNode[] = []
345 node.oneOf = branches
346 copyAnnotations(input, node)
347 for (let index = input.oneOf.length - 1; index >= 0; index--) {
348 tasks.push({
349 kind: 'value',
350 input: input.oneOf[index],
351 path: `${path}.oneOf[${index}]`,
352 allowRequired: false,
353 destination: { kind: 'one-of', target: branches, index },
354 })
355 }
356 continue
357 }
358
359 const inputType = Object.hasOwn(input, 'type') ? input.type : undefined
360 switch (inputType) {
361 case 'json':
362 assertAuthorKeys(input, path, [...authorKeys, 'type'])
363 copyAnnotations(input, node)
364 break
365 case 'object':
366 assertAuthorKeys(input, path, [...authorKeys, 'type', 'properties', 'additionalProperties'])
367 if (!Object.hasOwn(input, 'additionalProperties') || typeof input.additionalProperties !== 'boolean') {
368 authorError(`${path}.additionalProperties must be explicitly true or false`)
369 }
370 node.type = 'object'
371 copyAnnotations(input, node)
372 node.additionalProperties = input.additionalProperties
373 if (Object.hasOwn(input, 'properties')) {
374 tasks.push({
375 kind: 'property-map',
376 input: input.properties,
377 path: `${path}.properties`,
378 destination: { kind: 'object', target: node },
379 })
380 }
381 break
382 case 'array':
383 assertAuthorKeys(input, path, [...authorKeys, 'type', 'items'])
384 node.type = 'array'
385 copyAnnotations(input, node)
386 if (Object.hasOwn(input, 'items')) {
387 tasks.push({
388 kind: 'value',
389 input: input.items,
390 path: `${path}.items`,
391 allowRequired: false,
392 destination: { kind: 'item', target: node },
393 })
394 }
395 break
396 case 'string':
397 case 'number':
398 case 'integer':
399 case 'boolean':
400 case 'null':
401 assertAuthorKeys(input, path, [...authorKeys, 'type', 'enum', 'const'])
402 node.type = inputType
403 copyAnnotations(input, node)
404 if (Object.hasOwn(input, 'enum')) {
405 if (!isPlainJsonArray(input.enum)) authorError(`${path}.enum must be a non-empty array of scalar values`)
406 node.enum = Array.from(input.enum, entry => entry as JsonSchemaScalar)
407 }
408 if (Object.hasOwn(input, 'const')) node.const = input.const as JsonSchemaScalar
409 break
410 default:
411 authorError(`${path}.type must be string/number/integer/boolean/null/array/object/json, or use oneOf`)
412 }
413 }
414}
415
416/** Compile one implicit property map, collecting per-property requiredness. */
417function compilePropertyMap(input: unknown, path: string): CompiledPropertyMap {
418 const holder: CompileRoot<CompiledPropertyMap> = {}
419 runSchemaCompiler({ kind: 'property-map', input, path, destination: { kind: 'root', holder } })
420 /* v8 ignore next -- the root task assigns before scheduling any descendants. */
421 return holder.value ?? authorError(`${path} did not compile`)
422}
423
424/** Compile one author node without applying any consumer root restriction. */
425function compileValueSchema(input: unknown, path: string): JsonSchemaNode {
426 const holder: CompileRoot<JsonSchemaNode> = {}
427 runSchemaCompiler({ kind: 'value', input, path, allowRequired: false, destination: { kind: 'root', holder } })
428 /* v8 ignore next -- the root task assigns before scheduling any descendants. */
429 return holder.value ?? authorError(`${path} did not compile`)
430}
431
432/**
433 * Compile one author-facing value schema to the enforced raw JSON Schema
434 * subset. The author-only `json` node becomes an annotation-only schema.
435 * @param spec - schema for any JSON-value root.
436 * @returns The asserted raw schema projection.
437 */
438export function valueSchemaSpecToJsonSchema(spec: ValueSchemaSpec): JsonSchemaNode {
439 const schema = compileValueSchema(spec, 'schema')
440 assertSupportedJsonSchema(schema)
441 return schema
442}
443
444/**
445 * Compile the implicit open parameter object into raw JSON Schema.
446 * @param spec - per-property parameter definitions.
447 * @returns An object-rooted raw schema with no implicit-root openness override.
448 */
449export function parameterSchemaSpecToJsonSchema(spec: ParameterSchemaSpec): ParameterJsonSchema {
450 const compiled = compilePropertyMap(spec, 'parameters')
451 const schema: ParameterJsonSchema = {
452 type: 'object',
453 properties: compiled.properties,
454 ...(compiled.required === undefined ? {} : { required: compiled.required }),
455 }
456 assertSupportedJsonSchema(schema)
457 return schema
458}
459
460/** Invalid model-generated arguments for a typed tool. */
461export class ToolArgsError extends HarnessError {
462 /** Individual violations in schema-walk order. */
463 readonly violations: string[]
464
465 constructor(violations: string[]) {
466 super(`invalid arguments: ${violations.join('; ')}`, 'INVALID_ARGS')
467 this.name = 'ToolArgsError'
468 this.violations = violations
469 }
470}
471
472/**
473 * Validate model-generated arguments against an implicit parameter schema.
474 * @param spec - declared parameter schema.
475 * @param args - candidate arguments, however malformed.
476 * @returns Path-qualified violations; empty means valid.
477 */
478export function validateArgs(spec: ParameterSchemaSpec, args: unknown): string[] {
479 return validateJsonSchemaValue(parameterSchemaSpecToJsonSchema(spec), args, '')
480}
481
482/** Options for {@link defineTool}. */
483export interface DefineToolOptions<S extends ParameterSchemaSpec, O extends ValueSchemaSpec> {
484 /** Tool name (must be unique). */
485 readonly name: string
486 /** Human-readable description sent to the model. */
487 readonly description: string
488 /** Per-property parameter schema compiled to an implicit open object root. */
489 readonly parameters: S
490 /** Canonical output schema plus pure Native and presentation projections. */
491 readonly output: {
492 /** Schema enforced against every successful body or policy-replaced value. */
493 readonly schema: O
494 /** Pure Native/model rendering of one validated canonical value. */
495 render(args: InferArgs<S>, value: InferValue<NoInfer<O>>): ContentBlock[]
496 /** Pure replayable presentation metadata for direct top-level calls. */
497 presentationMeta?(args: InferArgs<S>, value: InferValue<NoInfer<O>>): JsonValue
498 }
499 /** Requests deferred loading of the tool definition; see {@link @deepseek-ai/dsh-llm#ToolSchema.deferLoading}. */
500 readonly deferLoading?: true
501 /** Optional positive cooperative timeout budget in milliseconds. */
502 readonly timeoutMs?: number
503 /**
504 * Pure classifier for sibling overlap.
505 * @param args - typed validated arguments.
506 * @returns Whether the call may join a parallel group.
507 */
508 isConcurrencySafe?(args: InferArgs<S>): boolean
509 /**
510 * Execute the tool after argument validation.
511 * @param args - typed validated arguments.
512 * @param exec - execution identity, caller, cancellation, and nesting data.
513 * @returns The canonical value declared by `output.schema`.
514 */
515 execute(args: InferArgs<S>, exec: ToolRunContext): Promise<InferValue<NoInfer<O>>>
516 /**
517 * Install execution-prepared content before result policies.
518 * @param exec - immutable execution identity and arguments.
519 * @param result - normalized outcome entering post-execute.
520 * @returns replacement content, or undefined to preserve it.
521 */
522 projectContent?(exec: Readonly<ToolExecution>, result: Readonly<ToolExecutionResult>): ContentBlock[] | undefined
523 /**
524 * Optional last-mile content transform for every normalized outcome. Unlike
525 * `execute`, arguments remain `unknown` because invalid-input failures also
526 * reach this callback. See {@link ToolDefinition.finalizeContent}.
527 * @param exec - immutable execution identity and arguments.
528 * @param result - complete normalized outcome before materialization.
529 * @returns replacement content, or `undefined` to preserve it.
530 */
531 finalizeContent?(exec: Readonly<ToolExecution>, result: Readonly<ToolExecutionResult>): ContentBlock[] | undefined
532 /**
533 * Pure pending-state presenter.
534 * @param args - typed validated arguments.
535 * @returns Tool-owned render intent, or `undefined` for the generic card.
536 */
537 presentCall?(args: InferArgs<S>): ToolCallView | undefined
538 /**
539 * Pure completed-state presenter.
540 * @param args - typed validated arguments.
541 * @param result - final model-facing tool result.
542 * @returns Tool-owned render intent, or `undefined` for the generic card.
543 */
544 presentResult?(args: InferArgs<S>, result: ToolResult): ToolResultView | undefined
545}
546
547/**
548 * Define a first-party tool with inferred arguments and strict execution
549 * validation. Replay-only presenters validate softly and fall back to generic
550 * rendering for obsolete logged arguments.
551 * @param options - typed definition and optional finalizer and presenters.
552 * @returns A registry-ready definition.
553 */
554export function defineTool<const S extends ParameterSchemaSpec, const O extends ValueSchemaSpec>(
555 options: DefineToolOptions<S, O>,
556): ToolDefinition {
557 // Object-literal methods do not use `this`; retaining references is safe.
558 // oxlint-disable-next-line typescript/unbound-method
559 const userExecute = options.execute
560 // oxlint-disable-next-line typescript/unbound-method
561 const userFinalizeContent = options.finalizeContent
562 // oxlint-disable-next-line typescript/unbound-method
563 const userProjectContent = options.projectContent
564 // oxlint-disable-next-line typescript/unbound-method
565 const userRender = options.output.render
566 // oxlint-disable-next-line typescript/unbound-method
567 const userPresentationMeta = options.output.presentationMeta
568 // oxlint-disable-next-line typescript/unbound-method
569 const userPresentCall = options.presentCall
570 // oxlint-disable-next-line typescript/unbound-method
571 const userPresentResult = options.presentResult
572 // oxlint-disable-next-line typescript/unbound-method
573 const userIsConcurrencySafe = options.isConcurrencySafe
574 if (options.timeoutMs !== undefined && (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0)) {
575 throw new Error(`defineTool(${options.name}): timeoutMs must be a positive finite number`)
576 }
577 const parameters = parameterSchemaSpecToJsonSchema(options.parameters)
578 const outputSchema = valueSchemaSpecToJsonSchema(options.output.schema)
579 const validate = (args: unknown): string[] => validateJsonSchemaValue(parameters, args, '')
580 const tool: ToolDefinition = {
581 name: options.name,
582 description: options.description,
583 parameters: parameters as unknown as Record<string, unknown>,
584 output: {
585 schema: outputSchema,
586 render(args: unknown, value: JsonValue): ContentBlock[] {
587 return userRender(args as InferArgs<S>, value as InferValue<NoInfer<O>>)
588 },
589 ...userPresentationMeta !== undefined ? {
590 presentationMeta(args: unknown, value: JsonValue): JsonValue {
591 return userPresentationMeta(args as InferArgs<S>, value as InferValue<NoInfer<O>>)
592 },
593 } : {},
594 },
595 ...(options.deferLoading === true ? { deferLoading: options.deferLoading } : {}),
596 ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
597 async execute(args: unknown, exec: ToolRunContext): Promise<JsonValue> {
598 const violations = validate(args)
599 if (violations.length > 0) throw new ToolArgsError(violations)
600 return userExecute(args as InferArgs<S>, exec) as Promise<JsonValue>
601 },
602 }
603 if (userProjectContent) {
604 tool.projectContent = (exec, result) => userProjectContent(exec, result)
605 }
606 if (userFinalizeContent) {
607 tool.finalizeContent = (exec, result) => userFinalizeContent(exec, result)
608 }
609 // Presentation is display-only and may run on REPLAY of arbitrary logged args
610 // (possibly from an older schema), so it must never throw: validate softly and
611 // fall back to `undefined` (a generic UI presentation) on any mismatch, rather
612 // than the hard `ToolArgsError` the execute path raises.
613 if (userPresentCall) {
614 tool.presentCall = (args: unknown): ToolCallView | undefined => {
615 if (validate(args).length > 0) return undefined
616 return userPresentCall(args as InferArgs<S>)
617 }
618 }
619 if (userPresentResult) {
620 tool.presentResult = (args: unknown, result: ToolResult): ToolResultView | undefined => {
621 if (validate(args).length > 0) return undefined
622 return userPresentResult(args as InferArgs<S>, result)
623 }
624 }
625 if (userIsConcurrencySafe) {
626 tool.isConcurrencySafe = (args: unknown): boolean => {
627 if (validate(args).length > 0) return false
628 return userIsConcurrencySafe(args as InferArgs<S>)
629 }
630 }
631 return tool
632}