@tanstack/ai 0.10.3 → 0.11.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 (39) hide show
  1. package/dist/esm/activities/chat/index.d.ts +4 -0
  2. package/dist/esm/activities/chat/index.js +50 -16
  3. package/dist/esm/activities/chat/index.js.map +1 -1
  4. package/dist/esm/activities/chat/middleware/compose.js +5 -4
  5. package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
  6. package/dist/esm/activities/chat/stream/processor.d.ts +16 -1
  7. package/dist/esm/activities/chat/stream/processor.js +120 -21
  8. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  9. package/dist/esm/activities/chat/stream/types.d.ts +1 -0
  10. package/dist/esm/activities/chat/tools/schema-converter.js +4 -2
  11. package/dist/esm/activities/chat/tools/schema-converter.js.map +1 -1
  12. package/dist/esm/activities/chat/tools/tool-calls.js +3 -1
  13. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  14. package/dist/esm/activities/generateVideo/index.js +6 -0
  15. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  16. package/dist/esm/activities/stream-generation-result.d.ts +3 -2
  17. package/dist/esm/activities/stream-generation-result.js +12 -4
  18. package/dist/esm/activities/stream-generation-result.js.map +1 -1
  19. package/dist/esm/index.js +2 -0
  20. package/dist/esm/index.js.map +1 -1
  21. package/dist/esm/stream-to-response.d.ts +2 -2
  22. package/dist/esm/stream-to-response.js +0 -1
  23. package/dist/esm/stream-to-response.js.map +1 -1
  24. package/dist/esm/strip-to-spec-middleware.d.ts +18 -0
  25. package/dist/esm/strip-to-spec-middleware.js +20 -0
  26. package/dist/esm/strip-to-spec-middleware.js.map +1 -0
  27. package/dist/esm/types.d.ts +229 -95
  28. package/package.json +3 -2
  29. package/src/activities/chat/index.ts +90 -25
  30. package/src/activities/chat/middleware/compose.ts +6 -4
  31. package/src/activities/chat/stream/processor.ts +152 -25
  32. package/src/activities/chat/stream/types.ts +1 -0
  33. package/src/activities/chat/tools/schema-converter.ts +8 -2
  34. package/src/activities/chat/tools/tool-calls.ts +4 -2
  35. package/src/activities/generateVideo/index.ts +13 -6
  36. package/src/activities/stream-generation-result.ts +19 -11
  37. package/src/stream-to-response.ts +2 -4
  38. package/src/strip-to-spec-middleware.ts +37 -0
  39. package/src/types.ts +276 -109
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { devtoolsMiddleware } from '@tanstack/ai-event-client'
9
+ import { stripToSpecMiddleware } from '../../strip-to-spec-middleware'
9
10
  import { streamToText } from '../../stream-to-response.js'
10
11
  import { LazyToolManager } from './tools/lazy-tool-manager'
11
12
  import {
@@ -104,6 +105,10 @@ export interface TextActivityOptions<
104
105
  agentLoopStrategy?: TextOptions['agentLoopStrategy']
105
106
  /** Unique conversation identifier for tracking */
106
107
  conversationId?: TextOptions['conversationId']
108
+ /** Thread/conversation ID for AG-UI protocol. Auto-generated if not provided. */
109
+ threadId?: TextOptions['threadId']
110
+ /** Run ID override for AG-UI protocol. Auto-generated by adapter if not provided. */
111
+ runId?: TextOptions['runId']
107
112
  /**
108
113
  * Optional Standard Schema for structured output.
109
114
  * When provided, the activity will:
@@ -263,6 +268,10 @@ class TextEngine<
263
268
  private readonly initialApprovals: Map<string, boolean>
264
269
  private readonly initialClientToolResults: Map<string, any>
265
270
 
271
+ // AG-UI protocol IDs
272
+ private threadId: string
273
+ private runIdOverride?: string
274
+
266
275
  // Middleware support
267
276
  private readonly middlewareRunner: MiddlewareRunner
268
277
  private readonly middlewareCtx: ChatMiddlewareContext
@@ -307,9 +316,18 @@ class TextEngine<
307
316
  ? { signal: config.params.abortController.signal }
308
317
  : undefined
309
318
  this.effectiveSignal = config.params.abortController?.signal
310
-
311
- // Initialize middleware — devtools middleware is always first
312
- const allMiddleware = [devtoolsMiddleware(), ...(config.middleware || [])]
319
+ this.threadId = config.params.threadId || this.createId('thread')
320
+ this.runIdOverride = config.params.runId
321
+
322
+ // Initialize middleware — devtools first, strip-to-spec always last.
323
+ // handleStreamChunk processes raw chunks BEFORE middleware, so internal
324
+ // state management sees extended fields (finishReason, delta, toolCallName, etc.).
325
+ // The strip middleware ensures the yielded public stream is AG-UI spec-compliant.
326
+ const allMiddleware = [
327
+ devtoolsMiddleware(),
328
+ ...(config.middleware || []),
329
+ stripToSpecMiddleware(),
330
+ ]
313
331
  this.middlewareRunner = new MiddlewareRunner(allMiddleware)
314
332
  this.middlewareAbortController = new AbortController()
315
333
  this.middlewareCtx = {
@@ -535,6 +553,8 @@ class TextEngine<
535
553
  request: this.effectiveRequest,
536
554
  modelOptions,
537
555
  systemPrompts: this.systemPrompts,
556
+ threadId: this.threadId,
557
+ runId: this.runIdOverride,
538
558
  })) {
539
559
  if (this.isCancelled()) {
540
560
  break
@@ -542,14 +562,17 @@ class TextEngine<
542
562
 
543
563
  this.totalChunkCount++
544
564
 
545
- // Pipe chunk through middleware (devtools middleware observes and emits events)
565
+ // Process the original (unstripped) chunk for internal state management
566
+ // BEFORE middleware, so fields like finishReason, delta, etc. are available
567
+ this.handleStreamChunk(chunk)
568
+
569
+ // Pipe chunk through middleware (devtools middleware observes; strip-to-spec cleans)
546
570
  const outputChunks = await this.middlewareRunner.runOnChunk(
547
571
  this.middlewareCtx,
548
572
  chunk,
549
573
  )
550
574
  for (const outputChunk of outputChunks) {
551
575
  yield outputChunk
552
- this.handleStreamChunk(outputChunk)
553
576
  this.middlewareCtx.chunkIndex++
554
577
  }
555
578
 
@@ -589,6 +612,18 @@ class TextEngine<
589
612
  this.handleStepFinishedEvent(chunk)
590
613
  break
591
614
 
615
+ case 'TOOL_CALL_RESULT':
616
+ // Tool result is already added to messages in buildToolResultChunks
617
+ break
618
+
619
+ case 'REASONING_START':
620
+ case 'REASONING_MESSAGE_START':
621
+ case 'REASONING_MESSAGE_CONTENT':
622
+ case 'REASONING_MESSAGE_END':
623
+ case 'REASONING_END':
624
+ // Reasoning events are handled by StreamProcessor
625
+ break
626
+
592
627
  default:
593
628
  // RUN_STARTED, TEXT_MESSAGE_START, TEXT_MESSAGE_END, STEP_STARTED,
594
629
  // STATE_SNAPSHOT, STATE_DELTA, CUSTOM
@@ -624,7 +659,7 @@ class TextEngine<
624
659
 
625
660
  private handleRunFinishedEvent(chunk: RunFinishedEvent): void {
626
661
  this.finishedEvent = chunk
627
- this.lastFinishReason = chunk.finishReason
662
+ this.lastFinishReason = chunk.finishReason ?? null
628
663
  }
629
664
 
630
665
  private handleRunErrorEvent(
@@ -675,7 +710,7 @@ class TextEngine<
675
710
  undiscoveredLazyResults,
676
711
  finishEvent,
677
712
  )) {
678
- yield chunk
713
+ yield* this.pipeThroughMiddleware(chunk)
679
714
  }
680
715
  }
681
716
 
@@ -748,7 +783,7 @@ class TextEngine<
748
783
  finishEvent,
749
784
  argsMap,
750
785
  )) {
751
- yield chunk
786
+ yield* this.pipeThroughMiddleware(chunk)
752
787
  }
753
788
  }
754
789
 
@@ -756,14 +791,14 @@ class TextEngine<
756
791
  executionResult.needsApproval,
757
792
  finishEvent,
758
793
  )) {
759
- yield chunk
794
+ yield* this.pipeThroughMiddleware(chunk)
760
795
  }
761
796
 
762
797
  for (const chunk of this.buildClientToolChunks(
763
798
  executionResult.needsClientExecution,
764
799
  finishEvent,
765
800
  )) {
766
- yield chunk
801
+ yield* this.pipeThroughMiddleware(chunk)
767
802
  }
768
803
 
769
804
  this.setToolPhase('wait')
@@ -777,7 +812,7 @@ class TextEngine<
777
812
  )
778
813
 
779
814
  for (const chunk of toolResultChunks) {
780
- yield chunk
815
+ yield* this.pipeThroughMiddleware(chunk)
781
816
  }
782
817
 
783
818
  return 'continue'
@@ -824,7 +859,7 @@ class TextEngine<
824
859
  undiscoveredLazyResults,
825
860
  finishEvt,
826
861
  )) {
827
- yield chunk
862
+ yield* this.pipeThroughMiddleware(chunk)
828
863
  }
829
864
  }
830
865
 
@@ -895,7 +930,7 @@ class TextEngine<
895
930
  executionResult.results,
896
931
  finishEvent,
897
932
  )) {
898
- yield chunk
933
+ yield* this.pipeThroughMiddleware(chunk)
899
934
  }
900
935
  }
901
936
 
@@ -903,14 +938,14 @@ class TextEngine<
903
938
  executionResult.needsApproval,
904
939
  finishEvent,
905
940
  )) {
906
- yield chunk
941
+ yield* this.pipeThroughMiddleware(chunk)
907
942
  }
908
943
 
909
944
  for (const chunk of this.buildClientToolChunks(
910
945
  executionResult.needsClientExecution,
911
946
  finishEvent,
912
947
  )) {
913
- yield chunk
948
+ yield* this.pipeThroughMiddleware(chunk)
914
949
  }
915
950
 
916
951
  this.setToolPhase('wait')
@@ -923,7 +958,7 @@ class TextEngine<
923
958
  )
924
959
 
925
960
  for (const chunk of toolResultChunks) {
926
- yield chunk
961
+ yield* this.pipeThroughMiddleware(chunk)
927
962
  }
928
963
 
929
964
  // Refresh tools if lazy tools were discovered in this batch
@@ -1057,7 +1092,7 @@ class TextEngine<
1057
1092
  needsApproval: true,
1058
1093
  },
1059
1094
  },
1060
- })
1095
+ } as StreamChunk)
1061
1096
  }
1062
1097
 
1063
1098
  return chunks
@@ -1080,7 +1115,7 @@ class TextEngine<
1080
1115
  toolName: clientTool.toolName,
1081
1116
  input: clientTool.input,
1082
1117
  },
1083
- })
1118
+ } as StreamChunk)
1084
1119
  }
1085
1120
 
1086
1121
  return chunks
@@ -1105,7 +1140,7 @@ class TextEngine<
1105
1140
  model: finishEvent.model,
1106
1141
  toolCallId: result.toolCallId,
1107
1142
  toolName: result.toolName,
1108
- })
1143
+ } as StreamChunk)
1109
1144
 
1110
1145
  const args = argsMap.get(result.toolCallId) ?? '{}'
1111
1146
  chunks.push({
@@ -1115,7 +1150,7 @@ class TextEngine<
1115
1150
  toolCallId: result.toolCallId,
1116
1151
  delta: args,
1117
1152
  args,
1118
- })
1153
+ } as StreamChunk)
1119
1154
  }
1120
1155
 
1121
1156
  chunks.push({
@@ -1123,9 +1158,21 @@ class TextEngine<
1123
1158
  timestamp: Date.now(),
1124
1159
  model: finishEvent.model,
1125
1160
  toolCallId: result.toolCallId,
1161
+ toolCallName: result.toolName,
1126
1162
  toolName: result.toolName,
1127
1163
  result: content,
1128
- })
1164
+ } as StreamChunk)
1165
+
1166
+ // AG-UI spec TOOL_CALL_RESULT event
1167
+ chunks.push({
1168
+ type: 'TOOL_CALL_RESULT',
1169
+ timestamp: Date.now(),
1170
+ model: finishEvent.model,
1171
+ messageId: this.createId('tool-result'),
1172
+ toolCallId: result.toolCallId,
1173
+ content,
1174
+ role: 'tool',
1175
+ } as StreamChunk)
1129
1176
 
1130
1177
  this.messages = [
1131
1178
  ...this.messages,
@@ -1186,10 +1233,11 @@ class TextEngine<
1186
1233
  return {
1187
1234
  type: 'RUN_FINISHED',
1188
1235
  runId: this.createId('pending'),
1236
+ threadId: this.threadId,
1189
1237
  model: this.params.model,
1190
1238
  timestamp: Date.now(),
1191
1239
  finishReason: 'tool_calls',
1192
- }
1240
+ } as RunFinishedEvent
1193
1241
  }
1194
1242
 
1195
1243
  private shouldContinue(): boolean {
@@ -1256,9 +1304,26 @@ class TextEngine<
1256
1304
  this.toolPhase = phase
1257
1305
  }
1258
1306
 
1307
+ /**
1308
+ * Pipe a single chunk through the middleware pipeline (strip-to-spec, devtools, etc.)
1309
+ * and yield all resulting output chunks.
1310
+ */
1311
+ private async *pipeThroughMiddleware(
1312
+ chunk: StreamChunk,
1313
+ ): AsyncGenerator<StreamChunk, void, void> {
1314
+ const outputChunks = await this.middlewareRunner.runOnChunk(
1315
+ this.middlewareCtx,
1316
+ chunk,
1317
+ )
1318
+ for (const outputChunk of outputChunks) {
1319
+ yield outputChunk
1320
+ this.middlewareCtx.chunkIndex++
1321
+ }
1322
+ }
1323
+
1259
1324
  /**
1260
1325
  * Drain an executeToolCalls async generator, yielding any CustomEvent chunks
1261
- * and returning the final ExecuteToolCallsResult.
1326
+ * through the middleware pipeline and returning the final ExecuteToolCallsResult.
1262
1327
  */
1263
1328
  private async *drainToolCallGenerator(
1264
1329
  generator: AsyncGenerator<
@@ -1281,7 +1346,7 @@ class TextEngine<
1281
1346
  > {
1282
1347
  let next = await generator.next()
1283
1348
  while (!next.done) {
1284
- yield next.value
1349
+ yield* this.pipeThroughMiddleware(next.value)
1285
1350
  next = await generator.next()
1286
1351
  }
1287
1352
  return next.value
@@ -1297,7 +1362,7 @@ class TextEngine<
1297
1362
  model: this.params.model,
1298
1363
  name: eventName,
1299
1364
  value,
1300
- }
1365
+ } as CustomEvent
1301
1366
  }
1302
1367
 
1303
1368
  private createId(prefix: string): string {
@@ -17,7 +17,7 @@ import type {
17
17
 
18
18
  /** Check if a middleware should be skipped for instrumentation events. */
19
19
  function shouldSkipInstrumentation(mw: ChatMiddleware): boolean {
20
- return mw.name === 'devtools'
20
+ return mw.name === 'devtools' || mw.name === 'strip-to-spec'
21
21
  }
22
22
 
23
23
  /** Build the base context for middleware instrumentation events. */
@@ -132,6 +132,8 @@ export class MiddlewareRunner {
132
132
 
133
133
  const nextChunks: Array<StreamChunk> = []
134
134
  for (const c of chunks) {
135
+ // Cast: @ag-ui/core Zod passthrough types prevent direct `.type` access
136
+ const chunkType = (c as StreamChunk & { type: string }).type
135
137
  const result = await mw.onChunk(ctx, c)
136
138
  if (result === null) {
137
139
  // Drop this chunk
@@ -139,7 +141,7 @@ export class MiddlewareRunner {
139
141
  aiEventClient.emit('middleware:chunk:transformed', {
140
142
  ...instrumentCtx(ctx),
141
143
  middlewareName: mw.name || 'unnamed',
142
- originalChunkType: c.type,
144
+ originalChunkType: chunkType,
143
145
  resultCount: 0,
144
146
  wasDropped: true,
145
147
  })
@@ -155,7 +157,7 @@ export class MiddlewareRunner {
155
157
  aiEventClient.emit('middleware:chunk:transformed', {
156
158
  ...instrumentCtx(ctx),
157
159
  middlewareName: mw.name || 'unnamed',
158
- originalChunkType: c.type,
160
+ originalChunkType: chunkType,
159
161
  resultCount: result.length,
160
162
  wasDropped: false,
161
163
  })
@@ -167,7 +169,7 @@ export class MiddlewareRunner {
167
169
  aiEventClient.emit('middleware:chunk:transformed', {
168
170
  ...instrumentCtx(ctx),
169
171
  middlewareName: mw.name || 'unnamed',
170
- originalChunkType: c.type,
172
+ originalChunkType: chunkType,
171
173
  resultCount: 1,
172
174
  wasDropped: false,
173
175
  })
@@ -431,7 +431,7 @@ export class StreamProcessor {
431
431
  *
432
432
  * Central dispatch for all AG-UI events. Each event type maps to a specific
433
433
  * handler. Events not listed in the switch are intentionally ignored
434
- * (RUN_STARTED, STEP_STARTED, STATE_DELTA).
434
+ * (STEP_STARTED, STATE_SNAPSHOT, STATE_DELTA).
435
435
  *
436
436
  * @see docs/chat-architecture.md#adapter-contract — Expected event types and ordering
437
437
  */
@@ -445,54 +445,100 @@ export class StreamProcessor {
445
445
  })
446
446
  }
447
447
 
448
- switch (chunk.type) {
448
+ // Cast needed: @ag-ui/core Zod passthrough types add `& { [k: string]: unknown }`
449
+ // which prevents TypeScript from narrowing the `type` discriminant in switch.
450
+ const c = chunk as StreamChunk & { type: string }
451
+ switch (c.type) {
449
452
  // AG-UI Events
450
453
  case 'TEXT_MESSAGE_START':
451
- this.handleTextMessageStartEvent(chunk)
454
+ this.handleTextMessageStartEvent(
455
+ chunk as Extract<StreamChunk, { type: 'TEXT_MESSAGE_START' }>,
456
+ )
452
457
  break
453
458
 
454
459
  case 'TEXT_MESSAGE_CONTENT':
455
- this.handleTextMessageContentEvent(chunk)
460
+ this.handleTextMessageContentEvent(
461
+ chunk as Extract<StreamChunk, { type: 'TEXT_MESSAGE_CONTENT' }>,
462
+ )
456
463
  break
457
464
 
458
465
  case 'TEXT_MESSAGE_END':
459
- this.handleTextMessageEndEvent(chunk)
466
+ this.handleTextMessageEndEvent(
467
+ chunk as Extract<StreamChunk, { type: 'TEXT_MESSAGE_END' }>,
468
+ )
460
469
  break
461
470
 
462
471
  case 'TOOL_CALL_START':
463
- this.handleToolCallStartEvent(chunk)
472
+ this.handleToolCallStartEvent(
473
+ chunk as Extract<StreamChunk, { type: 'TOOL_CALL_START' }>,
474
+ )
464
475
  break
465
476
 
466
477
  case 'TOOL_CALL_ARGS':
467
- this.handleToolCallArgsEvent(chunk)
478
+ this.handleToolCallArgsEvent(
479
+ chunk as Extract<StreamChunk, { type: 'TOOL_CALL_ARGS' }>,
480
+ )
468
481
  break
469
482
 
470
483
  case 'TOOL_CALL_END':
471
- this.handleToolCallEndEvent(chunk)
484
+ this.handleToolCallEndEvent(
485
+ chunk as Extract<StreamChunk, { type: 'TOOL_CALL_END' }>,
486
+ )
472
487
  break
473
488
 
474
489
  case 'RUN_FINISHED':
475
- this.handleRunFinishedEvent(chunk)
490
+ this.handleRunFinishedEvent(
491
+ chunk as Extract<StreamChunk, { type: 'RUN_FINISHED' }>,
492
+ )
476
493
  break
477
494
 
478
495
  case 'RUN_ERROR':
479
- this.handleRunErrorEvent(chunk)
496
+ this.handleRunErrorEvent(
497
+ chunk as Extract<StreamChunk, { type: 'RUN_ERROR' }>,
498
+ )
480
499
  break
481
500
 
482
501
  case 'STEP_FINISHED':
483
- this.handleStepFinishedEvent(chunk)
502
+ this.handleStepFinishedEvent(
503
+ chunk as Extract<StreamChunk, { type: 'STEP_FINISHED' }>,
504
+ )
484
505
  break
485
506
 
486
507
  case 'MESSAGES_SNAPSHOT':
487
- this.handleMessagesSnapshotEvent(chunk)
508
+ this.handleMessagesSnapshotEvent(
509
+ chunk as Extract<StreamChunk, { type: 'MESSAGES_SNAPSHOT' }>,
510
+ )
488
511
  break
489
512
 
490
513
  case 'CUSTOM':
491
- this.handleCustomEvent(chunk)
514
+ this.handleCustomEvent(
515
+ chunk as Extract<StreamChunk, { type: 'CUSTOM' }>,
516
+ )
492
517
  break
493
518
 
494
519
  case 'RUN_STARTED':
495
- this.handleRunStartedEvent(chunk)
520
+ this.handleRunStartedEvent(
521
+ chunk as Extract<StreamChunk, { type: 'RUN_STARTED' }>,
522
+ )
523
+ break
524
+
525
+ case 'REASONING_START':
526
+ case 'REASONING_MESSAGE_START':
527
+ case 'REASONING_MESSAGE_END':
528
+ case 'REASONING_END':
529
+ // No special handling needed
530
+ break
531
+
532
+ case 'REASONING_MESSAGE_CONTENT':
533
+ this.handleReasoningMessageContentEvent(
534
+ chunk as Extract<StreamChunk, { type: 'REASONING_MESSAGE_CONTENT' }>,
535
+ )
536
+ break
537
+
538
+ case 'TOOL_CALL_RESULT':
539
+ this.handleToolCallResultEvent(
540
+ chunk as Extract<StreamChunk, { type: 'TOOL_CALL_RESULT' }>,
541
+ )
496
542
  break
497
543
 
498
544
  default:
@@ -519,6 +565,7 @@ export class StreamProcessor {
519
565
  currentSegmentText: '',
520
566
  lastEmittedText: '',
521
567
  thinkingContent: '',
568
+ hasSeenReasoningEvents: false,
522
569
  toolCalls: new Map(),
523
570
  toolCallOrder: [],
524
571
  hasToolCallsSinceTextStart: false,
@@ -632,11 +679,11 @@ export class StreamProcessor {
632
679
  ): void {
633
680
  const { messageId, role } = chunk
634
681
 
635
- // Map 'tool' role to 'assistant' for both UIMessage and MessageStreamState
636
- // (UIMessage doesn't support 'tool' role, and lookups like
682
+ // Map 'tool' and 'developer' roles to 'assistant' for both UIMessage and MessageStreamState
683
+ // (UIMessage doesn't support 'tool'/'developer' role, and lookups like
637
684
  // getActiveAssistantMessageId() check state.role === 'assistant')
638
685
  const uiRole: 'system' | 'user' | 'assistant' =
639
- role === 'tool' ? 'assistant' : role
686
+ role === 'user' || role === 'system' ? role : 'assistant'
640
687
 
641
688
  // Case 1: A manual message was created via startAssistantMessage()
642
689
  if (this.pendingManualMessageId) {
@@ -739,7 +786,8 @@ export class StreamProcessor {
739
786
  chunk: Extract<StreamChunk, { type: 'MESSAGES_SNAPSHOT' }>,
740
787
  ): void {
741
788
  this.resetStreamState()
742
- this.messages = [...chunk.messages]
789
+ // AG-UI Message[] is compatible with UIMessage[] at runtime
790
+ this.messages = [...chunk.messages] as unknown as Array<UIMessage>
743
791
  this.emitMessagesChange()
744
792
  }
745
793
 
@@ -849,9 +897,11 @@ export class StreamProcessor {
849
897
  // New tool call starting
850
898
  const initialState: ToolCallState = 'awaiting-input'
851
899
 
900
+ const toolName = chunk.toolCallName
901
+
852
902
  const newToolCall: InternalToolCallState = {
853
903
  id: chunk.toolCallId,
854
- name: chunk.toolName,
904
+ name: toolName,
855
905
  arguments: '',
856
906
  state: initialState,
857
907
  parsedArguments: undefined,
@@ -867,7 +917,7 @@ export class StreamProcessor {
867
917
  // Update UIMessage
868
918
  this.messages = updateToolCallPart(this.messages, messageId, {
869
919
  id: chunk.toolCallId,
870
- name: chunk.toolName,
920
+ name: toolName,
871
921
  arguments: '',
872
922
  state: initialState,
873
923
  })
@@ -1012,6 +1062,44 @@ export class StreamProcessor {
1012
1062
  }
1013
1063
  }
1014
1064
 
1065
+ /**
1066
+ * Handle TOOL_CALL_RESULT event (AG-UI spec).
1067
+ *
1068
+ * Creates a tool-result part and updates the tool-call output field,
1069
+ * mirroring the logic from TOOL_CALL_END when it carries a result.
1070
+ * This is the spec-compliant path for delivering tool results to the client.
1071
+ */
1072
+ private handleToolCallResultEvent(
1073
+ chunk: Extract<StreamChunk, { type: 'TOOL_CALL_RESULT' }>,
1074
+ ): void {
1075
+ const messageId = this.toolCallToMessage.get(chunk.toolCallId)
1076
+ if (!messageId) return
1077
+
1078
+ // Step 1: Update the tool-call part's output field
1079
+ let output: unknown
1080
+ try {
1081
+ output = JSON.parse(chunk.content)
1082
+ } catch {
1083
+ output = chunk.content
1084
+ }
1085
+ this.messages = updateToolCallWithOutput(
1086
+ this.messages,
1087
+ chunk.toolCallId,
1088
+ output,
1089
+ )
1090
+
1091
+ // Step 2: Create/update the tool-result part
1092
+ const resultState: ToolResultState = 'complete'
1093
+ this.messages = updateToolResultPart(
1094
+ this.messages,
1095
+ messageId,
1096
+ chunk.toolCallId,
1097
+ chunk.content,
1098
+ resultState,
1099
+ )
1100
+ this.emitMessagesChange()
1101
+ }
1102
+
1015
1103
  /**
1016
1104
  * Handle RUN_STARTED event.
1017
1105
  *
@@ -1037,7 +1125,7 @@ export class StreamProcessor {
1037
1125
  private handleRunFinishedEvent(
1038
1126
  chunk: Extract<StreamChunk, { type: 'RUN_FINISHED' }>,
1039
1127
  ): void {
1040
- this.finishReason = chunk.finishReason
1128
+ this.finishReason = chunk.finishReason ?? null
1041
1129
  this.activeRuns.delete(chunk.runId)
1042
1130
 
1043
1131
  if (this.activeRuns.size === 0) {
@@ -1054,13 +1142,17 @@ export class StreamProcessor {
1054
1142
  chunk: Extract<StreamChunk, { type: 'RUN_ERROR' }>,
1055
1143
  ): void {
1056
1144
  this.hasError = true
1057
- if (chunk.runId) {
1058
- this.activeRuns.delete(chunk.runId)
1145
+ const runId = (chunk as any).runId as string | undefined
1146
+ if (runId) {
1147
+ this.activeRuns.delete(runId)
1059
1148
  } else {
1060
1149
  this.activeRuns.clear()
1061
1150
  }
1062
1151
  this.ensureAssistantMessage()
1063
- this.events.onError?.(new Error(chunk.error.message || 'An error occurred'))
1152
+ // Prefer spec field `message`; fall back to deprecated `error.message`
1153
+ const errorMessage =
1154
+ chunk.message || chunk.error?.message || 'An error occurred'
1155
+ this.events.onError?.(new Error(errorMessage))
1064
1156
  }
1065
1157
 
1066
1158
  /**
@@ -1078,6 +1170,14 @@ export class StreamProcessor {
1078
1170
  this.getActiveAssistantMessageId() ?? undefined,
1079
1171
  )
1080
1172
 
1173
+ // During the transition period, adapters emit BOTH STEP_FINISHED and
1174
+ // REASONING_MESSAGE_CONTENT with the same delta. If we've already processed
1175
+ // REASONING_MESSAGE_CONTENT events for this message, skip the duplicate
1176
+ // thinking content from STEP_FINISHED to avoid doubled content.
1177
+ if (state.hasSeenReasoningEvents) {
1178
+ return
1179
+ }
1180
+
1081
1181
  const previous = state.thinkingContent
1082
1182
  let nextThinking = previous
1083
1183
 
@@ -1108,6 +1208,33 @@ export class StreamProcessor {
1108
1208
  this.events.onThinkingUpdate?.(messageId, state.thinkingContent)
1109
1209
  }
1110
1210
 
1211
+ /**
1212
+ * Handle REASONING_MESSAGE_CONTENT event (AG-UI reasoning protocol).
1213
+ *
1214
+ * Accumulates reasoning delta into thinkingContent and updates the ThinkingPart
1215
+ * in the UIMessage.
1216
+ */
1217
+ private handleReasoningMessageContentEvent(
1218
+ chunk: Extract<StreamChunk, { type: 'REASONING_MESSAGE_CONTENT' }>,
1219
+ ): void {
1220
+ const { messageId, state } = this.ensureAssistantMessage(
1221
+ this.getActiveAssistantMessageId() ?? undefined,
1222
+ )
1223
+
1224
+ state.hasSeenReasoningEvents = true
1225
+ const delta = chunk.delta || ''
1226
+ state.thinkingContent = state.thinkingContent + delta
1227
+
1228
+ this.messages = updateThinkingPart(
1229
+ this.messages,
1230
+ messageId,
1231
+ state.thinkingContent,
1232
+ )
1233
+ this.emitMessagesChange()
1234
+
1235
+ this.events.onThinkingUpdate?.(messageId, state.thinkingContent)
1236
+ }
1237
+
1111
1238
  /**
1112
1239
  * Handle CUSTOM event.
1113
1240
  *
@@ -1178,7 +1305,7 @@ export class StreamProcessor {
1178
1305
  if (this.events.onCustomEvent) {
1179
1306
  const toolCallId =
1180
1307
  chunk.value && typeof chunk.value === 'object'
1181
- ? (chunk.value as any).toolCallId
1308
+ ? chunk.value.toolCallId
1182
1309
  : undefined
1183
1310
  this.events.onCustomEvent(chunk.name, chunk.value, { toolCallId })
1184
1311
  }
@@ -57,6 +57,7 @@ export interface MessageStreamState {
57
57
  currentSegmentText: string
58
58
  lastEmittedText: string
59
59
  thinkingContent: string
60
+ hasSeenReasoningEvents: boolean
60
61
  toolCalls: Map<string, InternalToolCallState>
61
62
  toolCallOrder: Array<string>
62
63
  hasToolCallsSinceTextStart: boolean
@@ -75,18 +75,24 @@ function makeStructuredOutputCompatible(
75
75
 
76
76
  // Recursively transform nested objects/arrays
77
77
  if (prop.type === 'object' && prop.properties) {
78
- properties[propName] = makeStructuredOutputCompatible(
78
+ const transformed = makeStructuredOutputCompatible(
79
79
  prop,
80
80
  prop.required || [],
81
81
  )
82
+ properties[propName] = wasOptional
83
+ ? { ...transformed, type: ['object', 'null'] }
84
+ : transformed
82
85
  } else if (prop.type === 'array' && prop.items) {
83
- properties[propName] = {
86
+ const transformed = {
84
87
  ...prop,
85
88
  items: makeStructuredOutputCompatible(
86
89
  prop.items,
87
90
  prop.items.required || [],
88
91
  ),
89
92
  }
93
+ properties[propName] = wasOptional
94
+ ? { ...transformed, type: ['array', 'null'] }
95
+ : transformed
90
96
  } else if (wasOptional) {
91
97
  // Make optional fields nullable by adding null to the type
92
98
  if (prop.type && !Array.isArray(prop.type)) {