@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.
- package/README.md +2 -1
- package/dist/esm/activities/chat/adapter.d.ts +5 -4
- package/dist/esm/activities/chat/adapter.js.map +1 -1
- package/dist/esm/activities/chat/index.d.ts +4 -7
- package/dist/esm/activities/chat/index.js +201 -243
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.js +115 -25
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.d.ts +38 -17
- package/dist/esm/activities/chat/stream/processor.js +186 -101
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-calls.d.ts +3 -2
- package/dist/esm/activities/chat/tools/tool-calls.js +15 -10
- package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
- package/dist/esm/activities/generateVideo/index.js +6 -6
- package/dist/esm/activities/generateVideo/index.js.map +1 -1
- package/dist/esm/activities/stream-generation-result.js +7 -8
- package/dist/esm/activities/stream-generation-result.js.map +1 -1
- package/dist/esm/activities/summarize/chat-stream-summarize.d.ts +2 -1
- package/dist/esm/activities/summarize/chat-stream-summarize.js +56 -53
- package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
- package/dist/esm/adapter-internals.d.ts +2 -0
- package/dist/esm/adapter-internals.js +3 -1
- package/dist/esm/client.d.ts +8 -1
- package/dist/esm/client.js +7 -2
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/index.d.ts +6 -0
- package/dist/esm/index.js +6 -2
- package/dist/esm/middlewares/otel.js +11 -4
- package/dist/esm/middlewares/otel.js.map +1 -1
- package/dist/esm/stream-to-response.js +8 -5
- package/dist/esm/stream-to-response.js.map +1 -1
- package/dist/esm/stream-to-websocket.js +4 -2
- package/dist/esm/stream-to-websocket.js.map +1 -1
- package/dist/esm/strip-to-spec-middleware.d.ts +10 -13
- package/dist/esm/strip-to-spec-middleware.js +24 -22
- package/dist/esm/strip-to-spec-middleware.js.map +1 -1
- package/dist/esm/types.d.ts +82 -305
- package/dist/esm/utilities/adapter-yield-chunk.d.ts +31 -0
- package/dist/esm/utilities/ag-ui-usage.d.ts +24 -0
- package/dist/esm/utilities/ag-ui-usage.js +66 -0
- package/dist/esm/utilities/ag-ui-usage.js.map +1 -0
- package/dist/esm/utilities/ag-ui-wire.d.ts +14 -7
- package/dist/esm/utilities/ag-ui-wire.js +71 -30
- package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
- package/dist/esm/utilities/chat-params.d.ts +3 -3
- package/dist/esm/utilities/chat-params.js +10 -30
- package/dist/esm/utilities/chat-params.js.map +1 -1
- package/dist/esm/utilities/chunk-ids.d.ts +5 -0
- package/dist/esm/utilities/chunk-ids.js +25 -0
- package/dist/esm/utilities/chunk-ids.js.map +1 -0
- package/dist/esm/utilities/merge-metadata.d.ts +14 -0
- package/dist/esm/utilities/merge-metadata.js +43 -0
- package/dist/esm/utilities/merge-metadata.js.map +1 -0
- package/dist/esm/utilities/normalize-stream-chunk.d.ts +3 -0
- package/dist/esm/utilities/normalize-stream-chunk.js +100 -0
- package/dist/esm/utilities/normalize-stream-chunk.js.map +1 -0
- package/dist/esm/utilities/reasoning-encrypted-value.d.ts +8 -0
- package/dist/esm/utilities/reasoning-encrypted-value.js +16 -0
- package/dist/esm/utilities/reasoning-encrypted-value.js.map +1 -0
- package/dist/esm/utilities/restore-inbound-chunk.d.ts +15 -0
- package/dist/esm/utilities/restore-inbound-chunk.js +43 -0
- package/dist/esm/utilities/restore-inbound-chunk.js.map +1 -0
- package/dist/esm/utilities/spec-event-keys.d.ts +2 -0
- package/dist/esm/utilities/spec-event-keys.js +56 -0
- package/dist/esm/utilities/spec-event-keys.js.map +1 -0
- package/dist/esm/utilities/structured-output-events.d.ts +3 -3
- package/dist/esm/utilities/structured-output-events.js.map +1 -1
- package/package.json +2 -2
- package/src/activities/chat/adapter.ts +4 -4
- package/src/activities/chat/index.ts +398 -400
- package/src/activities/chat/messages.ts +174 -34
- package/src/activities/chat/stream/processor.ts +289 -218
- package/src/activities/chat/tools/tool-calls.ts +23 -22
- package/src/activities/generateVideo/index.ts +7 -6
- package/src/activities/stream-generation-result.ts +8 -12
- package/src/activities/summarize/chat-stream-summarize.ts +94 -70
- package/src/adapter-internals.ts +2 -0
- package/src/client.ts +13 -0
- package/src/index.ts +6 -0
- package/src/middlewares/otel.ts +16 -8
- package/src/stream-to-response.ts +11 -4
- package/src/stream-to-websocket.ts +3 -1
- package/src/strip-to-spec-middleware.ts +48 -24
- package/src/types.ts +109 -393
- package/src/utilities/adapter-yield-chunk.ts +30 -0
- package/src/utilities/ag-ui-usage.test.ts +194 -0
- package/src/utilities/ag-ui-usage.ts +148 -0
- package/src/utilities/ag-ui-wire.ts +149 -31
- package/src/utilities/chat-params.ts +22 -39
- package/src/utilities/chunk-ids.ts +24 -0
- package/src/utilities/merge-metadata.test.ts +117 -0
- package/src/utilities/merge-metadata.ts +59 -0
- package/src/utilities/normalize-stream-chunk.test.ts +423 -0
- package/src/utilities/normalize-stream-chunk.ts +186 -0
- package/src/utilities/reasoning-encrypted-value.ts +18 -0
- package/src/utilities/restore-inbound-chunk.test.ts +133 -0
- package/src/utilities/restore-inbound-chunk.ts +72 -0
- package/src/utilities/spec-event-keys.test.ts +34 -0
- package/src/utilities/spec-event-keys.ts +74 -0
- 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
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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
|
|
334
|
-
*
|
|
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
|
|
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
|
+
}
|