@ably/ai-transport 0.0.1 → 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 (167) hide show
  1. package/README.md +114 -116
  2. package/dist/ably-ai-transport.js +1743 -961
  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 +117 -39
  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 +410 -101
  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 +97 -17
  18. package/dist/core/transport/index.d.ts +5 -3
  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 -8
  24. package/dist/core/transport/run-manager.d.ts +78 -0
  25. package/dist/core/transport/tree.d.ts +435 -0
  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 -402
  32. package/dist/core/transport/view.d.ts +354 -0
  33. package/dist/errors.d.ts +37 -9
  34. package/dist/index.d.ts +6 -6
  35. package/dist/logger.d.ts +12 -0
  36. package/dist/react/ably-ai-transport-react.js +1164 -645
  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 +16 -10
  44. package/dist/react/internal/use-resolved-session.d.ts +36 -0
  45. package/dist/react/use-ably-messages.d.ts +20 -11
  46. package/dist/react/use-client-session.d.ts +81 -0
  47. package/dist/react/use-create-view.d.ts +23 -0
  48. package/dist/react/use-tree.d.ts +35 -0
  49. package/dist/react/use-view.d.ts +110 -0
  50. package/dist/utils.d.ts +32 -23
  51. package/dist/vercel/ably-ai-transport-vercel.js +2748 -1625
  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 +50 -0
  61. package/dist/vercel/index.d.ts +4 -2
  62. package/dist/vercel/react/ably-ai-transport-vercel-react.js +10298 -1410
  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 +70 -1
  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 +33 -0
  67. package/dist/vercel/react/contexts/chat-transport-provider.d.ts +96 -0
  68. package/dist/vercel/react/index.d.ts +4 -0
  69. package/dist/vercel/react/use-chat-transport.d.ts +66 -21
  70. package/dist/vercel/react/use-message-sync.d.ts +31 -12
  71. package/dist/vercel/run-end-reason.d.ts +29 -0
  72. package/dist/vercel/transport/chat-transport.d.ts +71 -30
  73. package/dist/vercel/transport/index.d.ts +25 -18
  74. package/dist/vercel/transport/run-output-stream.d.ts +56 -0
  75. package/dist/version.d.ts +2 -0
  76. package/package.json +47 -34
  77. package/src/constants.ts +126 -47
  78. package/src/core/agent.ts +68 -0
  79. package/src/core/codec/decoder.ts +71 -98
  80. package/src/core/codec/encoder.ts +115 -58
  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 +438 -106
  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 +182 -19
  89. package/src/core/transport/index.ts +29 -22
  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 +58 -40
  95. package/src/core/transport/run-manager.ts +249 -0
  96. package/src/core/transport/tree.ts +1167 -0
  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 -527
  103. package/src/core/transport/view.ts +1271 -0
  104. package/src/errors.ts +42 -9
  105. package/src/event-emitter.ts +3 -2
  106. package/src/index.ts +55 -39
  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 +27 -10
  112. package/src/react/internal/use-resolved-session.ts +63 -0
  113. package/src/react/use-ably-messages.ts +47 -19
  114. package/src/react/use-client-session.ts +201 -0
  115. package/src/react/use-create-view.ts +72 -0
  116. package/src/react/use-tree.ts +84 -0
  117. package/src/react/use-view.ts +275 -0
  118. package/src/react/vite.config.ts +4 -1
  119. package/src/utils.ts +63 -45
  120. package/src/vercel/codec/decoder.ts +336 -255
  121. package/src/vercel/codec/encoder.ts +348 -196
  122. package/src/vercel/codec/events.ts +87 -0
  123. package/src/vercel/codec/index.ts +59 -14
  124. package/src/vercel/codec/reducer.ts +977 -0
  125. package/src/vercel/codec/tool-transitions.ts +122 -0
  126. package/src/vercel/index.ts +7 -3
  127. package/src/vercel/react/contexts/chat-transport-context.ts +41 -0
  128. package/src/vercel/react/contexts/chat-transport-provider.tsx +150 -0
  129. package/src/vercel/react/index.ts +13 -1
  130. package/src/vercel/react/use-chat-transport.ts +162 -42
  131. package/src/vercel/react/use-message-sync.ts +121 -22
  132. package/src/vercel/react/vite.config.ts +4 -2
  133. package/src/vercel/run-end-reason.ts +78 -0
  134. package/src/vercel/transport/chat-transport.ts +553 -113
  135. package/src/vercel/transport/index.ts +40 -28
  136. package/src/vercel/transport/run-output-stream.ts +170 -0
  137. package/src/version.ts +2 -0
  138. package/dist/core/transport/client-transport.d.ts +0 -10
  139. package/dist/core/transport/conversation-tree.d.ts +0 -9
  140. package/dist/core/transport/decode-history.d.ts +0 -41
  141. package/dist/core/transport/server-transport.d.ts +0 -7
  142. package/dist/core/transport/stream-router.d.ts +0 -19
  143. package/dist/core/transport/turn-manager.d.ts +0 -34
  144. package/dist/react/use-active-turns.d.ts +0 -8
  145. package/dist/react/use-client-transport.d.ts +0 -7
  146. package/dist/react/use-conversation-tree.d.ts +0 -20
  147. package/dist/react/use-edit.d.ts +0 -7
  148. package/dist/react/use-history.d.ts +0 -19
  149. package/dist/react/use-messages.d.ts +0 -7
  150. package/dist/react/use-regenerate.d.ts +0 -7
  151. package/dist/react/use-send.d.ts +0 -7
  152. package/dist/vercel/codec/accumulator.d.ts +0 -21
  153. package/src/core/transport/client-transport.ts +0 -959
  154. package/src/core/transport/conversation-tree.ts +0 -434
  155. package/src/core/transport/decode-history.ts +0 -337
  156. package/src/core/transport/server-transport.ts +0 -458
  157. package/src/core/transport/stream-router.ts +0 -118
  158. package/src/core/transport/turn-manager.ts +0 -147
  159. package/src/react/use-active-turns.ts +0 -61
  160. package/src/react/use-client-transport.ts +0 -37
  161. package/src/react/use-conversation-tree.ts +0 -71
  162. package/src/react/use-edit.ts +0 -24
  163. package/src/react/use-history.ts +0 -111
  164. package/src/react/use-messages.ts +0 -32
  165. package/src/react/use-regenerate.ts +0 -24
  166. package/src/react/use-send.ts +0 -25
  167. package/src/vercel/codec/accumulator.ts +0 -603
@@ -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_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,106 +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): Out[] => {
285
+ const decodeError = (
286
+ data: unknown,
287
+ runId: string,
288
+ lifecycle: LifecycleTracker<AI.UIMessageChunk>,
289
+ ): AI.UIMessageChunk[] => {
290
+ lifecycle.clearScope(runId);
281
291
  const errorText = typeof data === 'string' ? data : '';
282
- return event({ type: 'error', errorText });
292
+ return [{ type: 'error', errorText }];
283
293
  };
284
294
 
285
- const decodeAbort = (data: unknown, turnId: string, lifecycle: LifecycleTracker<AI.UIMessageChunk>): Out[] => {
286
- lifecycle.clearScope(turnId);
295
+ const decodeAbort = (
296
+ data: unknown,
297
+ runId: string,
298
+ lifecycle: LifecycleTracker<AI.UIMessageChunk>,
299
+ ): AI.UIMessageChunk[] => {
300
+ lifecycle.clearScope(runId);
287
301
  const reason = typeof data === 'string' && data ? data : undefined;
288
- return event(stripUndefined({ type: 'abort' as const, reason }));
302
+ return [stripUndefined({ type: 'abort' as const, reason })];
289
303
  };
290
304
 
291
- const decodeMessageMetadata = (r: VercelHeaderReader): Out[] =>
292
- event({ type: 'message-metadata', messageMetadata: r.json('messageMetadata') });
293
-
294
- const decodeFile = (r: VercelHeaderReader, data: unknown): Out[] =>
295
- event(
296
- stripUndefined({
297
- type: 'file' as const,
298
- url: typeof data === 'string' ? data : '',
299
- mediaType: r.strOr('mediaType', ''),
300
- providerMetadata: r.providerMetadata(),
301
- }),
302
- );
303
-
304
- const decodeSourceUrl = (r: VercelHeaderReader, data: unknown): Out[] =>
305
- event(
306
- stripUndefined({
307
- type: 'source-url' as const,
308
- sourceId: r.strOr('sourceId', ''),
309
- url: typeof data === 'string' ? data : '',
310
- title: r.str('title'),
311
- providerMetadata: r.providerMetadata(),
312
- }),
313
- );
314
-
315
- const decodeSourceDocument = (r: VercelHeaderReader): Out[] =>
316
- event(
317
- stripUndefined({
318
- type: 'source-document' as const,
319
- sourceId: r.strOr('sourceId', ''),
320
- mediaType: r.strOr('mediaType', ''),
321
- title: r.strOr('title', ''),
322
- filename: r.str('filename'),
323
- providerMetadata: r.providerMetadata(),
324
- }),
325
- );
326
-
327
- 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[] => {
328
340
  // CAST: Trust boundary — encoder produced the expected object shape.
329
341
  const parsed = data as ToolInputErrorWireData | undefined;
330
- return event(
342
+ return [
331
343
  stripUndefined({
332
344
  type: 'tool-input-error' as const,
333
345
  toolCallId: r.strOr('toolCallId', ''),
@@ -339,13 +351,13 @@ const decodeToolInputError = (r: VercelHeaderReader, data: unknown): Out[] => {
339
351
  providerExecuted: r.bool('providerExecuted'),
340
352
  providerMetadata: r.providerMetadata(),
341
353
  }),
342
- );
354
+ ];
343
355
  };
344
356
 
345
- const decodeToolOutputAvailable = (r: VercelHeaderReader, data: unknown): Out[] => {
357
+ const decodeAgentToolOutputAvailable = (r: VercelHeaderReader, data: unknown): AI.UIMessageChunk[] => {
346
358
  // CAST: Trust boundary — encoder produced the expected object shape.
347
359
  const parsed = data as ToolOutputAvailableWireData | undefined;
348
- return event(
360
+ return [
349
361
  stripUndefined({
350
362
  type: 'tool-output-available' as const,
351
363
  toolCallId: r.strOr('toolCallId', ''),
@@ -354,13 +366,13 @@ const decodeToolOutputAvailable = (r: VercelHeaderReader, data: unknown): Out[]
354
366
  providerExecuted: r.bool('providerExecuted'),
355
367
  preliminary: r.bool('preliminary'),
356
368
  }),
357
- );
369
+ ];
358
370
  };
359
371
 
360
- const decodeToolOutputError = (r: VercelHeaderReader, data: unknown): Out[] => {
372
+ const decodeAgentToolOutputError = (r: VercelHeaderReader, data: unknown): AI.UIMessageChunk[] => {
361
373
  // CAST: Trust boundary — encoder produced the expected object shape.
362
- const parsed = data as ToolOutputErrorWireData | undefined;
363
- return event(
374
+ const parsed = data as AgentToolOutputErrorWireData | undefined;
375
+ return [
364
376
  stripUndefined({
365
377
  type: 'tool-output-error' as const,
366
378
  toolCallId: r.strOr('toolCallId', ''),
@@ -368,92 +380,80 @@ const decodeToolOutputError = (r: VercelHeaderReader, data: unknown): Out[] => {
368
380
  dynamic: r.bool('dynamic'),
369
381
  providerExecuted: r.bool('providerExecuted'),
370
382
  }),
371
- );
383
+ ];
372
384
  };
373
385
 
374
- const decodeToolApprovalRequest = (r: VercelHeaderReader): Out[] =>
375
- event({
386
+ const decodeToolApprovalRequest = (r: VercelHeaderReader): AI.UIMessageChunk[] => [
387
+ {
376
388
  type: 'tool-approval-request',
377
389
  toolCallId: r.strOr('toolCallId', ''),
378
390
  approvalId: r.strOr('approvalId', ''),
379
- });
391
+ },
392
+ ];
380
393
 
381
- const decodeToolOutputDenied = (r: VercelHeaderReader): Out[] =>
382
- 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
+ ];
383
397
 
384
- const decodeDataEvent = (name: `data-${string}`, r: VercelHeaderReader, data: unknown): Out[] =>
385
- event(
386
- stripUndefined({
387
- type: name,
388
- data,
389
- id: r.str('id'),
390
- transient: r.bool('transient'),
391
- }),
392
- );
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
+ ];
393
406
 
394
407
  // ---------------------------------------------------------------------------
395
- // Non-streaming tool-input helper
408
+ // Non-streaming tool-input helper (agent-side)
396
409
  // ---------------------------------------------------------------------------
397
410
 
398
411
  const decodeNonStreamingToolInput = (
399
412
  r: VercelHeaderReader,
400
413
  data: unknown,
401
- turnId: string,
414
+ runId: string,
402
415
  lifecycle: LifecycleTracker<AI.UIMessageChunk>,
403
- ): Out[] => {
404
- const outputs = ensurePhases(lifecycle, turnId, { messageId: r.str('messageId') });
405
-
406
- outputs.push(
407
- {
408
- kind: 'event',
409
- event: stripUndefined({
410
- type: 'tool-input-start' as const,
411
- toolCallId: r.strOr('toolCallId', ''),
412
- toolName: r.strOr('toolName', ''),
413
- dynamic: r.bool('dynamic'),
414
- title: r.str('title'),
415
- providerExecuted: r.bool('providerExecuted'),
416
- providerMetadata: r.providerMetadata(),
417
- }),
418
- },
419
- {
420
- kind: 'event',
421
- event: stripUndefined({
422
- type: 'tool-input-available' as const,
423
- toolCallId: r.strOr('toolCallId', ''),
424
- toolName: r.strOr('toolName', ''),
425
- input: data,
426
- providerMetadata: r.providerMetadata(),
427
- }),
428
- },
429
- );
430
-
431
- return outputs;
432
- };
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
+ ];
433
435
 
434
436
  // ---------------------------------------------------------------------------
435
- // Discrete event dispatch
437
+ // Input-side decoders (ai-input → VercelInput)
436
438
  // ---------------------------------------------------------------------------
437
439
 
438
440
  /**
439
- * Reconstruct a UIMessage from a discrete message part published by writeMessages.
440
- * The encoder splits each UIMessage into per-part Ably messages with a shared
441
- * x-domain-messageId. This function rebuilds a single-part UIMessage from one
442
- * such Ably message. The transport's tree upsert merges parts that share the
443
- * same x-ably-msg-id, so multi-part messages accumulate correctly over
444
- * successive decoder calls.
445
- * @param input - The discrete message payload to decode.
446
- * @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.
447
447
  */
448
- const decodeDiscreteMessage = (input: MessagePayload): Out[] => {
449
- const h = input.headers ?? {};
450
- const r = headerReader(h);
451
- 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'];
452
451
  const messageId = r.str('messageId') ?? '';
452
+ const codecType = r.strOr('type', '');
453
453
 
454
454
  let part: AI.UIMessage['parts'][number] | undefined;
455
455
 
456
- switch (input.name) {
456
+ switch (codecType) {
457
457
  case 'text': {
458
458
  part = { type: 'text', text: typeof input.data === 'string' ? input.data : '' };
459
459
  break;
@@ -467,9 +467,8 @@ const decodeDiscreteMessage = (input: MessagePayload): Out[] => {
467
467
  break;
468
468
  }
469
469
  default: {
470
- if (isDataEventName(input.name)) {
471
- // CAST: data-* part type matches the DataUIPart shape.
472
- 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 });
473
472
  }
474
473
  break;
475
474
  }
@@ -478,74 +477,102 @@ const decodeDiscreteMessage = (input: MessagePayload): Out[] => {
478
477
  if (!part) return [];
479
478
 
480
479
  const message: AI.UIMessage = { id: messageId, role, parts: [part] };
481
- return [{ kind: 'message', message }];
480
+ const userMessage: UserMessage<AI.UIMessage> = { kind: 'user-message', message };
481
+ return [userMessage];
482
482
  };
483
483
 
484
- /**
485
- * Whether a message name represents a discrete message part (written by writeMessages)
486
- * rather than a streaming lifecycle event. Discrete message parts carry x-ably-role
487
- * and encode a single UIMessage part each.
488
- * @param name - The Ably message name to check.
489
- * @param headers - The Ably message headers to inspect for role presence.
490
- * @returns True if this is a discrete message part, false if it's a lifecycle event.
491
- */
492
- const isDiscreteMessagePart = (name: string, headers: Record<string, string>): boolean =>
493
- (name === 'text' || name === 'file' || isDataEventName(name)) && HEADER_ROLE in headers;
494
-
495
- const decodeDiscretePayload = (input: MessagePayload, lifecycle: LifecycleTracker<AI.UIMessageChunk>): Out[] => {
496
- const h = input.headers ?? {};
497
- const r = headerReader(h);
498
- const turnId = h[HEADER_TURN_ID] ?? '';
499
-
500
- // Discrete message parts from writeMessages (user messages, history entries).
501
- // Distinguished from lifecycle events by the presence of x-ably-role.
502
- if (isDiscreteMessagePart(input.name, h)) {
503
- return decodeDiscreteMessage(input);
504
- }
484
+ const isDiscreteMessagePart = (codecType: string, headers: Record<string, string>): boolean =>
485
+ (codecType === 'text' || codecType === 'file' || isDataEventName(codecType)) && HEADER_DISCRETE in headers;
505
486
 
506
- if (input.name === 'tool-input') {
507
- return decodeNonStreamingToolInput(r, input.data, turnId, lifecycle);
508
- }
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
+ // ---------------------------------------------------------------------------
509
526
 
510
- 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) {
511
535
  case 'start': {
512
- return decodeStart(r, turnId, lifecycle);
536
+ return decodeStart(r, runId, lifecycle);
513
537
  }
514
538
  case 'start-step': {
515
- return decodeStartStep(turnId, lifecycle);
539
+ return decodeStartStep(runId, lifecycle);
516
540
  }
517
541
  case 'finish-step': {
518
- return decodeFinishStep(turnId, lifecycle);
542
+ return decodeFinishStep(runId, lifecycle);
519
543
  }
520
544
  case 'finish': {
521
- return decodeFinish(r, turnId, lifecycle);
545
+ return decodeFinish(r, runId, lifecycle);
522
546
  }
523
547
  case 'error': {
524
- return decodeError(input.data);
548
+ return decodeError(data, runId, lifecycle);
525
549
  }
526
550
  case 'abort': {
527
- return decodeAbort(input.data, turnId, lifecycle);
551
+ return decodeAbort(data, runId, lifecycle);
528
552
  }
529
553
  case 'message-metadata': {
530
554
  return decodeMessageMetadata(r);
531
555
  }
532
556
  case 'file': {
533
- return decodeFile(r, input.data);
557
+ return decodeFile(r, data);
534
558
  }
535
559
  case 'source-url': {
536
- return decodeSourceUrl(r, input.data);
560
+ return decodeSourceUrl(r, data);
537
561
  }
538
562
  case 'source-document': {
539
563
  return decodeSourceDocument(r);
540
564
  }
565
+ case 'tool-input': {
566
+ return decodeNonStreamingToolInput(r, data, runId, lifecycle);
567
+ }
541
568
  case 'tool-input-error': {
542
- return decodeToolInputError(r, input.data);
569
+ return decodeToolInputError(r, data);
543
570
  }
544
571
  case 'tool-output-available': {
545
- return decodeToolOutputAvailable(r, input.data);
572
+ return decodeAgentToolOutputAvailable(r, data);
546
573
  }
547
574
  case 'tool-output-error': {
548
- return decodeToolOutputError(r, input.data);
575
+ return decodeAgentToolOutputError(r, data);
549
576
  }
550
577
  case 'tool-approval-request': {
551
578
  return decodeToolApprovalRequest(r);
@@ -554,50 +581,105 @@ const decodeDiscretePayload = (input: MessagePayload, lifecycle: LifecycleTracke
554
581
  return decodeToolOutputDenied(r);
555
582
  }
556
583
  default: {
557
- 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 [];
558
618
  }
559
619
  }
560
620
  };
561
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
+
562
638
  // ---------------------------------------------------------------------------
563
639
  // Decoder core hooks
564
640
  // ---------------------------------------------------------------------------
565
641
 
566
- const createHooks = (
567
- lifecycle: LifecycleTracker<AI.UIMessageChunk>,
568
- ): DecoderCoreHooks<AI.UIMessageChunk, AI.UIMessage> => ({
569
- buildStartEvents: (tracker: StreamTrackerState): Out[] => {
570
- const turnId = tracker.headers[HEADER_TURN_ID] ?? '';
571
- const messageId = headerReader(tracker.headers).str('messageId');
572
- const outputs = ensurePhases(lifecycle, turnId, { messageId });
573
- outputs.push({ kind: 'event', event: buildStartChunk(tracker) });
574
- 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)];
575
647
  },
576
648
 
577
- buildDeltaEvents: (tracker: StreamTrackerState, delta: string): Out[] => event(buildDeltaChunk(tracker, delta)),
649
+ buildDeltaEvents: (tracker: StreamTrackerState, delta: string): AnyEvent[] => [buildDeltaChunk(tracker, delta)],
578
650
 
579
- buildEndEvents: (tracker: StreamTrackerState, closingHeaders: Record<string, string>): Out[] =>
580
- event(buildEndChunk(tracker, closingHeaders)),
651
+ buildEndEvents: (tracker: StreamTrackerState, closingHeaders: Record<string, string>): AnyEvent[] => [
652
+ buildEndChunk(tracker, closingHeaders),
653
+ ],
581
654
 
582
- decodeDiscrete: (payload: MessagePayload): Out[] => decodeDiscretePayload(payload, lifecycle),
655
+ decodeDiscrete: (payload: MessagePayload): AnyEvent[] => decodeDiscretePayload(payload, lifecycle),
583
656
  });
584
657
 
585
658
  // ---------------------------------------------------------------------------
586
659
  // Default implementation
587
660
  // ---------------------------------------------------------------------------
588
661
 
589
- class DefaultUIMessageDecoder implements StreamDecoder<AI.UIMessageChunk, AI.UIMessage> {
590
- 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>;
591
666
 
592
667
  constructor(options: DecoderCoreOptions = {}) {
593
- this._core = createDecoderCore<AI.UIMessageChunk, AI.UIMessage>(
594
- createHooks(createVercelLifecycleTracker()),
595
- options,
596
- );
668
+ this._core = createDecoderCore<AnyEvent>(createHooks(createVercelLifecycleTracker()), options);
597
669
  }
598
670
 
599
- decode(message: Ably.InboundMessage): Out[] {
600
- 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 };
601
683
  }
602
684
  }
603
685
 
@@ -606,10 +688,9 @@ class DefaultUIMessageDecoder implements StreamDecoder<AI.UIMessageChunk, AI.UIM
606
688
  // ---------------------------------------------------------------------------
607
689
 
608
690
  /**
609
- * Create a Vercel AI SDK decoder that maps Ably messages to UIMessageChunk
610
- * events and UIMessage objects via the decoder core.
691
+ * Create a Vercel AI SDK decoder that maps Ably messages to {@link DecodedMessage}.
611
692
  * @param options - Decoder configuration (callbacks, logger).
612
- * @returns A {@link StreamDecoder} for UIMessageChunk/UIMessage.
693
+ * @returns A {@link Decoder} typed in both directions for the Vercel codec.
613
694
  */
614
- export const createDecoder = (options: DecoderCoreOptions = {}): StreamDecoder<AI.UIMessageChunk, AI.UIMessage> =>
695
+ export const createDecoder = (options: DecoderCoreOptions = {}): Decoder<VercelInput, VercelOutput> =>
615
696
  new DefaultUIMessageDecoder(options);