@ably/ai-transport 0.2.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 (166) hide show
  1. package/README.md +10 -19
  2. package/dist/ably-ai-transport.js +1790 -1091
  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 +2 -2
  7. package/dist/core/agent.d.ts +20 -5
  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 +4 -1
  11. package/dist/core/codec/define-codec.d.ts +100 -0
  12. package/dist/core/codec/encoder.d.ts +2 -7
  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 -1
  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/output-descriptor-decoder.d.ts +29 -0
  20. package/dist/core/codec/output-descriptor-encoder.d.ts +31 -0
  21. package/dist/core/codec/output-descriptors.d.ts +237 -0
  22. package/dist/core/codec/types.d.ts +95 -36
  23. package/dist/core/codec/well-known-inputs.d.ts +52 -0
  24. package/dist/core/transport/agent-view.d.ts +296 -0
  25. package/dist/core/transport/decode-fold.d.ts +40 -32
  26. package/dist/core/transport/headers.d.ts +30 -1
  27. package/dist/core/transport/index.d.ts +1 -1
  28. package/dist/core/transport/invocation.d.ts +1 -1
  29. package/dist/core/transport/load-history-pages.d.ts +71 -0
  30. package/dist/core/transport/load-history.d.ts +21 -16
  31. package/dist/core/transport/run-manager.d.ts +9 -11
  32. package/dist/core/transport/session-support.d.ts +55 -0
  33. package/dist/core/transport/tree.d.ts +165 -15
  34. package/dist/core/transport/types/agent.d.ts +120 -98
  35. package/dist/core/transport/types/client.d.ts +45 -12
  36. package/dist/core/transport/types/tree.d.ts +52 -10
  37. package/dist/core/transport/types/view.d.ts +55 -28
  38. package/dist/core/transport/view.d.ts +176 -58
  39. package/dist/core/transport/wire-log.d.ts +102 -0
  40. package/dist/errors.d.ts +10 -4
  41. package/dist/index.d.ts +6 -5
  42. package/dist/react/ably-ai-transport-react.js +784 -415
  43. package/dist/react/ably-ai-transport-react.js.map +1 -1
  44. package/dist/react/ably-ai-transport-react.umd.cjs +1 -1
  45. package/dist/react/ably-ai-transport-react.umd.cjs.map +1 -1
  46. package/dist/react/contexts/client-session-context.d.ts +2 -1
  47. package/dist/react/contexts/client-session-provider.d.ts +3 -0
  48. package/dist/react/index.d.ts +2 -1
  49. package/dist/react/internal/skipped-session.d.ts +8 -0
  50. package/dist/react/use-view.d.ts +3 -3
  51. package/dist/utils.d.ts +22 -54
  52. package/dist/vercel/ably-ai-transport-vercel.js +2297 -2026
  53. package/dist/vercel/ably-ai-transport-vercel.js.map +1 -1
  54. package/dist/vercel/ably-ai-transport-vercel.umd.cjs +1 -1
  55. package/dist/vercel/ably-ai-transport-vercel.umd.cjs.map +1 -1
  56. package/dist/vercel/codec/decode-lifecycle.d.ts +9 -0
  57. package/dist/vercel/codec/events.d.ts +1 -2
  58. package/dist/vercel/codec/fields.d.ts +44 -0
  59. package/dist/vercel/codec/fold-content.d.ts +16 -0
  60. package/dist/vercel/codec/fold-data.d.ts +16 -0
  61. package/dist/vercel/codec/fold-input.d.ts +67 -0
  62. package/dist/vercel/codec/fold-lifecycle.d.ts +16 -0
  63. package/dist/vercel/codec/fold-text.d.ts +16 -0
  64. package/dist/vercel/codec/fold-tool-input.d.ts +17 -0
  65. package/dist/vercel/codec/fold-tool-output.d.ts +16 -0
  66. package/dist/vercel/codec/index.d.ts +5 -30
  67. package/dist/vercel/codec/inputs.d.ts +11 -0
  68. package/dist/vercel/codec/outputs.d.ts +11 -0
  69. package/dist/vercel/codec/reducer-state.d.ts +121 -0
  70. package/dist/vercel/codec/reducer.d.ts +20 -102
  71. package/dist/vercel/codec/tool-transitions.d.ts +0 -6
  72. package/dist/vercel/codec/wire-data.d.ts +34 -0
  73. package/dist/vercel/index.d.ts +1 -0
  74. package/dist/vercel/react/ably-ai-transport-vercel-react.js +2013 -9500
  75. package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
  76. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +1 -70
  77. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
  78. package/dist/vercel/react/contexts/chat-transport-context.d.ts +2 -1
  79. package/dist/vercel/run-end-reason.d.ts +66 -11
  80. package/dist/vercel/tool-part.d.ts +21 -0
  81. package/dist/vercel/transport/chat-transport.d.ts +0 -2
  82. package/dist/vercel/transport/index.d.ts +1 -1
  83. package/dist/vercel/transport/run-output-stream.d.ts +6 -8
  84. package/dist/version.d.ts +1 -1
  85. package/package.json +2 -2
  86. package/src/constants.ts +2 -2
  87. package/src/core/agent.ts +43 -19
  88. package/src/core/channel-options.ts +89 -0
  89. package/src/core/codec/codec-event.ts +27 -0
  90. package/src/core/codec/decoder.ts +145 -21
  91. package/src/core/codec/define-codec.ts +432 -0
  92. package/src/core/codec/encoder.ts +13 -54
  93. package/src/core/codec/field-bag.ts +142 -0
  94. package/src/core/codec/fields.ts +193 -0
  95. package/src/core/codec/index.ts +43 -0
  96. package/src/core/codec/input-descriptor-decoder.ts +97 -0
  97. package/src/core/codec/input-descriptor-encoder.ts +150 -0
  98. package/src/core/codec/input-descriptors.ts +373 -0
  99. package/src/core/codec/output-descriptor-decoder.ts +139 -0
  100. package/src/core/codec/output-descriptor-encoder.ts +101 -0
  101. package/src/core/codec/output-descriptors.ts +307 -0
  102. package/src/core/codec/types.ts +99 -36
  103. package/src/core/codec/well-known-inputs.ts +96 -0
  104. package/src/core/transport/agent-session.ts +330 -589
  105. package/src/core/transport/agent-view.ts +738 -0
  106. package/src/core/transport/client-session.ts +74 -69
  107. package/src/core/transport/decode-fold.ts +57 -47
  108. package/src/core/transport/headers.ts +57 -4
  109. package/src/core/transport/index.ts +2 -1
  110. package/src/core/transport/invocation.ts +1 -1
  111. package/src/core/transport/load-history-pages.ts +220 -0
  112. package/src/core/transport/load-history.ts +63 -61
  113. package/src/core/transport/pipe-stream.ts +10 -1
  114. package/src/core/transport/run-manager.ts +25 -31
  115. package/src/core/transport/session-support.ts +96 -0
  116. package/src/core/transport/tree.ts +414 -47
  117. package/src/core/transport/types/agent.ts +129 -102
  118. package/src/core/transport/types/client.ts +49 -13
  119. package/src/core/transport/types/tree.ts +61 -12
  120. package/src/core/transport/types/view.ts +57 -28
  121. package/src/core/transport/view.ts +520 -172
  122. package/src/core/transport/wire-log.ts +189 -0
  123. package/src/errors.ts +10 -3
  124. package/src/index.ts +44 -11
  125. package/src/react/contexts/client-session-context.ts +1 -1
  126. package/src/react/contexts/client-session-provider.tsx +38 -2
  127. package/src/react/index.ts +2 -1
  128. package/src/react/internal/skipped-session.ts +62 -0
  129. package/src/react/use-client-session.ts +7 -30
  130. package/src/react/use-view.ts +3 -3
  131. package/src/utils.ts +31 -97
  132. package/src/vercel/codec/decode-lifecycle.ts +70 -0
  133. package/src/vercel/codec/events.ts +1 -3
  134. package/src/vercel/codec/fields.ts +58 -0
  135. package/src/vercel/codec/fold-content.ts +54 -0
  136. package/src/vercel/codec/fold-data.ts +46 -0
  137. package/src/vercel/codec/fold-input.ts +255 -0
  138. package/src/vercel/codec/fold-lifecycle.ts +85 -0
  139. package/src/vercel/codec/fold-text.ts +55 -0
  140. package/src/vercel/codec/fold-tool-input.ts +86 -0
  141. package/src/vercel/codec/fold-tool-output.ts +79 -0
  142. package/src/vercel/codec/index.ts +23 -63
  143. package/src/vercel/codec/inputs.ts +116 -0
  144. package/src/vercel/codec/outputs.ts +207 -0
  145. package/src/vercel/codec/reducer-state.ts +169 -0
  146. package/src/vercel/codec/reducer.ts +52 -838
  147. package/src/vercel/codec/tool-transitions.ts +1 -12
  148. package/src/vercel/codec/wire-data.ts +64 -0
  149. package/src/vercel/index.ts +1 -0
  150. package/src/vercel/react/contexts/chat-transport-context.ts +1 -1
  151. package/src/vercel/react/use-chat-transport.ts +8 -28
  152. package/src/vercel/react/use-message-sync.ts +5 -10
  153. package/src/vercel/run-end-reason.ts +95 -16
  154. package/src/vercel/tool-part.ts +25 -0
  155. package/src/vercel/transport/chat-transport.ts +10 -22
  156. package/src/vercel/transport/index.ts +1 -1
  157. package/src/vercel/transport/run-output-stream.ts +7 -8
  158. package/src/version.ts +1 -1
  159. package/dist/core/transport/branch-chain.d.ts +0 -43
  160. package/dist/core/transport/load-conversation.d.ts +0 -128
  161. package/dist/vercel/codec/decoder.d.ts +0 -9
  162. package/dist/vercel/codec/encoder.d.ts +0 -11
  163. package/src/core/transport/branch-chain.ts +0 -58
  164. package/src/core/transport/load-conversation.ts +0 -355
  165. package/src/vercel/codec/decoder.ts +0 -696
  166. package/src/vercel/codec/encoder.ts +0 -548
@@ -1,696 +0,0 @@
1
- /**
2
- * Vercel AI SDK Decoder.
3
- *
4
- * Maps Ably inbound messages to {@link DecodedMessage} — a `{ inputs,
5
- * outputs }` tagged result. The decoder routes by the wire `name`
6
- * (`ai-input` vs `ai-output`) so the SDK never has to inspect direction:
7
- * input-side messages produce `VercelInput` variants; output-side
8
- * messages produce `VercelOutput` (`UIMessageChunk`) variants.
9
- *
10
- * The `LifecycleTracker` is an internal helper used to pre-roll missing
11
- * `start` / `start-step` chunks on mid-stream join (history compaction,
12
- * rewind miss, partial page) so the reducer always sees a clean event
13
- * sequence for streamed output.
14
- *
15
- * Receive-side dispatch reads the wire `name` first and then routes by
16
- * the codec `type` header carrying the codec event type. Codec headers live
17
- * under `extras.ai.codec` and transport headers under `extras.ai.transport`;
18
- * both are read unprefixed from their respective tier.
19
- */
20
-
21
- import type * as Ably from 'ably';
22
- import type * as AI from 'ai';
23
-
24
- import {
25
- EVENT_AI_INPUT,
26
- EVENT_AI_OUTPUT,
27
- HEADER_CODEC_MESSAGE_ID,
28
- HEADER_DISCRETE,
29
- HEADER_ROLE,
30
- HEADER_RUN_ID,
31
- } from '../../constants.js';
32
- import type { DecoderCore, DecoderCoreHooks, DecoderCoreOptions } from '../../core/codec/decoder.js';
33
- import { createDecoderCore } from '../../core/codec/decoder.js';
34
- import type { LifecycleTracker } from '../../core/codec/lifecycle-tracker.js';
35
- import { createLifecycleTracker } from '../../core/codec/lifecycle-tracker.js';
36
- import type {
37
- DecodedMessage,
38
- Decoder,
39
- MessagePayload,
40
- StreamTrackerState,
41
- UserMessage,
42
- } from '../../core/codec/types.js';
43
- import { type DomainHeaderReader, headerReader as rawHeaderReader, stripUndefined } from '../../utils.js';
44
- import type { VercelInput, VercelOutput } from './events.js';
45
-
46
- // Decoder-internal union — the codec emits inputs and outputs through the
47
- // same flat list from the underlying core and partitions on the way out.
48
- type AnyEvent = VercelInput | VercelOutput;
49
-
50
- // ---------------------------------------------------------------------------
51
- // Vercel-specific header reader (casts providerMetadata to AI.ProviderMetadata)
52
- // ---------------------------------------------------------------------------
53
-
54
- interface VercelHeaderReader extends DomainHeaderReader {
55
- /** Read the `providerMetadata` domain header, cast to the AI SDK type. */
56
- providerMetadata(): AI.ProviderMetadata | undefined;
57
- }
58
-
59
- /**
60
- * Create a header reader that adds Vercel-specific `providerMetadata` typing.
61
- * @param headers - The raw headers record to read domain headers from.
62
- * @returns A typed accessor with Vercel-specific providerMetadata typing.
63
- */
64
- const headerReader = (headers: Record<string, string>): VercelHeaderReader => {
65
- const base = rawHeaderReader(headers);
66
- return {
67
- ...base,
68
- // CAST: Trust boundary — the encoder serialized a valid ProviderMetadata value.
69
- providerMetadata: () => base.json('providerMetadata') as AI.ProviderMetadata | undefined,
70
- };
71
- };
72
-
73
- // ---------------------------------------------------------------------------
74
- // Wire format types (trust boundaries for JSON-parsed data)
75
- // ---------------------------------------------------------------------------
76
-
77
- /** Wire format for the agent-side `tool-input-error` chunk data payload. */
78
- interface ToolInputErrorWireData {
79
- errorText?: string;
80
- input?: unknown;
81
- }
82
-
83
- /** Wire format for the `tool-output-available` (agent) / `tool-result` (client) data payload. */
84
- interface ToolOutputAvailableWireData {
85
- output?: unknown;
86
- }
87
-
88
- /** Wire format for the agent-side `tool-output-error` chunk data payload. */
89
- interface AgentToolOutputErrorWireData {
90
- errorText?: string;
91
- }
92
-
93
- /** Wire format for the client-side `tool-result-error` input data payload. */
94
- interface ClientToolResultErrorWireData {
95
- message?: string;
96
- }
97
-
98
- // ---------------------------------------------------------------------------
99
- // JSON boundary helpers
100
- // ---------------------------------------------------------------------------
101
-
102
- const parseFinishReason = (value: string | undefined, fallback: AI.FinishReason): AI.FinishReason => {
103
- if (
104
- value === 'stop' ||
105
- value === 'length' ||
106
- value === 'content-filter' ||
107
- value === 'tool-calls' ||
108
- value === 'error' ||
109
- value === 'other'
110
- ) {
111
- return value;
112
- }
113
- return fallback;
114
- };
115
-
116
- const isDataEventName = (name: string): name is `data-${string}` => name.startsWith('data-');
117
-
118
- const parseJsonOrString = (value: string): unknown => {
119
- if (!value) return undefined;
120
- try {
121
- // CAST: JSON.parse returns any; unknown is the safe trust-boundary type.
122
- return JSON.parse(value) as unknown;
123
- } catch {
124
- return value;
125
- }
126
- };
127
-
128
- // ---------------------------------------------------------------------------
129
- // Streamed message event builders (output-side)
130
- // ---------------------------------------------------------------------------
131
-
132
- /**
133
- * Read the codec event type from a tracker's codec headers. The encoder
134
- * stamps the codec `type` header on every `ai-output` publish; the value
135
- * carries the AI-SDK chunk family (`text` / `reasoning` / `tool-input`)
136
- * that the stream represents.
137
- * @param tracker - The stream tracker carrying the persistent headers.
138
- * @returns The codec event type, or the empty string when absent.
139
- */
140
- const codecTypeOf = (tracker: StreamTrackerState): string => headerReader(tracker.codecHeaders).strOr('type', '');
141
-
142
- const buildStartChunk = (tracker: StreamTrackerState): AI.UIMessageChunk => {
143
- const r = headerReader(tracker.codecHeaders);
144
- switch (codecTypeOf(tracker)) {
145
- case 'text': {
146
- return stripUndefined({
147
- type: 'text-start' as const,
148
- id: tracker.streamId,
149
- providerMetadata: r.providerMetadata(),
150
- });
151
- }
152
- case 'reasoning': {
153
- return stripUndefined({
154
- type: 'reasoning-start' as const,
155
- id: tracker.streamId,
156
- providerMetadata: r.providerMetadata(),
157
- });
158
- }
159
- case 'tool-input': {
160
- return stripUndefined({
161
- type: 'tool-input-start' as const,
162
- toolCallId: tracker.streamId,
163
- toolName: r.strOr('toolName', ''),
164
- dynamic: r.bool('dynamic'),
165
- title: r.str('title'),
166
- providerExecuted: r.bool('providerExecuted'),
167
- providerMetadata: r.providerMetadata(),
168
- });
169
- }
170
- default: {
171
- return { type: 'text-start', id: tracker.streamId };
172
- }
173
- }
174
- };
175
-
176
- const buildDeltaChunk = (tracker: StreamTrackerState, delta: string): AI.UIMessageChunk => {
177
- switch (codecTypeOf(tracker)) {
178
- case 'text': {
179
- return { type: 'text-delta', id: tracker.streamId, delta };
180
- }
181
- case 'reasoning': {
182
- return { type: 'reasoning-delta', id: tracker.streamId, delta };
183
- }
184
- case 'tool-input': {
185
- return { type: 'tool-input-delta', toolCallId: tracker.streamId, inputTextDelta: delta };
186
- }
187
- default: {
188
- return { type: 'text-delta', id: tracker.streamId, delta };
189
- }
190
- }
191
- };
192
-
193
- const buildEndChunk = (tracker: StreamTrackerState, closingHeaders: Record<string, string>): AI.UIMessageChunk => {
194
- const r = headerReader(closingHeaders);
195
- switch (codecTypeOf(tracker)) {
196
- case 'text': {
197
- return stripUndefined({
198
- type: 'text-end' as const,
199
- id: tracker.streamId,
200
- providerMetadata: r.providerMetadata(),
201
- });
202
- }
203
- case 'reasoning': {
204
- return stripUndefined({
205
- type: 'reasoning-end' as const,
206
- id: tracker.streamId,
207
- providerMetadata: r.providerMetadata(),
208
- });
209
- }
210
- case 'tool-input': {
211
- return stripUndefined({
212
- type: 'tool-input-available' as const,
213
- toolCallId: tracker.streamId,
214
- toolName: r.strOr('toolName', headerReader(tracker.codecHeaders).strOr('toolName', '')),
215
- input: parseJsonOrString(tracker.accumulated),
216
- providerMetadata: r.providerMetadata(),
217
- });
218
- }
219
- default: {
220
- return { type: 'text-end', id: tracker.streamId };
221
- }
222
- }
223
- };
224
-
225
- // ---------------------------------------------------------------------------
226
- // Lifecycle tracker configuration (synthetic event phases on mid-stream join)
227
- // ---------------------------------------------------------------------------
228
-
229
- const createVercelLifecycleTracker = (): LifecycleTracker<AI.UIMessageChunk> =>
230
- createLifecycleTracker<AI.UIMessageChunk>([
231
- {
232
- key: 'start',
233
- build: (ctx) => [stripUndefined({ type: 'start' as const, messageId: ctx.messageId })],
234
- },
235
- {
236
- key: 'start-step',
237
- build: () => [{ type: 'start-step' as const }],
238
- },
239
- ]);
240
-
241
- // ---------------------------------------------------------------------------
242
- // Discrete output decoders (ai-output → UIMessageChunk)
243
- // ---------------------------------------------------------------------------
244
-
245
- const decodeStart = (
246
- r: VercelHeaderReader,
247
- runId: string,
248
- lifecycle: LifecycleTracker<AI.UIMessageChunk>,
249
- ): AI.UIMessageChunk[] => {
250
- lifecycle.markEmitted(runId, 'start');
251
- return [
252
- stripUndefined({
253
- type: 'start' as const,
254
- messageId: r.str('messageId'),
255
- messageMetadata: r.json('messageMetadata'),
256
- }),
257
- ];
258
- };
259
-
260
- const decodeStartStep = (runId: string, lifecycle: LifecycleTracker<AI.UIMessageChunk>): AI.UIMessageChunk[] => {
261
- lifecycle.markEmitted(runId, 'start-step');
262
- return [{ type: 'start-step' }];
263
- };
264
-
265
- const decodeFinishStep = (runId: string, lifecycle: LifecycleTracker<AI.UIMessageChunk>): AI.UIMessageChunk[] => {
266
- lifecycle.resetPhase(runId, 'start-step');
267
- return [{ type: 'finish-step' }];
268
- };
269
-
270
- const decodeFinish = (
271
- r: VercelHeaderReader,
272
- runId: string,
273
- lifecycle: LifecycleTracker<AI.UIMessageChunk>,
274
- ): AI.UIMessageChunk[] => {
275
- lifecycle.clearScope(runId);
276
- return [
277
- stripUndefined({
278
- type: 'finish' as const,
279
- finishReason: parseFinishReason(r.str('finishReason'), 'stop'),
280
- messageMetadata: r.json('messageMetadata'),
281
- }),
282
- ];
283
- };
284
-
285
- const decodeError = (
286
- data: unknown,
287
- runId: string,
288
- lifecycle: LifecycleTracker<AI.UIMessageChunk>,
289
- ): AI.UIMessageChunk[] => {
290
- lifecycle.clearScope(runId);
291
- const errorText = typeof data === 'string' ? data : '';
292
- return [{ type: 'error', errorText }];
293
- };
294
-
295
- const decodeAbort = (
296
- data: unknown,
297
- runId: string,
298
- lifecycle: LifecycleTracker<AI.UIMessageChunk>,
299
- ): AI.UIMessageChunk[] => {
300
- lifecycle.clearScope(runId);
301
- const reason = typeof data === 'string' && data ? data : undefined;
302
- return [stripUndefined({ type: 'abort' as const, reason })];
303
- };
304
-
305
- const decodeMessageMetadata = (r: VercelHeaderReader): AI.UIMessageChunk[] => [
306
- { type: 'message-metadata', messageMetadata: r.json('messageMetadata') },
307
- ];
308
-
309
- const decodeFile = (r: VercelHeaderReader, data: unknown): AI.UIMessageChunk[] => [
310
- stripUndefined({
311
- type: 'file' as const,
312
- url: typeof data === 'string' ? data : '',
313
- mediaType: r.strOr('mediaType', ''),
314
- providerMetadata: r.providerMetadata(),
315
- }),
316
- ];
317
-
318
- const decodeSourceUrl = (r: VercelHeaderReader, data: unknown): AI.UIMessageChunk[] => [
319
- stripUndefined({
320
- type: 'source-url' as const,
321
- sourceId: r.strOr('sourceId', ''),
322
- url: typeof data === 'string' ? data : '',
323
- title: r.str('title'),
324
- providerMetadata: r.providerMetadata(),
325
- }),
326
- ];
327
-
328
- const decodeSourceDocument = (r: VercelHeaderReader): AI.UIMessageChunk[] => [
329
- stripUndefined({
330
- type: 'source-document' as const,
331
- sourceId: r.strOr('sourceId', ''),
332
- mediaType: r.strOr('mediaType', ''),
333
- title: r.strOr('title', ''),
334
- filename: r.str('filename'),
335
- providerMetadata: r.providerMetadata(),
336
- }),
337
- ];
338
-
339
- const decodeToolInputError = (r: VercelHeaderReader, data: unknown): AI.UIMessageChunk[] => {
340
- // CAST: Trust boundary — encoder produced the expected object shape.
341
- const parsed = data as ToolInputErrorWireData | undefined;
342
- return [
343
- stripUndefined({
344
- type: 'tool-input-error' as const,
345
- toolCallId: r.strOr('toolCallId', ''),
346
- toolName: r.strOr('toolName', ''),
347
- errorText: parsed?.errorText ?? '',
348
- input: parsed?.input,
349
- dynamic: r.bool('dynamic'),
350
- title: r.str('title'),
351
- providerExecuted: r.bool('providerExecuted'),
352
- providerMetadata: r.providerMetadata(),
353
- }),
354
- ];
355
- };
356
-
357
- const decodeAgentToolOutputAvailable = (r: VercelHeaderReader, data: unknown): AI.UIMessageChunk[] => {
358
- // CAST: Trust boundary — encoder produced the expected object shape.
359
- const parsed = data as ToolOutputAvailableWireData | undefined;
360
- return [
361
- stripUndefined({
362
- type: 'tool-output-available' as const,
363
- toolCallId: r.strOr('toolCallId', ''),
364
- output: parsed?.output,
365
- dynamic: r.bool('dynamic'),
366
- providerExecuted: r.bool('providerExecuted'),
367
- preliminary: r.bool('preliminary'),
368
- }),
369
- ];
370
- };
371
-
372
- const decodeAgentToolOutputError = (r: VercelHeaderReader, data: unknown): AI.UIMessageChunk[] => {
373
- // CAST: Trust boundary — encoder produced the expected object shape.
374
- const parsed = data as AgentToolOutputErrorWireData | undefined;
375
- return [
376
- stripUndefined({
377
- type: 'tool-output-error' as const,
378
- toolCallId: r.strOr('toolCallId', ''),
379
- errorText: parsed?.errorText ?? '',
380
- dynamic: r.bool('dynamic'),
381
- providerExecuted: r.bool('providerExecuted'),
382
- }),
383
- ];
384
- };
385
-
386
- const decodeToolApprovalRequest = (r: VercelHeaderReader): AI.UIMessageChunk[] => [
387
- {
388
- type: 'tool-approval-request',
389
- toolCallId: r.strOr('toolCallId', ''),
390
- approvalId: r.strOr('approvalId', ''),
391
- },
392
- ];
393
-
394
- const decodeToolOutputDenied = (r: VercelHeaderReader): AI.UIMessageChunk[] => [
395
- { type: 'tool-output-denied', toolCallId: r.strOr('toolCallId', '') },
396
- ];
397
-
398
- const decodeDataEvent = (name: `data-${string}`, r: VercelHeaderReader, data: unknown): AI.UIMessageChunk[] => [
399
- stripUndefined({
400
- type: name,
401
- data,
402
- id: r.str('id'),
403
- transient: r.bool('transient'),
404
- }),
405
- ];
406
-
407
- // ---------------------------------------------------------------------------
408
- // Non-streaming tool-input helper (agent-side)
409
- // ---------------------------------------------------------------------------
410
-
411
- const decodeNonStreamingToolInput = (
412
- r: VercelHeaderReader,
413
- data: unknown,
414
- runId: string,
415
- lifecycle: LifecycleTracker<AI.UIMessageChunk>,
416
- ): AI.UIMessageChunk[] => [
417
- ...lifecycle.ensurePhases(runId, { messageId: r.str('messageId') }),
418
- stripUndefined({
419
- type: 'tool-input-start' as const,
420
- toolCallId: r.strOr('toolCallId', ''),
421
- toolName: r.strOr('toolName', ''),
422
- dynamic: r.bool('dynamic'),
423
- title: r.str('title'),
424
- providerExecuted: r.bool('providerExecuted'),
425
- providerMetadata: r.providerMetadata(),
426
- }),
427
- stripUndefined({
428
- type: 'tool-input-available' as const,
429
- toolCallId: r.strOr('toolCallId', ''),
430
- toolName: r.strOr('toolName', ''),
431
- input: data,
432
- providerMetadata: r.providerMetadata(),
433
- }),
434
- ];
435
-
436
- // ---------------------------------------------------------------------------
437
- // Input-side decoders (ai-input → VercelInput)
438
- // ---------------------------------------------------------------------------
439
-
440
- /**
441
- * Decode a single discrete message part (from the user-message multi-part
442
- * wire format) into a {@link UserMessage} carrying a one-part
443
- * UIMessage. The reducer's `_foldUserMessage` merges parts that share
444
- * the same codec-message-id.
445
- * @param input - The discrete message payload (name, data, headers).
446
- * @returns A single `user-message` input, or an empty array when the part type is unrecognised.
447
- */
448
- const decodeDiscreteMessagePart = (input: MessagePayload): VercelInput[] => {
449
- const r = headerReader(input.codecHeaders ?? {});
450
- const role = (input.transportHeaders?.[HEADER_ROLE] ?? 'user') as AI.UIMessage['role'];
451
- const messageId = r.str('messageId') ?? '';
452
- const codecType = r.strOr('type', '');
453
-
454
- let part: AI.UIMessage['parts'][number] | undefined;
455
-
456
- switch (codecType) {
457
- case 'text': {
458
- part = { type: 'text', text: typeof input.data === 'string' ? input.data : '' };
459
- break;
460
- }
461
- case 'file': {
462
- part = {
463
- type: 'file',
464
- mediaType: r.strOr('mediaType', ''),
465
- url: typeof input.data === 'string' ? input.data : '',
466
- };
467
- break;
468
- }
469
- default: {
470
- if (isDataEventName(codecType)) {
471
- part = stripUndefined({ type: codecType, id: r.str('id'), data: input.data });
472
- }
473
- break;
474
- }
475
- }
476
-
477
- if (!part) return [];
478
-
479
- const message: AI.UIMessage = { id: messageId, role, parts: [part] };
480
- const userMessage: UserMessage<AI.UIMessage> = { kind: 'user-message', message };
481
- return [userMessage];
482
- };
483
-
484
- const isDiscreteMessagePart = (codecType: string, headers: Record<string, string>): boolean =>
485
- (codecType === 'text' || codecType === 'file' || isDataEventName(codecType)) && HEADER_DISCRETE in headers;
486
-
487
- const decodeClientToolResult = (codecMessageId: string, r: VercelHeaderReader, data: unknown): VercelInput[] => {
488
- // CAST: Trust boundary — encoder produced the expected object shape.
489
- const parsed = data as ToolOutputAvailableWireData | undefined;
490
- return [
491
- {
492
- kind: 'tool-result',
493
- codecMessageId,
494
- payload: { toolCallId: r.strOr('toolCallId', ''), output: parsed?.output },
495
- },
496
- ];
497
- };
498
-
499
- const decodeClientToolResultError = (codecMessageId: string, r: VercelHeaderReader, data: unknown): VercelInput[] => {
500
- // CAST: Trust boundary — encoder produced the expected object shape.
501
- const parsed = data as ClientToolResultErrorWireData | undefined;
502
- return [
503
- {
504
- kind: 'tool-result-error',
505
- codecMessageId,
506
- payload: { toolCallId: r.strOr('toolCallId', ''), message: parsed?.message ?? '' },
507
- },
508
- ];
509
- };
510
-
511
- const decodeClientToolApprovalResponse = (codecMessageId: string, r: VercelHeaderReader): VercelInput[] => [
512
- {
513
- kind: 'tool-approval-response',
514
- codecMessageId,
515
- payload: stripUndefined({
516
- toolCallId: r.strOr('toolCallId', ''),
517
- approved: r.bool('approved') ?? false,
518
- reason: r.str('reason'),
519
- }),
520
- },
521
- ];
522
-
523
- // ---------------------------------------------------------------------------
524
- // Discrete payload dispatch
525
- // ---------------------------------------------------------------------------
526
-
527
- const decodeAiOutputPayload = (
528
- codecType: string,
529
- r: VercelHeaderReader,
530
- data: unknown,
531
- runId: string,
532
- lifecycle: LifecycleTracker<AI.UIMessageChunk>,
533
- ): AnyEvent[] => {
534
- switch (codecType) {
535
- case 'start': {
536
- return decodeStart(r, runId, lifecycle);
537
- }
538
- case 'start-step': {
539
- return decodeStartStep(runId, lifecycle);
540
- }
541
- case 'finish-step': {
542
- return decodeFinishStep(runId, lifecycle);
543
- }
544
- case 'finish': {
545
- return decodeFinish(r, runId, lifecycle);
546
- }
547
- case 'error': {
548
- return decodeError(data, runId, lifecycle);
549
- }
550
- case 'abort': {
551
- return decodeAbort(data, runId, lifecycle);
552
- }
553
- case 'message-metadata': {
554
- return decodeMessageMetadata(r);
555
- }
556
- case 'file': {
557
- return decodeFile(r, data);
558
- }
559
- case 'source-url': {
560
- return decodeSourceUrl(r, data);
561
- }
562
- case 'source-document': {
563
- return decodeSourceDocument(r);
564
- }
565
- case 'tool-input': {
566
- return decodeNonStreamingToolInput(r, data, runId, lifecycle);
567
- }
568
- case 'tool-input-error': {
569
- return decodeToolInputError(r, data);
570
- }
571
- case 'tool-output-available': {
572
- return decodeAgentToolOutputAvailable(r, data);
573
- }
574
- case 'tool-output-error': {
575
- return decodeAgentToolOutputError(r, data);
576
- }
577
- case 'tool-approval-request': {
578
- return decodeToolApprovalRequest(r);
579
- }
580
- case 'tool-output-denied': {
581
- return decodeToolOutputDenied(r);
582
- }
583
- default: {
584
- return isDataEventName(codecType) ? decodeDataEvent(codecType, r, data) : [];
585
- }
586
- }
587
- };
588
-
589
- const decodeAiInputPayload = (codecType: string, input: MessagePayload, r: VercelHeaderReader): AnyEvent[] => {
590
- // Multi-part user-message parts (text / file / data-*) carry discrete
591
- // because they ride publishDiscreteBatch; the receive-side fans them back
592
- // out into a UserMessage.
593
- if (isDiscreteMessagePart(codecType, input.transportHeaders ?? {})) {
594
- return decodeDiscreteMessagePart(input);
595
- }
596
-
597
- const codecMessageId = input.transportHeaders?.[HEADER_CODEC_MESSAGE_ID] ?? '';
598
-
599
- switch (codecType) {
600
- case 'tool-result': {
601
- return decodeClientToolResult(codecMessageId, r, input.data);
602
- }
603
- case 'tool-result-error': {
604
- return decodeClientToolResultError(codecMessageId, r, input.data);
605
- }
606
- case 'tool-approval-response': {
607
- return decodeClientToolApprovalResponse(codecMessageId, r);
608
- }
609
- case 'regenerate': {
610
- // Wire-only signal — carries `parent` / `msg-regenerate` on transport
611
- // headers, no domain payload. The agent's input-event lookup reads
612
- // transport headers directly from the inbound Ably message; no
613
- // projection fold is needed here.
614
- return [];
615
- }
616
- default: {
617
- return [];
618
- }
619
- }
620
- };
621
-
622
- const decodeDiscretePayload = (input: MessagePayload, lifecycle: LifecycleTracker<AI.UIMessageChunk>): AnyEvent[] => {
623
- const r = headerReader(input.codecHeaders ?? {});
624
- const runId = input.transportHeaders?.[HEADER_RUN_ID] ?? '';
625
- const codecType = r.strOr('type', '');
626
-
627
- if (input.name === EVENT_AI_INPUT) {
628
- return decodeAiInputPayload(codecType, input, r);
629
- }
630
-
631
- if (input.name === EVENT_AI_OUTPUT) {
632
- return decodeAiOutputPayload(codecType, r, input.data, runId, lifecycle);
633
- }
634
-
635
- return [];
636
- };
637
-
638
- // ---------------------------------------------------------------------------
639
- // Decoder core hooks
640
- // ---------------------------------------------------------------------------
641
-
642
- const createHooks = (lifecycle: LifecycleTracker<AI.UIMessageChunk>): DecoderCoreHooks<AnyEvent> => ({
643
- buildStartEvents: (tracker: StreamTrackerState): AnyEvent[] => {
644
- const runId = tracker.transportHeaders[HEADER_RUN_ID] ?? '';
645
- const messageId = headerReader(tracker.codecHeaders).str('messageId');
646
- return [...lifecycle.ensurePhases(runId, { messageId }), buildStartChunk(tracker)];
647
- },
648
-
649
- buildDeltaEvents: (tracker: StreamTrackerState, delta: string): AnyEvent[] => [buildDeltaChunk(tracker, delta)],
650
-
651
- buildEndEvents: (tracker: StreamTrackerState, closingHeaders: Record<string, string>): AnyEvent[] => [
652
- buildEndChunk(tracker, closingHeaders),
653
- ],
654
-
655
- decodeDiscrete: (payload: MessagePayload): AnyEvent[] => decodeDiscretePayload(payload, lifecycle),
656
- });
657
-
658
- // ---------------------------------------------------------------------------
659
- // Default implementation
660
- // ---------------------------------------------------------------------------
661
-
662
- const isInput = (event: AnyEvent): event is VercelInput => 'kind' in event;
663
-
664
- class DefaultUIMessageDecoder implements Decoder<VercelInput, VercelOutput> {
665
- private readonly _core: DecoderCore<AnyEvent>;
666
-
667
- constructor(options: DecoderCoreOptions = {}) {
668
- this._core = createDecoderCore<AnyEvent>(createHooks(createVercelLifecycleTracker()), options);
669
- }
670
-
671
- decode(message: Ably.InboundMessage): DecodedMessage<VercelInput, VercelOutput> {
672
- const events = this._core.decode(message);
673
- const inputs: VercelInput[] = [];
674
- const outputs: VercelOutput[] = [];
675
- for (const event of events) {
676
- if (isInput(event)) {
677
- inputs.push(event);
678
- } else {
679
- outputs.push(event);
680
- }
681
- }
682
- return { inputs, outputs };
683
- }
684
- }
685
-
686
- // ---------------------------------------------------------------------------
687
- // Factory
688
- // ---------------------------------------------------------------------------
689
-
690
- /**
691
- * Create a Vercel AI SDK decoder that maps Ably messages to {@link DecodedMessage}.
692
- * @param options - Decoder configuration (callbacks, logger).
693
- * @returns A {@link Decoder} typed in both directions for the Vercel codec.
694
- */
695
- export const createDecoder = (options: DecoderCoreOptions = {}): Decoder<VercelInput, VercelOutput> =>
696
- new DefaultUIMessageDecoder(options);