@tanstack/ai 0.47.3 → 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 (101) 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 +115 -25
  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 +186 -101
  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 +56 -53
  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 +11 -4
  30. package/dist/esm/middlewares/otel.js.map +1 -1
  31. package/dist/esm/stream-to-response.js +8 -5
  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/merge-metadata.d.ts +14 -0
  53. package/dist/esm/utilities/merge-metadata.js +43 -0
  54. package/dist/esm/utilities/merge-metadata.js.map +1 -0
  55. package/dist/esm/utilities/normalize-stream-chunk.d.ts +3 -0
  56. package/dist/esm/utilities/normalize-stream-chunk.js +100 -0
  57. package/dist/esm/utilities/normalize-stream-chunk.js.map +1 -0
  58. package/dist/esm/utilities/reasoning-encrypted-value.d.ts +8 -0
  59. package/dist/esm/utilities/reasoning-encrypted-value.js +16 -0
  60. package/dist/esm/utilities/reasoning-encrypted-value.js.map +1 -0
  61. package/dist/esm/utilities/restore-inbound-chunk.d.ts +15 -0
  62. package/dist/esm/utilities/restore-inbound-chunk.js +43 -0
  63. package/dist/esm/utilities/restore-inbound-chunk.js.map +1 -0
  64. package/dist/esm/utilities/spec-event-keys.d.ts +2 -0
  65. package/dist/esm/utilities/spec-event-keys.js +56 -0
  66. package/dist/esm/utilities/spec-event-keys.js.map +1 -0
  67. package/dist/esm/utilities/structured-output-events.d.ts +3 -3
  68. package/dist/esm/utilities/structured-output-events.js.map +1 -1
  69. package/package.json +2 -2
  70. package/src/activities/chat/adapter.ts +4 -4
  71. package/src/activities/chat/index.ts +398 -400
  72. package/src/activities/chat/messages.ts +174 -34
  73. package/src/activities/chat/stream/processor.ts +289 -218
  74. package/src/activities/chat/tools/tool-calls.ts +23 -22
  75. package/src/activities/generateVideo/index.ts +7 -6
  76. package/src/activities/stream-generation-result.ts +8 -12
  77. package/src/activities/summarize/chat-stream-summarize.ts +94 -70
  78. package/src/adapter-internals.ts +2 -0
  79. package/src/client.ts +13 -0
  80. package/src/index.ts +6 -0
  81. package/src/middlewares/otel.ts +16 -8
  82. package/src/stream-to-response.ts +11 -4
  83. package/src/stream-to-websocket.ts +3 -1
  84. package/src/strip-to-spec-middleware.ts +48 -24
  85. package/src/types.ts +109 -393
  86. package/src/utilities/adapter-yield-chunk.ts +30 -0
  87. package/src/utilities/ag-ui-usage.test.ts +194 -0
  88. package/src/utilities/ag-ui-usage.ts +148 -0
  89. package/src/utilities/ag-ui-wire.ts +149 -31
  90. package/src/utilities/chat-params.ts +22 -39
  91. package/src/utilities/chunk-ids.ts +24 -0
  92. package/src/utilities/merge-metadata.test.ts +117 -0
  93. package/src/utilities/merge-metadata.ts +59 -0
  94. package/src/utilities/normalize-stream-chunk.test.ts +423 -0
  95. package/src/utilities/normalize-stream-chunk.ts +186 -0
  96. package/src/utilities/reasoning-encrypted-value.ts +18 -0
  97. package/src/utilities/restore-inbound-chunk.test.ts +133 -0
  98. package/src/utilities/restore-inbound-chunk.ts +72 -0
  99. package/src/utilities/spec-event-keys.test.ts +34 -0
  100. package/src/utilities/spec-event-keys.ts +74 -0
  101. package/src/utilities/structured-output-events.ts +3 -3
@@ -1,5 +1,6 @@
1
1
  import { isProviderExecutedToolCall } from '../../utilities/provider-executed'
2
2
  import { normalizeToolResult } from '../../utilities/tool-result'
3
+ import { tanstackMetadata } from '../../utilities/merge-metadata'
3
4
  import type { Message as AGUIMessage } from '@ag-ui/core'
4
5
  import type {
5
6
  ContentPart,
@@ -7,6 +8,7 @@ import type {
7
8
  ModelMessage,
8
9
  StructuredOutputPart,
9
10
  TextPart,
11
+ ToolCall,
10
12
  ToolCallPart,
11
13
  UIMessage,
12
14
  } from '../../types'
@@ -36,6 +38,34 @@ export function safeJsonStringify(value: unknown): string {
36
38
  }
37
39
  }
38
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
+
39
69
  function parseToolResultContent(content: string): unknown {
40
70
  try {
41
71
  return JSON.parse(content)
@@ -68,8 +98,13 @@ function collapseContentParts(
68
98
  * Extract text content from ModelMessage content (string, null, or ContentPart array).
69
99
  * Used when only the text portion is needed (e.g., tool result content).
70
100
  */
71
- function getTextContent(content: string | null | Array<ContentPart>): string {
72
- 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 ''
73
108
  if (typeof content === 'string') return content
74
109
  return content
75
110
  .filter((part): part is TextPart => part.type === 'text')
@@ -98,16 +133,15 @@ export function convertMessagesToModelMessages(
98
133
  }
99
134
 
100
135
  const modelMessages: Array<ModelMessage> = []
136
+ let pendingThinking: Array<{ content: string; signature?: string }> = []
101
137
  for (const msg of messages) {
102
138
  if ('parts' in msg) {
103
- // UIMessage anchor — existing fan-out path
104
139
  modelMessages.push(...uiMessageToModelMessages(msg))
105
140
  continue
106
141
  }
107
142
 
108
143
  const role = (msg as { role: string }).role
109
144
 
110
- // AG-UI tool fan-out duplicate — drop if anchor already covers it
111
145
  if (
112
146
  role === 'tool' &&
113
147
  msg.toolCallId &&
@@ -116,12 +150,22 @@ export function convertMessagesToModelMessages(
116
150
  continue
117
151
  }
118
152
 
119
- // AG-UI reasoning and activity — no ModelMessage equivalent today
120
- 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') {
121
166
  continue
122
167
  }
123
168
 
124
- // AG-UI developer — collapse to system
125
169
  if (role === 'developer') {
126
170
  modelMessages.push({
127
171
  role: 'system' as ModelMessage['role'],
@@ -130,8 +174,56 @@ export function convertMessagesToModelMessages(
130
174
  continue
131
175
  }
132
176
 
133
- // Already a ModelMessage (user, assistant, system, tool with no anchor) — pass through
134
- 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)
135
227
  }
136
228
  return modelMessages
137
229
  }
@@ -536,58 +628,106 @@ export function aguiSnapshotMessageToUIMessage(
536
628
  message: AGUIMessage | UIMessage,
537
629
  ): UIMessage {
538
630
  if ('parts' in message) {
539
- return { ...message, id: message.id || generateMessageId() }
631
+ return applySnapshotMetadata(message, {
632
+ ...message,
633
+ id: message.id || generateMessageId(),
634
+ })
540
635
  }
541
636
 
542
637
  const id = message.id || generateMessageId()
543
638
 
544
639
  switch (message.role) {
545
640
  case 'user':
546
- return {
641
+ return applySnapshotMetadata(message, {
547
642
  id,
548
643
  role: 'user',
549
644
  parts: aguiUserContentToParts(message.content),
550
- }
551
- case 'assistant':
552
- return modelMessageToUIMessage(
553
- {
554
- role: 'assistant',
555
- content: message.content ?? null,
556
- ...(message.toolCalls && { toolCalls: message.toolCalls }),
557
- },
558
- 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
+ ),
559
665
  )
666
+ }
560
667
  case 'tool':
561
- return modelMessageToUIMessage(
562
- {
563
- role: 'tool',
564
- content: message.content,
565
- toolCallId: message.toolCallId,
566
- },
567
- id,
668
+ return applySnapshotMetadata(
669
+ message,
670
+ modelMessageToUIMessage(
671
+ {
672
+ role: 'tool',
673
+ content: message.content,
674
+ toolCallId: message.toolCallId,
675
+ },
676
+ id,
677
+ ),
568
678
  )
569
679
  case 'system':
570
680
  case 'developer':
571
681
  // `ModelMessage` has no system/developer role; build the part directly.
572
- return {
682
+ return applySnapshotMetadata(message, {
573
683
  id,
574
684
  role: 'system',
575
685
  parts: message.content
576
686
  ? [{ type: 'text', content: message.content }]
577
687
  : [],
578
- }
579
- case 'reasoning':
580
- return {
688
+ })
689
+ case 'reasoning': {
690
+ const signature = encryptedValueFrom(message)
691
+ return applySnapshotMetadata(message, {
581
692
  id,
582
693
  role: 'assistant',
583
694
  parts: message.content
584
- ? [{ type: 'thinking', content: message.content }]
695
+ ? [
696
+ {
697
+ type: 'thinking' as const,
698
+ content: message.content,
699
+ ...(signature !== undefined ? { signature } : {}),
700
+ },
701
+ ]
585
702
  : [],
586
- }
703
+ })
704
+ }
587
705
  case 'activity':
588
706
  default:
589
707
  // `activity` (and any future role) has no text/parts equivalent today.
590
- 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 } : {}),
591
731
  }
592
732
  }
593
733