@tanstack/ai 0.23.1 → 0.24.0

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 (61) hide show
  1. package/dist/esm/activities/chat/index.d.ts +33 -9
  2. package/dist/esm/activities/chat/index.js +19 -9
  3. package/dist/esm/activities/chat/index.js.map +1 -1
  4. package/dist/esm/activities/chat/messages.js +2 -1
  5. package/dist/esm/activities/chat/messages.js.map +1 -1
  6. package/dist/esm/activities/chat/middleware/compose.d.ts +14 -14
  7. package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
  8. package/dist/esm/activities/chat/middleware/types.d.ts +16 -16
  9. package/dist/esm/activities/chat/runtime-context-types.d.ts +43 -0
  10. package/dist/esm/activities/chat/stream/message-updaters.d.ts +2 -2
  11. package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
  12. package/dist/esm/activities/chat/stream/processor.d.ts +1 -0
  13. package/dist/esm/activities/chat/stream/processor.js +35 -12
  14. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  15. package/dist/esm/activities/chat/tools/tool-calls.d.ts +15 -5
  16. package/dist/esm/activities/chat/tools/tool-calls.js +59 -19
  17. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  18. package/dist/esm/activities/chat/tools/tool-definition.d.ts +12 -8
  19. package/dist/esm/activities/chat/tools/tool-definition.js.map +1 -1
  20. package/dist/esm/activities/error-payload.d.ts +26 -0
  21. package/dist/esm/activities/error-payload.js +12 -1
  22. package/dist/esm/activities/error-payload.js.map +1 -1
  23. package/dist/esm/adapter-internals.d.ts +1 -1
  24. package/dist/esm/adapter-internals.js +3 -2
  25. package/dist/esm/client.d.ts +1 -1
  26. package/dist/esm/client.js +3 -1
  27. package/dist/esm/client.js.map +1 -1
  28. package/dist/esm/index.d.ts +2 -1
  29. package/dist/esm/index.js +7 -1
  30. package/dist/esm/index.js.map +1 -1
  31. package/dist/esm/tool-registry.d.ts +7 -7
  32. package/dist/esm/tool-registry.js +1 -1
  33. package/dist/esm/tool-registry.js.map +1 -1
  34. package/dist/esm/types.d.ts +40 -8
  35. package/dist/esm/utilities/ag-ui-wire.js +1 -1
  36. package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
  37. package/dist/esm/utilities/chat-params.d.ts +8 -3
  38. package/dist/esm/utilities/chat-params.js +6 -2
  39. package/dist/esm/utilities/chat-params.js.map +1 -1
  40. package/dist/esm/utilities/tool-result.d.ts +21 -0
  41. package/dist/esm/utilities/tool-result.js +37 -0
  42. package/dist/esm/utilities/tool-result.js.map +1 -0
  43. package/package.json +2 -2
  44. package/src/activities/chat/index.ts +219 -47
  45. package/src/activities/chat/messages.ts +2 -1
  46. package/src/activities/chat/middleware/compose.ts +23 -17
  47. package/src/activities/chat/middleware/types.ts +16 -16
  48. package/src/activities/chat/runtime-context-types.ts +68 -0
  49. package/src/activities/chat/stream/message-updaters.ts +2 -1
  50. package/src/activities/chat/stream/processor.ts +48 -8
  51. package/src/activities/chat/tools/tool-calls.ts +138 -43
  52. package/src/activities/chat/tools/tool-definition.ts +25 -31
  53. package/src/activities/error-payload.ts +44 -0
  54. package/src/adapter-internals.ts +4 -1
  55. package/src/client.ts +5 -1
  56. package/src/index.ts +7 -0
  57. package/src/tool-registry.ts +16 -14
  58. package/src/types.ts +86 -29
  59. package/src/utilities/ag-ui-wire.ts +4 -1
  60. package/src/utilities/chat-params.ts +22 -7
  61. package/src/utilities/tool-result.ts +60 -0
@@ -10,6 +10,7 @@ import { stripToSpecMiddleware } from '../../strip-to-spec-middleware'
10
10
  import { streamToText } from '../../stream-to-response.js'
11
11
  import { resolveDebugOption } from '../../logger/resolve'
12
12
  import { EventType } from '../../types'
13
+ import { normalizeToolResult } from '../../utilities/tool-result'
13
14
  import { LazyToolManager } from './tools/lazy-tool-manager'
14
15
  import {
15
16
  MiddlewareAbortError,
@@ -32,6 +33,7 @@ import type {
32
33
  import type { AnyTextAdapter, StructuredOutputOptions } from './adapter'
33
34
  import type {
34
35
  AgentLoopStrategy,
36
+ AnyTool,
35
37
  ConstrainedModelMessage,
36
38
  CustomEvent,
37
39
  InferSchemaType,
@@ -44,7 +46,6 @@ import type {
44
46
  StructuredOutputStream,
45
47
  TextMessageContentEvent,
46
48
  TextOptions,
47
- Tool,
48
49
  ToolCall,
49
50
  ToolCallArgsEvent,
50
51
  ToolCallEndEvent,
@@ -61,6 +62,13 @@ import type { SystemPrompt } from '../../system-prompts'
61
62
  import type { InternalLogger } from '../../logger/internal-logger'
62
63
  import type { DebugOption } from '../../logger/types'
63
64
  import type { ProviderTool } from '../../tools/provider-tool'
65
+ import type {
66
+ ContextFromMiddleware,
67
+ ContextFromTool,
68
+ DefinedContext,
69
+ MergeContext,
70
+ UnionToIntersection,
71
+ } from './runtime-context-types'
64
72
 
65
73
  // ===========================
66
74
  // Activity Kind
@@ -69,6 +77,71 @@ import type { ProviderTool } from '../../tools/provider-tool'
69
77
  /** The adapter kind this activity handles */
70
78
  export const kind = 'text' as const
71
79
 
80
+ type AnyRuntimeTool = AnyTool
81
+
82
+ // The leaf context-inference primitives (KnownContext, MergeContext,
83
+ // UnionToIntersection, DefinedContext, ContextFromTool, ContextFromMiddleware)
84
+ // are shared with the tool execution layer — see ./runtime-context-types.
85
+ type ContextFromConsumer<T> = ContextFromTool<T> | ContextFromMiddleware<T>
86
+
87
+ type RequiredContextFromConsumerUnion<T> = T extends unknown
88
+ ? undefined extends ContextFromConsumer<T>
89
+ ? never
90
+ : ContextFromConsumer<T>
91
+ : never
92
+
93
+ type ContextFromConsumerUnion<T> = [
94
+ UnionToIntersection<DefinedContext<ContextFromConsumer<T>>>,
95
+ ] extends [never]
96
+ ? never
97
+ : [RequiredContextFromConsumerUnion<T>] extends [never]
98
+ ? UnionToIntersection<DefinedContext<ContextFromConsumer<T>>> | undefined
99
+ : UnionToIntersection<DefinedContext<ContextFromConsumer<T>>>
100
+
101
+ type ContextFromArray<T> = T extends readonly [infer THead, ...infer TTail]
102
+ ? MergeContext<ContextFromConsumer<THead>, ContextFromArray<TTail>>
103
+ : T extends ReadonlyArray<infer TItem>
104
+ ? ContextFromConsumerUnion<TItem>
105
+ : never
106
+
107
+ type ContextFromInputs<TTools, TMiddleware> = MergeContext<
108
+ ContextFromArray<NonNullable<TTools>>,
109
+ ContextFromArray<NonNullable<TMiddleware>>
110
+ >
111
+
112
+ type InferredContext<TTools, TMiddleware> = [
113
+ ContextFromInputs<TTools, TMiddleware>,
114
+ ] extends [never]
115
+ ? unknown
116
+ : ContextFromInputs<TTools, TMiddleware>
117
+
118
+ type RequiredContextFromInputs<TTools, TMiddleware> = [
119
+ ContextFromInputs<TTools, TMiddleware>,
120
+ ] extends [never]
121
+ ? { context?: unknown }
122
+ : undefined extends ContextFromInputs<TTools, TMiddleware>
123
+ ? { context?: ContextFromInputs<TTools, TMiddleware> }
124
+ : { context: ContextFromInputs<TTools, TMiddleware> }
125
+
126
+ type TextActivityOptionsWithContext<
127
+ TAdapter extends AnyTextAdapter,
128
+ TSchema extends SchemaInput | undefined,
129
+ TStream extends boolean,
130
+ TTools extends TextActivityOptions<TAdapter, TSchema, TStream, any>['tools'],
131
+ TMiddleware extends TextActivityOptions<
132
+ TAdapter,
133
+ TSchema,
134
+ TStream,
135
+ any
136
+ >['middleware'],
137
+ > = Omit<
138
+ TextActivityOptions<TAdapter, TSchema, TStream, any>,
139
+ 'tools' | 'middleware' | 'context'
140
+ > & {
141
+ tools?: TTools
142
+ middleware?: TMiddleware
143
+ } & RequiredContextFromInputs<TTools, TMiddleware>
144
+
72
145
  // ===========================
73
146
  // Activity Options Type
74
147
  // ===========================
@@ -85,6 +158,7 @@ export interface TextActivityOptions<
85
158
  TAdapter extends AnyTextAdapter,
86
159
  TSchema extends SchemaInput | undefined,
87
160
  TStream extends boolean,
161
+ TContext = unknown,
88
162
  > {
89
163
  /** The text adapter to use (created by a provider function like openaiText('gpt-4o')) */
90
164
  adapter: TAdapter
@@ -130,7 +204,7 @@ export interface TextActivityOptions<
130
204
  */
131
205
  tools?:
132
206
  | Array<
133
- | (Tool & { readonly '~toolKind'?: never })
207
+ | (AnyRuntimeTool & { readonly '~toolKind'?: never })
134
208
  | ProviderTool<string, TAdapter['~types']['toolCapabilities'][number]>
135
209
  >
136
210
  | undefined
@@ -209,12 +283,11 @@ export interface TextActivityOptions<
209
283
  * })
210
284
  * ```
211
285
  */
212
- middleware?: Array<ChatMiddleware>
286
+ middleware?: Array<ChatMiddleware<TContext>>
213
287
  /**
214
- * Opaque user-provided context value passed to middleware hooks.
215
- * Can be used to pass request-scoped data (e.g., user ID, request context).
288
+ * Runtime context value passed to middleware hooks and server tools.
216
289
  */
217
- context?: unknown
290
+ context?: TContext
218
291
  /**
219
292
  * Enable debug logging. Pass `true` to enable all categories with the default
220
293
  * console logger, `false` to silence everything, or a `DebugConfig` object for
@@ -245,9 +318,37 @@ export function createChatOptions<
245
318
  TAdapter extends AnyTextAdapter,
246
319
  TSchema extends SchemaInput | undefined = undefined,
247
320
  TStream extends boolean = true,
321
+ const TTools extends TextActivityOptions<
322
+ TAdapter,
323
+ TSchema,
324
+ TStream,
325
+ any
326
+ >['tools'] = TextActivityOptions<TAdapter, TSchema, TStream, any>['tools'],
327
+ const TMiddleware extends TextActivityOptions<
328
+ TAdapter,
329
+ TSchema,
330
+ TStream,
331
+ any
332
+ >['middleware'] = TextActivityOptions<
333
+ TAdapter,
334
+ TSchema,
335
+ TStream,
336
+ any
337
+ >['middleware'],
248
338
  >(
249
- options: TextActivityOptions<TAdapter, TSchema, TStream>,
250
- ): TextActivityOptions<TAdapter, TSchema, TStream> {
339
+ options: TextActivityOptionsWithContext<
340
+ TAdapter,
341
+ TSchema,
342
+ TStream,
343
+ TTools,
344
+ TMiddleware
345
+ >,
346
+ ): TextActivityOptions<
347
+ TAdapter,
348
+ TSchema,
349
+ TStream,
350
+ InferredContext<TTools, TMiddleware>
351
+ > {
251
352
  return options
252
353
  }
253
354
 
@@ -288,13 +389,18 @@ export type TextActivityResult<
288
389
 
289
390
  interface TextEngineConfig<
290
391
  TAdapter extends AnyTextAdapter,
291
- TParams extends TextOptions<any, any> = TextOptions<any>,
392
+ TContext = unknown,
393
+ TParams extends TextOptions<any, any, TContext> = TextOptions<
394
+ any,
395
+ any,
396
+ TContext
397
+ >,
292
398
  > {
293
399
  adapter: TAdapter
294
400
  systemPrompts?: Array<SystemPrompt>
295
401
  params: TParams
296
- middleware?: Array<ChatMiddleware>
297
- context?: unknown
402
+ middleware?: Array<ChatMiddleware<TContext>>
403
+ context?: TContext
298
404
  /**
299
405
  * If set, after the agent loop finishes the engine runs a
300
406
  * structured-output finalization step through the same middleware
@@ -334,14 +440,19 @@ type CyclePhase = 'processText' | 'executeToolCalls'
334
440
 
335
441
  class TextEngine<
336
442
  TAdapter extends AnyTextAdapter,
337
- TParams extends TextOptions<any, any> = TextOptions<any>,
443
+ TContext = unknown,
444
+ TParams extends TextOptions<any, any, TContext> = TextOptions<
445
+ any,
446
+ any,
447
+ TContext
448
+ >,
338
449
  > {
339
450
  private readonly adapter: TAdapter
340
451
  private params: TParams
341
452
  private systemPrompts: Array<SystemPrompt>
342
- private tools: Array<Tool>
453
+ private tools: Array<AnyRuntimeTool>
343
454
  private readonly loopStrategy: AgentLoopStrategy
344
- private toolCallManager: ToolCallManager
455
+ private toolCallManager: ToolCallManager<ReadonlyArray<AnyTool>, TContext>
345
456
  private readonly lazyToolManager: LazyToolManager
346
457
  private readonly initialMessageCount: number
347
458
  private readonly requestId: string
@@ -376,8 +487,8 @@ class TextEngine<
376
487
  private readonly parentRunIdOverride?: string
377
488
 
378
489
  // Middleware support
379
- private readonly middlewareRunner: MiddlewareRunner
380
- private readonly middlewareCtx: ChatMiddlewareContext
490
+ private readonly middlewareRunner: MiddlewareRunner<TContext>
491
+ private readonly middlewareCtx: ChatMiddlewareContext<TContext>
381
492
  private readonly deferredPromises: Array<Promise<unknown>> = []
382
493
  private abortReason?: string
383
494
  private readonly middlewareAbortController?: AbortController
@@ -416,7 +527,7 @@ class TextEngine<
416
527
  }
417
528
 
418
529
  constructor(
419
- config: TextEngineConfig<TAdapter, TParams>,
530
+ config: TextEngineConfig<TAdapter, TContext, TParams>,
420
531
  logger: InternalLogger,
421
532
  ) {
422
533
  this.logger = logger
@@ -447,7 +558,10 @@ class TextEngine<
447
558
  this.messages,
448
559
  )
449
560
  this.tools = this.lazyToolManager.getActiveTools()
450
- this.toolCallManager = new ToolCallManager(this.tools)
561
+ this.toolCallManager = new ToolCallManager<
562
+ ReadonlyArray<AnyTool>,
563
+ TContext
564
+ >(this.tools)
451
565
  this.requestId = this.createId('chat')
452
566
  this.streamId = this.createId('stream')
453
567
  this.effectiveRequest = config.params.abortController
@@ -468,10 +582,7 @@ class TextEngine<
468
582
  // handleStreamChunk processes raw chunks BEFORE middleware, so internal
469
583
  // state management sees extended fields (finishReason, delta, toolCallName, etc.).
470
584
  // The strip middleware ensures the yielded public stream is AG-UI spec-compliant.
471
- // `devtoolsMiddleware()` returns a structurally compatible
472
- // `DevtoolsChatMiddleware` (defined in `@tanstack/ai-event-client` to
473
- // avoid a circular dep). Cast it to `ChatMiddleware` for the runner.
474
- const allMiddleware: Array<ChatMiddleware> = [
585
+ const allMiddleware: Array<ChatMiddleware<TContext>> = [
475
586
  devtoolsMiddleware(),
476
587
  ...(config.middleware || []),
477
588
  stripToSpecMiddleware(),
@@ -494,7 +605,7 @@ class TextEngine<
494
605
  this.abortReason = reason
495
606
  this.middlewareAbortController?.abort(reason)
496
607
  },
497
- context: config.context,
608
+ context: config.context as TContext,
498
609
  defer: (promise: Promise<unknown>) => {
499
610
  this.deferredPromises.push(promise)
500
611
  },
@@ -1125,6 +1236,7 @@ class TextEngine<
1125
1236
  )
1126
1237
  },
1127
1238
  },
1239
+ this.middlewareCtx.context,
1128
1240
  )
1129
1241
 
1130
1242
  // Consume the async generator, yielding custom events and collecting the return value
@@ -1285,6 +1397,7 @@ class TextEngine<
1285
1397
  )
1286
1398
  },
1287
1399
  },
1400
+ this.middlewareCtx.context,
1288
1401
  )
1289
1402
 
1290
1403
  // Consume the async generator, yielding custom events and collecting the return value
@@ -1349,7 +1462,10 @@ class TextEngine<
1349
1462
  // Refresh tools if lazy tools were discovered in this batch
1350
1463
  if (this.lazyToolManager.hasNewlyDiscoveredTools()) {
1351
1464
  this.tools = this.lazyToolManager.getActiveTools()
1352
- this.toolCallManager = new ToolCallManager(this.tools)
1465
+ this.toolCallManager = new ToolCallManager<
1466
+ ReadonlyArray<AnyTool>,
1467
+ TContext
1468
+ >(this.tools)
1353
1469
  this.setToolPhase('continue')
1354
1470
  return
1355
1471
  }
@@ -1435,12 +1551,18 @@ class TextEngine<
1435
1551
  // Check for ModelMessage format (role: 'tool' messages contain tool results)
1436
1552
  // This handles results sent back from the client after executing client-side tools
1437
1553
  if (message.role === 'tool' && message.toolCallId) {
1438
- // Parse content back to original output (was stringified by uiMessageToModelMessages)
1554
+ // Parse content back to original output (was stringified by
1555
+ // uiMessageToModelMessages). Multimodal results carry an
1556
+ // Array<ContentPart> directly — pass it through without parsing.
1439
1557
  let output: unknown
1440
- try {
1441
- output = JSON.parse(message.content as string)
1442
- } catch {
1558
+ if (Array.isArray(message.content)) {
1443
1559
  output = message.content
1560
+ } else {
1561
+ try {
1562
+ output = JSON.parse(message.content as string)
1563
+ } catch {
1564
+ output = message.content
1565
+ }
1444
1566
  }
1445
1567
  // Skip approval response messages (they have pendingExecution marker)
1446
1568
  // These are NOT real client tool results — they are synthetic tool messages
@@ -1519,7 +1641,15 @@ class TextEngine<
1519
1641
  const chunks: Array<StreamChunk> = []
1520
1642
 
1521
1643
  for (const result of results) {
1522
- const content = JSON.stringify(result.result)
1644
+ // `content` is the canonical value for the tool `ModelMessage` — it may
1645
+ // be an `Array<ContentPart>` (multimodal) which the adapters convert to
1646
+ // structured provider output on the next iteration. `wireContent` is the
1647
+ // string form emitted on the AG-UI stream events (TOOL_CALL_END.result /
1648
+ // TOOL_CALL_RESULT.content are string-only per the AG-UI spec); the
1649
+ // multimodal array travels via the message itself, not the wire event.
1650
+ const content = normalizeToolResult(result.result)
1651
+ const wireContent =
1652
+ typeof content === 'string' ? content : JSON.stringify(content)
1523
1653
 
1524
1654
  // Emit TOOL_CALL_START + TOOL_CALL_ARGS before TOOL_CALL_END so that
1525
1655
  // the client can reconstruct the full tool call during continuations.
@@ -1551,18 +1681,20 @@ class TextEngine<
1551
1681
  toolCallId: result.toolCallId,
1552
1682
  toolCallName: result.toolName,
1553
1683
  toolName: result.toolName,
1554
- result: content,
1684
+ result: wireContent,
1685
+ ...(result.state !== undefined && { state: result.state }),
1555
1686
  } as StreamChunk)
1556
1687
 
1557
- // AG-UI spec TOOL_CALL_RESULT event
1688
+ // AG-UI spec TOOL_CALL_RESULT event (content is string-only per spec)
1558
1689
  chunks.push({
1559
1690
  type: 'TOOL_CALL_RESULT',
1560
1691
  timestamp: Date.now(),
1561
1692
  model: finishEvent.model,
1562
1693
  messageId: this.createId('tool-result'),
1563
1694
  toolCallId: result.toolCallId,
1564
- content,
1695
+ content: wireContent,
1565
1696
  role: 'tool',
1697
+ ...(result.state !== undefined && { state: result.state }),
1566
1698
  } as StreamChunk)
1567
1699
 
1568
1700
  // If a placeholder tool message exists for this toolCallId (created by
@@ -2387,8 +2519,31 @@ export function chat<
2387
2519
  TAdapter extends AnyTextAdapter,
2388
2520
  TSchema extends SchemaInput | undefined = undefined,
2389
2521
  TStream extends boolean = boolean,
2522
+ const TTools extends TextActivityOptions<
2523
+ TAdapter,
2524
+ TSchema,
2525
+ TStream,
2526
+ any
2527
+ >['tools'] = TextActivityOptions<TAdapter, TSchema, TStream, any>['tools'],
2528
+ const TMiddleware extends TextActivityOptions<
2529
+ TAdapter,
2530
+ TSchema,
2531
+ TStream,
2532
+ any
2533
+ >['middleware'] = TextActivityOptions<
2534
+ TAdapter,
2535
+ TSchema,
2536
+ TStream,
2537
+ any
2538
+ >['middleware'],
2390
2539
  >(
2391
- options: TextActivityOptions<TAdapter, TSchema, TStream>,
2540
+ options: TextActivityOptionsWithContext<
2541
+ TAdapter,
2542
+ TSchema,
2543
+ TStream,
2544
+ TTools,
2545
+ TMiddleware
2546
+ >,
2392
2547
  ): TextActivityResult<TSchema, TStream> {
2393
2548
  const { outputSchema, stream } = options
2394
2549
 
@@ -2431,8 +2586,8 @@ export function chat<
2431
2586
  /**
2432
2587
  * Run streaming text (agentic or one-shot depending on tools)
2433
2588
  */
2434
- async function* runStreamingText(
2435
- options: TextActivityOptions<AnyTextAdapter, undefined, true>,
2589
+ async function* runStreamingText<TContext = unknown>(
2590
+ options: TextActivityOptions<AnyTextAdapter, undefined, true, TContext>,
2436
2591
  ): AsyncIterable<StreamChunk> {
2437
2592
  const { adapter, middleware, context, debug, ...textOptions } = options
2438
2593
  const model = adapter.model
@@ -2443,7 +2598,8 @@ async function* runStreamingText(
2443
2598
  adapter,
2444
2599
  params: { ...textOptions, model, logger } as TextOptions<
2445
2600
  Record<string, any>,
2446
- Record<string, any>
2601
+ Record<string, any>,
2602
+ TContext
2447
2603
  >,
2448
2604
  middleware,
2449
2605
  context,
@@ -2460,13 +2616,18 @@ async function* runStreamingText(
2460
2616
  * Run non-streaming text - collects all content and returns as a string.
2461
2617
  * Runs the full agentic loop (if tools are provided) but returns collected text.
2462
2618
  */
2463
- function runNonStreamingText(
2464
- options: TextActivityOptions<AnyTextAdapter, undefined, false>,
2619
+ function runNonStreamingText<TContext = unknown>(
2620
+ options: TextActivityOptions<AnyTextAdapter, undefined, false, TContext>,
2465
2621
  ): Promise<string> {
2466
2622
  // Run the streaming text and collect all text using streamToText.
2467
2623
  const stream = runStreamingText(
2468
2624
  // 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.
2469
- options as unknown as TextActivityOptions<AnyTextAdapter, undefined, true>,
2625
+ options as unknown as TextActivityOptions<
2626
+ AnyTextAdapter,
2627
+ undefined,
2628
+ true,
2629
+ TContext
2630
+ >,
2470
2631
  )
2471
2632
 
2472
2633
  return streamToText(stream)
@@ -2478,8 +2639,11 @@ function runNonStreamingText(
2478
2639
  * 2. Once complete, call adapter.structuredOutput with the conversation context
2479
2640
  * 3. Validate and return the structured result
2480
2641
  */
2481
- async function runAgenticStructuredOutput<TSchema extends SchemaInput>(
2482
- options: TextActivityOptions<AnyTextAdapter, TSchema, boolean>,
2642
+ async function runAgenticStructuredOutput<
2643
+ TSchema extends SchemaInput,
2644
+ TContext = unknown,
2645
+ >(
2646
+ options: TextActivityOptions<AnyTextAdapter, TSchema, boolean, TContext>,
2483
2647
  ): Promise<InferSchemaType<TSchema>> {
2484
2648
  const { adapter, outputSchema, middleware, context, debug, ...textOptions } =
2485
2649
  options
@@ -2521,7 +2685,8 @@ async function runAgenticStructuredOutput<TSchema extends SchemaInput>(
2521
2685
  adapter,
2522
2686
  params: { ...textOptions, model, logger } as TextOptions<
2523
2687
  Record<string, unknown>,
2524
- Record<string, unknown>
2688
+ Record<string, unknown>,
2689
+ TContext
2525
2690
  >,
2526
2691
  middleware,
2527
2692
  context,
@@ -2711,8 +2876,11 @@ async function* fallbackStructuredOutputStream(
2711
2876
  * synchronously at call time rather than as a yielded RUN_ERROR mid-stream —
2712
2877
  * those are programmer errors, not runtime conditions.
2713
2878
  */
2714
- function runStreamingStructuredOutput<TSchema extends SchemaInput>(
2715
- options: TextActivityOptions<AnyTextAdapter, TSchema, true>,
2879
+ function runStreamingStructuredOutput<
2880
+ TSchema extends SchemaInput,
2881
+ TContext = unknown,
2882
+ >(
2883
+ options: TextActivityOptions<AnyTextAdapter, TSchema, true, TContext>,
2716
2884
  ): StructuredOutputStream<InferSchemaType<TSchema>> {
2717
2885
  const { outputSchema } = options
2718
2886
 
@@ -2758,8 +2926,11 @@ type StructuredOutputStreamInternal<T> = AsyncIterable<
2758
2926
  StreamChunk | StructuredOutputCompleteEvent<T>
2759
2927
  >
2760
2928
 
2761
- async function* runStreamingStructuredOutputImpl<TSchema extends SchemaInput>(
2762
- options: TextActivityOptions<AnyTextAdapter, TSchema, true>,
2929
+ async function* runStreamingStructuredOutputImpl<
2930
+ TSchema extends SchemaInput,
2931
+ TContext = unknown,
2932
+ >(
2933
+ options: TextActivityOptions<AnyTextAdapter, TSchema, true, TContext>,
2763
2934
  jsonSchema: NonNullable<ReturnType<typeof convertSchemaToJsonSchema>>,
2764
2935
  ): StructuredOutputStreamInternal<InferSchemaType<TSchema>> {
2765
2936
  const { adapter, outputSchema, middleware, context, debug, ...textOptions } =
@@ -2784,7 +2955,8 @@ async function* runStreamingStructuredOutputImpl<TSchema extends SchemaInput>(
2784
2955
  adapter,
2785
2956
  params: { ...textOptions, model, logger } as TextOptions<
2786
2957
  Record<string, unknown>,
2787
- Record<string, unknown>
2958
+ Record<string, unknown>,
2959
+ TContext
2788
2960
  >,
2789
2961
  middleware,
2790
2962
  context,
@@ -1,3 +1,4 @@
1
+ import { normalizeToolResult } from '../../utilities/tool-result'
1
2
  import type {
2
3
  ContentPart,
3
4
  MessagePart,
@@ -339,7 +340,7 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
339
340
  if (part.output !== undefined && !emittedToolResultIds.has(part.id)) {
340
341
  messageList.push({
341
342
  role: 'tool',
342
- content: JSON.stringify(part.output),
343
+ content: normalizeToolResult(part.output),
343
344
  toolCallId: part.id,
344
345
  })
345
346
  emittedToolResultIds.add(part.id)
@@ -18,12 +18,12 @@ import type {
18
18
  } from './types'
19
19
 
20
20
  /** Check if a middleware should be skipped for instrumentation events. */
21
- function shouldSkipInstrumentation(mw: ChatMiddleware): boolean {
21
+ function shouldSkipInstrumentation(mw: ChatMiddleware<any>): boolean {
22
22
  return mw.name === 'devtools' || mw.name === 'strip-to-spec'
23
23
  }
24
24
 
25
25
  /** Build the base context for middleware instrumentation events. */
26
- function instrumentCtx(ctx: ChatMiddlewareContext) {
26
+ function instrumentCtx(ctx: ChatMiddlewareContext<any>) {
27
27
  return {
28
28
  requestId: ctx.requestId,
29
29
  streamId: ctx.streamId,
@@ -36,12 +36,12 @@ function instrumentCtx(ctx: ChatMiddlewareContext) {
36
36
  * Internal middleware runner that manages composed execution of middleware hooks.
37
37
  * Created once per chat() invocation.
38
38
  */
39
- export class MiddlewareRunner {
40
- private readonly middlewares: ReadonlyArray<ChatMiddleware>
39
+ export class MiddlewareRunner<TContext = unknown> {
40
+ private readonly middlewares: ReadonlyArray<ChatMiddleware<TContext>>
41
41
  private readonly logger: InternalLogger
42
42
 
43
43
  constructor(
44
- middlewares: ReadonlyArray<ChatMiddleware>,
44
+ middlewares: ReadonlyArray<ChatMiddleware<TContext>>,
45
45
  logger: InternalLogger,
46
46
  ) {
47
47
  this.middlewares = middlewares
@@ -58,7 +58,7 @@ export class MiddlewareRunner {
58
58
  * Partial returns are shallow-merged with the current config.
59
59
  */
60
60
  async runOnConfig(
61
- ctx: ChatMiddlewareContext,
61
+ ctx: ChatMiddlewareContext<TContext>,
62
62
  config: ChatMiddlewareConfig,
63
63
  ): Promise<ChatMiddlewareConfig> {
64
64
  let current = config
@@ -113,7 +113,7 @@ export class MiddlewareRunner {
113
113
  * same boundary (which receives a ChatMiddlewareConfig view, no outputSchema).
114
114
  */
115
115
  async runOnStructuredOutputConfig(
116
- ctx: ChatMiddlewareContext,
116
+ ctx: ChatMiddlewareContext<TContext>,
117
117
  config: StructuredOutputMiddlewareConfig,
118
118
  ): Promise<StructuredOutputMiddlewareConfig> {
119
119
  let current = config
@@ -166,7 +166,7 @@ export class MiddlewareRunner {
166
166
  /**
167
167
  * Call onStart on all middleware in order.
168
168
  */
169
- async runOnStart(ctx: ChatMiddlewareContext): Promise<void> {
169
+ async runOnStart(ctx: ChatMiddlewareContext<TContext>): Promise<void> {
170
170
  for (const mw of this.middlewares) {
171
171
  if (mw.onStart) {
172
172
  const skip = shouldSkipInstrumentation(mw)
@@ -200,7 +200,7 @@ export class MiddlewareRunner {
200
200
  * - null: drop the chunk entirely
201
201
  */
202
202
  async runOnChunk(
203
- ctx: ChatMiddlewareContext,
203
+ ctx: ChatMiddlewareContext<TContext>,
204
204
  chunk: StreamChunk,
205
205
  ): Promise<Array<StreamChunk>> {
206
206
  let chunks: Array<StreamChunk> = [chunk]
@@ -298,7 +298,7 @@ export class MiddlewareRunner {
298
298
  * Returns the first non-void decision, or undefined to continue normally.
299
299
  */
300
300
  async runOnBeforeToolCall(
301
- ctx: ChatMiddlewareContext,
301
+ ctx: ChatMiddlewareContext<TContext>,
302
302
  hookCtx: ToolCallHookContext,
303
303
  ): Promise<BeforeToolCallDecision> {
304
304
  for (const mw of this.middlewares) {
@@ -333,7 +333,7 @@ export class MiddlewareRunner {
333
333
  * Run onAfterToolCall on all middleware in order.
334
334
  */
335
335
  async runOnAfterToolCall(
336
- ctx: ChatMiddlewareContext,
336
+ ctx: ChatMiddlewareContext<TContext>,
337
337
  info: AfterToolCallInfo,
338
338
  ): Promise<void> {
339
339
  for (const mw of this.middlewares) {
@@ -363,7 +363,7 @@ export class MiddlewareRunner {
363
363
  * Run onUsage on all middleware in order.
364
364
  */
365
365
  async runOnUsage(
366
- ctx: ChatMiddlewareContext,
366
+ ctx: ChatMiddlewareContext<TContext>,
367
367
  usage: UsageInfo,
368
368
  ): Promise<void> {
369
369
  for (const mw of this.middlewares) {
@@ -393,7 +393,7 @@ export class MiddlewareRunner {
393
393
  * Run onFinish on all middleware in order.
394
394
  */
395
395
  async runOnFinish(
396
- ctx: ChatMiddlewareContext,
396
+ ctx: ChatMiddlewareContext<TContext>,
397
397
  info: FinishInfo,
398
398
  ): Promise<void> {
399
399
  for (const mw of this.middlewares) {
@@ -422,7 +422,10 @@ export class MiddlewareRunner {
422
422
  /**
423
423
  * Run onAbort on all middleware in order.
424
424
  */
425
- async runOnAbort(ctx: ChatMiddlewareContext, info: AbortInfo): Promise<void> {
425
+ async runOnAbort(
426
+ ctx: ChatMiddlewareContext<TContext>,
427
+ info: AbortInfo,
428
+ ): Promise<void> {
426
429
  for (const mw of this.middlewares) {
427
430
  if (mw.onAbort) {
428
431
  const skip = shouldSkipInstrumentation(mw)
@@ -449,7 +452,10 @@ export class MiddlewareRunner {
449
452
  /**
450
453
  * Run onError on all middleware in order.
451
454
  */
452
- async runOnError(ctx: ChatMiddlewareContext, info: ErrorInfo): Promise<void> {
455
+ async runOnError(
456
+ ctx: ChatMiddlewareContext<TContext>,
457
+ info: ErrorInfo,
458
+ ): Promise<void> {
453
459
  for (const mw of this.middlewares) {
454
460
  if (mw.onError) {
455
461
  const skip = shouldSkipInstrumentation(mw)
@@ -478,7 +484,7 @@ export class MiddlewareRunner {
478
484
  * Called at the start of each agent loop iteration.
479
485
  */
480
486
  async runOnIteration(
481
- ctx: ChatMiddlewareContext,
487
+ ctx: ChatMiddlewareContext<TContext>,
482
488
  info: IterationInfo,
483
489
  ): Promise<void> {
484
490
  for (const mw of this.middlewares) {
@@ -509,7 +515,7 @@ export class MiddlewareRunner {
509
515
  * Called after all tool calls in an iteration have been processed.
510
516
  */
511
517
  async runOnToolPhaseComplete(
512
- ctx: ChatMiddlewareContext,
518
+ ctx: ChatMiddlewareContext<TContext>,
513
519
  info: ToolPhaseCompleteInfo,
514
520
  ): Promise<void> {
515
521
  for (const mw of this.middlewares) {