@ably/ai-transport 0.1.0 → 0.2.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 (163) hide show
  1. package/README.md +91 -100
  2. package/dist/ably-ai-transport.js +1553 -1238
  3. package/dist/ably-ai-transport.js.map +1 -1
  4. package/dist/ably-ai-transport.umd.cjs +1 -1
  5. package/dist/ably-ai-transport.umd.cjs.map +1 -1
  6. package/dist/constants.d.ts +116 -42
  7. package/dist/core/agent.d.ts +29 -0
  8. package/dist/core/codec/decoder.d.ts +20 -23
  9. package/dist/core/codec/encoder.d.ts +11 -8
  10. package/dist/core/codec/index.d.ts +1 -2
  11. package/dist/core/codec/lifecycle-tracker.d.ts +10 -9
  12. package/dist/core/codec/types.d.ts +407 -115
  13. package/dist/core/transport/agent-session.d.ts +10 -0
  14. package/dist/core/transport/branch-chain.d.ts +43 -0
  15. package/dist/core/transport/client-session.d.ts +13 -0
  16. package/dist/core/transport/decode-fold.d.ts +47 -0
  17. package/dist/core/transport/headers.d.ts +96 -18
  18. package/dist/core/transport/index.d.ts +5 -6
  19. package/dist/core/transport/internal/bounded-map.d.ts +20 -0
  20. package/dist/core/transport/invocation.d.ts +74 -0
  21. package/dist/core/transport/load-conversation.d.ts +128 -0
  22. package/dist/core/transport/load-history.d.ts +39 -0
  23. package/dist/core/transport/pipe-stream.d.ts +9 -9
  24. package/dist/core/transport/run-manager.d.ts +78 -0
  25. package/dist/core/transport/tree.d.ts +373 -109
  26. package/dist/core/transport/types/agent.d.ts +353 -0
  27. package/dist/core/transport/types/client.d.ts +168 -0
  28. package/dist/core/transport/types/shared.d.ts +24 -0
  29. package/dist/core/transport/types/tree.d.ts +315 -0
  30. package/dist/core/transport/types/view.d.ts +222 -0
  31. package/dist/core/transport/types.d.ts +13 -553
  32. package/dist/core/transport/view.d.ts +272 -84
  33. package/dist/errors.d.ts +21 -10
  34. package/dist/index.d.ts +6 -8
  35. package/dist/logger.d.ts +12 -0
  36. package/dist/react/ably-ai-transport-react.js +976 -990
  37. package/dist/react/ably-ai-transport-react.js.map +1 -1
  38. package/dist/react/ably-ai-transport-react.umd.cjs +1 -1
  39. package/dist/react/ably-ai-transport-react.umd.cjs.map +1 -1
  40. package/dist/react/contexts/client-session-context.d.ts +36 -0
  41. package/dist/react/contexts/client-session-provider.d.ts +53 -0
  42. package/dist/react/create-session-hooks.d.ts +116 -0
  43. package/dist/react/index.d.ts +12 -12
  44. package/dist/react/internal/use-resolved-session.d.ts +36 -0
  45. package/dist/react/use-ably-messages.d.ts +17 -14
  46. package/dist/react/use-client-session.d.ts +81 -0
  47. package/dist/react/use-create-view.d.ts +14 -13
  48. package/dist/react/use-tree.d.ts +30 -15
  49. package/dist/react/use-view.d.ts +82 -51
  50. package/dist/utils.d.ts +32 -23
  51. package/dist/vercel/ably-ai-transport-vercel.js +2573 -2086
  52. package/dist/vercel/ably-ai-transport-vercel.js.map +1 -1
  53. package/dist/vercel/ably-ai-transport-vercel.umd.cjs +1 -1
  54. package/dist/vercel/ably-ai-transport-vercel.umd.cjs.map +1 -1
  55. package/dist/vercel/codec/decoder.d.ts +5 -18
  56. package/dist/vercel/codec/encoder.d.ts +6 -36
  57. package/dist/vercel/codec/events.d.ts +51 -0
  58. package/dist/vercel/codec/index.d.ts +24 -12
  59. package/dist/vercel/codec/reducer.d.ts +144 -0
  60. package/dist/vercel/codec/tool-transitions.d.ts +2 -2
  61. package/dist/vercel/index.d.ts +4 -5
  62. package/dist/vercel/react/ably-ai-transport-vercel-react.js +3907 -3266
  63. package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
  64. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +33 -8
  65. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
  66. package/dist/vercel/react/contexts/chat-transport-context.d.ts +7 -6
  67. package/dist/vercel/react/contexts/chat-transport-provider.d.ts +53 -41
  68. package/dist/vercel/react/index.d.ts +1 -2
  69. package/dist/vercel/react/use-chat-transport.d.ts +30 -26
  70. package/dist/vercel/react/use-message-sync.d.ts +17 -30
  71. package/dist/vercel/run-end-reason.d.ts +29 -0
  72. package/dist/vercel/transport/chat-transport.d.ts +43 -24
  73. package/dist/vercel/transport/index.d.ts +25 -21
  74. package/dist/vercel/transport/run-output-stream.d.ts +56 -0
  75. package/dist/version.d.ts +2 -0
  76. package/package.json +30 -23
  77. package/src/constants.ts +124 -51
  78. package/src/core/agent.ts +68 -0
  79. package/src/core/codec/decoder.ts +71 -98
  80. package/src/core/codec/encoder.ts +113 -65
  81. package/src/core/codec/index.ts +13 -6
  82. package/src/core/codec/lifecycle-tracker.ts +10 -9
  83. package/src/core/codec/types.ts +436 -120
  84. package/src/core/transport/agent-session.ts +1344 -0
  85. package/src/core/transport/branch-chain.ts +58 -0
  86. package/src/core/transport/client-session.ts +775 -0
  87. package/src/core/transport/decode-fold.ts +91 -0
  88. package/src/core/transport/headers.ts +181 -22
  89. package/src/core/transport/index.ts +25 -26
  90. package/src/core/transport/internal/bounded-map.ts +27 -0
  91. package/src/core/transport/invocation.ts +98 -0
  92. package/src/core/transport/load-conversation.ts +355 -0
  93. package/src/core/transport/load-history.ts +269 -0
  94. package/src/core/transport/pipe-stream.ts +54 -39
  95. package/src/core/transport/run-manager.ts +249 -0
  96. package/src/core/transport/tree.ts +926 -308
  97. package/src/core/transport/types/agent.ts +407 -0
  98. package/src/core/transport/types/client.ts +211 -0
  99. package/src/core/transport/types/shared.ts +27 -0
  100. package/src/core/transport/types/tree.ts +344 -0
  101. package/src/core/transport/types/view.ts +259 -0
  102. package/src/core/transport/types.ts +13 -706
  103. package/src/core/transport/view.ts +864 -433
  104. package/src/errors.ts +22 -9
  105. package/src/event-emitter.ts +3 -2
  106. package/src/index.ts +52 -41
  107. package/src/logger.ts +14 -1
  108. package/src/react/contexts/client-session-context.ts +41 -0
  109. package/src/react/contexts/client-session-provider.tsx +186 -0
  110. package/src/react/create-session-hooks.ts +141 -0
  111. package/src/react/index.ts +23 -13
  112. package/src/react/internal/use-resolved-session.ts +63 -0
  113. package/src/react/use-ably-messages.ts +32 -22
  114. package/src/react/use-client-session.ts +201 -0
  115. package/src/react/use-create-view.ts +33 -29
  116. package/src/react/use-tree.ts +61 -30
  117. package/src/react/use-view.ts +139 -97
  118. package/src/utils.ts +63 -45
  119. package/src/vercel/codec/decoder.ts +336 -258
  120. package/src/vercel/codec/encoder.ts +343 -205
  121. package/src/vercel/codec/events.ts +87 -0
  122. package/src/vercel/codec/index.ts +60 -13
  123. package/src/vercel/codec/reducer.ts +977 -0
  124. package/src/vercel/codec/tool-transitions.ts +2 -2
  125. package/src/vercel/index.ts +6 -19
  126. package/src/vercel/react/contexts/chat-transport-context.ts +7 -6
  127. package/src/vercel/react/contexts/chat-transport-provider.tsx +87 -59
  128. package/src/vercel/react/index.ts +3 -5
  129. package/src/vercel/react/use-chat-transport.ts +47 -49
  130. package/src/vercel/react/use-message-sync.ts +80 -39
  131. package/src/vercel/run-end-reason.ts +78 -0
  132. package/src/vercel/transport/chat-transport.ts +392 -98
  133. package/src/vercel/transport/index.ts +39 -38
  134. package/src/vercel/transport/run-output-stream.ts +170 -0
  135. package/src/version.ts +2 -0
  136. package/dist/core/transport/client-transport.d.ts +0 -10
  137. package/dist/core/transport/decode-history.d.ts +0 -43
  138. package/dist/core/transport/server-transport.d.ts +0 -7
  139. package/dist/core/transport/stream-router.d.ts +0 -29
  140. package/dist/core/transport/turn-manager.d.ts +0 -37
  141. package/dist/react/contexts/transport-context.d.ts +0 -31
  142. package/dist/react/contexts/transport-provider.d.ts +0 -49
  143. package/dist/react/create-transport-hooks.d.ts +0 -124
  144. package/dist/react/use-active-turns.d.ts +0 -12
  145. package/dist/react/use-client-transport.d.ts +0 -80
  146. package/dist/vercel/codec/accumulator.d.ts +0 -21
  147. package/dist/vercel/react/use-staged-add-tool-approval-response.d.ts +0 -30
  148. package/dist/vercel/tool-approvals.d.ts +0 -124
  149. package/dist/vercel/tool-events.d.ts +0 -26
  150. package/src/core/transport/client-transport.ts +0 -977
  151. package/src/core/transport/decode-history.ts +0 -485
  152. package/src/core/transport/server-transport.ts +0 -612
  153. package/src/core/transport/stream-router.ts +0 -136
  154. package/src/core/transport/turn-manager.ts +0 -165
  155. package/src/react/contexts/transport-context.ts +0 -37
  156. package/src/react/contexts/transport-provider.tsx +0 -164
  157. package/src/react/create-transport-hooks.ts +0 -144
  158. package/src/react/use-active-turns.ts +0 -72
  159. package/src/react/use-client-transport.ts +0 -197
  160. package/src/vercel/codec/accumulator.ts +0 -588
  161. package/src/vercel/react/use-staged-add-tool-approval-response.ts +0 -87
  162. package/src/vercel/tool-approvals.ts +0 -380
  163. package/src/vercel/tool-events.ts +0 -53
@@ -1,588 +0,0 @@
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
- import { toolBase, transitionToolPart } from './tool-transitions.js';
21
-
22
- // ---------------------------------------------------------------------------
23
- // Internal types
24
- // ---------------------------------------------------------------------------
25
-
26
- /** Status of a streamed message (text, reasoning, or tool-input). */
27
- type StreamStatus = 'streaming' | 'finished' | 'aborted';
28
-
29
- /**
30
- * Tracks an in-progress tool part's position and accumulated streaming input.
31
- * Text and reasoning parts don't need this — we write directly to the part.
32
- * Tool parts need the extra `inputText` buffer because deltas arrive as raw
33
- * JSON fragments that must be accumulated before parsing.
34
- */
35
- interface ToolPartTracker {
36
- /** Index in the message's parts array. */
37
- partIndex: number;
38
- /** Accumulated streaming input text (for JSON parsing on completion). */
39
- inputText: string;
40
- }
41
-
42
- /** Bundled per-message state for an in-progress message. */
43
- interface ActiveMessageState {
44
- message: AI.UIMessage;
45
- textStreams: DeltaStreamTracker;
46
- reasoningStreams: DeltaStreamTracker;
47
- toolTrackers: Record<string, ToolPartTracker>;
48
- streamStatus: Map<string, StreamStatus>;
49
- }
50
-
51
- // ---------------------------------------------------------------------------
52
- // DeltaStreamTracker — manages text or reasoning stream accumulation
53
- // ---------------------------------------------------------------------------
54
-
55
- /**
56
- * Tracks in-progress text or reasoning streams within a single message.
57
- * Owns the mapping from stream ID to part index, enforcing the pairing
58
- * of part type and index map by construction.
59
- */
60
- class DeltaStreamTracker {
61
- private readonly _partType: 'text' | 'reasoning';
62
- private _activeIndex = new Map<string, number>();
63
-
64
- constructor(partType: 'text' | 'reasoning') {
65
- this._partType = partType;
66
- }
67
-
68
- start(id: string, msg: AI.UIMessage, streamStatus: Map<string, StreamStatus>): void {
69
- this._activeIndex.set(id, msg.parts.length);
70
- msg.parts.push({ type: this._partType, text: '' });
71
- streamStatus.set(id, 'streaming');
72
- }
73
-
74
- delta(id: string, msg: AI.UIMessage, text: string): void {
75
- const idx = this._activeIndex.get(id);
76
- if (idx === undefined) return;
77
- const part = msg.parts[idx];
78
- if (part?.type === this._partType) {
79
- part.text += text;
80
- }
81
- }
82
-
83
- end(id: string, streamStatus: Map<string, StreamStatus>): void {
84
- streamStatus.set(id, 'finished');
85
- this._activeIndex.delete(id);
86
- }
87
-
88
- reset(): void {
89
- this._activeIndex = new Map();
90
- }
91
- }
92
-
93
- // ---------------------------------------------------------------------------
94
- // Default implementation
95
- // ---------------------------------------------------------------------------
96
-
97
- class DefaultUIMessageAccumulator implements MessageAccumulator<AI.UIMessageChunk, AI.UIMessage> {
98
- private readonly _messageList: AI.UIMessage[] = [];
99
- private readonly _activeMessages = new Map<string, ActiveMessageState>();
100
-
101
- get messages(): AI.UIMessage[] {
102
- return this._messageList;
103
- }
104
-
105
- get completedMessages(): AI.UIMessage[] {
106
- const activeSet = new Set<AI.UIMessage>();
107
- for (const state of this._activeMessages.values()) {
108
- activeSet.add(state.message);
109
- }
110
- return this._messageList.filter((msg) => !activeSet.has(msg));
111
- }
112
-
113
- get hasActiveStream(): boolean {
114
- for (const state of this._activeMessages.values()) {
115
- for (const status of state.streamStatus.values()) {
116
- if (status === 'streaming') return true;
117
- }
118
- }
119
- return false;
120
- }
121
-
122
- processOutputs(outputs: DecoderOutput<AI.UIMessageChunk, AI.UIMessage>[]): void {
123
- for (const output of outputs) {
124
- if (output.kind === 'message') {
125
- this._messageList.push(output.message);
126
- } else if (output.messageId !== undefined) {
127
- this._processEvent(output.event, output.messageId);
128
- }
129
- }
130
- }
131
-
132
- updateMessage(message: AI.UIMessage): void {
133
- const idx = this._messageList.findIndex((m) => m.id === message.id);
134
- if (idx !== -1) {
135
- this._messageList[idx] = message;
136
- }
137
- }
138
-
139
- initMessage(messageId: string, message: AI.UIMessage): void {
140
- const existing = this._activeMessages.get(messageId);
141
-
142
- if (existing) {
143
- // Already active — sync with the externally updated message.
144
- // Replace the message and rebuild tool trackers so the accumulator
145
- // reflects updates (e.g. cross-turn amendments applied to the tree)
146
- // that happened outside the streaming flow.
147
- const cloned = structuredClone(message);
148
- const listIdx = this._messageList.indexOf(existing.message);
149
- existing.message = cloned;
150
- if (listIdx !== -1) {
151
- this._messageList[listIdx] = cloned;
152
- }
153
- existing.toolTrackers = {};
154
- for (let i = 0; i < cloned.parts.length; i++) {
155
- const part = cloned.parts[i];
156
- if (part?.type === 'dynamic-tool') {
157
- existing.toolTrackers[part.toolCallId] = { partIndex: i, inputText: '' };
158
- existing.streamStatus.set(part.toolCallId, 'finished');
159
- }
160
- }
161
- return;
162
- }
163
-
164
- // Not active — create tracking state from the existing message.
165
- const cloned = structuredClone(message);
166
- const toolTrackers: Record<string, ToolPartTracker> = {};
167
- const streamStatus = new Map<string, StreamStatus>();
168
-
169
- for (let i = 0; i < cloned.parts.length; i++) {
170
- const part = cloned.parts[i];
171
- if (part?.type === 'dynamic-tool') {
172
- toolTrackers[part.toolCallId] = { partIndex: i, inputText: '' };
173
- streamStatus.set(part.toolCallId, 'finished');
174
- }
175
- }
176
-
177
- const state: ActiveMessageState = {
178
- message: cloned,
179
- textStreams: new DeltaStreamTracker('text'),
180
- reasoningStreams: new DeltaStreamTracker('reasoning'),
181
- toolTrackers,
182
- streamStatus,
183
- };
184
-
185
- this._activeMessages.set(messageId, state);
186
-
187
- // If this message is already in the list (completed previously),
188
- // replace in-place. Otherwise push as a new entry.
189
- const existingIdx = this._messageList.findIndex((m) => m.id === message.id);
190
- if (existingIdx === -1) {
191
- this._messageList.push(state.message);
192
- } else {
193
- this._messageList[existingIdx] = state.message;
194
- }
195
- }
196
-
197
- completeMessage(messageId: string): void {
198
- this._activeMessages.delete(messageId);
199
- }
200
-
201
- // -------------------------------------------------------------------------
202
- // Shared helpers
203
- // -------------------------------------------------------------------------
204
-
205
- private _ensureActiveMessage(messageId: string): ActiveMessageState {
206
- const existing = this._activeMessages.get(messageId);
207
- if (existing) return existing;
208
-
209
- const state: ActiveMessageState = {
210
- message: { id: messageId, role: 'assistant', parts: [] },
211
- textStreams: new DeltaStreamTracker('text'),
212
- reasoningStreams: new DeltaStreamTracker('reasoning'),
213
- toolTrackers: {},
214
- streamStatus: new Map(),
215
- };
216
- this._activeMessages.set(messageId, state);
217
- this._messageList.push(state.message);
218
- return state;
219
- }
220
-
221
- /**
222
- * Look up a tracked tool part by toolCallId within a message state.
223
- * @param toolCallId - The tool call identifier to look up.
224
- * @param state - The active message state to search in.
225
- * @returns The tracker and current part, or undefined if not found.
226
- */
227
- private _getToolPart(
228
- toolCallId: string,
229
- state: ActiveMessageState,
230
- ): { tracker: ToolPartTracker; part: AI.DynamicToolUIPart } | undefined {
231
- const tracker = state.toolTrackers[toolCallId];
232
- if (!tracker) return undefined;
233
-
234
- const existing = state.message.parts[tracker.partIndex];
235
- if (existing?.type !== 'dynamic-tool') return undefined;
236
-
237
- return { tracker, part: existing };
238
- }
239
-
240
- // -------------------------------------------------------------------------
241
- // Event dispatch
242
- // -------------------------------------------------------------------------
243
-
244
- private _processEvent(chunk: AI.UIMessageChunk, messageId: string): void {
245
- switch (chunk.type) {
246
- case 'start':
247
- case 'start-step':
248
- case 'finish-step':
249
- case 'finish':
250
- case 'abort':
251
- case 'error':
252
- case 'message-metadata': {
253
- this._processLifecycle(chunk, messageId);
254
- break;
255
- }
256
-
257
- case 'text-start':
258
- case 'text-delta':
259
- case 'text-end':
260
- case 'reasoning-start':
261
- case 'reasoning-delta':
262
- case 'reasoning-end': {
263
- this._processTextOrReasoning(chunk, messageId);
264
- break;
265
- }
266
-
267
- case 'tool-input-start':
268
- case 'tool-input-delta':
269
- case 'tool-input-available':
270
- case 'tool-input-error': {
271
- this._processToolInput(chunk, messageId);
272
- break;
273
- }
274
-
275
- case 'tool-output-available':
276
- case 'tool-output-error':
277
- case 'tool-output-denied':
278
- case 'tool-approval-request': {
279
- this._processToolOutput(chunk, messageId);
280
- break;
281
- }
282
-
283
- case 'file':
284
- case 'source-url':
285
- case 'source-document': {
286
- this._processContentPart(chunk, messageId);
287
- break;
288
- }
289
-
290
- default: {
291
- if (chunk.type.startsWith('data-')) {
292
- if (chunk.transient) break;
293
-
294
- const state = this._ensureActiveMessage(messageId);
295
-
296
- // CAST: chunk.type is `data-${string}` which satisfies DataUIPart,
297
- // but TypeScript cannot verify the template literal matches a
298
- // specific UIMessagePart variant at the type level.
299
- const dataPart = stripUndefined({
300
- type: chunk.type,
301
- id: chunk.id,
302
- data: chunk.data,
303
- }) as AI.UIMessage['parts'][number];
304
-
305
- if (chunk.id !== undefined) {
306
- const idx = state.message.parts.findIndex((p) => p.type === chunk.type && 'id' in p && p.id === chunk.id);
307
- if (idx !== -1) {
308
- state.message.parts[idx] = dataPart;
309
- break;
310
- }
311
- }
312
-
313
- state.message.parts.push(dataPart);
314
- }
315
- break;
316
- }
317
- }
318
- }
319
-
320
- // -------------------------------------------------------------------------
321
- // Lifecycle events
322
- // -------------------------------------------------------------------------
323
-
324
- private _processLifecycle(
325
- chunk: Extract<
326
- AI.UIMessageChunk,
327
- { type: 'start' | 'start-step' | 'finish-step' | 'finish' | 'abort' | 'error' | 'message-metadata' }
328
- >,
329
- messageId: string,
330
- ): void {
331
- switch (chunk.type) {
332
- case 'start': {
333
- const state = this._ensureActiveMessage(messageId);
334
- if (chunk.messageId) state.message.id = chunk.messageId;
335
- if (chunk.messageMetadata !== undefined) {
336
- state.message.metadata = chunk.messageMetadata;
337
- }
338
- break;
339
- }
340
-
341
- case 'start-step': {
342
- const state = this._ensureActiveMessage(messageId);
343
- state.message.parts.push({ type: 'step-start' });
344
- break;
345
- }
346
-
347
- case 'finish-step': {
348
- const state = this._activeMessages.get(messageId);
349
- if (state) {
350
- state.textStreams.reset();
351
- state.reasoningStreams.reset();
352
- }
353
- break;
354
- }
355
-
356
- case 'finish': {
357
- const state = this._activeMessages.get(messageId);
358
- if (state && chunk.messageMetadata !== undefined) {
359
- state.message.metadata = chunk.messageMetadata;
360
- }
361
- this._activeMessages.delete(messageId);
362
- break;
363
- }
364
-
365
- case 'abort': {
366
- const state = this._activeMessages.get(messageId);
367
- if (state) {
368
- for (const [id, status] of state.streamStatus) {
369
- if (status === 'streaming') {
370
- state.streamStatus.set(id, 'aborted');
371
- }
372
- }
373
- }
374
- this._activeMessages.delete(messageId);
375
- break;
376
- }
377
-
378
- case 'error': {
379
- break;
380
- }
381
-
382
- case 'message-metadata': {
383
- const state = this._activeMessages.get(messageId);
384
- if (state && chunk.messageMetadata !== undefined) {
385
- state.message.metadata = chunk.messageMetadata;
386
- }
387
- break;
388
- }
389
- }
390
- }
391
-
392
- // -------------------------------------------------------------------------
393
- // Text and reasoning streaming
394
- // -------------------------------------------------------------------------
395
-
396
- private _processTextOrReasoning(
397
- chunk: Extract<
398
- AI.UIMessageChunk,
399
- { type: 'text-start' | 'text-delta' | 'text-end' | 'reasoning-start' | 'reasoning-delta' | 'reasoning-end' }
400
- >,
401
- messageId: string,
402
- ): void {
403
- const state = this._ensureActiveMessage(messageId);
404
-
405
- switch (chunk.type) {
406
- case 'text-start': {
407
- state.textStreams.start(chunk.id, state.message, state.streamStatus);
408
- break;
409
- }
410
- case 'text-delta': {
411
- state.textStreams.delta(chunk.id, state.message, chunk.delta);
412
- break;
413
- }
414
- case 'text-end': {
415
- state.textStreams.end(chunk.id, state.streamStatus);
416
- break;
417
- }
418
- case 'reasoning-start': {
419
- state.reasoningStreams.start(chunk.id, state.message, state.streamStatus);
420
- break;
421
- }
422
- case 'reasoning-delta': {
423
- state.reasoningStreams.delta(chunk.id, state.message, chunk.delta);
424
- break;
425
- }
426
- case 'reasoning-end': {
427
- state.reasoningStreams.end(chunk.id, state.streamStatus);
428
- break;
429
- }
430
- }
431
- }
432
-
433
- // -------------------------------------------------------------------------
434
- // Tool input streaming
435
- // -------------------------------------------------------------------------
436
-
437
- private _processToolInput(
438
- chunk: Extract<
439
- AI.UIMessageChunk,
440
- { type: 'tool-input-start' | 'tool-input-delta' | 'tool-input-available' | 'tool-input-error' }
441
- >,
442
- messageId: string,
443
- ): void {
444
- switch (chunk.type) {
445
- case 'tool-input-start': {
446
- const state = this._ensureActiveMessage(messageId);
447
- const partIndex = state.message.parts.length;
448
- state.message.parts.push({ ...toolBase(chunk), state: 'input-streaming', input: undefined });
449
- state.toolTrackers[chunk.toolCallId] = { partIndex, inputText: '' };
450
- state.streamStatus.set(chunk.toolCallId, 'streaming');
451
- break;
452
- }
453
-
454
- case 'tool-input-delta': {
455
- const state = this._ensureActiveMessage(messageId);
456
- const tracker = state.toolTrackers[chunk.toolCallId];
457
- if (!tracker) break;
458
- tracker.inputText += chunk.inputTextDelta;
459
-
460
- let parsedInput: unknown;
461
- try {
462
- // CAST: JSON.parse returns any; unknown is the safe trust-boundary type.
463
- parsedInput = JSON.parse(tracker.inputText) as unknown;
464
- } catch {
465
- parsedInput = undefined;
466
- }
467
-
468
- const found = this._getToolPart(chunk.toolCallId, state);
469
- if (!found) break;
470
- state.message.parts[found.tracker.partIndex] = {
471
- ...toolBase(found.part),
472
- state: 'input-streaming',
473
- input: parsedInput,
474
- };
475
- break;
476
- }
477
-
478
- case 'tool-input-available': {
479
- const state = this._ensureActiveMessage(messageId);
480
- const found = this._getToolPart(chunk.toolCallId, state);
481
- if (!found) break;
482
- state.message.parts[found.tracker.partIndex] = {
483
- ...toolBase(found.part),
484
- state: 'input-available',
485
- input: chunk.input,
486
- };
487
- state.streamStatus.set(chunk.toolCallId, 'finished');
488
- break;
489
- }
490
-
491
- case 'tool-input-error': {
492
- const state = this._ensureActiveMessage(messageId);
493
- const found = this._getToolPart(chunk.toolCallId, state);
494
- if (found) {
495
- state.message.parts[found.tracker.partIndex] = {
496
- ...toolBase(found.part),
497
- state: 'output-error',
498
- input: chunk.input,
499
- errorText: chunk.errorText,
500
- };
501
- } else {
502
- const partIndex = state.message.parts.length;
503
- state.message.parts.push({
504
- ...toolBase(chunk),
505
- state: 'output-error',
506
- input: chunk.input,
507
- errorText: chunk.errorText,
508
- });
509
- state.toolTrackers[chunk.toolCallId] = { partIndex, inputText: '' };
510
- }
511
- state.streamStatus.set(chunk.toolCallId, 'finished');
512
- break;
513
- }
514
- }
515
- }
516
-
517
- // -------------------------------------------------------------------------
518
- // Tool output transitions
519
- // -------------------------------------------------------------------------
520
-
521
- private _processToolOutput(
522
- chunk: Extract<
523
- AI.UIMessageChunk,
524
- { type: 'tool-output-available' | 'tool-output-error' | 'tool-output-denied' | 'tool-approval-request' }
525
- >,
526
- messageId: string,
527
- ): void {
528
- const state = this._ensureActiveMessage(messageId);
529
- const found = this._getToolPart(chunk.toolCallId, state);
530
- if (!found) return;
531
-
532
- state.message.parts[found.tracker.partIndex] = transitionToolPart(found.part, chunk);
533
- }
534
-
535
- // -------------------------------------------------------------------------
536
- // Content parts
537
- // -------------------------------------------------------------------------
538
-
539
- private _processContentPart(
540
- chunk: Extract<AI.UIMessageChunk, { type: 'file' | 'source-url' | 'source-document' }>,
541
- messageId: string,
542
- ): void {
543
- const state = this._ensureActiveMessage(messageId);
544
-
545
- switch (chunk.type) {
546
- case 'file': {
547
- state.message.parts.push({ type: 'file', mediaType: chunk.mediaType, url: chunk.url });
548
- break;
549
- }
550
-
551
- case 'source-url': {
552
- state.message.parts.push(
553
- stripUndefined({
554
- type: 'source-url' as const,
555
- sourceId: chunk.sourceId,
556
- url: chunk.url,
557
- title: chunk.title,
558
- }),
559
- );
560
- break;
561
- }
562
-
563
- case 'source-document': {
564
- state.message.parts.push(
565
- stripUndefined({
566
- type: 'source-document' as const,
567
- sourceId: chunk.sourceId,
568
- mediaType: chunk.mediaType,
569
- title: chunk.title,
570
- filename: chunk.filename,
571
- }),
572
- );
573
- break;
574
- }
575
- }
576
- }
577
- }
578
-
579
- // ---------------------------------------------------------------------------
580
- // Factory
581
- // ---------------------------------------------------------------------------
582
-
583
- /**
584
- * Create a Vercel AI SDK accumulator that builds UIMessage[] from decoder outputs.
585
- * @returns A {@link MessageAccumulator} for UIMessageChunk/UIMessage.
586
- */
587
- export const createAccumulator = (): MessageAccumulator<AI.UIMessageChunk, AI.UIMessage> =>
588
- new DefaultUIMessageAccumulator();
@@ -1,87 +0,0 @@
1
- /**
2
- * useStagedAddToolApprovalResponse — wrap useChat's `addToolApprovalResponse`
3
- * so the approval response is also applied to the transport tree
4
- * synchronously at click time.
5
- *
6
- * Patching the tree at click time eliminates the useChat↔tree divergence
7
- * the ChatTransport would otherwise have to reconcile via a history
8
- * overlay, and closes the observer-turn race that could wipe the
9
- * approval state between `addToolApprovalResponse` and
10
- * `sendAutomaticallyWhen`'s evaluation.
11
- *
12
- * Use this in place of useChat's raw `addToolApprovalResponse` wherever
13
- * you wire Approve / Deny buttons.
14
- */
15
-
16
- import type * as AI from 'ai';
17
- import type { ChatAddToolApproveResponseFunction } from 'ai';
18
- import { useCallback } from 'react';
19
-
20
- import type { ClientTransport } from '../../core/transport/types.js';
21
-
22
- /**
23
- * Returns a function with the same signature as useChat's
24
- * `addToolApprovalResponse`, but additionally applies the approval
25
- * response to the transport tree via `stageMessage` before delegating.
26
- *
27
- * If the tool call identified by `opts.id` isn't found in the tree,
28
- * the tree update is skipped and the raw function is still called —
29
- * matches useChat's tolerant behavior for stale approval ids.
30
- * @param transport - The client transport whose tree to patch.
31
- * @param addToolApprovalResponse - The raw function from `useChat()`.
32
- * @returns A drop-in replacement that patches the tree then delegates.
33
- */
34
- export const useStagedAddToolApprovalResponse = (
35
- transport: ClientTransport<AI.UIMessageChunk, AI.UIMessage>,
36
- addToolApprovalResponse: ChatAddToolApproveResponseFunction,
37
- ): ChatAddToolApproveResponseFunction =>
38
- useCallback<ChatAddToolApproveResponseFunction>(
39
- (opts) => {
40
- stageApprovalResponseOnTree(transport, opts);
41
- return addToolApprovalResponse(opts);
42
- },
43
- [transport, addToolApprovalResponse],
44
- );
45
-
46
- /**
47
- * Locate the assistant message whose `dynamic-tool` part carries the
48
- * given `approval.id`, build a patched copy with the part transitioned
49
- * to `approval-responded`, and stage the patched message on the tree.
50
- * @param transport - The transport whose tree to patch.
51
- * @param opts - The approval response being applied.
52
- * @param opts.id - The approval id matching a dynamic-tool part in the tree.
53
- * @param opts.approved - Whether the user approved or denied.
54
- * @param opts.reason - Optional reason accompanying the response.
55
- */
56
- const stageApprovalResponseOnTree = (
57
- transport: ClientTransport<AI.UIMessageChunk, AI.UIMessage>,
58
- opts: { id: string; approved: boolean; reason?: string },
59
- ): void => {
60
- const nodes = transport.view.flattenNodes();
61
- for (const node of nodes) {
62
- const partIndex = node.message.parts.findIndex((p) => p.type === 'dynamic-tool' && p.approval?.id === opts.id);
63
- if (partIndex === -1) continue;
64
-
65
- // CAST: findIndex predicate above narrows this to a dynamic-tool part
66
- // with a non-undefined approval.
67
- const part = node.message.parts[partIndex] as AI.DynamicToolUIPart;
68
-
69
- // Build the approval-responded variant directly rather than spreading
70
- // `part`, which TypeScript narrows to whichever source-state variant
71
- // the union discriminator inferred and then rejects when we change
72
- // `state` to a variant with different approval/output constraints.
73
- const patchedPart: AI.DynamicToolUIPart = {
74
- type: 'dynamic-tool',
75
- toolName: part.toolName,
76
- toolCallId: part.toolCallId,
77
- state: 'approval-responded',
78
- input: part.input,
79
- approval: { id: opts.id, approved: opts.approved, reason: opts.reason },
80
- };
81
-
82
- const patchedParts = [...node.message.parts];
83
- patchedParts[partIndex] = patchedPart;
84
- transport.stageMessage(node.msgId, { ...node.message, parts: patchedParts });
85
- return;
86
- }
87
- };