@ably/ai-transport 0.0.1

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 (118) hide show
  1. package/LICENSE +176 -0
  2. package/README.md +426 -0
  3. package/dist/ably-ai-transport.js +1388 -0
  4. package/dist/ably-ai-transport.js.map +1 -0
  5. package/dist/ably-ai-transport.umd.cjs +2 -0
  6. package/dist/ably-ai-transport.umd.cjs.map +1 -0
  7. package/dist/constants.d.ts +50 -0
  8. package/dist/core/codec/decoder.d.ts +62 -0
  9. package/dist/core/codec/encoder.d.ts +56 -0
  10. package/dist/core/codec/index.d.ts +8 -0
  11. package/dist/core/codec/lifecycle-tracker.d.ts +74 -0
  12. package/dist/core/codec/types.d.ts +188 -0
  13. package/dist/core/transport/client-transport.d.ts +10 -0
  14. package/dist/core/transport/conversation-tree.d.ts +9 -0
  15. package/dist/core/transport/decode-history.d.ts +41 -0
  16. package/dist/core/transport/headers.d.ts +26 -0
  17. package/dist/core/transport/index.d.ts +4 -0
  18. package/dist/core/transport/pipe-stream.d.ts +16 -0
  19. package/dist/core/transport/server-transport.d.ts +7 -0
  20. package/dist/core/transport/stream-router.d.ts +19 -0
  21. package/dist/core/transport/turn-manager.d.ts +34 -0
  22. package/dist/core/transport/types.d.ts +407 -0
  23. package/dist/errors.d.ts +46 -0
  24. package/dist/event-emitter.d.ts +65 -0
  25. package/dist/index.d.ts +11 -0
  26. package/dist/logger.d.ts +103 -0
  27. package/dist/react/ably-ai-transport-react.js +823 -0
  28. package/dist/react/ably-ai-transport-react.js.map +1 -0
  29. package/dist/react/ably-ai-transport-react.umd.cjs +2 -0
  30. package/dist/react/ably-ai-transport-react.umd.cjs.map +1 -0
  31. package/dist/react/index.d.ts +11 -0
  32. package/dist/react/use-ably-messages.d.ts +18 -0
  33. package/dist/react/use-active-turns.d.ts +8 -0
  34. package/dist/react/use-client-transport.d.ts +7 -0
  35. package/dist/react/use-conversation-tree.d.ts +20 -0
  36. package/dist/react/use-edit.d.ts +7 -0
  37. package/dist/react/use-history.d.ts +19 -0
  38. package/dist/react/use-messages.d.ts +7 -0
  39. package/dist/react/use-regenerate.d.ts +7 -0
  40. package/dist/react/use-send.d.ts +7 -0
  41. package/dist/utils.d.ts +127 -0
  42. package/dist/vercel/ably-ai-transport-vercel.js +2331 -0
  43. package/dist/vercel/ably-ai-transport-vercel.js.map +1 -0
  44. package/dist/vercel/ably-ai-transport-vercel.umd.cjs +2 -0
  45. package/dist/vercel/ably-ai-transport-vercel.umd.cjs.map +1 -0
  46. package/dist/vercel/codec/accumulator.d.ts +21 -0
  47. package/dist/vercel/codec/decoder.d.ts +22 -0
  48. package/dist/vercel/codec/encoder.d.ts +41 -0
  49. package/dist/vercel/codec/index.d.ts +22 -0
  50. package/dist/vercel/index.d.ts +3 -0
  51. package/dist/vercel/react/ably-ai-transport-vercel-react.js +2082 -0
  52. package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -0
  53. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +2 -0
  54. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -0
  55. package/dist/vercel/react/index.d.ts +3 -0
  56. package/dist/vercel/react/use-chat-transport.d.ts +29 -0
  57. package/dist/vercel/react/use-message-sync.d.ts +19 -0
  58. package/dist/vercel/transport/chat-transport.d.ts +118 -0
  59. package/dist/vercel/transport/index.d.ts +36 -0
  60. package/package.json +123 -0
  61. package/react/README.md +3 -0
  62. package/react/index.d.ts +1 -0
  63. package/react/index.js +1 -0
  64. package/react/index.umd.cjs +1 -0
  65. package/src/constants.ts +98 -0
  66. package/src/core/codec/decoder.ts +402 -0
  67. package/src/core/codec/encoder.ts +470 -0
  68. package/src/core/codec/index.ts +28 -0
  69. package/src/core/codec/lifecycle-tracker.ts +140 -0
  70. package/src/core/codec/types.ts +249 -0
  71. package/src/core/transport/client-transport.ts +959 -0
  72. package/src/core/transport/conversation-tree.ts +434 -0
  73. package/src/core/transport/decode-history.ts +337 -0
  74. package/src/core/transport/headers.ts +46 -0
  75. package/src/core/transport/index.ts +34 -0
  76. package/src/core/transport/pipe-stream.ts +95 -0
  77. package/src/core/transport/server-transport.ts +458 -0
  78. package/src/core/transport/stream-router.ts +118 -0
  79. package/src/core/transport/turn-manager.ts +147 -0
  80. package/src/core/transport/types.ts +533 -0
  81. package/src/errors.ts +58 -0
  82. package/src/event-emitter.ts +103 -0
  83. package/src/index.ts +89 -0
  84. package/src/logger.ts +241 -0
  85. package/src/react/index.ts +11 -0
  86. package/src/react/use-ably-messages.ts +37 -0
  87. package/src/react/use-active-turns.ts +61 -0
  88. package/src/react/use-client-transport.ts +37 -0
  89. package/src/react/use-conversation-tree.ts +71 -0
  90. package/src/react/use-edit.ts +24 -0
  91. package/src/react/use-history.ts +111 -0
  92. package/src/react/use-messages.ts +32 -0
  93. package/src/react/use-regenerate.ts +24 -0
  94. package/src/react/use-send.ts +25 -0
  95. package/src/react/vite.config.ts +32 -0
  96. package/src/tsconfig.json +25 -0
  97. package/src/utils.ts +230 -0
  98. package/src/vercel/codec/accumulator.ts +603 -0
  99. package/src/vercel/codec/decoder.ts +615 -0
  100. package/src/vercel/codec/encoder.ts +396 -0
  101. package/src/vercel/codec/index.ts +37 -0
  102. package/src/vercel/index.ts +12 -0
  103. package/src/vercel/react/index.ts +4 -0
  104. package/src/vercel/react/use-chat-transport.ts +60 -0
  105. package/src/vercel/react/use-message-sync.ts +34 -0
  106. package/src/vercel/react/vite.config.ts +33 -0
  107. package/src/vercel/transport/chat-transport.ts +278 -0
  108. package/src/vercel/transport/index.ts +56 -0
  109. package/src/vercel/vite.config.ts +33 -0
  110. package/src/vite.config.ts +31 -0
  111. package/vercel/README.md +3 -0
  112. package/vercel/index.d.ts +1 -0
  113. package/vercel/index.js +1 -0
  114. package/vercel/index.umd.cjs +1 -0
  115. package/vercel/react/README.md +3 -0
  116. package/vercel/react/index.d.ts +1 -0
  117. package/vercel/react/index.js +1 -0
  118. package/vercel/react/index.umd.cjs +1 -0
@@ -0,0 +1,603 @@
1
+ /**
2
+ * Vercel AI SDK Message Accumulator
3
+ *
4
+ * Builds and maintains a UIMessage[] list from decoder outputs.
5
+ * Implements MessageAccumulator<UIMessageChunk, UIMessage>.
6
+ *
7
+ * The accumulator consumes DecoderOutput[] from the decoder and groups
8
+ * streaming events into UIMessage objects using lifecycle boundaries
9
+ * (start/finish). Complete messages (from writeMessages) are inserted
10
+ * directly.
11
+ *
12
+ * Multiple messages can be in-progress concurrently — each is identified
13
+ * by the `messageId` field on DecoderOutput (read from x-ably-msg-id).
14
+ */
15
+
16
+ import type * as AI from 'ai';
17
+
18
+ import type { DecoderOutput, MessageAccumulator } from '../../core/codec/types.js';
19
+ import { stripUndefined } from '../../utils.js';
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Internal types
23
+ // ---------------------------------------------------------------------------
24
+
25
+ /** Status of a streamed message (text, reasoning, or tool-input). */
26
+ type StreamStatus = 'streaming' | 'finished' | 'aborted';
27
+
28
+ /**
29
+ * Tracks an in-progress tool part's position and accumulated streaming input.
30
+ * Text and reasoning parts don't need this — we write directly to the part.
31
+ * Tool parts need the extra `inputText` buffer because deltas arrive as raw
32
+ * JSON fragments that must be accumulated before parsing.
33
+ */
34
+ interface ToolPartTracker {
35
+ /** Index in the message's parts array. */
36
+ partIndex: number;
37
+ /** Accumulated streaming input text (for JSON parsing on completion). */
38
+ inputText: string;
39
+ }
40
+
41
+ /** Fields shared by all DynamicToolUIPart state variants. */
42
+ interface ToolBaseFields {
43
+ type: 'dynamic-tool';
44
+ toolName: string;
45
+ toolCallId: string;
46
+ title?: string;
47
+ providerExecuted?: boolean;
48
+ }
49
+
50
+ /** Bundled per-message state for an in-progress message. */
51
+ interface ActiveMessageState {
52
+ message: AI.UIMessage;
53
+ textStreams: DeltaStreamTracker;
54
+ reasoningStreams: DeltaStreamTracker;
55
+ toolTrackers: Record<string, ToolPartTracker>;
56
+ streamStatus: Map<string, StreamStatus>;
57
+ }
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // Tool base helper
61
+ // ---------------------------------------------------------------------------
62
+
63
+ /**
64
+ * Extract the state-independent base fields for a DynamicToolUIPart.
65
+ * Works with both chunks (tool-input-start, etc.) and existing parts.
66
+ * @param source - Any object containing the required tool identity fields.
67
+ * @param source.toolCallId - The tool call identifier.
68
+ * @param source.toolName - The tool name.
69
+ * @param source.title - Optional display title.
70
+ * @param source.providerExecuted - Whether the provider executed the tool.
71
+ * @returns Base fields shared across all DynamicToolUIPart state variants.
72
+ */
73
+ const toolBase = (source: {
74
+ toolCallId: string;
75
+ toolName: string;
76
+ title?: string;
77
+ providerExecuted?: boolean;
78
+ }): ToolBaseFields =>
79
+ stripUndefined({
80
+ type: 'dynamic-tool' as const,
81
+ toolCallId: source.toolCallId,
82
+ toolName: source.toolName,
83
+ title: source.title,
84
+ providerExecuted: source.providerExecuted,
85
+ });
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // DeltaStreamTracker — manages text or reasoning stream accumulation
89
+ // ---------------------------------------------------------------------------
90
+
91
+ /**
92
+ * Tracks in-progress text or reasoning streams within a single message.
93
+ * Owns the mapping from stream ID to part index, enforcing the pairing
94
+ * of part type and index map by construction.
95
+ */
96
+ class DeltaStreamTracker {
97
+ private readonly _partType: 'text' | 'reasoning';
98
+ private _activeIndex = new Map<string, number>();
99
+
100
+ constructor(partType: 'text' | 'reasoning') {
101
+ this._partType = partType;
102
+ }
103
+
104
+ start(id: string, msg: AI.UIMessage, streamStatus: Map<string, StreamStatus>): void {
105
+ this._activeIndex.set(id, msg.parts.length);
106
+ msg.parts.push({ type: this._partType, text: '' });
107
+ streamStatus.set(id, 'streaming');
108
+ }
109
+
110
+ delta(id: string, msg: AI.UIMessage, text: string): void {
111
+ const idx = this._activeIndex.get(id);
112
+ if (idx === undefined) return;
113
+ const part = msg.parts[idx];
114
+ if (part?.type === this._partType) {
115
+ part.text += text;
116
+ }
117
+ }
118
+
119
+ end(id: string, streamStatus: Map<string, StreamStatus>): void {
120
+ streamStatus.set(id, 'finished');
121
+ this._activeIndex.delete(id);
122
+ }
123
+
124
+ reset(): void {
125
+ this._activeIndex = new Map();
126
+ }
127
+ }
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // Default implementation
131
+ // ---------------------------------------------------------------------------
132
+
133
+ class DefaultUIMessageAccumulator implements MessageAccumulator<AI.UIMessageChunk, AI.UIMessage> {
134
+ private readonly _messageList: AI.UIMessage[] = [];
135
+ private readonly _activeMessages = new Map<string, ActiveMessageState>();
136
+
137
+ get messages(): AI.UIMessage[] {
138
+ return this._messageList;
139
+ }
140
+
141
+ get completedMessages(): AI.UIMessage[] {
142
+ const activeSet = new Set<AI.UIMessage>();
143
+ for (const state of this._activeMessages.values()) {
144
+ activeSet.add(state.message);
145
+ }
146
+ return this._messageList.filter((msg) => !activeSet.has(msg));
147
+ }
148
+
149
+ get hasActiveStream(): boolean {
150
+ for (const state of this._activeMessages.values()) {
151
+ for (const status of state.streamStatus.values()) {
152
+ if (status === 'streaming') return true;
153
+ }
154
+ }
155
+ return false;
156
+ }
157
+
158
+ processOutputs(outputs: DecoderOutput<AI.UIMessageChunk, AI.UIMessage>[]): void {
159
+ for (const output of outputs) {
160
+ if (output.kind === 'message') {
161
+ this._messageList.push(output.message);
162
+ } else if (output.messageId !== undefined) {
163
+ this._processEvent(output.event, output.messageId);
164
+ }
165
+ }
166
+ }
167
+
168
+ updateMessage(message: AI.UIMessage): void {
169
+ const idx = this._messageList.findIndex((m) => m.id === message.id);
170
+ if (idx !== -1) {
171
+ this._messageList[idx] = message;
172
+ }
173
+ }
174
+
175
+ // -------------------------------------------------------------------------
176
+ // Shared helpers
177
+ // -------------------------------------------------------------------------
178
+
179
+ private _ensureActiveMessage(messageId: string): ActiveMessageState {
180
+ const existing = this._activeMessages.get(messageId);
181
+ if (existing) return existing;
182
+
183
+ const state: ActiveMessageState = {
184
+ message: { id: messageId, role: 'assistant', parts: [] },
185
+ textStreams: new DeltaStreamTracker('text'),
186
+ reasoningStreams: new DeltaStreamTracker('reasoning'),
187
+ toolTrackers: {},
188
+ streamStatus: new Map(),
189
+ };
190
+ this._activeMessages.set(messageId, state);
191
+ this._messageList.push(state.message);
192
+ return state;
193
+ }
194
+
195
+ /**
196
+ * Look up a tracked tool part by toolCallId within a message state.
197
+ * @param toolCallId - The tool call identifier to look up.
198
+ * @param state - The active message state to search in.
199
+ * @returns The tracker and current part, or undefined if not found.
200
+ */
201
+ private _getToolPart(
202
+ toolCallId: string,
203
+ state: ActiveMessageState,
204
+ ): { tracker: ToolPartTracker; part: AI.DynamicToolUIPart } | undefined {
205
+ const tracker = state.toolTrackers[toolCallId];
206
+ if (!tracker) return undefined;
207
+
208
+ const existing = state.message.parts[tracker.partIndex];
209
+ if (existing?.type !== 'dynamic-tool') return undefined;
210
+
211
+ return { tracker, part: existing };
212
+ }
213
+
214
+ // -------------------------------------------------------------------------
215
+ // Event dispatch
216
+ // -------------------------------------------------------------------------
217
+
218
+ private _processEvent(chunk: AI.UIMessageChunk, messageId: string): void {
219
+ switch (chunk.type) {
220
+ case 'start':
221
+ case 'start-step':
222
+ case 'finish-step':
223
+ case 'finish':
224
+ case 'abort':
225
+ case 'error':
226
+ case 'message-metadata': {
227
+ this._processLifecycle(chunk, messageId);
228
+ break;
229
+ }
230
+
231
+ case 'text-start':
232
+ case 'text-delta':
233
+ case 'text-end':
234
+ case 'reasoning-start':
235
+ case 'reasoning-delta':
236
+ case 'reasoning-end': {
237
+ this._processTextOrReasoning(chunk, messageId);
238
+ break;
239
+ }
240
+
241
+ case 'tool-input-start':
242
+ case 'tool-input-delta':
243
+ case 'tool-input-available':
244
+ case 'tool-input-error': {
245
+ this._processToolInput(chunk, messageId);
246
+ break;
247
+ }
248
+
249
+ case 'tool-output-available':
250
+ case 'tool-output-error':
251
+ case 'tool-output-denied':
252
+ case 'tool-approval-request': {
253
+ this._processToolOutput(chunk, messageId);
254
+ break;
255
+ }
256
+
257
+ case 'file':
258
+ case 'source-url':
259
+ case 'source-document': {
260
+ this._processContentPart(chunk, messageId);
261
+ break;
262
+ }
263
+
264
+ default: {
265
+ if (chunk.type.startsWith('data-')) {
266
+ if (chunk.transient) break;
267
+
268
+ const state = this._ensureActiveMessage(messageId);
269
+
270
+ // CAST: chunk.type is `data-${string}` which satisfies DataUIPart,
271
+ // but TypeScript cannot verify the template literal matches a
272
+ // specific UIMessagePart variant at the type level.
273
+ const dataPart = stripUndefined({
274
+ type: chunk.type,
275
+ id: chunk.id,
276
+ data: chunk.data,
277
+ }) as AI.UIMessage['parts'][number];
278
+
279
+ if (chunk.id !== undefined) {
280
+ const idx = state.message.parts.findIndex((p) => p.type === chunk.type && 'id' in p && p.id === chunk.id);
281
+ if (idx !== -1) {
282
+ state.message.parts[idx] = dataPart;
283
+ break;
284
+ }
285
+ }
286
+
287
+ state.message.parts.push(dataPart);
288
+ }
289
+ break;
290
+ }
291
+ }
292
+ }
293
+
294
+ // -------------------------------------------------------------------------
295
+ // Lifecycle events
296
+ // -------------------------------------------------------------------------
297
+
298
+ private _processLifecycle(
299
+ chunk: Extract<
300
+ AI.UIMessageChunk,
301
+ { type: 'start' | 'start-step' | 'finish-step' | 'finish' | 'abort' | 'error' | 'message-metadata' }
302
+ >,
303
+ messageId: string,
304
+ ): void {
305
+ switch (chunk.type) {
306
+ case 'start': {
307
+ const state = this._ensureActiveMessage(messageId);
308
+ if (chunk.messageId) state.message.id = chunk.messageId;
309
+ if (chunk.messageMetadata !== undefined) {
310
+ state.message.metadata = chunk.messageMetadata;
311
+ }
312
+ break;
313
+ }
314
+
315
+ case 'start-step': {
316
+ const state = this._ensureActiveMessage(messageId);
317
+ state.message.parts.push({ type: 'step-start' });
318
+ break;
319
+ }
320
+
321
+ case 'finish-step': {
322
+ const state = this._activeMessages.get(messageId);
323
+ if (state) {
324
+ state.textStreams.reset();
325
+ state.reasoningStreams.reset();
326
+ }
327
+ break;
328
+ }
329
+
330
+ case 'finish': {
331
+ const state = this._activeMessages.get(messageId);
332
+ if (state && chunk.messageMetadata !== undefined) {
333
+ state.message.metadata = chunk.messageMetadata;
334
+ }
335
+ this._activeMessages.delete(messageId);
336
+ break;
337
+ }
338
+
339
+ case 'abort': {
340
+ const state = this._activeMessages.get(messageId);
341
+ if (state) {
342
+ for (const [id, status] of state.streamStatus) {
343
+ if (status === 'streaming') {
344
+ state.streamStatus.set(id, 'aborted');
345
+ }
346
+ }
347
+ }
348
+ this._activeMessages.delete(messageId);
349
+ break;
350
+ }
351
+
352
+ case 'error': {
353
+ break;
354
+ }
355
+
356
+ case 'message-metadata': {
357
+ const state = this._activeMessages.get(messageId);
358
+ if (state && chunk.messageMetadata !== undefined) {
359
+ state.message.metadata = chunk.messageMetadata;
360
+ }
361
+ break;
362
+ }
363
+ }
364
+ }
365
+
366
+ // -------------------------------------------------------------------------
367
+ // Text and reasoning streaming
368
+ // -------------------------------------------------------------------------
369
+
370
+ private _processTextOrReasoning(
371
+ chunk: Extract<
372
+ AI.UIMessageChunk,
373
+ { type: 'text-start' | 'text-delta' | 'text-end' | 'reasoning-start' | 'reasoning-delta' | 'reasoning-end' }
374
+ >,
375
+ messageId: string,
376
+ ): void {
377
+ const state = this._ensureActiveMessage(messageId);
378
+
379
+ switch (chunk.type) {
380
+ case 'text-start': {
381
+ state.textStreams.start(chunk.id, state.message, state.streamStatus);
382
+ break;
383
+ }
384
+ case 'text-delta': {
385
+ state.textStreams.delta(chunk.id, state.message, chunk.delta);
386
+ break;
387
+ }
388
+ case 'text-end': {
389
+ state.textStreams.end(chunk.id, state.streamStatus);
390
+ break;
391
+ }
392
+ case 'reasoning-start': {
393
+ state.reasoningStreams.start(chunk.id, state.message, state.streamStatus);
394
+ break;
395
+ }
396
+ case 'reasoning-delta': {
397
+ state.reasoningStreams.delta(chunk.id, state.message, chunk.delta);
398
+ break;
399
+ }
400
+ case 'reasoning-end': {
401
+ state.reasoningStreams.end(chunk.id, state.streamStatus);
402
+ break;
403
+ }
404
+ }
405
+ }
406
+
407
+ // -------------------------------------------------------------------------
408
+ // Tool input streaming
409
+ // -------------------------------------------------------------------------
410
+
411
+ private _processToolInput(
412
+ chunk: Extract<
413
+ AI.UIMessageChunk,
414
+ { type: 'tool-input-start' | 'tool-input-delta' | 'tool-input-available' | 'tool-input-error' }
415
+ >,
416
+ messageId: string,
417
+ ): void {
418
+ switch (chunk.type) {
419
+ case 'tool-input-start': {
420
+ const state = this._ensureActiveMessage(messageId);
421
+ const partIndex = state.message.parts.length;
422
+ state.message.parts.push({ ...toolBase(chunk), state: 'input-streaming', input: undefined });
423
+ state.toolTrackers[chunk.toolCallId] = { partIndex, inputText: '' };
424
+ state.streamStatus.set(chunk.toolCallId, 'streaming');
425
+ break;
426
+ }
427
+
428
+ case 'tool-input-delta': {
429
+ const state = this._ensureActiveMessage(messageId);
430
+ const tracker = state.toolTrackers[chunk.toolCallId];
431
+ if (!tracker) break;
432
+ tracker.inputText += chunk.inputTextDelta;
433
+
434
+ let parsedInput: unknown;
435
+ try {
436
+ // CAST: JSON.parse returns any; unknown is the safe trust-boundary type.
437
+ parsedInput = JSON.parse(tracker.inputText) as unknown;
438
+ } catch {
439
+ parsedInput = undefined;
440
+ }
441
+
442
+ const found = this._getToolPart(chunk.toolCallId, state);
443
+ if (!found) break;
444
+ state.message.parts[found.tracker.partIndex] = {
445
+ ...toolBase(found.part),
446
+ state: 'input-streaming',
447
+ input: parsedInput,
448
+ };
449
+ break;
450
+ }
451
+
452
+ case 'tool-input-available': {
453
+ const state = this._ensureActiveMessage(messageId);
454
+ const found = this._getToolPart(chunk.toolCallId, state);
455
+ if (!found) break;
456
+ state.message.parts[found.tracker.partIndex] = {
457
+ ...toolBase(found.part),
458
+ state: 'input-available',
459
+ input: chunk.input,
460
+ };
461
+ state.streamStatus.set(chunk.toolCallId, 'finished');
462
+ break;
463
+ }
464
+
465
+ case 'tool-input-error': {
466
+ const state = this._ensureActiveMessage(messageId);
467
+ const found = this._getToolPart(chunk.toolCallId, state);
468
+ if (found) {
469
+ state.message.parts[found.tracker.partIndex] = {
470
+ ...toolBase(found.part),
471
+ state: 'output-error',
472
+ input: chunk.input,
473
+ errorText: chunk.errorText,
474
+ };
475
+ } else {
476
+ const partIndex = state.message.parts.length;
477
+ state.message.parts.push({
478
+ ...toolBase(chunk),
479
+ state: 'output-error',
480
+ input: chunk.input,
481
+ errorText: chunk.errorText,
482
+ });
483
+ state.toolTrackers[chunk.toolCallId] = { partIndex, inputText: '' };
484
+ }
485
+ state.streamStatus.set(chunk.toolCallId, 'finished');
486
+ break;
487
+ }
488
+ }
489
+ }
490
+
491
+ // -------------------------------------------------------------------------
492
+ // Tool output transitions
493
+ // -------------------------------------------------------------------------
494
+
495
+ private _processToolOutput(
496
+ chunk: Extract<
497
+ AI.UIMessageChunk,
498
+ { type: 'tool-output-available' | 'tool-output-error' | 'tool-output-denied' | 'tool-approval-request' }
499
+ >,
500
+ messageId: string,
501
+ ): void {
502
+ const state = this._ensureActiveMessage(messageId);
503
+ const found = this._getToolPart(chunk.toolCallId, state);
504
+ if (!found) return;
505
+
506
+ switch (chunk.type) {
507
+ case 'tool-output-available': {
508
+ state.message.parts[found.tracker.partIndex] = stripUndefined({
509
+ ...toolBase(found.part),
510
+ state: 'output-available' as const,
511
+ input: found.part.input,
512
+ output: chunk.output,
513
+ preliminary: chunk.preliminary,
514
+ });
515
+ break;
516
+ }
517
+
518
+ case 'tool-output-error': {
519
+ state.message.parts[found.tracker.partIndex] = {
520
+ ...toolBase(found.part),
521
+ state: 'output-error',
522
+ input: found.part.input,
523
+ errorText: chunk.errorText,
524
+ };
525
+ break;
526
+ }
527
+
528
+ case 'tool-output-denied': {
529
+ state.message.parts[found.tracker.partIndex] = {
530
+ ...toolBase(found.part),
531
+ state: 'output-denied',
532
+ input: found.part.input,
533
+ approval: { id: '', approved: false },
534
+ };
535
+ break;
536
+ }
537
+
538
+ case 'tool-approval-request': {
539
+ state.message.parts[found.tracker.partIndex] = {
540
+ ...toolBase(found.part),
541
+ state: 'approval-requested',
542
+ input: found.part.input,
543
+ approval: { id: chunk.approvalId },
544
+ };
545
+ break;
546
+ }
547
+ }
548
+ }
549
+
550
+ // -------------------------------------------------------------------------
551
+ // Content parts
552
+ // -------------------------------------------------------------------------
553
+
554
+ private _processContentPart(
555
+ chunk: Extract<AI.UIMessageChunk, { type: 'file' | 'source-url' | 'source-document' }>,
556
+ messageId: string,
557
+ ): void {
558
+ const state = this._ensureActiveMessage(messageId);
559
+
560
+ switch (chunk.type) {
561
+ case 'file': {
562
+ state.message.parts.push({ type: 'file', mediaType: chunk.mediaType, url: chunk.url });
563
+ break;
564
+ }
565
+
566
+ case 'source-url': {
567
+ state.message.parts.push(
568
+ stripUndefined({
569
+ type: 'source-url' as const,
570
+ sourceId: chunk.sourceId,
571
+ url: chunk.url,
572
+ title: chunk.title,
573
+ }),
574
+ );
575
+ break;
576
+ }
577
+
578
+ case 'source-document': {
579
+ state.message.parts.push(
580
+ stripUndefined({
581
+ type: 'source-document' as const,
582
+ sourceId: chunk.sourceId,
583
+ mediaType: chunk.mediaType,
584
+ title: chunk.title,
585
+ filename: chunk.filename,
586
+ }),
587
+ );
588
+ break;
589
+ }
590
+ }
591
+ }
592
+ }
593
+
594
+ // ---------------------------------------------------------------------------
595
+ // Factory
596
+ // ---------------------------------------------------------------------------
597
+
598
+ /**
599
+ * Create a Vercel AI SDK accumulator that builds UIMessage[] from decoder outputs.
600
+ * @returns A {@link MessageAccumulator} for UIMessageChunk/UIMessage.
601
+ */
602
+ export const createAccumulator = (): MessageAccumulator<AI.UIMessageChunk, AI.UIMessage> =>
603
+ new DefaultUIMessageAccumulator();