@tanstack/ai 0.47.2 → 0.48.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 (105) hide show
  1. package/README.md +2 -1
  2. package/dist/esm/activities/chat/adapter.d.ts +5 -4
  3. package/dist/esm/activities/chat/adapter.js.map +1 -1
  4. package/dist/esm/activities/chat/index.d.ts +4 -7
  5. package/dist/esm/activities/chat/index.js +201 -243
  6. package/dist/esm/activities/chat/index.js.map +1 -1
  7. package/dist/esm/activities/chat/messages.js +126 -31
  8. package/dist/esm/activities/chat/messages.js.map +1 -1
  9. package/dist/esm/activities/chat/stream/processor.d.ts +38 -17
  10. package/dist/esm/activities/chat/stream/processor.js +197 -110
  11. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  12. package/dist/esm/activities/chat/tools/tool-calls.d.ts +3 -2
  13. package/dist/esm/activities/chat/tools/tool-calls.js +15 -10
  14. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  15. package/dist/esm/activities/generateVideo/index.js +6 -6
  16. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  17. package/dist/esm/activities/stream-generation-result.js +7 -8
  18. package/dist/esm/activities/stream-generation-result.js.map +1 -1
  19. package/dist/esm/activities/summarize/chat-stream-summarize.d.ts +2 -1
  20. package/dist/esm/activities/summarize/chat-stream-summarize.js +59 -54
  21. package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
  22. package/dist/esm/adapter-internals.d.ts +2 -0
  23. package/dist/esm/adapter-internals.js +3 -1
  24. package/dist/esm/client.d.ts +8 -1
  25. package/dist/esm/client.js +7 -2
  26. package/dist/esm/client.js.map +1 -1
  27. package/dist/esm/index.d.ts +6 -0
  28. package/dist/esm/index.js +6 -2
  29. package/dist/esm/middlewares/otel.js +12 -5
  30. package/dist/esm/middlewares/otel.js.map +1 -1
  31. package/dist/esm/stream-to-response.js +13 -6
  32. package/dist/esm/stream-to-response.js.map +1 -1
  33. package/dist/esm/stream-to-websocket.js +4 -2
  34. package/dist/esm/stream-to-websocket.js.map +1 -1
  35. package/dist/esm/strip-to-spec-middleware.d.ts +10 -13
  36. package/dist/esm/strip-to-spec-middleware.js +24 -22
  37. package/dist/esm/strip-to-spec-middleware.js.map +1 -1
  38. package/dist/esm/types.d.ts +82 -305
  39. package/dist/esm/utilities/adapter-yield-chunk.d.ts +31 -0
  40. package/dist/esm/utilities/ag-ui-usage.d.ts +24 -0
  41. package/dist/esm/utilities/ag-ui-usage.js +66 -0
  42. package/dist/esm/utilities/ag-ui-usage.js.map +1 -0
  43. package/dist/esm/utilities/ag-ui-wire.d.ts +14 -7
  44. package/dist/esm/utilities/ag-ui-wire.js +71 -30
  45. package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
  46. package/dist/esm/utilities/chat-params.d.ts +3 -3
  47. package/dist/esm/utilities/chat-params.js +10 -30
  48. package/dist/esm/utilities/chat-params.js.map +1 -1
  49. package/dist/esm/utilities/chunk-ids.d.ts +5 -0
  50. package/dist/esm/utilities/chunk-ids.js +25 -0
  51. package/dist/esm/utilities/chunk-ids.js.map +1 -0
  52. package/dist/esm/utilities/errors.d.ts +9 -0
  53. package/dist/esm/utilities/errors.js +13 -1
  54. package/dist/esm/utilities/errors.js.map +1 -1
  55. package/dist/esm/utilities/merge-metadata.d.ts +14 -0
  56. package/dist/esm/utilities/merge-metadata.js +43 -0
  57. package/dist/esm/utilities/merge-metadata.js.map +1 -0
  58. package/dist/esm/utilities/normalize-stream-chunk.d.ts +3 -0
  59. package/dist/esm/utilities/normalize-stream-chunk.js +100 -0
  60. package/dist/esm/utilities/normalize-stream-chunk.js.map +1 -0
  61. package/dist/esm/utilities/reasoning-encrypted-value.d.ts +8 -0
  62. package/dist/esm/utilities/reasoning-encrypted-value.js +16 -0
  63. package/dist/esm/utilities/reasoning-encrypted-value.js.map +1 -0
  64. package/dist/esm/utilities/restore-inbound-chunk.d.ts +15 -0
  65. package/dist/esm/utilities/restore-inbound-chunk.js +43 -0
  66. package/dist/esm/utilities/restore-inbound-chunk.js.map +1 -0
  67. package/dist/esm/utilities/spec-event-keys.d.ts +2 -0
  68. package/dist/esm/utilities/spec-event-keys.js +56 -0
  69. package/dist/esm/utilities/spec-event-keys.js.map +1 -0
  70. package/dist/esm/utilities/structured-output-events.d.ts +3 -3
  71. package/dist/esm/utilities/structured-output-events.js.map +1 -1
  72. package/package.json +3 -3
  73. package/src/activities/chat/adapter.ts +4 -4
  74. package/src/activities/chat/index.ts +398 -400
  75. package/src/activities/chat/messages.ts +183 -36
  76. package/src/activities/chat/stream/processor.ts +313 -239
  77. package/src/activities/chat/tools/tool-calls.ts +23 -22
  78. package/src/activities/generateVideo/index.ts +7 -6
  79. package/src/activities/stream-generation-result.ts +8 -12
  80. package/src/activities/summarize/chat-stream-summarize.ts +98 -70
  81. package/src/adapter-internals.ts +2 -0
  82. package/src/client.ts +13 -0
  83. package/src/index.ts +6 -0
  84. package/src/middlewares/otel.ts +16 -8
  85. package/src/stream-to-response.ts +16 -4
  86. package/src/stream-to-websocket.ts +3 -1
  87. package/src/strip-to-spec-middleware.ts +48 -24
  88. package/src/types.ts +109 -393
  89. package/src/utilities/adapter-yield-chunk.ts +30 -0
  90. package/src/utilities/ag-ui-usage.test.ts +194 -0
  91. package/src/utilities/ag-ui-usage.ts +148 -0
  92. package/src/utilities/ag-ui-wire.ts +149 -31
  93. package/src/utilities/chat-params.ts +22 -39
  94. package/src/utilities/chunk-ids.ts +24 -0
  95. package/src/utilities/errors.ts +23 -0
  96. package/src/utilities/merge-metadata.test.ts +117 -0
  97. package/src/utilities/merge-metadata.ts +59 -0
  98. package/src/utilities/normalize-stream-chunk.test.ts +423 -0
  99. package/src/utilities/normalize-stream-chunk.ts +186 -0
  100. package/src/utilities/reasoning-encrypted-value.ts +18 -0
  101. package/src/utilities/restore-inbound-chunk.test.ts +133 -0
  102. package/src/utilities/restore-inbound-chunk.ts +72 -0
  103. package/src/utilities/spec-event-keys.test.ts +34 -0
  104. package/src/utilities/spec-event-keys.ts +74 -0
  105. package/src/utilities/structured-output-events.ts +3 -3
@@ -1,4 +1,6 @@
1
+ import { isProviderExecutedToolCall } from '../../utilities/provider-executed'
1
2
  import { normalizeToolResult } from '../../utilities/tool-result'
3
+ import { tanstackMetadata } from '../../utilities/merge-metadata'
2
4
  import type { Message as AGUIMessage } from '@ag-ui/core'
3
5
  import type {
4
6
  ContentPart,
@@ -6,6 +8,7 @@ import type {
6
8
  ModelMessage,
7
9
  StructuredOutputPart,
8
10
  TextPart,
11
+ ToolCall,
9
12
  ToolCallPart,
10
13
  UIMessage,
11
14
  } from '../../types'
@@ -35,6 +38,34 @@ export function safeJsonStringify(value: unknown): string {
35
38
  }
36
39
  }
37
40
 
41
+ function nonEmptyString(value: unknown): string | undefined {
42
+ return typeof value === 'string' && value !== '' ? value : undefined
43
+ }
44
+
45
+ function encryptedValueFrom(value: object): string | undefined {
46
+ if ('encryptedValue' in value) {
47
+ const fromSpec = nonEmptyString(value.encryptedValue)
48
+ if (fromSpec !== undefined) return fromSpec
49
+ }
50
+ return nonEmptyString(tanstackMetadata(value)?.signature)
51
+ }
52
+
53
+ function toolCallFromWire(toolCall: ToolCall, bag: unknown): ToolCall {
54
+ const fromBag =
55
+ bag != null && typeof bag === 'object' && !Array.isArray(bag)
56
+ ? bag
57
+ : undefined
58
+ const encrypted = encryptedValueFrom(toolCall)
59
+ if (fromBag === undefined && encrypted === undefined) return toolCall
60
+ return {
61
+ ...toolCall,
62
+ metadata: {
63
+ ...(fromBag ?? {}),
64
+ ...(encrypted !== undefined ? { thoughtSignature: encrypted } : {}),
65
+ },
66
+ }
67
+ }
68
+
38
69
  function parseToolResultContent(content: string): unknown {
39
70
  try {
40
71
  return JSON.parse(content)
@@ -67,8 +98,13 @@ function collapseContentParts(
67
98
  * Extract text content from ModelMessage content (string, null, or ContentPart array).
68
99
  * Used when only the text portion is needed (e.g., tool result content).
69
100
  */
70
- function getTextContent(content: string | null | Array<ContentPart>): string {
71
- if (content === null) return ''
101
+ function getTextContent(
102
+ content: string | null | undefined | Array<ContentPart>,
103
+ ): string {
104
+ // Tool-call-only assistant turns carry no text and reach here as `null` or
105
+ // `undefined`; both must collapse to an empty string rather than crash on
106
+ // `.filter` (issue #532 — the interrupt-boundary MessagesSnapshot).
107
+ if (content === null || content === undefined) return ''
72
108
  if (typeof content === 'string') return content
73
109
  return content
74
110
  .filter((part): part is TextPart => part.type === 'text')
@@ -97,16 +133,15 @@ export function convertMessagesToModelMessages(
97
133
  }
98
134
 
99
135
  const modelMessages: Array<ModelMessage> = []
136
+ let pendingThinking: Array<{ content: string; signature?: string }> = []
100
137
  for (const msg of messages) {
101
138
  if ('parts' in msg) {
102
- // UIMessage anchor — existing fan-out path
103
139
  modelMessages.push(...uiMessageToModelMessages(msg))
104
140
  continue
105
141
  }
106
142
 
107
143
  const role = (msg as { role: string }).role
108
144
 
109
- // AG-UI tool fan-out duplicate — drop if anchor already covers it
110
145
  if (
111
146
  role === 'tool' &&
112
147
  msg.toolCallId &&
@@ -115,12 +150,22 @@ export function convertMessagesToModelMessages(
115
150
  continue
116
151
  }
117
152
 
118
- // AG-UI reasoning and activity — no ModelMessage equivalent today
119
- if (role === 'reasoning' || role === 'activity') {
153
+ if (role === 'reasoning') {
154
+ const content = (msg as { content?: string }).content
155
+ if (content) {
156
+ const signature = encryptedValueFrom(msg)
157
+ pendingThinking.push({
158
+ content,
159
+ ...(signature !== undefined ? { signature } : {}),
160
+ })
161
+ }
162
+ continue
163
+ }
164
+
165
+ if (role === 'activity') {
120
166
  continue
121
167
  }
122
168
 
123
- // AG-UI developer — collapse to system
124
169
  if (role === 'developer') {
125
170
  modelMessages.push({
126
171
  role: 'system' as ModelMessage['role'],
@@ -129,8 +174,56 @@ export function convertMessagesToModelMessages(
129
174
  continue
130
175
  }
131
176
 
132
- // Already a ModelMessage (user, assistant, system, tool with no anchor) — pass through
133
- modelMessages.push(msg)
177
+ if (
178
+ role === 'user' &&
179
+ Array.isArray((msg as { content?: unknown }).content)
180
+ ) {
181
+ const content = (msg as { content: Array<{ type: string }> }).content
182
+ // TanStack ModelMessage text parts use `{ content }`. AG-UI wire text
183
+ // parts use `{ text }`. Only rewrite the AG-UI shape.
184
+ if (
185
+ !content.some(
186
+ (part) =>
187
+ part.type === 'text' && 'text' in part && !('content' in part),
188
+ )
189
+ ) {
190
+ modelMessages.push(msg as ModelMessage)
191
+ continue
192
+ }
193
+ const parts = aguiUserContentToParts(
194
+ content as Extract<AGUIMessage, { role: 'user' }>['content'],
195
+ )
196
+ const contentParts = parts.filter(isContentPart)
197
+ modelMessages.push({
198
+ role: 'user',
199
+ content: collapseContentParts(contentParts),
200
+ ...((msg as { id?: string }).id !== undefined && {
201
+ id: (msg as { id: string }).id,
202
+ }),
203
+ })
204
+ continue
205
+ }
206
+
207
+ if (role === 'assistant') {
208
+ const source = msg as ModelMessage
209
+ const toolCallMetadata = tanstackMetadata(msg)?.toolCallMetadata
210
+ const toolCalls = source.toolCalls?.map((toolCall) =>
211
+ toolCallFromWire(toolCall, toolCallMetadata?.[toolCall.id]),
212
+ )
213
+ modelMessages.push({
214
+ ...source,
215
+ ...(toolCalls !== undefined ? { toolCalls } : {}),
216
+ ...(pendingThinking.length > 0
217
+ ? {
218
+ thinking: [...(source.thinking ?? []), ...pendingThinking],
219
+ }
220
+ : {}),
221
+ })
222
+ pendingThinking = []
223
+ continue
224
+ }
225
+
226
+ modelMessages.push(msg as ModelMessage)
134
227
  }
135
228
  return modelMessages
136
229
  }
@@ -249,14 +342,15 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
249
342
  const content = collapseContentParts(current.contentParts)
250
343
  const hasContent = content !== null
251
344
  const hasToolCalls = current.toolCalls.length > 0
345
+ const hasThinking = pendingThinking.length > 0
252
346
 
253
- if (hasContent || hasToolCalls) {
347
+ if (hasContent || hasToolCalls || hasThinking) {
254
348
  messageList.push({
255
349
  id: uiMessage.id,
256
350
  role: 'assistant',
257
351
  content,
258
352
  ...(hasToolCalls && { toolCalls: current.toolCalls }),
259
- ...(pendingThinking.length > 0 && { thinking: pendingThinking }),
353
+ ...(hasThinking && { thinking: pendingThinking }),
260
354
  ...(current.structuredOutput && {
261
355
  structuredOutput: current.structuredOutput,
262
356
  }),
@@ -317,6 +411,11 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
317
411
 
318
412
  case 'thinking':
319
413
  if (part.content) {
414
+ // Provider-executed tools have no tool-result part, so thinking
415
+ // after them has to start the next segment or it replays first.
416
+ if (current.toolCalls.some(isProviderExecutedToolCall)) {
417
+ flushSegment()
418
+ }
320
419
  pendingThinking.push({
321
420
  content: part.content,
322
421
  ...(part.signature && { signature: part.signature }),
@@ -529,58 +628,106 @@ export function aguiSnapshotMessageToUIMessage(
529
628
  message: AGUIMessage | UIMessage,
530
629
  ): UIMessage {
531
630
  if ('parts' in message) {
532
- return { ...message, id: message.id || generateMessageId() }
631
+ return applySnapshotMetadata(message, {
632
+ ...message,
633
+ id: message.id || generateMessageId(),
634
+ })
533
635
  }
534
636
 
535
637
  const id = message.id || generateMessageId()
536
638
 
537
639
  switch (message.role) {
538
640
  case 'user':
539
- return {
641
+ return applySnapshotMetadata(message, {
540
642
  id,
541
643
  role: 'user',
542
644
  parts: aguiUserContentToParts(message.content),
543
- }
544
- case 'assistant':
545
- return modelMessageToUIMessage(
546
- {
547
- role: 'assistant',
548
- content: message.content ?? null,
549
- ...(message.toolCalls && { toolCalls: message.toolCalls }),
550
- },
551
- id,
645
+ })
646
+ case 'assistant': {
647
+ const toolCallMetadata = tanstackMetadata(message)?.toolCallMetadata
648
+ const toolCalls = message.toolCalls?.map((toolCall) => {
649
+ const metadata =
650
+ toolCallMetadata != null && typeof toolCallMetadata === 'object'
651
+ ? (toolCallMetadata as Record<string, unknown>)[toolCall.id]
652
+ : undefined
653
+ return metadata !== undefined ? { ...toolCall, metadata } : toolCall
654
+ })
655
+ return applySnapshotMetadata(
656
+ message,
657
+ modelMessageToUIMessage(
658
+ {
659
+ role: 'assistant',
660
+ content: message.content ?? null,
661
+ ...(toolCalls && { toolCalls }),
662
+ },
663
+ id,
664
+ ),
552
665
  )
666
+ }
553
667
  case 'tool':
554
- return modelMessageToUIMessage(
555
- {
556
- role: 'tool',
557
- content: message.content,
558
- toolCallId: message.toolCallId,
559
- },
560
- id,
668
+ return applySnapshotMetadata(
669
+ message,
670
+ modelMessageToUIMessage(
671
+ {
672
+ role: 'tool',
673
+ content: message.content,
674
+ toolCallId: message.toolCallId,
675
+ },
676
+ id,
677
+ ),
561
678
  )
562
679
  case 'system':
563
680
  case 'developer':
564
681
  // `ModelMessage` has no system/developer role; build the part directly.
565
- return {
682
+ return applySnapshotMetadata(message, {
566
683
  id,
567
684
  role: 'system',
568
685
  parts: message.content
569
686
  ? [{ type: 'text', content: message.content }]
570
687
  : [],
571
- }
572
- case 'reasoning':
573
- return {
688
+ })
689
+ case 'reasoning': {
690
+ const signature = encryptedValueFrom(message)
691
+ return applySnapshotMetadata(message, {
574
692
  id,
575
693
  role: 'assistant',
576
694
  parts: message.content
577
- ? [{ type: 'thinking', content: message.content }]
695
+ ? [
696
+ {
697
+ type: 'thinking' as const,
698
+ content: message.content,
699
+ ...(signature !== undefined ? { signature } : {}),
700
+ },
701
+ ]
578
702
  : [],
579
- }
703
+ })
704
+ }
580
705
  case 'activity':
581
706
  default:
582
707
  // `activity` (and any future role) has no text/parts equivalent today.
583
- return { id, role: 'assistant', parts: [] }
708
+ return applySnapshotMetadata(message, {
709
+ id,
710
+ role: 'assistant',
711
+ parts: [],
712
+ })
713
+ }
714
+ }
715
+
716
+ /** Copy snapshot metadata when it is a record. Rebuild createdAt from tanstack.createdAt. */
717
+ function applySnapshotMetadata(source: object, ui: UIMessage): UIMessage {
718
+ if (!('metadata' in source)) return ui
719
+ const raw = source.metadata
720
+ if (raw == null || typeof raw !== 'object' || Array.isArray(raw)) return ui
721
+ const metadata = raw as NonNullable<UIMessage['metadata']>
722
+ const createdAtRaw = tanstackMetadata(metadata)?.createdAt
723
+ const createdAt =
724
+ typeof createdAtRaw === 'string' ? new Date(createdAtRaw) : undefined
725
+ const createdAtValid =
726
+ createdAt !== undefined && !Number.isNaN(createdAt.getTime())
727
+ return {
728
+ ...ui,
729
+ metadata,
730
+ ...(createdAtValid ? { createdAt } : {}),
584
731
  }
585
732
  }
586
733