@tanstack/ai 0.20.0 → 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/dist/esm/activities/chat/adapter.js +3 -1
  2. package/dist/esm/activities/chat/adapter.js.map +1 -1
  3. package/dist/esm/activities/chat/index.d.ts +3 -3
  4. package/dist/esm/activities/chat/index.js +46 -18
  5. package/dist/esm/activities/chat/index.js.map +1 -1
  6. package/dist/esm/activities/chat/messages.js +1 -1
  7. package/dist/esm/activities/chat/messages.js.map +1 -1
  8. package/dist/esm/activities/chat/middleware/compose.js +2 -0
  9. package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
  10. package/dist/esm/activities/chat/middleware/types.d.ts +5 -5
  11. package/dist/esm/activities/chat/stream/processor.d.ts +8 -8
  12. package/dist/esm/activities/chat/stream/processor.js +29 -21
  13. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  14. package/dist/esm/activities/chat/stream/strategies.d.ts +3 -3
  15. package/dist/esm/activities/chat/stream/strategies.js +4 -4
  16. package/dist/esm/activities/chat/stream/strategies.js.map +1 -1
  17. package/dist/esm/activities/chat/tools/lazy-tool-manager.js +5 -0
  18. package/dist/esm/activities/chat/tools/lazy-tool-manager.js.map +1 -1
  19. package/dist/esm/activities/chat/tools/schema-converter.d.ts +1 -1
  20. package/dist/esm/activities/chat/tools/schema-converter.js +36 -35
  21. package/dist/esm/activities/chat/tools/schema-converter.js.map +1 -1
  22. package/dist/esm/activities/chat/tools/tool-calls.d.ts +2 -2
  23. package/dist/esm/activities/chat/tools/tool-calls.js +17 -9
  24. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  25. package/dist/esm/activities/chat/tools/tool-definition.js +1 -1
  26. package/dist/esm/activities/chat/tools/tool-definition.js.map +1 -1
  27. package/dist/esm/activities/generateAudio/adapter.js +3 -1
  28. package/dist/esm/activities/generateAudio/adapter.js.map +1 -1
  29. package/dist/esm/activities/generateAudio/index.d.ts +5 -1
  30. package/dist/esm/activities/generateAudio/index.js.map +1 -1
  31. package/dist/esm/activities/generateImage/adapter.js +3 -1
  32. package/dist/esm/activities/generateImage/adapter.js.map +1 -1
  33. package/dist/esm/activities/generateImage/index.js +5 -0
  34. package/dist/esm/activities/generateImage/index.js.map +1 -1
  35. package/dist/esm/activities/generateSpeech/adapter.js +3 -1
  36. package/dist/esm/activities/generateSpeech/adapter.js.map +1 -1
  37. package/dist/esm/activities/generateTranscription/adapter.js +3 -1
  38. package/dist/esm/activities/generateTranscription/adapter.js.map +1 -1
  39. package/dist/esm/activities/generateVideo/adapter.js +3 -1
  40. package/dist/esm/activities/generateVideo/adapter.js.map +1 -1
  41. package/dist/esm/activities/stream-generation-result.js +6 -2
  42. package/dist/esm/activities/stream-generation-result.js.map +1 -1
  43. package/dist/esm/activities/summarize/adapter.js +3 -1
  44. package/dist/esm/activities/summarize/adapter.js.map +1 -1
  45. package/dist/esm/activities/summarize/chat-stream-summarize.d.ts +1 -1
  46. package/dist/esm/activities/summarize/chat-stream-summarize.js +5 -0
  47. package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
  48. package/dist/esm/activities/summarize/index.js.map +1 -1
  49. package/dist/esm/index.d.ts +1 -0
  50. package/dist/esm/index.js +2 -0
  51. package/dist/esm/index.js.map +1 -1
  52. package/dist/esm/logger/internal-logger.js +2 -0
  53. package/dist/esm/logger/internal-logger.js.map +1 -1
  54. package/dist/esm/middlewares/content-guard.js +5 -4
  55. package/dist/esm/middlewares/content-guard.js.map +1 -1
  56. package/dist/esm/middlewares/otel.js +24 -14
  57. package/dist/esm/middlewares/otel.js.map +1 -1
  58. package/dist/esm/realtime/index.d.ts +1 -1
  59. package/dist/esm/realtime/index.js.map +1 -1
  60. package/dist/esm/tools/provider-tool.d.ts +9 -0
  61. package/dist/esm/tools/provider-tool.js +7 -0
  62. package/dist/esm/tools/provider-tool.js.map +1 -0
  63. package/dist/esm/types.d.ts +6 -6
  64. package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
  65. package/dist/esm/utilities/chat-params.js.map +1 -1
  66. package/package.json +2 -2
  67. package/src/activities/chat/index.ts +34 -26
  68. package/src/activities/chat/messages.ts +2 -1
  69. package/src/activities/chat/middleware/types.ts +11 -9
  70. package/src/activities/chat/stream/processor.ts +19 -19
  71. package/src/activities/chat/stream/strategies.ts +3 -3
  72. package/src/activities/chat/tools/schema-converter.ts +77 -52
  73. package/src/activities/chat/tools/tool-calls.ts +18 -11
  74. package/src/activities/chat/tools/tool-definition.ts +6 -1
  75. package/src/activities/generateAudio/index.ts +5 -4
  76. package/src/activities/generateImage/index.ts +5 -0
  77. package/src/activities/stream-generation-result.ts +11 -2
  78. package/src/activities/summarize/chat-stream-summarize.ts +5 -2
  79. package/src/activities/summarize/index.ts +2 -2
  80. package/src/index.ts +1 -0
  81. package/src/middlewares/content-guard.ts +11 -3
  82. package/src/middlewares/otel.ts +25 -14
  83. package/src/realtime/index.ts +1 -1
  84. package/src/tools/provider-tool.ts +14 -0
  85. package/src/types.ts +12 -8
  86. package/src/utilities/ag-ui-wire.ts +2 -2
  87. package/src/utilities/chat-params.ts +2 -2
@@ -95,14 +95,16 @@ export interface TextActivityOptions<
95
95
  *
96
96
  * The three shapes can be mixed in a single array (e.g., when forwarding a wire payload that includes both anchor UIMessages and AG-UI fan-out ModelMessages).
97
97
  */
98
- messages?: Array<
99
- | UIMessage
100
- | ModelMessage
101
- | ConstrainedModelMessage<{
102
- inputModalities: TAdapter['~types']['inputModalities']
103
- messageMetadataByModality: TAdapter['~types']['messageMetadataByModality']
104
- }>
105
- >
98
+ messages?:
99
+ | Array<
100
+ | UIMessage
101
+ | ModelMessage
102
+ | ConstrainedModelMessage<{
103
+ inputModalities: TAdapter['~types']['inputModalities']
104
+ messageMetadataByModality: TAdapter['~types']['messageMetadataByModality']
105
+ }>
106
+ >
107
+ | undefined
106
108
  /**
107
109
  * System prompts to prepend to the conversation.
108
110
  *
@@ -112,9 +114,9 @@ export interface TextActivityOptions<
112
114
  * caching), providers without per-prompt metadata reject the field
113
115
  * entirely.
114
116
  */
115
- systemPrompts?: Array<
116
- SystemPrompt<TAdapter['~types']['systemPromptMetadata']>
117
- >
117
+ systemPrompts?:
118
+ | Array<SystemPrompt<TAdapter['~types']['systemPromptMetadata']>>
119
+ | undefined
118
120
  /**
119
121
  * Tools for function calling (auto-executed when called).
120
122
  *
@@ -125,10 +127,12 @@ export interface TextActivityOptions<
125
127
  * `supports.tools` list. Passing an unsupported tool produces a
126
128
  * compile-time error on the array element.
127
129
  */
128
- tools?: Array<
129
- | (Tool & { readonly '~toolKind'?: never })
130
- | ProviderTool<string, TAdapter['~types']['toolCapabilities'][number]>
131
- >
130
+ tools?:
131
+ | Array<
132
+ | (Tool & { readonly '~toolKind'?: never })
133
+ | ProviderTool<string, TAdapter['~types']['toolCapabilities'][number]>
134
+ >
135
+ | undefined
132
136
  /** Controls the randomness of the output. Higher values make output more random. Range: [0.0, 2.0] */
133
137
  temperature?: TextOptions['temperature']
134
138
  /** Nucleus sampling parameter. The model considers tokens with topP probability mass. */
@@ -323,7 +327,7 @@ class TextEngine<
323
327
  []
324
328
  private currentThinkingContent = ''
325
329
  private currentThinkingSignature = ''
326
- private eventOptions?: Record<string, unknown>
330
+ private eventOptions?: Record<string, unknown> | undefined
327
331
  private eventToolNames?: Array<string>
328
332
  private finishedEvent: RunFinishedEvent | null = null
329
333
  private earlyTermination = false
@@ -334,16 +338,16 @@ class TextEngine<
334
338
  private readonly initialClientToolResults: Map<string, any>
335
339
 
336
340
  // AG-UI protocol IDs
337
- private threadId: string
338
- private runIdOverride?: string
339
- private parentRunIdOverride?: string
341
+ private readonly threadId: string
342
+ private readonly runIdOverride?: string
343
+ private readonly parentRunIdOverride?: string
340
344
 
341
345
  // Middleware support
342
346
  private readonly middlewareRunner: MiddlewareRunner
343
347
  private readonly middlewareCtx: ChatMiddlewareContext
344
348
  private readonly deferredPromises: Array<Promise<unknown>> = []
345
349
  private abortReason?: string
346
- private middlewareAbortController?: AbortController
350
+ private readonly middlewareAbortController?: AbortController
347
351
  private terminalHookCalled = false
348
352
 
349
353
  private readonly logger: InternalLogger
@@ -402,8 +406,11 @@ class TextEngine<
402
406
  // handleStreamChunk processes raw chunks BEFORE middleware, so internal
403
407
  // state management sees extended fields (finishReason, delta, toolCallName, etc.).
404
408
  // The strip middleware ensures the yielded public stream is AG-UI spec-compliant.
405
- const allMiddleware = [
406
- devtoolsMiddleware(),
409
+ // `devtoolsMiddleware()` returns a structurally compatible
410
+ // `DevtoolsChatMiddleware` (defined in `@tanstack/ai-event-client` to
411
+ // avoid a circular dep). Cast it to `ChatMiddleware` for the runner.
412
+ const allMiddleware: Array<ChatMiddleware> = [
413
+ devtoolsMiddleware() as ChatMiddleware,
407
414
  ...(config.middleware || []),
408
415
  stripToSpecMiddleware(),
409
416
  ]
@@ -703,6 +710,7 @@ class TextEngine<
703
710
  }
704
711
 
705
712
  private handleStreamChunk(chunk: StreamChunk): void {
713
+ // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check -- AG-UI EventType enum members vs string-literal case labels; default branch handles untraced events.
706
714
  switch (chunk.type) {
707
715
  // AG-UI Events
708
716
  case 'TEXT_MESSAGE_CONTENT':
@@ -1001,11 +1009,10 @@ class TextEngine<
1001
1009
  return true
1002
1010
  })
1003
1011
 
1004
- if (undiscoveredLazyResults.length > 0) {
1005
- const finishEvt = this.finishedEvent!
1012
+ if (undiscoveredLazyResults.length > 0 && this.finishedEvent) {
1006
1013
  for (const chunk of this.buildToolResultChunks(
1007
1014
  undiscoveredLazyResults,
1008
- finishEvt,
1015
+ this.finishedEvent,
1009
1016
  )) {
1010
1017
  yield* this.pipeThroughMiddleware(chunk)
1011
1018
  }
@@ -1702,8 +1709,9 @@ async function* runStreamingText(
1702
1709
  function runNonStreamingText(
1703
1710
  options: TextActivityOptions<AnyTextAdapter, undefined, false>,
1704
1711
  ): Promise<string> {
1705
- // Run the streaming text and collect all text using streamToText
1712
+ // Run the streaming text and collect all text using streamToText.
1706
1713
  const stream = runStreamingText(
1714
+ // eslint-disable-next-line no-restricted-syntax -- generic-stream remap: caller is non-streaming (false), but runStreamingText is invoked internally to collect text; concrete `false`→`true` literals don't structurally overlap.
1707
1715
  options as unknown as TextActivityOptions<AnyTextAdapter, undefined, true>,
1708
1716
  )
1709
1717
 
@@ -463,12 +463,13 @@ export function modelMessagesToUIMessages(
463
463
  if (msg.role === 'tool') {
464
464
  // Tool result - merge into the last assistant message if possible
465
465
  if (
466
+ msg.toolCallId !== undefined &&
466
467
  currentAssistantMessage &&
467
468
  currentAssistantMessage.role === 'assistant'
468
469
  ) {
469
470
  currentAssistantMessage.parts.push({
470
471
  type: 'tool-result',
471
- toolCallId: msg.toolCallId!,
472
+ toolCallId: msg.toolCallId,
472
473
  content: getTextContent(msg.content),
473
474
  state: 'complete',
474
475
  })
@@ -79,9 +79,9 @@ export interface ChatMiddlewareContext {
79
79
  /** Names of configured tools, if any */
80
80
  toolNames?: Array<string>
81
81
  /** Flattened generation options (temperature, topP, maxTokens, metadata) */
82
- options?: Record<string, unknown>
82
+ options?: Record<string, unknown> | undefined
83
83
  /** Provider-specific model options */
84
- modelOptions?: Record<string, unknown>
84
+ modelOptions?: Record<string, unknown> | undefined
85
85
 
86
86
  // --- Computed info ---
87
87
 
@@ -121,8 +121,8 @@ export interface ChatMiddlewareConfig {
121
121
  temperature?: number
122
122
  topP?: number
123
123
  maxTokens?: number
124
- metadata?: Record<string, unknown>
125
- modelOptions?: Record<string, unknown>
124
+ metadata?: Record<string, unknown> | undefined
125
+ modelOptions?: Record<string, unknown> | undefined
126
126
  }
127
127
 
128
128
  // ===========================
@@ -257,11 +257,13 @@ export interface FinishInfo {
257
257
  /** Final accumulated text content */
258
258
  content: string
259
259
  /** Final usage totals, if available */
260
- usage?: {
261
- promptTokens: number
262
- completionTokens: number
263
- totalTokens: number
264
- }
260
+ usage?:
261
+ | {
262
+ promptTokens: number
263
+ completionTokens: number
264
+ totalTokens: number
265
+ }
266
+ | undefined
265
267
  }
266
268
 
267
269
  /**
@@ -133,25 +133,25 @@ export interface StreamProcessorOptions {
133
133
  * @see docs/chat-architecture.md#adapter-contract — What this class expects from adapters
134
134
  */
135
135
  export class StreamProcessor {
136
- private chunkStrategy: ChunkStrategy
137
- private events: StreamProcessorEvents
138
- private jsonParser: { parse: (jsonString: string) => any }
136
+ private readonly chunkStrategy: ChunkStrategy
137
+ private readonly events: StreamProcessorEvents
138
+ private readonly jsonParser: { parse: (jsonString: string) => any }
139
139
  private recordingEnabled: boolean
140
140
 
141
141
  // Message state
142
142
  private messages: Array<UIMessage> = []
143
143
 
144
144
  // Per-message stream state
145
- private messageStates: Map<string, MessageStreamState> = new Map()
146
- private activeMessageIds: Set<string> = new Set()
147
- private toolCallToMessage: Map<string, string> = new Map()
145
+ private readonly messageStates: Map<string, MessageStreamState> = new Map()
146
+ private readonly activeMessageIds: Set<string> = new Set()
147
+ private readonly toolCallToMessage: Map<string, string> = new Map()
148
148
  private pendingManualMessageId: string | null = null
149
149
  private pendingThinkingStepId: string | null = null
150
150
 
151
- private structuredMessageIds: Set<string> = new Set()
151
+ private readonly structuredMessageIds: Set<string> = new Set()
152
152
 
153
153
  // Run tracking (for concurrent run safety)
154
- private activeRuns = new Set<string>()
154
+ private readonly activeRuns = new Set<string>()
155
155
 
156
156
  // Shared stream state
157
157
  private finishReason: string | null = null
@@ -480,6 +480,7 @@ export class StreamProcessor {
480
480
  // Cast needed: @ag-ui/core Zod passthrough types add `& { [k: string]: unknown }`
481
481
  // which prevents TypeScript from narrowing the `type` discriminant in switch.
482
482
  const c = chunk as StreamChunk & { type: string }
483
+ // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check -- AG-UI EventType enum members vs string-literal case labels; default branch handles untraced events.
483
484
  switch (c.type) {
484
485
  // AG-UI Events
485
486
  case 'TEXT_MESSAGE_START':
@@ -645,10 +646,9 @@ export class StreamProcessor {
645
646
  * Used as fallback for events that don't include a messageId.
646
647
  */
647
648
  private getActiveAssistantMessageId(): string | null {
648
- // Set iteration is insertion-order; convert to array and search from the end
649
- const ids = Array.from(this.activeMessageIds)
650
- for (let i = ids.length - 1; i >= 0; i--) {
651
- const id = ids[i]!
649
+ // Set iteration is insertion-order; reverse-iterate to search from the end
650
+ const ids = Array.from(this.activeMessageIds).reverse()
651
+ for (const id of ids) {
652
652
  const state = this.messageStates.get(id)
653
653
  if (state && state.role === 'assistant') {
654
654
  return id
@@ -679,8 +679,8 @@ export class StreamProcessor {
679
679
  // Try active assistant message
680
680
  const activeId = this.getActiveAssistantMessageId()
681
681
  if (activeId) {
682
- const state = this.getMessageState(activeId)!
683
- return { messageId: activeId, state }
682
+ const state = this.getMessageState(activeId)
683
+ if (state) return { messageId: activeId, state }
684
684
  }
685
685
 
686
686
  // Check if a message with preferredId already exists (reconnect/resume case).
@@ -781,10 +781,10 @@ export class StreamProcessor {
781
781
  const existingMsg = this.messages.find((m) => m.id === messageId)
782
782
  if (existingMsg) {
783
783
  this.activeMessageIds.add(messageId)
784
- if (!this.messageStates.has(messageId)) {
784
+ const existingState = this.messageStates.get(messageId)
785
+ if (!existingState) {
785
786
  this.createMessageState(messageId, uiRole)
786
787
  } else {
787
- const existingState = this.messageStates.get(messageId)!
788
788
  // If tool calls happened since last text, this TEXT_MESSAGE_START
789
789
  // signals a new text segment — reset segment accumulation
790
790
  if (existingState.hasToolCallsSinceTextStart) {
@@ -845,7 +845,7 @@ export class StreamProcessor {
845
845
  ): void {
846
846
  this.resetStreamState()
847
847
  // AG-UI Message[] is compatible with UIMessage[] at runtime
848
- this.messages = [...chunk.messages] as unknown as Array<UIMessage>
848
+ this.messages = [...chunk.messages] as Array<UIMessage>
849
849
  this.emitMessagesChange()
850
850
  }
851
851
 
@@ -1365,7 +1365,7 @@ export class StreamProcessor {
1365
1365
  state.currentThinkingStepId = stepId
1366
1366
  }
1367
1367
 
1368
- const previous = state.thinkingSteps.get(stepId)!
1368
+ const previous = state.thinkingSteps.get(stepId) ?? ''
1369
1369
  let nextThinking = previous
1370
1370
 
1371
1371
  // Prefer delta over content
@@ -1900,7 +1900,7 @@ export function createReplayStream(
1900
1900
  recording: ChunkRecording,
1901
1901
  ): AsyncIterable<StreamChunk> {
1902
1902
  return {
1903
- // eslint-disable-next-line @typescript-eslint/require-await
1903
+ // eslint-disable-next-line @typescript-eslint/require-await -- async generator required by AsyncIterable contract; body has no await
1904
1904
  async *[Symbol.asyncIterator]() {
1905
1905
  for (const { chunk } of recording.chunks) {
1906
1906
  yield chunk
@@ -20,7 +20,7 @@ export class ImmediateStrategy implements ChunkStrategy {
20
20
  * Useful for natural text flow in UI
21
21
  */
22
22
  export class PunctuationStrategy implements ChunkStrategy {
23
- private punctuation = /[.,!?;:\n]/
23
+ private readonly punctuation = /[.,!?;:\n]/
24
24
 
25
25
  shouldEmit(chunk: string, _accumulated: string): boolean {
26
26
  return this.punctuation.test(chunk)
@@ -34,7 +34,7 @@ export class PunctuationStrategy implements ChunkStrategy {
34
34
  export class BatchStrategy implements ChunkStrategy {
35
35
  private chunkCount = 0
36
36
 
37
- constructor(private batchSize: number = 5) {}
37
+ constructor(private readonly batchSize: number = 5) {}
38
38
 
39
39
  shouldEmit(_chunk: string, _accumulated: string): boolean {
40
40
  this.chunkCount++
@@ -66,7 +66,7 @@ export class WordBoundaryStrategy implements ChunkStrategy {
66
66
  * Emits if ANY strategy says to emit
67
67
  */
68
68
  export class CompositeStrategy implements ChunkStrategy {
69
- constructor(private strategies: Array<ChunkStrategy>) {}
69
+ constructor(private readonly strategies: Array<ChunkStrategy>) {}
70
70
 
71
71
  shouldEmit(chunk: string, accumulated: string): boolean {
72
72
  return this.strategies.some((s) => s.shouldEmit(chunk, accumulated))
@@ -1,11 +1,29 @@
1
- /* eslint-disable @typescript-eslint/no-unnecessary-condition */
2
-
3
1
  import type {
4
2
  StandardJSONSchemaV1,
5
3
  StandardSchemaV1,
6
4
  } from '@standard-schema/spec'
7
5
  import type { JSONSchema, SchemaInput } from '../../../types'
8
6
 
7
+ /**
8
+ * Build a JSONSchema object from any plain key/value source. The `JSONSchema`
9
+ * interface's `[key: string]: any` index signature makes every property
10
+ * assignable through bracket access without a type cast — copying keys here
11
+ * lets us narrow either `Record<string, unknown>` (returned by
12
+ * `~standard.jsonSchema.input()`) or a `JSONSchema` (from the SchemaInput
13
+ * pass-through arm) into the typed view used by the rest of this module.
14
+ *
15
+ * Accepts `object` so callers don't need a cast when narrowing from union
16
+ * types like `SchemaInput`.
17
+ */
18
+ function toJsonSchema(obj: object): JSONSchema {
19
+ const result: JSONSchema = {}
20
+ for (const [key, value] of Object.entries(obj)) {
21
+ if (key === '$schema') continue // not needed by LLM providers
22
+ result[key] = value
23
+ }
24
+ return result
25
+ }
26
+
9
27
  /**
10
28
  * Check if a value is a Standard JSON Schema compliant schema.
11
29
  * Standard JSON Schema compliant libraries (Zod v4+, ArkType, Valibot with toStandardJsonSchema, etc.)
@@ -19,6 +37,7 @@ export function isStandardJSONSchema(
19
37
  schema !== null &&
20
38
  '~standard' in schema &&
21
39
  typeof (schema as StandardJSONSchemaV1)['~standard'] === 'object' &&
40
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime guard for caller-provided unknown; type assertion narrows but doesn't validate the wire payload
22
41
  (schema as StandardJSONSchemaV1)['~standard'].version === 1 &&
23
42
  typeof (schema as StandardJSONSchemaV1)['~standard'].jsonSchema ===
24
43
  'object' &&
@@ -37,9 +56,9 @@ export function isStandardSchema(schema: unknown): schema is StandardSchemaV1 {
37
56
  schema !== null &&
38
57
  '~standard' in schema &&
39
58
  typeof schema['~standard'] === 'object' &&
40
- schema !== null &&
41
59
  schema['~standard'] !== null &&
42
60
  'version' in schema['~standard'] &&
61
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime guard for caller-provided unknown; in-operator narrows but doesn't validate the wire payload
43
62
  schema['~standard'].version === 1 &&
44
63
  'validate' in schema['~standard'] &&
45
64
  typeof schema['~standard'].validate === 'function'
@@ -58,19 +77,20 @@ export function isStandardSchema(schema: unknown): schema is StandardSchemaV1 {
58
77
  * @returns Transformed schema compatible with OpenAI structured output
59
78
  */
60
79
  function makeStructuredOutputCompatible(
61
- schema: Record<string, any>,
80
+ schema: JSONSchema,
62
81
  originalRequired: Array<string> = [],
63
- ): Record<string, any> {
64
- const result = { ...schema }
82
+ ): JSONSchema {
83
+ const result: JSONSchema = { ...schema }
65
84
 
66
85
  // Handle object types
67
86
  if (result.type === 'object' && result.properties) {
68
- const properties = { ...result.properties }
87
+ const properties: Record<string, JSONSchema> = { ...result.properties }
69
88
  const allPropertyNames = Object.keys(properties)
70
89
 
71
90
  // Transform each property
72
91
  for (const propName of allPropertyNames) {
73
92
  const prop = properties[propName]
93
+ if (!prop) continue
74
94
  const wasOptional = !originalRequired.includes(propName)
75
95
 
76
96
  // Recursively transform nested objects/arrays
@@ -83,12 +103,12 @@ function makeStructuredOutputCompatible(
83
103
  ? { ...transformed, type: ['object', 'null'] }
84
104
  : transformed
85
105
  } else if (prop.type === 'array' && prop.items) {
86
- const transformed = {
106
+ const items = Array.isArray(prop.items) ? prop.items[0] : prop.items
107
+ const transformed: JSONSchema = {
87
108
  ...prop,
88
- items: makeStructuredOutputCompatible(
89
- prop.items,
90
- prop.items.required || [],
91
- ),
109
+ items: items
110
+ ? makeStructuredOutputCompatible(items, items.required || [])
111
+ : prop.items,
92
112
  }
93
113
  properties[propName] = wasOptional
94
114
  ? { ...transformed, type: ['array', 'null'] }
@@ -118,10 +138,10 @@ function makeStructuredOutputCompatible(
118
138
 
119
139
  // Handle array types with object items
120
140
  if (result.type === 'array' && result.items) {
121
- result.items = makeStructuredOutputCompatible(
122
- result.items,
123
- result.items.required || [],
124
- )
141
+ const items = Array.isArray(result.items) ? result.items[0] : result.items
142
+ if (items) {
143
+ result.items = makeStructuredOutputCompatible(items, items.required || [])
144
+ }
125
145
  }
126
146
 
127
147
  return result
@@ -216,42 +236,33 @@ export function convertSchemaToJsonSchema(
216
236
  target: 'draft-07',
217
237
  })
218
238
 
219
- let result = jsonSchema
220
-
221
- if (typeof result === 'object' && '$schema' in result) {
222
- // Remove $schema property as it's not needed for LLM providers
223
- const { $schema, ...rest } = result
224
- result = rest
225
- }
239
+ // Rebuild structurally so the typed JSONSchema view is acquired without
240
+ // a `Record<string, unknown> as JSONSchema` cast; `toJsonSchema()` also
241
+ // drops the `$schema` key which LLM providers don't need.
242
+ let result: JSONSchema = toJsonSchema(jsonSchema)
226
243
 
227
244
  // Ensure object schemas always have type: "object"
245
+ // If it has properties (even empty), it should be an object type
246
+ if ('properties' in result && !result.type) {
247
+ result.type = 'object'
248
+ }
228
249
 
229
- if (typeof result === 'object') {
230
- // If it has properties (even empty), it should be an object type
231
- if ('properties' in result && !result.type) {
232
- result.type = 'object'
233
- }
234
-
235
- // Ensure properties exists for object types (even if empty)
236
- if (result.type === 'object' && !('properties' in result)) {
237
- result.properties = {}
238
- }
250
+ // Ensure properties exists for object types (even if empty)
251
+ if (result.type === 'object' && !('properties' in result)) {
252
+ result.properties = {}
253
+ }
239
254
 
240
- // Ensure required exists for object types (even if empty array)
241
- if (result.type === 'object' && !('required' in result)) {
242
- result.required = []
243
- }
255
+ // Ensure required exists for object types (even if empty array)
256
+ if (result.type === 'object' && !('required' in result)) {
257
+ result.required = []
258
+ }
244
259
 
245
- // Apply structured output transformation if requested
246
- if (forStructuredOutput) {
247
- result = makeStructuredOutputCompatible(
248
- result,
249
- (result.required as Array<string>) || [],
250
- )
251
- }
260
+ // Apply structured output transformation if requested
261
+ if (forStructuredOutput) {
262
+ result = makeStructuredOutputCompatible(result, result.required || [])
252
263
  }
253
264
 
254
- return result as JSONSchema
265
+ return result
255
266
  }
256
267
 
257
268
  // Detect Standard Schema validators (Zod, ArkType, Valibot, …) that don't
@@ -271,14 +282,25 @@ export function convertSchemaToJsonSchema(
271
282
  // If it's not a Standard JSON Schema, assume it's already a JSONSchema and pass through
272
283
  // Still apply structured output transformation if requested
273
284
 
274
- if (forStructuredOutput && typeof schema === 'object') {
275
- return makeStructuredOutputCompatible(
276
- schema as Record<string, any>,
277
- ((schema as JSONSchema).required as Array<string>) || [],
278
- ) as JSONSchema
285
+ // At this branch, `schema` is the plain `JSONSchema` arm of `SchemaInput`
286
+ // (the two `~standard` arms were handled above). When no transformation
287
+ // is requested we pass the schema through by reference to preserve
288
+ // identity for callers that compare via `===`.
289
+ if (typeof schema !== 'object') {
290
+ // The SchemaInput union is object-shaped on every arm; if we ever hit a
291
+ // non-object here, propagate it untouched and let the downstream
292
+ // provider error loudly rather than silently widen.
293
+ return schema
279
294
  }
280
295
 
281
- return schema as JSONSchema
296
+ if (forStructuredOutput) {
297
+ // Build a typed view structurally so we don't need a SchemaInput→JSONSchema
298
+ // cast on the transformation path.
299
+ const typedView = toJsonSchema(schema)
300
+ return makeStructuredOutputCompatible(typedView, typedView.required || [])
301
+ }
302
+
303
+ return schema
282
304
  }
283
305
 
284
306
  /**
@@ -293,7 +315,10 @@ export async function validateWithStandardSchema<T>(
293
315
  data: unknown,
294
316
  ): Promise<
295
317
  | { success: true; data: T }
296
- | { success: false; issues: Array<{ message: string; path?: Array<string> }> }
318
+ | {
319
+ success: false
320
+ issues: Array<{ message: string; path?: Array<string> | undefined }>
321
+ }
297
322
  > {
298
323
  if (!isStandardSchema(schema)) {
299
324
  // If it's not a Standard Schema, just return the data as-is
@@ -81,8 +81,8 @@ export class MiddlewareAbortError extends Error {
81
81
  * ```
82
82
  */
83
83
  export class ToolCallManager {
84
- private toolCallsMap = new Map<number, ToolCall>()
85
- private tools: ReadonlyArray<Tool>
84
+ private readonly toolCallsMap = new Map<number, ToolCall>()
85
+ private readonly tools: ReadonlyArray<Tool>
86
86
 
87
87
  constructor(tools: ReadonlyArray<Tool>) {
88
88
  this.tools = tools
@@ -320,14 +320,16 @@ async function* executeWithEventPolling<T>(
320
320
  ])
321
321
 
322
322
  // Flush any pending events
323
- while (pendingEvents.length > 0) {
324
- yield pendingEvents.shift()!
323
+ let event: CustomEvent | undefined
324
+ while ((event = pendingEvents.shift()) !== undefined) {
325
+ yield event
325
326
  }
326
327
  }
327
328
 
328
329
  // Final flush in case events were emitted right at completion
329
- while (pendingEvents.length > 0) {
330
- yield pendingEvents.shift()!
330
+ let event: CustomEvent | undefined
331
+ while ((event = pendingEvents.shift()) !== undefined) {
332
+ yield event
331
333
  }
332
334
 
333
335
  return state.result
@@ -404,13 +406,17 @@ async function* executeServerTool(
404
406
  ): AsyncGenerator<CustomEvent, void, void> {
405
407
  const startTime = Date.now()
406
408
  try {
407
- const executionPromise = Promise.resolve(tool.execute!(input, context))
409
+ if (!tool.execute) {
410
+ throw new Error(`Tool ${toolName} has no execute() implementation`)
411
+ }
412
+ const executionPromise = Promise.resolve(tool.execute(input, context))
408
413
  let result = yield* executeWithEventPolling(executionPromise, pendingEvents)
409
414
  const duration = Date.now() - startTime
410
415
 
411
416
  // Flush remaining events
412
- while (pendingEvents.length > 0) {
413
- yield pendingEvents.shift()!
417
+ let pendingEvent: CustomEvent | undefined
418
+ while ((pendingEvent = pendingEvents.shift()) !== undefined) {
419
+ yield pendingEvent
414
420
  }
415
421
 
416
422
  // Validate output against outputSchema if provided
@@ -448,8 +454,9 @@ async function* executeServerTool(
448
454
  const duration = Date.now() - startTime
449
455
 
450
456
  // Flush remaining events
451
- while (pendingEvents.length > 0) {
452
- yield pendingEvents.shift()!
457
+ let pendingEvent: CustomEvent | undefined
458
+ while ((pendingEvent = pendingEvents.shift()) !== undefined) {
459
+ yield pendingEvent
453
460
  }
454
461
 
455
462
  if (error instanceof MiddlewareAbortError) {
@@ -29,6 +29,11 @@ export interface ClientTool<
29
29
  __toolSide: 'client'
30
30
  name: TName
31
31
  description: string
32
+ // Note: `inputSchema` / `outputSchema` stay as bare optionals (not
33
+ // widened to `| undefined`). They participate in inference via
34
+ // `InferToolInput` / `InferToolOutput` — widening with `| undefined`
35
+ // breaks the `infer TInput extends StandardJSONSchemaV1<...>` chain
36
+ // because `undefined` doesn't extend the schema constraint.
32
37
  inputSchema?: TInput
33
38
  outputSchema?: TOutput
34
39
  needsApproval?: boolean
@@ -215,7 +220,7 @@ export function toolDefinition<
215
220
  return {
216
221
  __toolSide: 'client',
217
222
  ...config,
218
- execute,
223
+ ...(execute !== undefined && { execute }),
219
224
  }
220
225
  },
221
226
  }
@@ -27,10 +27,11 @@ export const kind = 'audio' as const
27
27
  /**
28
28
  * Extract provider options from an AudioAdapter via ~types.
29
29
  */
30
- export type AudioProviderOptions<TAdapter> =
31
- TAdapter extends AudioAdapter<any, any>
32
- ? TAdapter['~types']['providerOptions']
33
- : object
30
+ export type AudioProviderOptions<TAdapter> = TAdapter extends {
31
+ '~types': { providerOptions: infer P extends object }
32
+ }
33
+ ? P
34
+ : object
34
35
 
35
36
  // ===========================
36
37
  // Activity Options Type
@@ -227,6 +227,11 @@ async function runGenerateImage<
227
227
  requestId,
228
228
  provider: adapter.name,
229
229
  model,
230
+ // GeneratedImage is a discriminated `{ url } | { b64Json }` union, but the
231
+ // wire shape on the devtools event is a plain optional pair. Use
232
+ // conditional spreads so the emitted record only sets the field actually
233
+ // present — `exactOptionalPropertyTypes` rejects `field: undefined`
234
+ // against `field?: string` targets.
230
235
  images: result.images.map((image) => ({
231
236
  url: image.url,
232
237
  b64Json: image.b64Json,