@tanstack/ai 0.20.1 → 0.21.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 (40) hide show
  1. package/dist/esm/activities/chat/index.d.ts +1 -1
  2. package/dist/esm/activities/chat/index.js +357 -258
  3. package/dist/esm/activities/chat/index.js.map +1 -1
  4. package/dist/esm/activities/chat/messages.js.map +1 -1
  5. package/dist/esm/activities/chat/middleware/compose.d.ts +10 -1
  6. package/dist/esm/activities/chat/middleware/compose.js +55 -0
  7. package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
  8. package/dist/esm/activities/chat/middleware/index.d.ts +1 -1
  9. package/dist/esm/activities/chat/middleware/types.d.ts +35 -3
  10. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  11. package/dist/esm/activities/chat/tools/schema-converter.d.ts +12 -1
  12. package/dist/esm/activities/chat/tools/schema-converter.js +13 -4
  13. package/dist/esm/activities/chat/tools/schema-converter.js.map +1 -1
  14. package/dist/esm/activities/generateImage/index.js.map +1 -1
  15. package/dist/esm/extend-adapter.js.map +1 -1
  16. package/dist/esm/index.d.ts +2 -2
  17. package/dist/esm/index.js +2 -1
  18. package/dist/esm/middlewares/content-guard.js.map +1 -1
  19. package/dist/esm/middlewares/otel.js +1 -4
  20. package/dist/esm/middlewares/otel.js.map +1 -1
  21. package/dist/esm/strip-to-spec-middleware.js.map +1 -1
  22. package/dist/esm/utilities/ag-ui-wire.js +4 -1
  23. package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
  24. package/package.json +3 -3
  25. package/skills/ai-core/middleware/SKILL.md +124 -18
  26. package/skills/ai-core/structured-outputs/SKILL.md +13 -0
  27. package/src/activities/chat/index.ts +653 -370
  28. package/src/activities/chat/messages.ts +1 -1
  29. package/src/activities/chat/middleware/compose.ts +65 -5
  30. package/src/activities/chat/middleware/index.ts +1 -0
  31. package/src/activities/chat/middleware/types.ts +53 -2
  32. package/src/activities/chat/stream/processor.ts +2 -2
  33. package/src/activities/chat/tools/schema-converter.ts +22 -7
  34. package/src/activities/generateImage/index.ts +3 -3
  35. package/src/extend-adapter.ts +1 -1
  36. package/src/index.ts +5 -1
  37. package/src/middlewares/content-guard.ts +1 -1
  38. package/src/middlewares/otel.ts +7 -10
  39. package/src/strip-to-spec-middleware.ts +1 -4
  40. package/src/utilities/ag-ui-wire.ts +2 -1
@@ -114,7 +114,7 @@ export function convertMessagesToModelMessages(
114
114
  modelMessages.push({
115
115
  role: 'system' as ModelMessage['role'],
116
116
  content: (msg as { content: string }).content,
117
- } as ModelMessage)
117
+ })
118
118
  continue
119
119
  }
120
120
 
@@ -11,6 +11,7 @@ import type {
11
11
  ErrorInfo,
12
12
  FinishInfo,
13
13
  IterationInfo,
14
+ StructuredOutputMiddlewareConfig,
14
15
  ToolCallHookContext,
15
16
  ToolPhaseCompleteInfo,
16
17
  UsageInfo,
@@ -71,7 +72,7 @@ export class MiddlewareRunner {
71
72
  current = { ...current, ...result }
72
73
  if (!skip) {
73
74
  this.logger.config(
74
- `middleware=${mw.name ?? 'unnamed'} keys=${Object.keys(result as object).join(',')}`,
75
+ `middleware=${mw.name ?? 'unnamed'} keys=${Object.keys(result).join(',')}`,
75
76
  {
76
77
  middleware: mw.name ?? 'unnamed',
77
78
  changes: result,
@@ -94,7 +95,66 @@ export class MiddlewareRunner {
94
95
  ...base,
95
96
  middlewareName: mw.name || 'unnamed',
96
97
  iteration: ctx.iteration,
97
- changes: result as Record<string, unknown>,
98
+ changes: result,
99
+ })
100
+ }
101
+ }
102
+ }
103
+ }
104
+ return current
105
+ }
106
+
107
+ /**
108
+ * Pipe config through all middleware onStructuredOutputConfig hooks in order.
109
+ * Each middleware receives the merged config from previous middleware.
110
+ * Partial returns are shallow-merged with the current config.
111
+ *
112
+ * Called once at the structured-output boundary, before runOnConfig at the
113
+ * same boundary (which receives a ChatMiddlewareConfig view, no outputSchema).
114
+ */
115
+ async runOnStructuredOutputConfig(
116
+ ctx: ChatMiddlewareContext,
117
+ config: StructuredOutputMiddlewareConfig,
118
+ ): Promise<StructuredOutputMiddlewareConfig> {
119
+ let current = config
120
+ for (const mw of this.middlewares) {
121
+ if (mw.onStructuredOutputConfig) {
122
+ const skip = shouldSkipInstrumentation(mw)
123
+ const start = Date.now()
124
+ const result = await mw.onStructuredOutputConfig(ctx, current)
125
+ const hasTransform = result !== undefined && result !== null
126
+ if (hasTransform) {
127
+ current = { ...current, ...result }
128
+ if (!skip) {
129
+ this.logger.config(
130
+ `middleware=${mw.name ?? 'unnamed'} keys=${Object.keys(result).join(',')}`,
131
+ {
132
+ middleware: mw.name ?? 'unnamed',
133
+ changes: result,
134
+ },
135
+ )
136
+ }
137
+ }
138
+ if (!skip) {
139
+ const base = instrumentCtx(ctx)
140
+ aiEventClient.emit('middleware:hook:executed', {
141
+ ...base,
142
+ middlewareName: mw.name || 'unnamed',
143
+ hookName: 'onStructuredOutputConfig',
144
+ iteration: ctx.iteration,
145
+ duration: Date.now() - start,
146
+ hasTransform,
147
+ })
148
+ if (hasTransform) {
149
+ aiEventClient.emit('middleware:config:transformed', {
150
+ ...base,
151
+ middlewareName: mw.name || 'unnamed',
152
+ iteration: ctx.iteration,
153
+ // `result` is `Partial<StructuredOutputMiddlewareConfig>` —
154
+ // Object.fromEntries(Object.entries(result)) yields the
155
+ // structural `Record<string, unknown>` the event emitter wants
156
+ // without an `as` cast.
157
+ changes: Object.fromEntries(Object.entries(result)),
98
158
  })
99
159
  }
100
160
  }
@@ -152,7 +212,7 @@ export class MiddlewareRunner {
152
212
  const nextChunks: Array<StreamChunk> = []
153
213
  for (const c of chunks) {
154
214
  // Cast: @ag-ui/core Zod passthrough types prevent direct `.type` access
155
- const chunkType = (c as StreamChunk & { type: string }).type
215
+ const chunkType = c.type
156
216
  if (!skip) {
157
217
  this.logger.middleware(
158
218
  `hook=onChunk middleware=${mw.name ?? 'unnamed'} in=${chunkType}`,
@@ -188,7 +248,7 @@ export class MiddlewareRunner {
188
248
  nextChunks.push(...result)
189
249
  if (!skip) {
190
250
  this.logger.middleware(
191
- `hook=onChunk middleware=${mw.name ?? 'unnamed'} in=${chunkType} out=[${result.map((r: StreamChunk) => (r as StreamChunk & { type: string }).type).join(',')}]`,
251
+ `hook=onChunk middleware=${mw.name ?? 'unnamed'} in=${chunkType} out=[${result.map((r: StreamChunk) => r.type).join(',')}]`,
192
252
  {
193
253
  middleware: mw.name ?? 'unnamed',
194
254
  hook: 'onChunk',
@@ -209,7 +269,7 @@ export class MiddlewareRunner {
209
269
  nextChunks.push(result)
210
270
  if (!skip) {
211
271
  this.logger.middleware(
212
- `hook=onChunk middleware=${mw.name ?? 'unnamed'} in=${chunkType} out=${(result as StreamChunk & { type: string }).type}`,
272
+ `hook=onChunk middleware=${mw.name ?? 'unnamed'} in=${chunkType} out=${result.type}`,
213
273
  {
214
274
  middleware: mw.name ?? 'unnamed',
215
275
  hook: 'onChunk',
@@ -3,6 +3,7 @@ export type {
3
3
  ChatMiddlewareContext,
4
4
  ChatMiddlewarePhase,
5
5
  ChatMiddlewareConfig,
6
+ StructuredOutputMiddlewareConfig,
6
7
  ToolCallHookContext,
7
8
  BeforeToolCallDecision,
8
9
  AfterToolCallInfo,
@@ -1,4 +1,10 @@
1
- import type { ModelMessage, StreamChunk, Tool, ToolCall } from '../../../types'
1
+ import type {
2
+ JSONSchema,
3
+ ModelMessage,
4
+ StreamChunk,
5
+ Tool,
6
+ ToolCall,
7
+ } from '../../../types'
2
8
  import type { SystemPrompt } from '../../../system-prompts'
3
9
 
4
10
  // ===========================
@@ -12,6 +18,8 @@ import type { SystemPrompt } from '../../../system-prompts'
12
18
  * - 'modelStream': During model streaming
13
19
  * - 'beforeTools': Before tool execution phase
14
20
  * - 'afterTools': After tool execution phase
21
+ * - 'structuredOutput': During the final structured-output adapter call (set
22
+ * for chunks from adapter.structuredOutputStream or the synthesized fallback)
15
23
  */
16
24
  export type ChatMiddlewarePhase =
17
25
  | 'init'
@@ -19,6 +27,7 @@ export type ChatMiddlewarePhase =
19
27
  | 'modelStream'
20
28
  | 'beforeTools'
21
29
  | 'afterTools'
30
+ | 'structuredOutput'
22
31
 
23
32
  /**
24
33
  * Stable context object passed to all middleware hooks.
@@ -125,6 +134,24 @@ export interface ChatMiddlewareConfig {
125
134
  modelOptions?: Record<string, unknown> | undefined
126
135
  }
127
136
 
137
+ /**
138
+ * Config passed to onStructuredOutputConfig.
139
+ *
140
+ * Mirrors ChatMiddlewareConfig minus `tools` (the final structured-output call
141
+ * is a single typed-response request, not an agentic loop — tools cannot be
142
+ * forwarded to it), plus the `outputSchema` being sent to the provider.
143
+ * Middleware may transform the schema (e.g., inject $defs, strip
144
+ * vendor-incompatible keywords) by returning a partial that includes
145
+ * `outputSchema`.
146
+ */
147
+ export interface StructuredOutputMiddlewareConfig extends Omit<
148
+ ChatMiddlewareConfig,
149
+ 'tools'
150
+ > {
151
+ /** JSON Schema being sent to the provider for structured output. */
152
+ outputSchema: JSONSchema
153
+ }
154
+
128
155
  // ===========================
129
156
  // Tool Call Hook Context
130
157
  // ===========================
@@ -337,7 +364,31 @@ export interface ChatMiddleware {
337
364
  | void
338
365
  | null
339
366
  | Partial<ChatMiddlewareConfig>
340
- | Promise<void | Partial<ChatMiddlewareConfig>>
367
+ | Promise<void | null | Partial<ChatMiddlewareConfig>>
368
+
369
+ /**
370
+ * Called at the start of the final structured-output call (when the chat
371
+ * was invoked with outputSchema). Pipes through middleware in order, like
372
+ * onConfig, but with access to the JSON Schema being sent to the provider.
373
+ *
374
+ * Return a partial to shallow-merge into the current config, or void to
375
+ * pass through.
376
+ *
377
+ * Fires BEFORE onConfig at the structured-output boundary. onConfig also
378
+ * re-fires at the same boundary with ctx.phase === 'structuredOutput',
379
+ * receiving the post-onStructuredOutputConfig view of the config (minus
380
+ * outputSchema). Use onConfig for general-purpose transforms that apply
381
+ * to every adapter call; use this hook when you need to transform the
382
+ * outputSchema or apply structured-output-specific behavior.
383
+ */
384
+ onStructuredOutputConfig?: (
385
+ ctx: ChatMiddlewareContext,
386
+ config: StructuredOutputMiddlewareConfig,
387
+ ) =>
388
+ | void
389
+ | null
390
+ | Partial<StructuredOutputMiddlewareConfig>
391
+ | Promise<void | null | Partial<StructuredOutputMiddlewareConfig>>
341
392
 
342
393
  /**
343
394
  * Called when the chat run starts (after initial onConfig).
@@ -216,7 +216,7 @@ export class StreamProcessor {
216
216
  ? [{ type: 'text', content }]
217
217
  : content.map((part) => {
218
218
  // ContentPart types (text, image, audio, video, document) are compatible with MessagePart
219
- return part as MessagePart
219
+ return part
220
220
  })
221
221
 
222
222
  const userMessage: UIMessage = {
@@ -479,7 +479,7 @@ export class StreamProcessor {
479
479
 
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
- const c = chunk as StreamChunk & { type: string }
482
+ const c = chunk
483
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.
484
484
  switch (c.type) {
485
485
  // AG-UI Events
@@ -58,7 +58,6 @@ export function isStandardSchema(schema: unknown): schema is StandardSchemaV1 {
58
58
  typeof schema['~standard'] === 'object' &&
59
59
  schema['~standard'] !== null &&
60
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
62
61
  schema['~standard'].version === 1 &&
63
62
  'validate' in schema['~standard'] &&
64
63
  typeof schema['~standard'].validate === 'function'
@@ -340,6 +339,25 @@ export async function validateWithStandardSchema<T>(
340
339
  }
341
340
  }
342
341
 
342
+ /**
343
+ * Error thrown when Standard Schema validation fails. Carries the original
344
+ * `issues` array so consumers (middleware `onError`, callers catching from
345
+ * `chat({ outputSchema })`) can programmatically inspect each failure.
346
+ */
347
+ export class StandardSchemaValidationError extends Error {
348
+ override readonly name = 'StandardSchemaValidationError'
349
+ readonly issues: ReadonlyArray<StandardSchemaV1.Issue>
350
+
351
+ constructor(issues: ReadonlyArray<StandardSchemaV1.Issue>) {
352
+ super(
353
+ `Validation failed: ${issues
354
+ .map((i) => i.message || 'Validation failed')
355
+ .join(', ')}`,
356
+ )
357
+ this.issues = issues
358
+ }
359
+ }
360
+
343
361
  /**
344
362
  * Synchronously validates data against a Standard Schema compliant schema.
345
363
  * Note: Some Standard Schema implementations may only support async validation.
@@ -348,7 +366,8 @@ export async function validateWithStandardSchema<T>(
348
366
  * @param schema - Standard Schema compliant schema
349
367
  * @param data - Data to validate
350
368
  * @returns Parsed/validated data
351
- * @throws Error if validation fails or if the schema only supports async validation
369
+ * @throws StandardSchemaValidationError if validation fails; Error if the
370
+ * schema only supports async validation.
352
371
  */
353
372
  export function parseWithStandardSchema<T>(schema: unknown, data: unknown): T {
354
373
  if (!isStandardSchema(schema)) {
@@ -369,9 +388,5 @@ export function parseWithStandardSchema<T>(schema: unknown, data: unknown): T {
369
388
  return result.value as T
370
389
  }
371
390
 
372
- // invalid validation, throw error with all issues
373
- const errorMessages = result.issues
374
- .map((issue) => issue.message || 'Validation failed')
375
- .join(', ')
376
- throw new Error(`Validation failed: ${errorMessages}`)
391
+ throw new StandardSchemaValidationError(result.issues)
377
392
  }
@@ -210,7 +210,7 @@ async function runGenerateImage<
210
210
  prompt: rest.prompt,
211
211
  numberOfImages: rest.numberOfImages,
212
212
  size: rest.size,
213
- modelOptions: rest.modelOptions as Record<string, unknown> | undefined,
213
+ modelOptions: rest.modelOptions,
214
214
  timestamp: startTime,
215
215
  })
216
216
 
@@ -237,7 +237,7 @@ async function runGenerateImage<
237
237
  b64Json: image.b64Json,
238
238
  })),
239
239
  duration,
240
- modelOptions: rest.modelOptions as Record<string, unknown> | undefined,
240
+ modelOptions: rest.modelOptions,
241
241
  timestamp: Date.now(),
242
242
  })
243
243
 
@@ -246,7 +246,7 @@ async function runGenerateImage<
246
246
  requestId,
247
247
  model,
248
248
  usage: result.usage,
249
- modelOptions: rest.modelOptions as Record<string, unknown> | undefined,
249
+ modelOptions: rest.modelOptions,
250
250
  timestamp: Date.now(),
251
251
  })
252
252
  }
@@ -65,7 +65,7 @@ export function createModel<
65
65
  return {
66
66
  name,
67
67
  input,
68
- modelOptions: {} as unknown,
68
+ modelOptions: {},
69
69
  }
70
70
  }
71
71
 
package/src/index.ts CHANGED
@@ -53,7 +53,10 @@ export {
53
53
  } from './activities/chat/tools/tool-definition'
54
54
 
55
55
  // Schema conversion (Standard JSON Schema compliant)
56
- export { convertSchemaToJsonSchema } from './activities/chat/tools/schema-converter'
56
+ export {
57
+ convertSchemaToJsonSchema,
58
+ StandardSchemaValidationError,
59
+ } from './activities/chat/tools/schema-converter'
57
60
 
58
61
  // Stream utilities
59
62
  export {
@@ -91,6 +94,7 @@ export type {
91
94
  ChatMiddlewareContext,
92
95
  ChatMiddlewarePhase,
93
96
  ChatMiddlewareConfig,
97
+ StructuredOutputMiddlewareConfig,
94
98
  ToolCallHookContext,
95
99
  BeforeToolCallDecision,
96
100
  AfterToolCallInfo,
@@ -151,7 +151,7 @@ function createDeltaStrategy(
151
151
  return {
152
152
  ...rest,
153
153
  delta: filtered,
154
- } as StreamChunk
154
+ }
155
155
  },
156
156
  }
157
157
  }
@@ -256,10 +256,7 @@ export function otelMiddleware(options: OtelMiddlewareOptions): ChatMiddleware {
256
256
  const span = state.currentIterationSpan
257
257
  const iteration = state.iterationCount - 1
258
258
  safeCall('otel.onSpanEnd', () =>
259
- onSpanEnd?.(
260
- { kind: 'iteration', ctx, iteration } as OtelSpanInfo<'iteration'>,
261
- span,
262
- ),
259
+ onSpanEnd?.({ kind: 'iteration', ctx, iteration }, span),
263
260
  )
264
261
  span.end()
265
262
  state.currentIterationSpan = null
@@ -676,7 +673,7 @@ export function otelMiddleware(options: OtelMiddlewareOptions): ChatMiddleware {
676
673
  toolName: info.toolName,
677
674
  toolCallId: info.toolCallId,
678
675
  iteration: state.iterationCount - 1,
679
- } as OtelSpanInfo<'tool'>,
676
+ },
680
677
  toolSpan,
681
678
  ),
682
679
  )
@@ -709,7 +706,7 @@ export function otelMiddleware(options: OtelMiddlewareOptions): ChatMiddleware {
709
706
  kind: 'iteration',
710
707
  ctx,
711
708
  iteration: state.iterationCount - 1,
712
- } as OtelSpanInfo<'iteration'>,
709
+ },
713
710
  iterationSpan,
714
711
  ),
715
712
  )
@@ -729,7 +726,7 @@ export function otelMiddleware(options: OtelMiddlewareOptions): ChatMiddleware {
729
726
  toolCallId: id,
730
727
  toolName,
731
728
  iteration: state.iterationCount - 1,
732
- } as OtelSpanInfo<'tool'>,
729
+ },
733
730
  span,
734
731
  ),
735
732
  )
@@ -782,7 +779,7 @@ export function otelMiddleware(options: OtelMiddlewareOptions): ChatMiddleware {
782
779
  kind: 'iteration',
783
780
  ctx,
784
781
  iteration: state.iterationCount - 1,
785
- } as OtelSpanInfo<'iteration'>,
782
+ },
786
783
  iterationSpan,
787
784
  ),
788
785
  )
@@ -800,7 +797,7 @@ export function otelMiddleware(options: OtelMiddlewareOptions): ChatMiddleware {
800
797
  toolCallId: id,
801
798
  toolName,
802
799
  iteration: state.iterationCount - 1,
803
- } as OtelSpanInfo<'tool'>,
800
+ },
804
801
  span,
805
802
  ),
806
803
  )
@@ -845,7 +842,7 @@ export function otelMiddleware(options: OtelMiddlewareOptions): ChatMiddleware {
845
842
  toolCallId: id,
846
843
  toolName,
847
844
  iteration: state.iterationCount - 1,
848
- } as OtelSpanInfo<'tool'>,
845
+ },
849
846
  span,
850
847
  ),
851
848
  )
@@ -12,10 +12,7 @@ import type { StreamChunk } from './types'
12
12
  */
13
13
  export function stripToSpec(chunk: StreamChunk): StreamChunk {
14
14
  // Only strip the deprecated nested error object from RUN_ERROR
15
- if (
16
- (chunk as StreamChunk & { type: string }).type === 'RUN_ERROR' &&
17
- 'error' in chunk
18
- ) {
15
+ if (chunk.type === 'RUN_ERROR' && 'error' in chunk) {
19
16
  const { error: _deprecated, ...rest } = chunk as Record<string, unknown>
20
17
  return rest as StreamChunk
21
18
  }
@@ -53,6 +53,7 @@ export function uiMessagesToWire(
53
53
  // Defensive: if parts is missing (ModelMessage-shaped input), pass through as-is.
54
54
  // UIMessage always has parts; ModelMessage uses content directly.
55
55
  const parts: ReadonlyArray<MessagePart> =
56
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- runtime input may be ModelMessage-shaped (no `parts`); cast forces the optional-chain fallback below to remain in scope
56
57
  (msg.parts as ReadonlyArray<MessagePart> | undefined) ?? []
57
58
 
58
59
  if (msg.role === 'system') {
@@ -161,7 +162,7 @@ function collectUserContent(
161
162
  p.type === 'video' ||
162
163
  p.type === 'document'
163
164
  ) {
164
- out.push(p as AGUIInputContent)
165
+ out.push(p)
165
166
  }
166
167
  }
167
168
  return out