@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
@@ -0,0 +1,423 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import type { InterruptSubmissionError } from '../interrupts'
3
+ import { EventType } from '../types'
4
+ import type { StreamChunk } from '../types'
5
+ import type { AdapterYieldChunk } from './adapter-yield-chunk'
6
+ import { tanstackMetadata } from './merge-metadata'
7
+ import { normalizeStreamChunk } from './normalize-stream-chunk'
8
+ import { isSpecTopLevelKey } from './spec-event-keys'
9
+
10
+ function assertSpec(chunk: { type: string }) {
11
+ for (const key of Object.keys(chunk)) {
12
+ expect(isSpecTopLevelKey(chunk.type, key), key).toBe(true)
13
+ }
14
+ }
15
+
16
+ function normalizeAll(chunk: AdapterYieldChunk) {
17
+ const out = normalizeStreamChunk(chunk)
18
+ for (const specChunk of out) {
19
+ assertSpec(specChunk)
20
+ }
21
+ return out
22
+ }
23
+
24
+ function chunkAt(out: Array<StreamChunk>, index: number) {
25
+ const chunk = out[index]
26
+ if (chunk === undefined) {
27
+ throw new Error(`expected stream chunk at ${String(index)}`)
28
+ }
29
+ return chunk
30
+ }
31
+
32
+ function normalizeOne(chunk: AdapterYieldChunk) {
33
+ const out = normalizeAll(chunk)
34
+ expect(out).toHaveLength(1)
35
+ return chunkAt(out, 0)
36
+ }
37
+
38
+ describe('normalizeStreamChunk', () => {
39
+ it('maps RUN_FINISHED TokenUsage onto spec usage[] and leftover metadata', () => {
40
+ const chunk = {
41
+ type: EventType.RUN_FINISHED,
42
+ threadId: 't1',
43
+ runId: 'r1',
44
+ model: 'gpt-5.5',
45
+ finishReason: 'stop',
46
+ usage: {
47
+ promptTokens: 10,
48
+ completionTokens: 5,
49
+ totalTokens: 15,
50
+ cost: 0.02,
51
+ promptTokensDetails: { cachedTokens: 3, audioTokens: 1 },
52
+ completionTokensDetails: { reasoningTokens: 2 },
53
+ },
54
+ } as AdapterYieldChunk
55
+
56
+ const out = normalizeOne(chunk)
57
+
58
+ expect(out).toEqual({
59
+ type: EventType.RUN_FINISHED,
60
+ threadId: 't1',
61
+ runId: 'r1',
62
+ usage: [
63
+ {
64
+ model: 'gpt-5.5',
65
+ inputTokens: 10,
66
+ outputTokens: 5,
67
+ totalTokens: 15,
68
+ cachedInputTokens: 3,
69
+ reasoningTokens: 2,
70
+ },
71
+ ],
72
+ metadata: {
73
+ tanstack: {
74
+ model: 'gpt-5.5',
75
+ finishReason: 'stop',
76
+ usage: {
77
+ cost: 0.02,
78
+ promptTokensDetails: { audioTokens: 1 },
79
+ },
80
+ },
81
+ },
82
+ })
83
+ expect(out).not.toHaveProperty('model')
84
+ expect(out).not.toHaveProperty('finishReason')
85
+ })
86
+
87
+ it('moves RUN_STARTED model into metadata.tanstack.model only', () => {
88
+ const out = normalizeOne({
89
+ type: EventType.RUN_STARTED,
90
+ threadId: 't1',
91
+ runId: 'r1',
92
+ model: 'gpt-5.5',
93
+ } as AdapterYieldChunk)
94
+
95
+ expect(out).toEqual({
96
+ type: EventType.RUN_STARTED,
97
+ threadId: 't1',
98
+ runId: 'r1',
99
+ metadata: { tanstack: { model: 'gpt-5.5' } },
100
+ })
101
+ expect(out).not.toHaveProperty('model')
102
+ expect(tanstackMetadata(out)).toEqual({ model: 'gpt-5.5' })
103
+ })
104
+
105
+ it('puts TEXT_MESSAGE_START model in metadata and drops TEXT_MESSAGE_CONTENT extras', () => {
106
+ const start = normalizeOne({
107
+ type: EventType.TEXT_MESSAGE_START,
108
+ messageId: 'm1',
109
+ role: 'assistant',
110
+ model: 'gpt-5.5',
111
+ } as AdapterYieldChunk)
112
+
113
+ expect(start).toEqual({
114
+ type: EventType.TEXT_MESSAGE_START,
115
+ messageId: 'm1',
116
+ role: 'assistant',
117
+ metadata: { tanstack: { model: 'gpt-5.5' } },
118
+ })
119
+
120
+ const content = normalizeOne({
121
+ type: EventType.TEXT_MESSAGE_CONTENT,
122
+ messageId: 'm1',
123
+ delta: 'Hi',
124
+ content: 'Hello Hi',
125
+ model: 'gpt-5.5',
126
+ } as AdapterYieldChunk)
127
+
128
+ expect(content).toEqual({
129
+ type: EventType.TEXT_MESSAGE_CONTENT,
130
+ messageId: 'm1',
131
+ delta: 'Hi',
132
+ })
133
+ expect(content).not.toHaveProperty('content')
134
+ expect(content).not.toHaveProperty('model')
135
+ expect(content).not.toHaveProperty('metadata')
136
+ })
137
+
138
+ it('strips RUN_ERROR nested error and moves interruptErrors + ids into metadata.tanstack', () => {
139
+ const interruptError: InterruptSubmissionError = {
140
+ scope: 'batch',
141
+ code: 'server',
142
+ message: 'Interrupt persistence failed.',
143
+ threadId: 't1',
144
+ interruptedRunId: 'r1',
145
+ generation: 0,
146
+ source: 'server',
147
+ retryable: false,
148
+ interruptIds: [],
149
+ }
150
+ const interruptErrors = [interruptError]
151
+
152
+ const out = normalizeOne({
153
+ type: EventType.RUN_ERROR,
154
+ message: 'Interrupt persistence failed.',
155
+ code: 'server',
156
+ error: {
157
+ message: 'Interrupt persistence failed.',
158
+ code: 'server',
159
+ },
160
+ 'tanstack:interruptErrors': interruptErrors,
161
+ threadId: 't1',
162
+ runId: 'r1',
163
+ } as AdapterYieldChunk)
164
+
165
+ expect(out).not.toHaveProperty('error')
166
+ expect(out).not.toHaveProperty('tanstack:interruptErrors')
167
+ expect(out).not.toHaveProperty('threadId')
168
+ expect(out).not.toHaveProperty('runId')
169
+ expect(out).toEqual({
170
+ type: EventType.RUN_ERROR,
171
+ message: 'Interrupt persistence failed.',
172
+ code: 'server',
173
+ metadata: {
174
+ tanstack: {
175
+ interruptErrors,
176
+ threadId: 't1',
177
+ runId: 'r1',
178
+ },
179
+ },
180
+ })
181
+ expect(tanstackMetadata(out)?.interruptErrors).toBe(interruptErrors)
182
+ expect(tanstackMetadata(out)?.interruptErrors?.[0]).toBe(interruptError)
183
+ })
184
+
185
+ it('keeps STATE_SNAPSHOT snapshot and drops the state alias and model', () => {
186
+ const snapshot = { count: 1 }
187
+ const out = normalizeOne({
188
+ type: EventType.STATE_SNAPSHOT,
189
+ snapshot,
190
+ state: { count: 2 },
191
+ model: 'gpt-5.5',
192
+ } as AdapterYieldChunk)
193
+
194
+ expect(out).toMatchObject({
195
+ type: EventType.STATE_SNAPSHOT,
196
+ snapshot,
197
+ })
198
+ expect(out).not.toHaveProperty('state')
199
+ expect(out).not.toHaveProperty('model')
200
+ })
201
+
202
+ it('moves CUSTOM generation:result threadId/runId/model into metadata.tanstack', () => {
203
+ const value = { url: 'https://example.com/img.png' }
204
+ const out = normalizeOne({
205
+ type: EventType.CUSTOM,
206
+ name: 'generation:result',
207
+ value,
208
+ threadId: 't1',
209
+ runId: 'r1',
210
+ model: 'gpt-5.5',
211
+ } as AdapterYieldChunk)
212
+
213
+ expect(out).toEqual({
214
+ type: EventType.CUSTOM,
215
+ name: 'generation:result',
216
+ value,
217
+ metadata: {
218
+ tanstack: {
219
+ model: 'gpt-5.5',
220
+ threadId: 't1',
221
+ runId: 'r1',
222
+ },
223
+ },
224
+ })
225
+ expect(out).not.toHaveProperty('threadId')
226
+ expect(out).not.toHaveProperty('runId')
227
+ expect(out).not.toHaveProperty('model')
228
+ })
229
+
230
+ it('returns an already-spec TEXT_MESSAGE_END chunk unchanged', () => {
231
+ const chunk = {
232
+ type: EventType.TEXT_MESSAGE_END,
233
+ messageId: 'm1',
234
+ timestamp: 1,
235
+ } as AdapterYieldChunk
236
+
237
+ const out = normalizeStreamChunk(chunk)
238
+ expect(out).toHaveLength(1)
239
+ assertSpec(chunkAt(out, 0))
240
+ expect(out[0]).toEqual(chunk)
241
+ expect(out[0]).not.toHaveProperty('metadata')
242
+ })
243
+
244
+ it('keeps TOOL_CALL_START toolCallName and emits REASONING_ENCRYPTED_VALUE for thoughtSignature', () => {
245
+ const out = normalizeAll({
246
+ type: EventType.TOOL_CALL_START,
247
+ toolCallId: 'tc1',
248
+ toolCallName: 'get_weather',
249
+ toolName: 'get_weather',
250
+ index: 0,
251
+ parentMessageId: 'm1',
252
+ metadata: { thoughtSignature: 'sig-1' },
253
+ } as AdapterYieldChunk)
254
+
255
+ expect(out).toHaveLength(2)
256
+ expect(out[0]).toEqual({
257
+ type: EventType.TOOL_CALL_START,
258
+ toolCallId: 'tc1',
259
+ toolCallName: 'get_weather',
260
+ parentMessageId: 'm1',
261
+ metadata: {
262
+ thoughtSignature: 'sig-1',
263
+ tanstack: { index: 0 },
264
+ },
265
+ })
266
+ expect(out[0]).not.toHaveProperty('toolName')
267
+ expect(out[0]).not.toHaveProperty('index')
268
+ expect(out[1]).toEqual({
269
+ type: EventType.REASONING_ENCRYPTED_VALUE,
270
+ subtype: 'tool-call',
271
+ entityId: 'tc1',
272
+ encryptedValue: 'sig-1',
273
+ })
274
+ })
275
+
276
+ it('drops TOOL_CALL_ARGS accumulated args', () => {
277
+ const out = normalizeOne({
278
+ type: EventType.TOOL_CALL_ARGS,
279
+ toolCallId: 'tc1',
280
+ delta: '{"q":',
281
+ args: '{"q":',
282
+ model: 'gpt-5.5',
283
+ } as AdapterYieldChunk)
284
+
285
+ expect(out).toEqual({
286
+ type: EventType.TOOL_CALL_ARGS,
287
+ toolCallId: 'tc1',
288
+ delta: '{"q":',
289
+ })
290
+ expect(out).not.toHaveProperty('args')
291
+ expect(out).not.toHaveProperty('model')
292
+ expect(out).not.toHaveProperty('metadata')
293
+ })
294
+
295
+ it('splits TOOL_CALL_END with result into spec END then RESULT', () => {
296
+ const out = normalizeAll({
297
+ type: EventType.TOOL_CALL_END,
298
+ toolCallId: 'tc1',
299
+ toolCallName: 'get_weather',
300
+ toolName: 'get_weather',
301
+ parentMessageId: 'm1',
302
+ result: '{"temp":72}',
303
+ input: { q: 'sf' },
304
+ output: { temp: 72 },
305
+ } as AdapterYieldChunk)
306
+
307
+ expect(out).toHaveLength(2)
308
+ expect(out[0]).toEqual({
309
+ type: EventType.TOOL_CALL_END,
310
+ toolCallId: 'tc1',
311
+ metadata: {
312
+ tanstack: {
313
+ toolCallName: 'get_weather',
314
+ toolName: 'get_weather',
315
+ parentMessageId: 'm1',
316
+ output: { temp: 72 },
317
+ input: { q: 'sf' },
318
+ },
319
+ },
320
+ })
321
+ expect(out[0]).not.toHaveProperty('input')
322
+ expect(out[1]).toEqual({
323
+ type: EventType.TOOL_CALL_RESULT,
324
+ toolCallId: 'tc1',
325
+ content: '{"temp":72}',
326
+ messageId: 'm1',
327
+ })
328
+ })
329
+
330
+ it('returns one spec TOOL_CALL_END when result is missing', () => {
331
+ const out = normalizeOne({
332
+ type: EventType.TOOL_CALL_END,
333
+ toolCallId: 'tc1',
334
+ toolCallName: 'get_weather',
335
+ } as AdapterYieldChunk)
336
+
337
+ expect(out).toEqual({
338
+ type: EventType.TOOL_CALL_END,
339
+ toolCallId: 'tc1',
340
+ metadata: { tanstack: { toolCallName: 'get_weather' } },
341
+ })
342
+ expect(out).not.toHaveProperty('result')
343
+ expect(out).not.toHaveProperty('toolCallName')
344
+ })
345
+
346
+ it('stringifies an array TOOL_CALL_END result onto RESULT content', () => {
347
+ const parts = [{ type: 'text', content: 'hello' }]
348
+ const out = normalizeAll({
349
+ type: EventType.TOOL_CALL_END,
350
+ toolCallId: 'tc1',
351
+ result: parts,
352
+ } as AdapterYieldChunk)
353
+
354
+ expect(out).toHaveLength(2)
355
+ expect(out[1]).toEqual({
356
+ type: EventType.TOOL_CALL_RESULT,
357
+ toolCallId: 'tc1',
358
+ content: JSON.stringify(parts),
359
+ messageId: 'tc1',
360
+ })
361
+ })
362
+
363
+ it('copies TOOL_CALL_END output-error state onto RESULT metadata.tanstack.state', () => {
364
+ const out = normalizeAll({
365
+ type: EventType.TOOL_CALL_END,
366
+ toolCallId: 'tc1',
367
+ result: 'Error executing tool: boom',
368
+ state: 'output-error',
369
+ } as AdapterYieldChunk)
370
+
371
+ expect(out).toHaveLength(2)
372
+ expect(out[0]).toEqual({
373
+ type: EventType.TOOL_CALL_END,
374
+ toolCallId: 'tc1',
375
+ metadata: { tanstack: { state: 'output-error' } },
376
+ })
377
+ expect(out[1]).toEqual({
378
+ type: EventType.TOOL_CALL_RESULT,
379
+ toolCallId: 'tc1',
380
+ content: 'Error executing tool: boom',
381
+ messageId: 'tc1',
382
+ metadata: { tanstack: { state: 'output-error' } },
383
+ })
384
+ expect(tanstackMetadata(chunkAt(out, 1))).toEqual({
385
+ state: 'output-error',
386
+ })
387
+ })
388
+
389
+ it('strips STEP_FINISHED thinking extras and emits REASONING_ENCRYPTED_VALUE for the signature', () => {
390
+ const out = normalizeAll({
391
+ type: EventType.STEP_FINISHED,
392
+ stepName: 'thinking',
393
+ stepId: 's1',
394
+ delta: 'hmm',
395
+ content: 'hmm full',
396
+ signature: 'sig',
397
+ } as AdapterYieldChunk)
398
+
399
+ expect(out).toHaveLength(2)
400
+ expect(out[0]).toEqual({
401
+ type: EventType.STEP_FINISHED,
402
+ stepName: 'thinking',
403
+ metadata: {
404
+ tanstack: {
405
+ content: 'hmm full',
406
+ delta: 'hmm',
407
+ signature: 'sig',
408
+ stepId: 's1',
409
+ },
410
+ },
411
+ })
412
+ expect(out[0]).not.toHaveProperty('delta')
413
+ expect(out[0]).not.toHaveProperty('content')
414
+ expect(out[0]).not.toHaveProperty('signature')
415
+ expect(out[0]).not.toHaveProperty('stepId')
416
+ expect(out[1]).toEqual({
417
+ type: EventType.REASONING_ENCRYPTED_VALUE,
418
+ subtype: 'message',
419
+ entityId: 's1',
420
+ encryptedValue: 'sig',
421
+ })
422
+ })
423
+ })
@@ -0,0 +1,186 @@
1
+ import { EventType } from '../types'
2
+ import type { StreamChunk, ToolCallResultEvent } from '../types'
3
+ import type { AdapterYieldChunk } from './adapter-yield-chunk'
4
+ import { isTanstackUsage, toSpecTokenUsage } from './ag-ui-usage'
5
+ import type { MetadataRecord } from './merge-metadata'
6
+ import { withTanstackMetadata } from './merge-metadata'
7
+ import { reasoningEncryptedValue } from './reasoning-encrypted-value'
8
+ import { specKeysFor } from './spec-event-keys'
9
+
10
+ function stringField(value: unknown): string | undefined {
11
+ return typeof value === 'string' && value !== '' ? value : undefined
12
+ }
13
+
14
+ function encryptedValueExtras(chunk: AdapterYieldChunk): Array<StreamChunk> {
15
+ const extras: Array<StreamChunk> = []
16
+ const timestamp =
17
+ 'timestamp' in chunk && typeof chunk.timestamp === 'number'
18
+ ? chunk.timestamp
19
+ : undefined
20
+
21
+ if (typeof chunk.signature === 'string' && chunk.signature !== '') {
22
+ const source = chunk as Record<string, unknown>
23
+ const toolCallId = stringField(source.toolCallId)
24
+ const entityId =
25
+ stringField(chunk.stepId) ??
26
+ stringField(source.stepName) ??
27
+ stringField(source.messageId) ??
28
+ toolCallId
29
+ if (entityId !== undefined) {
30
+ extras.push(
31
+ reasoningEncryptedValue({
32
+ subtype: toolCallId && !chunk.stepId ? 'tool-call' : 'message',
33
+ entityId,
34
+ encryptedValue: chunk.signature,
35
+ timestamp,
36
+ }),
37
+ )
38
+ }
39
+ }
40
+
41
+ if (chunk.type === EventType.TOOL_CALL_START) {
42
+ const thoughtSignature = stringField(
43
+ (chunk.metadata as { thoughtSignature?: unknown } | undefined)
44
+ ?.thoughtSignature,
45
+ )
46
+ if (thoughtSignature !== undefined && chunk.toolCallId) {
47
+ extras.push(
48
+ reasoningEncryptedValue({
49
+ subtype: 'tool-call',
50
+ entityId: chunk.toolCallId,
51
+ encryptedValue: thoughtSignature,
52
+ timestamp,
53
+ }),
54
+ )
55
+ }
56
+ }
57
+
58
+ return extras
59
+ }
60
+
61
+ export function normalizeStreamChunk(
62
+ chunk: AdapterYieldChunk,
63
+ ): Array<StreamChunk> {
64
+ const specKeys = specKeysFor(chunk.type)
65
+ const source = chunk as Record<string, unknown>
66
+ const specChunk: Record<string, unknown> & {
67
+ metadata?: MetadataRecord | null
68
+ } = {}
69
+
70
+ for (const key of Object.keys(chunk)) {
71
+ if (specKeys.has(key)) {
72
+ specChunk[key] = source[key]
73
+ }
74
+ }
75
+
76
+ if (chunk.type === EventType.TOOL_CALL_START) {
77
+ if (specChunk.toolCallName === undefined && chunk.toolName) {
78
+ specChunk.toolCallName = chunk.toolName
79
+ }
80
+ }
81
+
82
+ if (chunk.type === EventType.RUN_ERROR && chunk.error != null) {
83
+ if (specChunk.message === undefined && chunk.error.message) {
84
+ specChunk.message = chunk.error.message
85
+ }
86
+ if (specChunk.code === undefined && chunk.error.code !== undefined) {
87
+ specChunk.code = chunk.error.code
88
+ }
89
+ }
90
+
91
+ const tanstack: MetadataRecord = {}
92
+
93
+ if (
94
+ chunk.model !== undefined &&
95
+ chunk.type !== EventType.TEXT_MESSAGE_CONTENT &&
96
+ chunk.type !== EventType.TOOL_CALL_ARGS
97
+ ) {
98
+ tanstack.model = chunk.model
99
+ }
100
+
101
+ if (chunk.finishReason !== undefined) {
102
+ tanstack.finishReason = chunk.finishReason
103
+ }
104
+
105
+ const interruptErrors = chunk['tanstack:interruptErrors']
106
+ if (interruptErrors !== undefined) {
107
+ tanstack.interruptErrors = interruptErrors
108
+ }
109
+
110
+ if (chunk.type === EventType.CUSTOM || chunk.type === EventType.RUN_ERROR) {
111
+ if (chunk.threadId !== undefined) {
112
+ tanstack.threadId = chunk.threadId
113
+ }
114
+ if (chunk.runId !== undefined) {
115
+ tanstack.runId = chunk.runId
116
+ }
117
+ }
118
+
119
+ const skipLeftover = new Set(['result', 'error', 'tanstack:interruptErrors'])
120
+ if (
121
+ chunk.type === EventType.TEXT_MESSAGE_CONTENT ||
122
+ chunk.type === EventType.TOOL_CALL_ARGS
123
+ ) {
124
+ skipLeftover.add('model')
125
+ skipLeftover.add('content')
126
+ skipLeftover.add('args')
127
+ }
128
+ if (chunk.type === EventType.TOOL_CALL_START) {
129
+ skipLeftover.add('toolName')
130
+ }
131
+ for (const key of Object.keys(chunk)) {
132
+ if (specKeys.has(key) || skipLeftover.has(key)) continue
133
+ if (tanstack[key] !== undefined) continue
134
+ const value = source[key]
135
+ if (value !== undefined) {
136
+ tanstack[key] = value
137
+ }
138
+ }
139
+
140
+ if (
141
+ (chunk.type === EventType.RUN_FINISHED ||
142
+ chunk.type === EventType.RUN_ERROR) &&
143
+ isTanstackUsage(specChunk.usage)
144
+ ) {
145
+ const { usage, leftover } = toSpecTokenUsage(specChunk.usage, {
146
+ model: typeof chunk.model === 'string' ? chunk.model : undefined,
147
+ })
148
+ specChunk.usage = usage
149
+ if (leftover !== undefined) {
150
+ tanstack.usage = leftover
151
+ }
152
+ }
153
+
154
+ const normalized =
155
+ Object.keys(tanstack).length === 0
156
+ ? specChunk
157
+ : withTanstackMetadata(specChunk, tanstack)
158
+
159
+ const extras = encryptedValueExtras(chunk)
160
+ const main: Array<StreamChunk> = [normalized as StreamChunk]
161
+
162
+ if (chunk.type === EventType.TOOL_CALL_END && chunk.result !== undefined) {
163
+ const parentMessageId = source.parentMessageId
164
+ const resultChunk: ToolCallResultEvent = {
165
+ type: EventType.TOOL_CALL_RESULT,
166
+ toolCallId: chunk.toolCallId,
167
+ content: Array.isArray(chunk.result)
168
+ ? JSON.stringify(chunk.result)
169
+ : chunk.result,
170
+ messageId:
171
+ typeof parentMessageId === 'string' && parentMessageId !== ''
172
+ ? parentMessageId
173
+ : chunk.toolCallId,
174
+ }
175
+ if (chunk.state === 'output-error') {
176
+ main.push({
177
+ ...resultChunk,
178
+ metadata: { tanstack: { state: chunk.state } },
179
+ })
180
+ } else {
181
+ main.push(resultChunk)
182
+ }
183
+ }
184
+
185
+ return [...main, ...extras]
186
+ }
@@ -0,0 +1,18 @@
1
+ import { EventType } from '../types'
2
+ import type { ReasoningEncryptedValueEvent } from '../types'
3
+
4
+ /** Spec event that carries a provider thinking / tool-call signature blob. */
5
+ export function reasoningEncryptedValue(opts: {
6
+ subtype: 'message' | 'tool-call'
7
+ entityId: string
8
+ encryptedValue: string
9
+ timestamp?: number
10
+ }): ReasoningEncryptedValueEvent {
11
+ return {
12
+ type: EventType.REASONING_ENCRYPTED_VALUE,
13
+ subtype: opts.subtype,
14
+ entityId: opts.entityId,
15
+ encryptedValue: opts.encryptedValue,
16
+ ...(opts.timestamp !== undefined ? { timestamp: opts.timestamp } : {}),
17
+ }
18
+ }