@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
@@ -12,32 +12,6 @@ import type {
12
12
  UIMessage,
13
13
  } from '../types'
14
14
 
15
- const KNOWN_PART_TYPES = new Set([
16
- 'text',
17
- 'image',
18
- 'audio',
19
- 'video',
20
- 'document',
21
- 'tool-call',
22
- 'tool-result',
23
- 'thinking',
24
- 'structured-output',
25
- ])
26
-
27
- function isValidParts(value: unknown): value is Array<{ type: string }> {
28
- if (!Array.isArray(value)) return false
29
- for (const p of value) {
30
- if (!p || typeof p !== 'object') return false
31
- const type = (p as { type?: unknown }).type
32
- if (typeof type !== 'string' || !KNOWN_PART_TYPES.has(type)) return false
33
- if (type === 'structured-output') {
34
- const raw = (p as { raw?: unknown }).raw
35
- if (raw !== undefined && typeof raw !== 'string') return false
36
- }
37
- }
38
- return true
39
- }
40
-
41
15
  /**
42
16
  * Keyed by `AGUIRole` so a role added upstream fails to compile here until it
43
17
  * is handled, rather than silently falling through as an unknown role.
@@ -86,7 +60,7 @@ function requireArray(value: unknown, at: string): Array<unknown> {
86
60
  /**
87
61
  * Assert one AG-UI `Message`, discriminating on `role` exactly as the upstream
88
62
  * `MessageSchema` discriminated union does. The record view is retained on the
89
- * asserted type so callers can still inspect non-AG-UI extras like `parts`.
63
+ * asserted type so callers can still inspect extras like `metadata`.
90
64
  */
91
65
  function assertAGUIMessage(
92
66
  value: Record<string, unknown>,
@@ -141,16 +115,25 @@ function validateMessage(value: unknown, index: number): AGUIMessage {
141
115
  if (!isRecord(value)) invalidBody(`${at} must be an object`)
142
116
  assertAGUIMessage(value, at)
143
117
 
144
- // `parts` is TanStack's canonical extra, carried through so the UIMessage
145
- // path inside `chat()` can use it. Keep it only when it holds recognized
146
- // part types — the previous schema-based path dropped `parts` during parse
147
- // and re-attached it from the raw body behind this same check.
148
- if ('parts' in value && !isValidParts(value.parts)) {
149
- const withoutParts = { ...value }
150
- Reflect.deleteProperty(withoutParts, 'parts')
151
- return withoutParts
118
+ if (value.metadata !== undefined) {
119
+ if (!isRecord(value.metadata)) {
120
+ invalidBody(`${at}.metadata must be an object`)
121
+ }
152
122
  }
153
- return value
123
+
124
+ // Hard cut: inbound `parts` from old clients are dropped. Content,
125
+ // toolCalls, and metadata stay on the record. Copy so we do not mutate
126
+ // the caller's message object.
127
+ return dropInboundParts(value)
128
+ }
129
+
130
+ function dropInboundParts(
131
+ message: AGUIMessage & { parts?: unknown },
132
+ ): AGUIMessage {
133
+ if (!('parts' in message)) return message
134
+ const rest = { ...message }
135
+ delete rest.parts
136
+ return rest
154
137
  }
155
138
 
156
139
  function validateTool(
@@ -330,8 +313,8 @@ export async function chatParamsFromRequest(
330
313
 
331
314
  /**
332
315
  * Client-declared tool stub (no execute). `name` is `string`, so arrays that
333
- * include these stubs intentionally widen `TypedStreamChunk` tool-name
334
- * discrimination — pass server tools alone when you need a closed name union.
316
+ * include these stubs intentionally widen tool-name discrimination —
317
+ * pass server tools alone when you need a closed name union.
335
318
  */
336
319
  export type ClientToolDeclaration = {
337
320
  name: string
@@ -360,7 +343,7 @@ export type MergedAgentTools<TServerTools extends ReadonlyArray<AnyTool>> =
360
343
  * Typing:
361
344
  * - Empty `clientTools` preserves the server tuple (closed name union).
362
345
  * - Non-empty `clientTools` returns a widened array that honestly includes
363
- * client stubs, so `TypedStreamChunk` does not claim a closed server-only
346
+ * client stubs, so the merged array does not claim a closed server-only
364
347
  * name union.
365
348
  *
366
349
  * @param serverTools - The server's tool array (e.g. from
@@ -0,0 +1,24 @@
1
+ import type { StreamChunk } from '../types'
2
+ import { tanstackMetadata } from './merge-metadata'
3
+
4
+ function stringField(value: unknown): string | undefined {
5
+ return typeof value === 'string' && value !== '' ? value : undefined
6
+ }
7
+
8
+ /** Run id on the spec event, or `metadata.tanstack.runId` after normalize. */
9
+ export function getChunkRunId(chunk: StreamChunk): string | undefined {
10
+ if ('runId' in chunk) {
11
+ const top = stringField(chunk.runId)
12
+ if (top !== undefined) return top
13
+ }
14
+ return stringField(tanstackMetadata(chunk)?.runId)
15
+ }
16
+
17
+ /** Thread id on the spec event, or `metadata.tanstack.threadId` after normalize. */
18
+ export function getChunkThreadId(chunk: StreamChunk): string | undefined {
19
+ if ('threadId' in chunk) {
20
+ const top = stringField(chunk.threadId)
21
+ if (top !== undefined) return top
22
+ }
23
+ return stringField(tanstackMetadata(chunk)?.threadId)
24
+ }
@@ -0,0 +1,117 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ mergeMetadata,
4
+ tanstackMetadata,
5
+ withTanstackMetadata,
6
+ } from './merge-metadata'
7
+
8
+ describe('mergeMetadata', () => {
9
+ it('deep-merges tanstack and last-write-wins sibling keys', () => {
10
+ const merged = mergeMetadata(
11
+ { author: 'dana', tanstack: { model: 'gpt-5.5' } },
12
+ { tanstack: { createdAt: '2026-08-20T00:00:00.000Z' } },
13
+ )
14
+ expect(merged).toEqual({
15
+ author: 'dana',
16
+ tanstack: {
17
+ model: 'gpt-5.5',
18
+ createdAt: '2026-08-20T00:00:00.000Z',
19
+ },
20
+ })
21
+ })
22
+
23
+ it('last write wins for overlapping tanstack keys', () => {
24
+ const merged = mergeMetadata(
25
+ { tanstack: { model: 'gpt-5.5', finishReason: 'stop' } },
26
+ { tanstack: { model: 'claude' } },
27
+ )
28
+ expect(merged).toEqual({
29
+ tanstack: { model: 'claude', finishReason: 'stop' },
30
+ })
31
+ })
32
+
33
+ it('keeps a null value under a key', () => {
34
+ expect(mergeMetadata({ author: 'dana' }, { author: null })).toEqual({
35
+ author: null,
36
+ })
37
+ })
38
+
39
+ it('treats incoming null as absent', () => {
40
+ expect(mergeMetadata({ author: 'dana' }, null)).toEqual({ author: 'dana' })
41
+ })
42
+
43
+ it('treats incoming undefined as absent', () => {
44
+ expect(mergeMetadata({ author: 'dana' }, undefined)).toEqual({
45
+ author: 'dana',
46
+ })
47
+ })
48
+
49
+ it('returns incoming when current is missing', () => {
50
+ expect(mergeMetadata(undefined, { author: 'dana' })).toEqual({
51
+ author: 'dana',
52
+ })
53
+ })
54
+ })
55
+
56
+ describe('tanstackMetadata', () => {
57
+ it('reads metadata.tanstack when it is an object', () => {
58
+ expect(
59
+ tanstackMetadata({ metadata: { tanstack: { model: 'gpt-5.5' } } }),
60
+ ).toEqual({ model: 'gpt-5.5' })
61
+ })
62
+
63
+ it('returns undefined when tanstack is missing or not an object', () => {
64
+ expect(tanstackMetadata({})).toBeUndefined()
65
+ expect(tanstackMetadata({ metadata: { tanstack: 'nope' } })).toBeUndefined()
66
+ expect(tanstackMetadata({ metadata: { tanstack: [] } })).toBeUndefined()
67
+ })
68
+
69
+ it('returns undefined when nested metadata is null', () => {
70
+ expect(tanstackMetadata({ metadata: null })).toBeUndefined()
71
+ })
72
+
73
+ it('reads tanstack from a raw record', () => {
74
+ expect(tanstackMetadata({ tanstack: { model: 'gpt-5.5' } })).toEqual({
75
+ model: 'gpt-5.5',
76
+ })
77
+ })
78
+ })
79
+
80
+ describe('withTanstackMetadata', () => {
81
+ it('writes metadata.tanstack and does not set the reserved ag-ui key', () => {
82
+ const next = withTanstackMetadata(
83
+ { type: 'RUN_STARTED' },
84
+ { model: 'gpt-5.5' },
85
+ )
86
+ expect(next.metadata).toEqual({ tanstack: { model: 'gpt-5.5' } })
87
+ expect(next.metadata).not.toHaveProperty('ag-ui')
88
+ })
89
+
90
+ it('merges new tanstack keys into existing metadata without dropping user keys', () => {
91
+ const next = withTanstackMetadata(
92
+ { metadata: { author: 'dana', tanstack: { model: 'gpt-5.5' } } },
93
+ { finishReason: 'stop' },
94
+ )
95
+ expect(next.metadata).toEqual({
96
+ author: 'dana',
97
+ tanstack: { model: 'gpt-5.5', finishReason: 'stop' },
98
+ })
99
+ })
100
+
101
+ it('last write wins for overlapping tanstack keys', () => {
102
+ const next = withTanstackMetadata(
103
+ { metadata: { tanstack: { model: 'gpt-5.5' } } },
104
+ { model: 'claude' },
105
+ )
106
+ expect(next.metadata).toEqual({ tanstack: { model: 'claude' } })
107
+ })
108
+
109
+ it('treats null metadata as absent and still writes tanstack', () => {
110
+ expect(() =>
111
+ withTanstackMetadata({ metadata: null }, { model: 'gpt-5.5' }),
112
+ ).not.toThrow()
113
+ expect(
114
+ withTanstackMetadata({ metadata: null }, { model: 'gpt-5.5' }).metadata,
115
+ ).toEqual({ tanstack: { model: 'gpt-5.5' } })
116
+ })
117
+ })
@@ -0,0 +1,59 @@
1
+ import type { TanStackMessageMetadata, TanStackRunMetadata } from '../types'
2
+
3
+ export type MetadataRecord = Record<string, any>
4
+
5
+ /** `metadata.tanstack` on a message or run event. Internal getter return type. */
6
+ export type TanStackMetadata = TanStackMessageMetadata & TanStackRunMetadata
7
+
8
+ function isPlainRecord(value: unknown): value is MetadataRecord {
9
+ return value != null && typeof value === 'object' && !Array.isArray(value)
10
+ }
11
+
12
+ export function mergeMetadata(
13
+ current: MetadataRecord | undefined,
14
+ incoming: MetadataRecord | null | undefined,
15
+ ): MetadataRecord | undefined {
16
+ if (incoming == null) return current
17
+ if (current == null) return { ...incoming }
18
+ const merged: MetadataRecord = { ...current, ...incoming }
19
+ if (isPlainRecord(current.tanstack) && isPlainRecord(incoming.tanstack)) {
20
+ merged.tanstack = { ...current.tanstack, ...incoming.tanstack }
21
+ }
22
+ return merged
23
+ }
24
+
25
+ /** Read `metadata.tanstack`. Not part of the public SDK surface. */
26
+ export function tanstackMetadata(
27
+ value: { metadata?: MetadataRecord | null } | MetadataRecord | undefined,
28
+ ): TanStackMetadata | undefined {
29
+ if (value == null) return undefined
30
+ const nested = 'metadata' in value ? value.metadata : undefined
31
+ const metadata =
32
+ nested != null && typeof nested === 'object' && !Array.isArray(nested)
33
+ ? nested
34
+ : (value as MetadataRecord)
35
+ const tanstack = metadata.tanstack
36
+ if (
37
+ tanstack == null ||
38
+ typeof tanstack !== 'object' ||
39
+ Array.isArray(tanstack)
40
+ ) {
41
+ return undefined
42
+ }
43
+ return tanstack as TanStackMetadata
44
+ }
45
+
46
+ export function withTanstackMetadata<T>(
47
+ value: T & { metadata?: MetadataRecord | null },
48
+ tanstack: MetadataRecord,
49
+ ): Omit<T, 'metadata'> & { metadata: MetadataRecord } {
50
+ const current = value.metadata
51
+ const currentTanstack = tanstackMetadata(value) ?? {}
52
+ return {
53
+ ...value,
54
+ metadata: {
55
+ ...(current ?? {}),
56
+ tanstack: { ...currentTanstack, ...tanstack },
57
+ },
58
+ }
59
+ }