@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,26 +1,51 @@
1
1
  /**
2
- * Vercel AI SDK Decoder
2
+ * Vercel AI SDK Decoder.
3
3
  *
4
- * Maps Ably inbound messages to DecoderOutput<UIMessageChunk, UIMessage>[].
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.
5
9
  *
6
- * Delegates action dispatch and serial tracking to the decoder core.
7
- * This file contains only the Vercel-specific event building, discrete
8
- * event decoding, and synthetic event emission.
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.
9
14
  *
10
- * Domain-specific headers use the `x-domain-` prefix. Transport-level
11
- * headers use the `x-ably-` prefix.
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.
12
19
  */
13
20
 
14
21
  import type * as Ably from 'ably';
15
22
  import type * as AI from 'ai';
16
23
 
17
- import { HEADER_DISCRETE, HEADER_ROLE, HEADER_TURN_ID } from '../../constants.js';
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';
18
32
  import type { DecoderCore, DecoderCoreHooks, DecoderCoreOptions } from '../../core/codec/decoder.js';
19
- import { createDecoderCore, eventOutput } from '../../core/codec/decoder.js';
33
+ import { createDecoderCore } from '../../core/codec/decoder.js';
20
34
  import type { LifecycleTracker } from '../../core/codec/lifecycle-tracker.js';
21
35
  import { createLifecycleTracker } from '../../core/codec/lifecycle-tracker.js';
22
- import type { DecoderOutput, MessagePayload, StreamDecoder, StreamTrackerState } from '../../core/codec/types.js';
36
+ import type {
37
+ DecodedMessage,
38
+ Decoder,
39
+ MessagePayload,
40
+ StreamTrackerState,
41
+ UserMessage,
42
+ } from '../../core/codec/types.js';
23
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;
24
49
 
25
50
  // ---------------------------------------------------------------------------
26
51
  // Vercel-specific header reader (casts providerMetadata to AI.ProviderMetadata)
@@ -49,45 +74,31 @@ const headerReader = (headers: Record<string, string>): VercelHeaderReader => {
49
74
  // Wire format types (trust boundaries for JSON-parsed data)
50
75
  // ---------------------------------------------------------------------------
51
76
 
52
- /** Wire format for tool-input-error data payload. */
77
+ /** Wire format for the agent-side `tool-input-error` chunk data payload. */
53
78
  interface ToolInputErrorWireData {
54
79
  errorText?: string;
55
80
  input?: unknown;
56
81
  }
57
82
 
58
- /** Wire format for tool-output-available data payload. */
83
+ /** Wire format for the `tool-output-available` (agent) / `tool-result` (client) data payload. */
59
84
  interface ToolOutputAvailableWireData {
60
85
  output?: unknown;
61
86
  }
62
87
 
63
- /** Wire format for tool-output-error data payload. */
64
- interface ToolOutputErrorWireData {
88
+ /** Wire format for the agent-side `tool-output-error` chunk data payload. */
89
+ interface AgentToolOutputErrorWireData {
65
90
  errorText?: string;
66
91
  }
67
92
 
68
- // ---------------------------------------------------------------------------
69
- // Shared output type alias
70
- // ---------------------------------------------------------------------------
71
-
72
- type Out = DecoderOutput<AI.UIMessageChunk, AI.UIMessage>;
73
-
74
- /**
75
- * Bind eventOutput to the Vercel domain types.
76
- * @param chunk - The UIMessageChunk to wrap.
77
- * @returns A single-element decoder output array.
78
- */
79
- const event = (chunk: AI.UIMessageChunk): Out[] => eventOutput<AI.UIMessageChunk, AI.UIMessage>(chunk);
93
+ /** Wire format for the client-side `tool-result-error` input data payload. */
94
+ interface ClientToolResultErrorWireData {
95
+ message?: string;
96
+ }
80
97
 
81
98
  // ---------------------------------------------------------------------------
82
99
  // JSON boundary helpers
83
100
  // ---------------------------------------------------------------------------
84
101
 
85
- /**
86
- * Validate a finish reason string against the FinishReason union.
87
- * @param value - The finish reason string from the wire, or undefined.
88
- * @param fallback - Default finish reason if the value is not recognized.
89
- * @returns The validated FinishReason.
90
- */
91
102
  const parseFinishReason = (value: string | undefined, fallback: AI.FinishReason): AI.FinishReason => {
92
103
  if (
93
104
  value === 'stop' ||
@@ -102,19 +113,8 @@ const parseFinishReason = (value: string | undefined, fallback: AI.FinishReason)
102
113
  return fallback;
103
114
  };
104
115
 
105
- /**
106
- * Type predicate for data-* message names.
107
- * @param name - The message name to check.
108
- * @returns True if the name starts with "data-".
109
- */
110
116
  const isDataEventName = (name: string): name is `data-${string}` => name.startsWith('data-');
111
117
 
112
- /**
113
- * Parse a string as JSON, returning the raw string if parsing fails
114
- * or undefined if empty.
115
- * @param value - The string to attempt JSON parsing on.
116
- * @returns The parsed value, the raw string on parse failure, or undefined if empty.
117
- */
118
118
  const parseJsonOrString = (value: string): unknown => {
119
119
  if (!value) return undefined;
120
120
  try {
@@ -126,12 +126,22 @@ const parseJsonOrString = (value: string): unknown => {
126
126
  };
127
127
 
128
128
  // ---------------------------------------------------------------------------
129
- // Streamed message event builders
129
+ // Streamed message event builders (output-side)
130
130
  // ---------------------------------------------------------------------------
131
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
+
132
142
  const buildStartChunk = (tracker: StreamTrackerState): AI.UIMessageChunk => {
133
- const r = headerReader(tracker.headers);
134
- switch (tracker.name) {
143
+ const r = headerReader(tracker.codecHeaders);
144
+ switch (codecTypeOf(tracker)) {
135
145
  case 'text': {
136
146
  return stripUndefined({
137
147
  type: 'text-start' as const,
@@ -164,7 +174,7 @@ const buildStartChunk = (tracker: StreamTrackerState): AI.UIMessageChunk => {
164
174
  };
165
175
 
166
176
  const buildDeltaChunk = (tracker: StreamTrackerState, delta: string): AI.UIMessageChunk => {
167
- switch (tracker.name) {
177
+ switch (codecTypeOf(tracker)) {
168
178
  case 'text': {
169
179
  return { type: 'text-delta', id: tracker.streamId, delta };
170
180
  }
@@ -182,7 +192,7 @@ const buildDeltaChunk = (tracker: StreamTrackerState, delta: string): AI.UIMessa
182
192
 
183
193
  const buildEndChunk = (tracker: StreamTrackerState, closingHeaders: Record<string, string>): AI.UIMessageChunk => {
184
194
  const r = headerReader(closingHeaders);
185
- switch (tracker.name) {
195
+ switch (codecTypeOf(tracker)) {
186
196
  case 'text': {
187
197
  return stripUndefined({
188
198
  type: 'text-end' as const,
@@ -201,7 +211,7 @@ const buildEndChunk = (tracker: StreamTrackerState, closingHeaders: Record<strin
201
211
  return stripUndefined({
202
212
  type: 'tool-input-available' as const,
203
213
  toolCallId: tracker.streamId,
204
- toolName: r.strOr('toolName', headerReader(tracker.headers).strOr('toolName', '')),
214
+ toolName: r.strOr('toolName', headerReader(tracker.codecHeaders).strOr('toolName', '')),
205
215
  input: parseJsonOrString(tracker.accumulated),
206
216
  providerMetadata: r.providerMetadata(),
207
217
  });
@@ -213,7 +223,7 @@ const buildEndChunk = (tracker: StreamTrackerState, closingHeaders: Record<strin
213
223
  };
214
224
 
215
225
  // ---------------------------------------------------------------------------
216
- // Lifecycle tracker configuration (synthetic event phases)
226
+ // Lifecycle tracker configuration (synthetic event phases on mid-stream join)
217
227
  // ---------------------------------------------------------------------------
218
228
 
219
229
  const createVercelLifecycleTracker = (): LifecycleTracker<AI.UIMessageChunk> =>
@@ -228,107 +238,108 @@ const createVercelLifecycleTracker = (): LifecycleTracker<AI.UIMessageChunk> =>
228
238
  },
229
239
  ]);
230
240
 
231
- /**
232
- * Run the lifecycle tracker and wrap results as DecoderOutput events.
233
- * @param lifecycle - The lifecycle tracker instance.
234
- * @param turnId - The turn scope ID.
235
- * @param context - Context passed through to phase build functions.
236
- * @returns Decoder outputs for any synthesized lifecycle events.
237
- */
238
- const ensurePhases = (
239
- lifecycle: LifecycleTracker<AI.UIMessageChunk>,
240
- turnId: string,
241
- context: Record<string, string | undefined>,
242
- ): Out[] => lifecycle.ensurePhases(turnId, context).map((e) => ({ kind: 'event', event: e }));
243
-
244
241
  // ---------------------------------------------------------------------------
245
- // Discrete event decoders (one function per event type)
242
+ // Discrete output decoders (ai-output UIMessageChunk)
246
243
  // ---------------------------------------------------------------------------
247
244
 
248
- const decodeStart = (r: VercelHeaderReader, turnId: string, lifecycle: LifecycleTracker<AI.UIMessageChunk>): Out[] => {
249
- lifecycle.markEmitted(turnId, 'start');
250
- return event(
245
+ const decodeStart = (
246
+ r: VercelHeaderReader,
247
+ runId: string,
248
+ lifecycle: LifecycleTracker<AI.UIMessageChunk>,
249
+ ): AI.UIMessageChunk[] => {
250
+ lifecycle.markEmitted(runId, 'start');
251
+ return [
251
252
  stripUndefined({
252
253
  type: 'start' as const,
253
254
  messageId: r.str('messageId'),
254
255
  messageMetadata: r.json('messageMetadata'),
255
256
  }),
256
- );
257
+ ];
257
258
  };
258
259
 
259
- const decodeStartStep = (turnId: string, lifecycle: LifecycleTracker<AI.UIMessageChunk>): Out[] => {
260
- lifecycle.markEmitted(turnId, 'start-step');
261
- return event({ type: 'start-step' });
260
+ const decodeStartStep = (runId: string, lifecycle: LifecycleTracker<AI.UIMessageChunk>): AI.UIMessageChunk[] => {
261
+ lifecycle.markEmitted(runId, 'start-step');
262
+ return [{ type: 'start-step' }];
262
263
  };
263
264
 
264
- const decodeFinishStep = (turnId: string, lifecycle: LifecycleTracker<AI.UIMessageChunk>): Out[] => {
265
- lifecycle.resetPhase(turnId, 'start-step');
266
- return event({ type: 'finish-step' });
265
+ const decodeFinishStep = (runId: string, lifecycle: LifecycleTracker<AI.UIMessageChunk>): AI.UIMessageChunk[] => {
266
+ lifecycle.resetPhase(runId, 'start-step');
267
+ return [{ type: 'finish-step' }];
267
268
  };
268
269
 
269
- const decodeFinish = (r: VercelHeaderReader, turnId: string, lifecycle: LifecycleTracker<AI.UIMessageChunk>): Out[] => {
270
- lifecycle.clearScope(turnId);
271
- return event(
270
+ const decodeFinish = (
271
+ r: VercelHeaderReader,
272
+ runId: string,
273
+ lifecycle: LifecycleTracker<AI.UIMessageChunk>,
274
+ ): AI.UIMessageChunk[] => {
275
+ lifecycle.clearScope(runId);
276
+ return [
272
277
  stripUndefined({
273
278
  type: 'finish' as const,
274
279
  finishReason: parseFinishReason(r.str('finishReason'), 'stop'),
275
280
  messageMetadata: r.json('messageMetadata'),
276
281
  }),
277
- );
282
+ ];
278
283
  };
279
284
 
280
- const decodeError = (data: unknown, turnId: string, lifecycle: LifecycleTracker<AI.UIMessageChunk>): Out[] => {
281
- lifecycle.clearScope(turnId);
285
+ const decodeError = (
286
+ data: unknown,
287
+ runId: string,
288
+ lifecycle: LifecycleTracker<AI.UIMessageChunk>,
289
+ ): AI.UIMessageChunk[] => {
290
+ lifecycle.clearScope(runId);
282
291
  const errorText = typeof data === 'string' ? data : '';
283
- return event({ type: 'error', errorText });
292
+ return [{ type: 'error', errorText }];
284
293
  };
285
294
 
286
- const decodeAbort = (data: unknown, turnId: string, lifecycle: LifecycleTracker<AI.UIMessageChunk>): Out[] => {
287
- lifecycle.clearScope(turnId);
295
+ const decodeAbort = (
296
+ data: unknown,
297
+ runId: string,
298
+ lifecycle: LifecycleTracker<AI.UIMessageChunk>,
299
+ ): AI.UIMessageChunk[] => {
300
+ lifecycle.clearScope(runId);
288
301
  const reason = typeof data === 'string' && data ? data : undefined;
289
- return event(stripUndefined({ type: 'abort' as const, reason }));
302
+ return [stripUndefined({ type: 'abort' as const, reason })];
290
303
  };
291
304
 
292
- const decodeMessageMetadata = (r: VercelHeaderReader): Out[] =>
293
- event({ type: 'message-metadata', messageMetadata: r.json('messageMetadata') });
294
-
295
- const decodeFile = (r: VercelHeaderReader, data: unknown): Out[] =>
296
- event(
297
- stripUndefined({
298
- type: 'file' as const,
299
- url: typeof data === 'string' ? data : '',
300
- mediaType: r.strOr('mediaType', ''),
301
- providerMetadata: r.providerMetadata(),
302
- }),
303
- );
304
-
305
- const decodeSourceUrl = (r: VercelHeaderReader, data: unknown): Out[] =>
306
- event(
307
- stripUndefined({
308
- type: 'source-url' as const,
309
- sourceId: r.strOr('sourceId', ''),
310
- url: typeof data === 'string' ? data : '',
311
- title: r.str('title'),
312
- providerMetadata: r.providerMetadata(),
313
- }),
314
- );
315
-
316
- const decodeSourceDocument = (r: VercelHeaderReader): Out[] =>
317
- event(
318
- stripUndefined({
319
- type: 'source-document' as const,
320
- sourceId: r.strOr('sourceId', ''),
321
- mediaType: r.strOr('mediaType', ''),
322
- title: r.strOr('title', ''),
323
- filename: r.str('filename'),
324
- providerMetadata: r.providerMetadata(),
325
- }),
326
- );
327
-
328
- const decodeToolInputError = (r: VercelHeaderReader, data: unknown): Out[] => {
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[] => {
329
340
  // CAST: Trust boundary — encoder produced the expected object shape.
330
341
  const parsed = data as ToolInputErrorWireData | undefined;
331
- return event(
342
+ return [
332
343
  stripUndefined({
333
344
  type: 'tool-input-error' as const,
334
345
  toolCallId: r.strOr('toolCallId', ''),
@@ -340,13 +351,13 @@ const decodeToolInputError = (r: VercelHeaderReader, data: unknown): Out[] => {
340
351
  providerExecuted: r.bool('providerExecuted'),
341
352
  providerMetadata: r.providerMetadata(),
342
353
  }),
343
- );
354
+ ];
344
355
  };
345
356
 
346
- const decodeToolOutputAvailable = (r: VercelHeaderReader, data: unknown): Out[] => {
357
+ const decodeAgentToolOutputAvailable = (r: VercelHeaderReader, data: unknown): AI.UIMessageChunk[] => {
347
358
  // CAST: Trust boundary — encoder produced the expected object shape.
348
359
  const parsed = data as ToolOutputAvailableWireData | undefined;
349
- return event(
360
+ return [
350
361
  stripUndefined({
351
362
  type: 'tool-output-available' as const,
352
363
  toolCallId: r.strOr('toolCallId', ''),
@@ -355,13 +366,13 @@ const decodeToolOutputAvailable = (r: VercelHeaderReader, data: unknown): Out[]
355
366
  providerExecuted: r.bool('providerExecuted'),
356
367
  preliminary: r.bool('preliminary'),
357
368
  }),
358
- );
369
+ ];
359
370
  };
360
371
 
361
- const decodeToolOutputError = (r: VercelHeaderReader, data: unknown): Out[] => {
372
+ const decodeAgentToolOutputError = (r: VercelHeaderReader, data: unknown): AI.UIMessageChunk[] => {
362
373
  // CAST: Trust boundary — encoder produced the expected object shape.
363
- const parsed = data as ToolOutputErrorWireData | undefined;
364
- return event(
374
+ const parsed = data as AgentToolOutputErrorWireData | undefined;
375
+ return [
365
376
  stripUndefined({
366
377
  type: 'tool-output-error' as const,
367
378
  toolCallId: r.strOr('toolCallId', ''),
@@ -369,92 +380,80 @@ const decodeToolOutputError = (r: VercelHeaderReader, data: unknown): Out[] => {
369
380
  dynamic: r.bool('dynamic'),
370
381
  providerExecuted: r.bool('providerExecuted'),
371
382
  }),
372
- );
383
+ ];
373
384
  };
374
385
 
375
- const decodeToolApprovalRequest = (r: VercelHeaderReader): Out[] =>
376
- event({
386
+ const decodeToolApprovalRequest = (r: VercelHeaderReader): AI.UIMessageChunk[] => [
387
+ {
377
388
  type: 'tool-approval-request',
378
389
  toolCallId: r.strOr('toolCallId', ''),
379
390
  approvalId: r.strOr('approvalId', ''),
380
- });
391
+ },
392
+ ];
381
393
 
382
- const decodeToolOutputDenied = (r: VercelHeaderReader): Out[] =>
383
- event({ type: 'tool-output-denied', toolCallId: r.strOr('toolCallId', '') });
394
+ const decodeToolOutputDenied = (r: VercelHeaderReader): AI.UIMessageChunk[] => [
395
+ { type: 'tool-output-denied', toolCallId: r.strOr('toolCallId', '') },
396
+ ];
384
397
 
385
- const decodeDataEvent = (name: `data-${string}`, r: VercelHeaderReader, data: unknown): Out[] =>
386
- event(
387
- stripUndefined({
388
- type: name,
389
- data,
390
- id: r.str('id'),
391
- transient: r.bool('transient'),
392
- }),
393
- );
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
+ ];
394
406
 
395
407
  // ---------------------------------------------------------------------------
396
- // Non-streaming tool-input helper
408
+ // Non-streaming tool-input helper (agent-side)
397
409
  // ---------------------------------------------------------------------------
398
410
 
399
411
  const decodeNonStreamingToolInput = (
400
412
  r: VercelHeaderReader,
401
413
  data: unknown,
402
- turnId: string,
414
+ runId: string,
403
415
  lifecycle: LifecycleTracker<AI.UIMessageChunk>,
404
- ): Out[] => {
405
- const outputs = ensurePhases(lifecycle, turnId, { messageId: r.str('messageId') });
406
-
407
- outputs.push(
408
- {
409
- kind: 'event',
410
- event: stripUndefined({
411
- type: 'tool-input-start' as const,
412
- toolCallId: r.strOr('toolCallId', ''),
413
- toolName: r.strOr('toolName', ''),
414
- dynamic: r.bool('dynamic'),
415
- title: r.str('title'),
416
- providerExecuted: r.bool('providerExecuted'),
417
- providerMetadata: r.providerMetadata(),
418
- }),
419
- },
420
- {
421
- kind: 'event',
422
- event: stripUndefined({
423
- type: 'tool-input-available' as const,
424
- toolCallId: r.strOr('toolCallId', ''),
425
- toolName: r.strOr('toolName', ''),
426
- input: data,
427
- providerMetadata: r.providerMetadata(),
428
- }),
429
- },
430
- );
431
-
432
- return outputs;
433
- };
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
+ ];
434
435
 
435
436
  // ---------------------------------------------------------------------------
436
- // Discrete event dispatch
437
+ // Input-side decoders (ai-input → VercelInput)
437
438
  // ---------------------------------------------------------------------------
438
439
 
439
440
  /**
440
- * Reconstruct a UIMessage from a discrete message part published by writeMessages.
441
- * The encoder splits each UIMessage into per-part Ably messages with a shared
442
- * x-domain-messageId. This function rebuilds a single-part UIMessage from one
443
- * such Ably message. The transport's tree upsert merges parts that share the
444
- * same x-ably-msg-id, so multi-part messages accumulate correctly over
445
- * successive decoder calls.
446
- * @param input - The discrete message payload to decode.
447
- * @returns A single-element array with the reconstructed UIMessage, or empty if unrecognized.
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.
448
447
  */
449
- const decodeDiscreteMessage = (input: MessagePayload): Out[] => {
450
- const h = input.headers ?? {};
451
- const r = headerReader(h);
452
- const role = (h[HEADER_ROLE] ?? 'user') as AI.UIMessage['role'];
448
+ const decodeDiscreteMessagePart = (input: MessagePayload): VercelInput[] => {
449
+ const r = headerReader(input.codecHeaders ?? {});
450
+ const role = (input.transportHeaders?.[HEADER_ROLE] ?? 'user') as AI.UIMessage['role'];
453
451
  const messageId = r.str('messageId') ?? '';
452
+ const codecType = r.strOr('type', '');
454
453
 
455
454
  let part: AI.UIMessage['parts'][number] | undefined;
456
455
 
457
- switch (input.name) {
456
+ switch (codecType) {
458
457
  case 'text': {
459
458
  part = { type: 'text', text: typeof input.data === 'string' ? input.data : '' };
460
459
  break;
@@ -468,9 +467,8 @@ const decodeDiscreteMessage = (input: MessagePayload): Out[] => {
468
467
  break;
469
468
  }
470
469
  default: {
471
- if (isDataEventName(input.name)) {
472
- // CAST: data-* part type matches the DataUIPart shape.
473
- part = stripUndefined({ type: input.name, id: r.str('id'), data: input.data }) as AI.UIMessage['parts'][number];
470
+ if (isDataEventName(codecType)) {
471
+ part = stripUndefined({ type: codecType, id: r.str('id'), data: input.data });
474
472
  }
475
473
  break;
476
474
  }
@@ -479,76 +477,102 @@ const decodeDiscreteMessage = (input: MessagePayload): Out[] => {
479
477
  if (!part) return [];
480
478
 
481
479
  const message: AI.UIMessage = { id: messageId, role, parts: [part] };
482
- return [{ kind: 'message', message }];
480
+ const userMessage: UserMessage<AI.UIMessage> = { kind: 'user-message', message };
481
+ return [userMessage];
483
482
  };
484
483
 
485
- /**
486
- * Whether a message name represents a discrete message part (written by writeMessages)
487
- * rather than a streaming lifecycle event. Distinguished by the `x-ably-discrete` header
488
- * which {@link publishDiscreteBatch} sets on batch-published message payloads. Lifecycle
489
- * events published via {@link publishDiscrete} (including streaming `data-*` chunks)
490
- * do not carry this header.
491
- * @param name - The Ably message name to check.
492
- * @param headers - The Ably message headers to inspect for discrete marker presence.
493
- * @returns True if this is a discrete message part, false if it's a lifecycle event.
494
- */
495
- const isDiscreteMessagePart = (name: string, headers: Record<string, string>): boolean =>
496
- (name === 'text' || name === 'file' || isDataEventName(name)) && HEADER_DISCRETE in headers;
497
-
498
- const decodeDiscretePayload = (input: MessagePayload, lifecycle: LifecycleTracker<AI.UIMessageChunk>): Out[] => {
499
- const h = input.headers ?? {};
500
- const r = headerReader(h);
501
- const turnId = h[HEADER_TURN_ID] ?? '';
502
-
503
- // Discrete message parts from writeMessages (user messages, history entries).
504
- // Distinguished from lifecycle events by the presence of x-ably-discrete.
505
- if (isDiscreteMessagePart(input.name, h)) {
506
- return decodeDiscreteMessage(input);
507
- }
484
+ const isDiscreteMessagePart = (codecType: string, headers: Record<string, string>): boolean =>
485
+ (codecType === 'text' || codecType === 'file' || isDataEventName(codecType)) && HEADER_DISCRETE in headers;
508
486
 
509
- if (input.name === 'tool-input') {
510
- return decodeNonStreamingToolInput(r, input.data, turnId, lifecycle);
511
- }
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
+ // ---------------------------------------------------------------------------
512
526
 
513
- switch (input.name) {
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) {
514
535
  case 'start': {
515
- return decodeStart(r, turnId, lifecycle);
536
+ return decodeStart(r, runId, lifecycle);
516
537
  }
517
538
  case 'start-step': {
518
- return decodeStartStep(turnId, lifecycle);
539
+ return decodeStartStep(runId, lifecycle);
519
540
  }
520
541
  case 'finish-step': {
521
- return decodeFinishStep(turnId, lifecycle);
542
+ return decodeFinishStep(runId, lifecycle);
522
543
  }
523
544
  case 'finish': {
524
- return decodeFinish(r, turnId, lifecycle);
545
+ return decodeFinish(r, runId, lifecycle);
525
546
  }
526
547
  case 'error': {
527
- return decodeError(input.data, turnId, lifecycle);
548
+ return decodeError(data, runId, lifecycle);
528
549
  }
529
550
  case 'abort': {
530
- return decodeAbort(input.data, turnId, lifecycle);
551
+ return decodeAbort(data, runId, lifecycle);
531
552
  }
532
553
  case 'message-metadata': {
533
554
  return decodeMessageMetadata(r);
534
555
  }
535
556
  case 'file': {
536
- return decodeFile(r, input.data);
557
+ return decodeFile(r, data);
537
558
  }
538
559
  case 'source-url': {
539
- return decodeSourceUrl(r, input.data);
560
+ return decodeSourceUrl(r, data);
540
561
  }
541
562
  case 'source-document': {
542
563
  return decodeSourceDocument(r);
543
564
  }
565
+ case 'tool-input': {
566
+ return decodeNonStreamingToolInput(r, data, runId, lifecycle);
567
+ }
544
568
  case 'tool-input-error': {
545
- return decodeToolInputError(r, input.data);
569
+ return decodeToolInputError(r, data);
546
570
  }
547
571
  case 'tool-output-available': {
548
- return decodeToolOutputAvailable(r, input.data);
572
+ return decodeAgentToolOutputAvailable(r, data);
549
573
  }
550
574
  case 'tool-output-error': {
551
- return decodeToolOutputError(r, input.data);
575
+ return decodeAgentToolOutputError(r, data);
552
576
  }
553
577
  case 'tool-approval-request': {
554
578
  return decodeToolApprovalRequest(r);
@@ -557,50 +581,105 @@ const decodeDiscretePayload = (input: MessagePayload, lifecycle: LifecycleTracke
557
581
  return decodeToolOutputDenied(r);
558
582
  }
559
583
  default: {
560
- return isDataEventName(input.name) ? decodeDataEvent(input.name, r, input.data) : [];
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 [];
561
618
  }
562
619
  }
563
620
  };
564
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
+
565
638
  // ---------------------------------------------------------------------------
566
639
  // Decoder core hooks
567
640
  // ---------------------------------------------------------------------------
568
641
 
569
- const createHooks = (
570
- lifecycle: LifecycleTracker<AI.UIMessageChunk>,
571
- ): DecoderCoreHooks<AI.UIMessageChunk, AI.UIMessage> => ({
572
- buildStartEvents: (tracker: StreamTrackerState): Out[] => {
573
- const turnId = tracker.headers[HEADER_TURN_ID] ?? '';
574
- const messageId = headerReader(tracker.headers).str('messageId');
575
- const outputs = ensurePhases(lifecycle, turnId, { messageId });
576
- outputs.push({ kind: 'event', event: buildStartChunk(tracker) });
577
- return outputs;
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)];
578
647
  },
579
648
 
580
- buildDeltaEvents: (tracker: StreamTrackerState, delta: string): Out[] => event(buildDeltaChunk(tracker, delta)),
649
+ buildDeltaEvents: (tracker: StreamTrackerState, delta: string): AnyEvent[] => [buildDeltaChunk(tracker, delta)],
581
650
 
582
- buildEndEvents: (tracker: StreamTrackerState, closingHeaders: Record<string, string>): Out[] =>
583
- event(buildEndChunk(tracker, closingHeaders)),
651
+ buildEndEvents: (tracker: StreamTrackerState, closingHeaders: Record<string, string>): AnyEvent[] => [
652
+ buildEndChunk(tracker, closingHeaders),
653
+ ],
584
654
 
585
- decodeDiscrete: (payload: MessagePayload): Out[] => decodeDiscretePayload(payload, lifecycle),
655
+ decodeDiscrete: (payload: MessagePayload): AnyEvent[] => decodeDiscretePayload(payload, lifecycle),
586
656
  });
587
657
 
588
658
  // ---------------------------------------------------------------------------
589
659
  // Default implementation
590
660
  // ---------------------------------------------------------------------------
591
661
 
592
- class DefaultUIMessageDecoder implements StreamDecoder<AI.UIMessageChunk, AI.UIMessage> {
593
- private readonly _core: DecoderCore<AI.UIMessageChunk, AI.UIMessage>;
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>;
594
666
 
595
667
  constructor(options: DecoderCoreOptions = {}) {
596
- this._core = createDecoderCore<AI.UIMessageChunk, AI.UIMessage>(
597
- createHooks(createVercelLifecycleTracker()),
598
- options,
599
- );
668
+ this._core = createDecoderCore<AnyEvent>(createHooks(createVercelLifecycleTracker()), options);
600
669
  }
601
670
 
602
- decode(message: Ably.InboundMessage): Out[] {
603
- return this._core.decode(message);
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 };
604
683
  }
605
684
  }
606
685
 
@@ -609,10 +688,9 @@ class DefaultUIMessageDecoder implements StreamDecoder<AI.UIMessageChunk, AI.UIM
609
688
  // ---------------------------------------------------------------------------
610
689
 
611
690
  /**
612
- * Create a Vercel AI SDK decoder that maps Ably messages to UIMessageChunk
613
- * events and UIMessage objects via the decoder core.
691
+ * Create a Vercel AI SDK decoder that maps Ably messages to {@link DecodedMessage}.
614
692
  * @param options - Decoder configuration (callbacks, logger).
615
- * @returns A {@link StreamDecoder} for UIMessageChunk/UIMessage.
693
+ * @returns A {@link Decoder} typed in both directions for the Vercel codec.
616
694
  */
617
- export const createDecoder = (options: DecoderCoreOptions = {}): StreamDecoder<AI.UIMessageChunk, AI.UIMessage> =>
695
+ export const createDecoder = (options: DecoderCoreOptions = {}): Decoder<VercelInput, VercelOutput> =>
618
696
  new DefaultUIMessageDecoder(options);