@ably/ai-transport 0.1.0 → 0.3.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 (221) hide show
  1. package/README.md +93 -111
  2. package/dist/ably-ai-transport.js +2401 -1387
  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 +44 -0
  8. package/dist/core/channel-options.d.ts +57 -0
  9. package/dist/core/codec/codec-event.d.ts +9 -0
  10. package/dist/core/codec/decoder.d.ts +24 -24
  11. package/dist/core/codec/define-codec.d.ts +100 -0
  12. package/dist/core/codec/encoder.d.ts +10 -12
  13. package/dist/core/codec/field-bag.d.ts +85 -0
  14. package/dist/core/codec/fields.d.ts +141 -0
  15. package/dist/core/codec/index.d.ts +8 -2
  16. package/dist/core/codec/input-descriptor-decoder.d.ts +19 -0
  17. package/dist/core/codec/input-descriptor-encoder.d.ts +22 -0
  18. package/dist/core/codec/input-descriptors.d.ts +281 -0
  19. package/dist/core/codec/lifecycle-tracker.d.ts +10 -9
  20. package/dist/core/codec/output-descriptor-decoder.d.ts +29 -0
  21. package/dist/core/codec/output-descriptor-encoder.d.ts +31 -0
  22. package/dist/core/codec/output-descriptors.d.ts +237 -0
  23. package/dist/core/codec/types.d.ts +470 -119
  24. package/dist/core/codec/well-known-inputs.d.ts +52 -0
  25. package/dist/core/transport/agent-session.d.ts +10 -0
  26. package/dist/core/transport/agent-view.d.ts +296 -0
  27. package/dist/core/transport/client-session.d.ts +13 -0
  28. package/dist/core/transport/decode-fold.d.ts +55 -0
  29. package/dist/core/transport/headers.d.ts +121 -14
  30. package/dist/core/transport/index.d.ts +5 -6
  31. package/dist/core/transport/internal/bounded-map.d.ts +20 -0
  32. package/dist/core/transport/invocation.d.ts +74 -0
  33. package/dist/core/transport/load-history-pages.d.ts +71 -0
  34. package/dist/core/transport/load-history.d.ts +44 -0
  35. package/dist/core/transport/pipe-stream.d.ts +9 -9
  36. package/dist/core/transport/run-manager.d.ts +76 -0
  37. package/dist/core/transport/session-support.d.ts +55 -0
  38. package/dist/core/transport/tree.d.ts +523 -109
  39. package/dist/core/transport/types/agent.d.ts +375 -0
  40. package/dist/core/transport/types/client.d.ts +201 -0
  41. package/dist/core/transport/types/shared.d.ts +24 -0
  42. package/dist/core/transport/types/tree.d.ts +357 -0
  43. package/dist/core/transport/types/view.d.ts +249 -0
  44. package/dist/core/transport/types.d.ts +13 -553
  45. package/dist/core/transport/view.d.ts +390 -84
  46. package/dist/core/transport/wire-log.d.ts +102 -0
  47. package/dist/errors.d.ts +27 -10
  48. package/dist/index.d.ts +8 -9
  49. package/dist/logger.d.ts +12 -0
  50. package/dist/react/ably-ai-transport-react.js +1365 -1010
  51. package/dist/react/ably-ai-transport-react.js.map +1 -1
  52. package/dist/react/ably-ai-transport-react.umd.cjs +1 -1
  53. package/dist/react/ably-ai-transport-react.umd.cjs.map +1 -1
  54. package/dist/react/contexts/client-session-context.d.ts +37 -0
  55. package/dist/react/contexts/client-session-provider.d.ts +56 -0
  56. package/dist/react/create-session-hooks.d.ts +116 -0
  57. package/dist/react/index.d.ts +13 -12
  58. package/dist/react/internal/skipped-session.d.ts +8 -0
  59. package/dist/react/internal/use-resolved-session.d.ts +36 -0
  60. package/dist/react/use-ably-messages.d.ts +17 -14
  61. package/dist/react/use-client-session.d.ts +81 -0
  62. package/dist/react/use-create-view.d.ts +14 -13
  63. package/dist/react/use-tree.d.ts +30 -15
  64. package/dist/react/use-view.d.ts +81 -50
  65. package/dist/utils.d.ts +48 -71
  66. package/dist/vercel/ably-ai-transport-vercel.js +3257 -2499
  67. package/dist/vercel/ably-ai-transport-vercel.js.map +1 -1
  68. package/dist/vercel/ably-ai-transport-vercel.umd.cjs +1 -1
  69. package/dist/vercel/ably-ai-transport-vercel.umd.cjs.map +1 -1
  70. package/dist/vercel/codec/decode-lifecycle.d.ts +9 -0
  71. package/dist/vercel/codec/events.d.ts +50 -0
  72. package/dist/vercel/codec/fields.d.ts +44 -0
  73. package/dist/vercel/codec/fold-content.d.ts +16 -0
  74. package/dist/vercel/codec/fold-data.d.ts +16 -0
  75. package/dist/vercel/codec/fold-input.d.ts +67 -0
  76. package/dist/vercel/codec/fold-lifecycle.d.ts +16 -0
  77. package/dist/vercel/codec/fold-text.d.ts +16 -0
  78. package/dist/vercel/codec/fold-tool-input.d.ts +17 -0
  79. package/dist/vercel/codec/fold-tool-output.d.ts +16 -0
  80. package/dist/vercel/codec/index.d.ts +7 -20
  81. package/dist/vercel/codec/inputs.d.ts +11 -0
  82. package/dist/vercel/codec/outputs.d.ts +11 -0
  83. package/dist/vercel/codec/reducer-state.d.ts +121 -0
  84. package/dist/vercel/codec/reducer.d.ts +62 -0
  85. package/dist/vercel/codec/tool-transitions.d.ts +2 -8
  86. package/dist/vercel/codec/wire-data.d.ts +34 -0
  87. package/dist/vercel/index.d.ts +5 -5
  88. package/dist/vercel/react/ably-ai-transport-vercel-react.js +2859 -9705
  89. package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
  90. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +1 -45
  91. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
  92. package/dist/vercel/react/contexts/chat-transport-context.d.ts +9 -7
  93. package/dist/vercel/react/contexts/chat-transport-provider.d.ts +53 -41
  94. package/dist/vercel/react/index.d.ts +1 -2
  95. package/dist/vercel/react/use-chat-transport.d.ts +30 -26
  96. package/dist/vercel/react/use-message-sync.d.ts +17 -30
  97. package/dist/vercel/run-end-reason.d.ts +84 -0
  98. package/dist/vercel/tool-part.d.ts +21 -0
  99. package/dist/vercel/transport/chat-transport.d.ts +41 -24
  100. package/dist/vercel/transport/index.d.ts +24 -20
  101. package/dist/vercel/transport/run-output-stream.d.ts +54 -0
  102. package/dist/version.d.ts +2 -0
  103. package/package.json +31 -24
  104. package/src/constants.ts +124 -51
  105. package/src/core/agent.ts +92 -0
  106. package/src/core/channel-options.ts +89 -0
  107. package/src/core/codec/codec-event.ts +27 -0
  108. package/src/core/codec/decoder.ts +202 -105
  109. package/src/core/codec/define-codec.ts +432 -0
  110. package/src/core/codec/encoder.ts +114 -107
  111. package/src/core/codec/field-bag.ts +142 -0
  112. package/src/core/codec/fields.ts +193 -0
  113. package/src/core/codec/index.ts +56 -6
  114. package/src/core/codec/input-descriptor-decoder.ts +97 -0
  115. package/src/core/codec/input-descriptor-encoder.ts +150 -0
  116. package/src/core/codec/input-descriptors.ts +373 -0
  117. package/src/core/codec/lifecycle-tracker.ts +10 -9
  118. package/src/core/codec/output-descriptor-decoder.ts +139 -0
  119. package/src/core/codec/output-descriptor-encoder.ts +101 -0
  120. package/src/core/codec/output-descriptors.ts +307 -0
  121. package/src/core/codec/types.ts +505 -126
  122. package/src/core/codec/well-known-inputs.ts +96 -0
  123. package/src/core/transport/agent-session.ts +1085 -0
  124. package/src/core/transport/agent-view.ts +738 -0
  125. package/src/core/transport/client-session.ts +780 -0
  126. package/src/core/transport/decode-fold.ts +101 -0
  127. package/src/core/transport/headers.ts +234 -22
  128. package/src/core/transport/index.ts +27 -27
  129. package/src/core/transport/internal/bounded-map.ts +27 -0
  130. package/src/core/transport/invocation.ts +98 -0
  131. package/src/core/transport/load-history-pages.ts +220 -0
  132. package/src/core/transport/load-history.ts +271 -0
  133. package/src/core/transport/pipe-stream.ts +63 -39
  134. package/src/core/transport/run-manager.ts +243 -0
  135. package/src/core/transport/session-support.ts +96 -0
  136. package/src/core/transport/tree.ts +1293 -308
  137. package/src/core/transport/types/agent.ts +434 -0
  138. package/src/core/transport/types/client.ts +247 -0
  139. package/src/core/transport/types/shared.ts +27 -0
  140. package/src/core/transport/types/tree.ts +393 -0
  141. package/src/core/transport/types/view.ts +288 -0
  142. package/src/core/transport/types.ts +13 -706
  143. package/src/core/transport/view.ts +1229 -450
  144. package/src/core/transport/wire-log.ts +189 -0
  145. package/src/errors.ts +29 -9
  146. package/src/event-emitter.ts +3 -2
  147. package/src/index.ts +86 -42
  148. package/src/logger.ts +14 -1
  149. package/src/react/contexts/client-session-context.ts +41 -0
  150. package/src/react/contexts/client-session-provider.tsx +222 -0
  151. package/src/react/create-session-hooks.ts +141 -0
  152. package/src/react/index.ts +24 -13
  153. package/src/react/internal/skipped-session.ts +62 -0
  154. package/src/react/internal/use-resolved-session.ts +63 -0
  155. package/src/react/use-ably-messages.ts +32 -22
  156. package/src/react/use-client-session.ts +178 -0
  157. package/src/react/use-create-view.ts +33 -29
  158. package/src/react/use-tree.ts +61 -30
  159. package/src/react/use-view.ts +138 -96
  160. package/src/utils.ts +83 -131
  161. package/src/vercel/codec/decode-lifecycle.ts +70 -0
  162. package/src/vercel/codec/events.ts +85 -0
  163. package/src/vercel/codec/fields.ts +58 -0
  164. package/src/vercel/codec/fold-content.ts +54 -0
  165. package/src/vercel/codec/fold-data.ts +46 -0
  166. package/src/vercel/codec/fold-input.ts +255 -0
  167. package/src/vercel/codec/fold-lifecycle.ts +85 -0
  168. package/src/vercel/codec/fold-text.ts +55 -0
  169. package/src/vercel/codec/fold-tool-input.ts +86 -0
  170. package/src/vercel/codec/fold-tool-output.ts +79 -0
  171. package/src/vercel/codec/index.ts +28 -21
  172. package/src/vercel/codec/inputs.ts +116 -0
  173. package/src/vercel/codec/outputs.ts +207 -0
  174. package/src/vercel/codec/reducer-state.ts +169 -0
  175. package/src/vercel/codec/reducer.ts +191 -0
  176. package/src/vercel/codec/tool-transitions.ts +3 -14
  177. package/src/vercel/codec/wire-data.ts +64 -0
  178. package/src/vercel/index.ts +7 -19
  179. package/src/vercel/react/contexts/chat-transport-context.ts +8 -7
  180. package/src/vercel/react/contexts/chat-transport-provider.tsx +87 -59
  181. package/src/vercel/react/index.ts +3 -5
  182. package/src/vercel/react/use-chat-transport.ts +44 -66
  183. package/src/vercel/react/use-message-sync.ts +75 -39
  184. package/src/vercel/run-end-reason.ts +157 -0
  185. package/src/vercel/tool-part.ts +25 -0
  186. package/src/vercel/transport/chat-transport.ts +380 -98
  187. package/src/vercel/transport/index.ts +38 -37
  188. package/src/vercel/transport/run-output-stream.ts +169 -0
  189. package/src/version.ts +2 -0
  190. package/dist/core/transport/client-transport.d.ts +0 -10
  191. package/dist/core/transport/decode-history.d.ts +0 -43
  192. package/dist/core/transport/server-transport.d.ts +0 -7
  193. package/dist/core/transport/stream-router.d.ts +0 -29
  194. package/dist/core/transport/turn-manager.d.ts +0 -37
  195. package/dist/react/contexts/transport-context.d.ts +0 -31
  196. package/dist/react/contexts/transport-provider.d.ts +0 -49
  197. package/dist/react/create-transport-hooks.d.ts +0 -124
  198. package/dist/react/use-active-turns.d.ts +0 -12
  199. package/dist/react/use-client-transport.d.ts +0 -80
  200. package/dist/vercel/codec/accumulator.d.ts +0 -21
  201. package/dist/vercel/codec/decoder.d.ts +0 -22
  202. package/dist/vercel/codec/encoder.d.ts +0 -41
  203. package/dist/vercel/react/use-staged-add-tool-approval-response.d.ts +0 -30
  204. package/dist/vercel/tool-approvals.d.ts +0 -124
  205. package/dist/vercel/tool-events.d.ts +0 -26
  206. package/src/core/transport/client-transport.ts +0 -977
  207. package/src/core/transport/decode-history.ts +0 -485
  208. package/src/core/transport/server-transport.ts +0 -612
  209. package/src/core/transport/stream-router.ts +0 -136
  210. package/src/core/transport/turn-manager.ts +0 -165
  211. package/src/react/contexts/transport-context.ts +0 -37
  212. package/src/react/contexts/transport-provider.tsx +0 -164
  213. package/src/react/create-transport-hooks.ts +0 -144
  214. package/src/react/use-active-turns.ts +0 -72
  215. package/src/react/use-client-transport.ts +0 -197
  216. package/src/vercel/codec/accumulator.ts +0 -588
  217. package/src/vercel/codec/decoder.ts +0 -618
  218. package/src/vercel/codec/encoder.ts +0 -410
  219. package/src/vercel/react/use-staged-add-tool-approval-response.ts +0 -87
  220. package/src/vercel/tool-approvals.ts +0 -380
  221. 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();