@ably/ai-transport 0.0.1

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 (118) hide show
  1. package/LICENSE +176 -0
  2. package/README.md +426 -0
  3. package/dist/ably-ai-transport.js +1388 -0
  4. package/dist/ably-ai-transport.js.map +1 -0
  5. package/dist/ably-ai-transport.umd.cjs +2 -0
  6. package/dist/ably-ai-transport.umd.cjs.map +1 -0
  7. package/dist/constants.d.ts +50 -0
  8. package/dist/core/codec/decoder.d.ts +62 -0
  9. package/dist/core/codec/encoder.d.ts +56 -0
  10. package/dist/core/codec/index.d.ts +8 -0
  11. package/dist/core/codec/lifecycle-tracker.d.ts +74 -0
  12. package/dist/core/codec/types.d.ts +188 -0
  13. package/dist/core/transport/client-transport.d.ts +10 -0
  14. package/dist/core/transport/conversation-tree.d.ts +9 -0
  15. package/dist/core/transport/decode-history.d.ts +41 -0
  16. package/dist/core/transport/headers.d.ts +26 -0
  17. package/dist/core/transport/index.d.ts +4 -0
  18. package/dist/core/transport/pipe-stream.d.ts +16 -0
  19. package/dist/core/transport/server-transport.d.ts +7 -0
  20. package/dist/core/transport/stream-router.d.ts +19 -0
  21. package/dist/core/transport/turn-manager.d.ts +34 -0
  22. package/dist/core/transport/types.d.ts +407 -0
  23. package/dist/errors.d.ts +46 -0
  24. package/dist/event-emitter.d.ts +65 -0
  25. package/dist/index.d.ts +11 -0
  26. package/dist/logger.d.ts +103 -0
  27. package/dist/react/ably-ai-transport-react.js +823 -0
  28. package/dist/react/ably-ai-transport-react.js.map +1 -0
  29. package/dist/react/ably-ai-transport-react.umd.cjs +2 -0
  30. package/dist/react/ably-ai-transport-react.umd.cjs.map +1 -0
  31. package/dist/react/index.d.ts +11 -0
  32. package/dist/react/use-ably-messages.d.ts +18 -0
  33. package/dist/react/use-active-turns.d.ts +8 -0
  34. package/dist/react/use-client-transport.d.ts +7 -0
  35. package/dist/react/use-conversation-tree.d.ts +20 -0
  36. package/dist/react/use-edit.d.ts +7 -0
  37. package/dist/react/use-history.d.ts +19 -0
  38. package/dist/react/use-messages.d.ts +7 -0
  39. package/dist/react/use-regenerate.d.ts +7 -0
  40. package/dist/react/use-send.d.ts +7 -0
  41. package/dist/utils.d.ts +127 -0
  42. package/dist/vercel/ably-ai-transport-vercel.js +2331 -0
  43. package/dist/vercel/ably-ai-transport-vercel.js.map +1 -0
  44. package/dist/vercel/ably-ai-transport-vercel.umd.cjs +2 -0
  45. package/dist/vercel/ably-ai-transport-vercel.umd.cjs.map +1 -0
  46. package/dist/vercel/codec/accumulator.d.ts +21 -0
  47. package/dist/vercel/codec/decoder.d.ts +22 -0
  48. package/dist/vercel/codec/encoder.d.ts +41 -0
  49. package/dist/vercel/codec/index.d.ts +22 -0
  50. package/dist/vercel/index.d.ts +3 -0
  51. package/dist/vercel/react/ably-ai-transport-vercel-react.js +2082 -0
  52. package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -0
  53. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +2 -0
  54. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -0
  55. package/dist/vercel/react/index.d.ts +3 -0
  56. package/dist/vercel/react/use-chat-transport.d.ts +29 -0
  57. package/dist/vercel/react/use-message-sync.d.ts +19 -0
  58. package/dist/vercel/transport/chat-transport.d.ts +118 -0
  59. package/dist/vercel/transport/index.d.ts +36 -0
  60. package/package.json +123 -0
  61. package/react/README.md +3 -0
  62. package/react/index.d.ts +1 -0
  63. package/react/index.js +1 -0
  64. package/react/index.umd.cjs +1 -0
  65. package/src/constants.ts +98 -0
  66. package/src/core/codec/decoder.ts +402 -0
  67. package/src/core/codec/encoder.ts +470 -0
  68. package/src/core/codec/index.ts +28 -0
  69. package/src/core/codec/lifecycle-tracker.ts +140 -0
  70. package/src/core/codec/types.ts +249 -0
  71. package/src/core/transport/client-transport.ts +959 -0
  72. package/src/core/transport/conversation-tree.ts +434 -0
  73. package/src/core/transport/decode-history.ts +337 -0
  74. package/src/core/transport/headers.ts +46 -0
  75. package/src/core/transport/index.ts +34 -0
  76. package/src/core/transport/pipe-stream.ts +95 -0
  77. package/src/core/transport/server-transport.ts +458 -0
  78. package/src/core/transport/stream-router.ts +118 -0
  79. package/src/core/transport/turn-manager.ts +147 -0
  80. package/src/core/transport/types.ts +533 -0
  81. package/src/errors.ts +58 -0
  82. package/src/event-emitter.ts +103 -0
  83. package/src/index.ts +89 -0
  84. package/src/logger.ts +241 -0
  85. package/src/react/index.ts +11 -0
  86. package/src/react/use-ably-messages.ts +37 -0
  87. package/src/react/use-active-turns.ts +61 -0
  88. package/src/react/use-client-transport.ts +37 -0
  89. package/src/react/use-conversation-tree.ts +71 -0
  90. package/src/react/use-edit.ts +24 -0
  91. package/src/react/use-history.ts +111 -0
  92. package/src/react/use-messages.ts +32 -0
  93. package/src/react/use-regenerate.ts +24 -0
  94. package/src/react/use-send.ts +25 -0
  95. package/src/react/vite.config.ts +32 -0
  96. package/src/tsconfig.json +25 -0
  97. package/src/utils.ts +230 -0
  98. package/src/vercel/codec/accumulator.ts +603 -0
  99. package/src/vercel/codec/decoder.ts +615 -0
  100. package/src/vercel/codec/encoder.ts +396 -0
  101. package/src/vercel/codec/index.ts +37 -0
  102. package/src/vercel/index.ts +12 -0
  103. package/src/vercel/react/index.ts +4 -0
  104. package/src/vercel/react/use-chat-transport.ts +60 -0
  105. package/src/vercel/react/use-message-sync.ts +34 -0
  106. package/src/vercel/react/vite.config.ts +33 -0
  107. package/src/vercel/transport/chat-transport.ts +278 -0
  108. package/src/vercel/transport/index.ts +56 -0
  109. package/src/vercel/vite.config.ts +33 -0
  110. package/src/vite.config.ts +31 -0
  111. package/vercel/README.md +3 -0
  112. package/vercel/index.d.ts +1 -0
  113. package/vercel/index.js +1 -0
  114. package/vercel/index.umd.cjs +1 -0
  115. package/vercel/react/README.md +3 -0
  116. package/vercel/react/index.d.ts +1 -0
  117. package/vercel/react/index.js +1 -0
  118. package/vercel/react/index.umd.cjs +1 -0
@@ -0,0 +1,396 @@
1
+ /**
2
+ * Vercel AI SDK Encoder
3
+ *
4
+ * Maps UIMessageChunk events and complete UIMessage objects to Ably channel
5
+ * operations (publish, appendMessage, updateMessage).
6
+ *
7
+ * Delegates the message append lifecycle (publish, append, close, abort,
8
+ * flush/recover) to the encoder core. This file contains only the
9
+ * Vercel-specific event-to-operation mapping.
10
+ *
11
+ * Domain-specific headers use the `x-domain-` prefix to distinguish them
12
+ * from transport-level `x-ably-` headers.
13
+ *
14
+ * ## Core operations and domain headers
15
+ *
16
+ * Each UIMessageChunk maps to exactly one encoder core operation. Domain
17
+ * headers are passed to every operation that accepts them — the core handles
18
+ * merging, persistence, and deduplication:
19
+ *
20
+ * - **`startStream`**: Opens a message stream. Domain headers become
21
+ * "persistent headers" — the core repeats them on every subsequent append.
22
+ * - **`appendStream`**: Appends a text delta. Data only, no headers parameter.
23
+ * The core automatically carries persistent headers from start.
24
+ * - **`closeStream`**: Closes the stream. Pass all domain headers from the
25
+ * chunk — the core merges them on top of persistent headers, so changed
26
+ * values (e.g. updated providerMetadata) are picked up and unchanged
27
+ * values are harmlessly deduplicated.
28
+ * - **`publishDiscrete`**: Publishes a standalone message. All domain headers
29
+ * for the chunk are passed directly.
30
+ */
31
+
32
+ import * as Ably from 'ably';
33
+ import type * as AI from 'ai';
34
+ import { isDataUIPart } from 'ai';
35
+
36
+ import { HEADER_STATUS } from '../../constants.js';
37
+ import type { EncoderCore, EncoderCoreOptions } from '../../core/codec/encoder.js';
38
+ import { createEncoderCore } from '../../core/codec/encoder.js';
39
+ import type { ChannelWriter, MessagePayload, StreamEncoder, WriteOptions } from '../../core/codec/types.js';
40
+ import { ErrorCode, errorInfoIs } from '../../errors.js';
41
+ import { headerWriter } from '../../utils.js';
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Default implementation
45
+ // ---------------------------------------------------------------------------
46
+
47
+ class DefaultUIMessageEncoder implements StreamEncoder<AI.UIMessageChunk, AI.UIMessage> {
48
+ private readonly _core: EncoderCore;
49
+ private _aborted = false;
50
+
51
+ constructor(writer: ChannelWriter, options: EncoderCoreOptions = {}) {
52
+ this._core = createEncoderCore(writer, options);
53
+ }
54
+
55
+ async appendEvent(chunk: AI.UIMessageChunk, perWrite?: WriteOptions): Promise<void> {
56
+ switch (chunk.type) {
57
+ // -- Stream start: open a message stream with persistent headers -------
58
+
59
+ case 'text-start': {
60
+ const h = headerWriter().str('id', chunk.id).json('providerMetadata', chunk.providerMetadata).build();
61
+ await this._core.startStream(chunk.id, { name: 'text', data: '', headers: h }, perWrite);
62
+ break;
63
+ }
64
+
65
+ case 'reasoning-start': {
66
+ const h = headerWriter().str('id', chunk.id).json('providerMetadata', chunk.providerMetadata).build();
67
+ await this._core.startStream(chunk.id, { name: 'reasoning', data: '', headers: h }, perWrite);
68
+ break;
69
+ }
70
+
71
+ case 'tool-input-start': {
72
+ const h = headerWriter()
73
+ .str('toolCallId', chunk.toolCallId)
74
+ .str('toolName', chunk.toolName)
75
+ .bool('dynamic', chunk.dynamic)
76
+ .str('title', chunk.title)
77
+ .bool('providerExecuted', chunk.providerExecuted)
78
+ .json('providerMetadata', chunk.providerMetadata)
79
+ .build();
80
+ await this._core.startStream(chunk.toolCallId, { name: 'tool-input', data: '', headers: h }, perWrite);
81
+ break;
82
+ }
83
+
84
+ // -- Stream append: data only, core carries persistent headers --------
85
+
86
+ case 'text-delta': {
87
+ this._core.appendStream(chunk.id, chunk.delta);
88
+ break;
89
+ }
90
+
91
+ case 'reasoning-delta': {
92
+ this._core.appendStream(chunk.id, chunk.delta);
93
+ break;
94
+ }
95
+
96
+ case 'tool-input-delta': {
97
+ this._core.appendStream(chunk.toolCallId, chunk.inputTextDelta);
98
+ break;
99
+ }
100
+
101
+ // -- Stream close: pass all chunk headers, core merges with persistent
102
+
103
+ case 'text-end': {
104
+ const h = headerWriter().str('id', chunk.id).json('providerMetadata', chunk.providerMetadata).build();
105
+ await this._core.closeStream(chunk.id, { name: 'text', data: '', headers: h });
106
+ break;
107
+ }
108
+
109
+ case 'reasoning-end': {
110
+ const h = headerWriter().str('id', chunk.id).json('providerMetadata', chunk.providerMetadata).build();
111
+ await this._core.closeStream(chunk.id, { name: 'reasoning', data: '', headers: h });
112
+ break;
113
+ }
114
+
115
+ case 'tool-input-available': {
116
+ // If a stream tracker exists, this tool call was streamed — close it.
117
+ // Otherwise it's a non-streaming tool call — publish discrete.
118
+ try {
119
+ const h = headerWriter()
120
+ .str('toolCallId', chunk.toolCallId)
121
+ .str('toolName', chunk.toolName)
122
+ .json('providerMetadata', chunk.providerMetadata)
123
+ .build();
124
+ await this._core.closeStream(chunk.toolCallId, { name: 'tool-input', data: '', headers: h });
125
+ } catch (error: unknown) {
126
+ // Only fall through to discrete for "no active stream" — rethrow real failures
127
+ if (!(error instanceof Ably.ErrorInfo && errorInfoIs(error, ErrorCode.InvalidArgument))) {
128
+ throw error;
129
+ }
130
+ const h = headerWriter()
131
+ .str('toolCallId', chunk.toolCallId)
132
+ .str('toolName', chunk.toolName)
133
+ .bool('dynamic', chunk.dynamic)
134
+ .str('title', chunk.title)
135
+ .bool('providerExecuted', chunk.providerExecuted)
136
+ .json('providerMetadata', chunk.providerMetadata)
137
+ .build();
138
+ await this._core.publishDiscrete({ name: 'tool-input', data: chunk.input, headers: h });
139
+ }
140
+ break;
141
+ }
142
+
143
+ // -- Discrete: lifecycle events ---------------------------------------
144
+
145
+ case 'start': {
146
+ const h = headerWriter()
147
+ .str('messageId', chunk.messageId)
148
+ .json('messageMetadata', chunk.messageMetadata)
149
+ .build();
150
+ await this._core.publishDiscrete({ name: 'start', data: '', headers: h }, perWrite);
151
+ break;
152
+ }
153
+
154
+ case 'start-step': {
155
+ await this._core.publishDiscrete({ name: 'start-step', data: '' }, perWrite);
156
+ break;
157
+ }
158
+
159
+ case 'finish-step': {
160
+ await this._core.publishDiscrete({ name: 'finish-step', data: '' }, perWrite);
161
+ break;
162
+ }
163
+
164
+ case 'finish': {
165
+ const h = headerWriter()
166
+ .str('finishReason', chunk.finishReason)
167
+ .json('messageMetadata', chunk.messageMetadata)
168
+ .build();
169
+ await this._core.publishDiscrete({ name: 'finish', data: '', headers: h }, perWrite);
170
+ break;
171
+ }
172
+
173
+ case 'error': {
174
+ await this._core.publishDiscrete({ name: 'error', data: chunk.errorText }, perWrite);
175
+ break;
176
+ }
177
+
178
+ case 'abort': {
179
+ this._aborted = true;
180
+ await this._core.abortAllStreams(perWrite);
181
+ await this._core.publishDiscrete(
182
+ { name: 'abort', data: chunk.reason ?? '', headers: { [HEADER_STATUS]: 'aborted' } },
183
+ perWrite,
184
+ );
185
+ break;
186
+ }
187
+
188
+ // -- Discrete: tool lifecycle events ----------------------------------
189
+
190
+ case 'tool-input-error': {
191
+ const h = headerWriter()
192
+ .str('toolCallId', chunk.toolCallId)
193
+ .str('toolName', chunk.toolName)
194
+ .bool('dynamic', chunk.dynamic)
195
+ .str('title', chunk.title)
196
+ .bool('providerExecuted', chunk.providerExecuted)
197
+ .json('providerMetadata', chunk.providerMetadata)
198
+ .build();
199
+ await this._core.publishDiscrete({
200
+ name: 'tool-input-error',
201
+ data: { errorText: chunk.errorText, input: chunk.input },
202
+ headers: h,
203
+ });
204
+ break;
205
+ }
206
+
207
+ case 'tool-output-available': {
208
+ const h = headerWriter()
209
+ .str('toolCallId', chunk.toolCallId)
210
+ .bool('dynamic', chunk.dynamic)
211
+ .bool('providerExecuted', chunk.providerExecuted)
212
+ .bool('preliminary', chunk.preliminary)
213
+ .build();
214
+ await this._core.publishDiscrete({
215
+ name: 'tool-output-available',
216
+ data: { output: chunk.output },
217
+ headers: h,
218
+ });
219
+ break;
220
+ }
221
+
222
+ case 'tool-output-error': {
223
+ const h = headerWriter()
224
+ .str('toolCallId', chunk.toolCallId)
225
+ .bool('dynamic', chunk.dynamic)
226
+ .bool('providerExecuted', chunk.providerExecuted)
227
+ .build();
228
+ await this._core.publishDiscrete({
229
+ name: 'tool-output-error',
230
+ data: { errorText: chunk.errorText },
231
+ headers: h,
232
+ });
233
+ break;
234
+ }
235
+
236
+ case 'tool-approval-request': {
237
+ const h = headerWriter().str('toolCallId', chunk.toolCallId).str('approvalId', chunk.approvalId).build();
238
+ await this._core.publishDiscrete({ name: 'tool-approval-request', data: '', headers: h }, perWrite);
239
+ break;
240
+ }
241
+
242
+ case 'tool-output-denied': {
243
+ const h = headerWriter().str('toolCallId', chunk.toolCallId).build();
244
+ await this._core.publishDiscrete({ name: 'tool-output-denied', data: '', headers: h }, perWrite);
245
+ break;
246
+ }
247
+
248
+ // -- Discrete: content parts ------------------------------------------
249
+
250
+ case 'file': {
251
+ const h = headerWriter()
252
+ .str('mediaType', chunk.mediaType)
253
+ .json('providerMetadata', chunk.providerMetadata)
254
+ .build();
255
+ await this._core.publishDiscrete({ name: 'file', data: chunk.url, headers: h }, perWrite);
256
+ break;
257
+ }
258
+
259
+ case 'source-url': {
260
+ const h = headerWriter()
261
+ .str('sourceId', chunk.sourceId)
262
+ .str('title', chunk.title)
263
+ .json('providerMetadata', chunk.providerMetadata)
264
+ .build();
265
+ await this._core.publishDiscrete({ name: 'source-url', data: chunk.url, headers: h }, perWrite);
266
+ break;
267
+ }
268
+
269
+ case 'source-document': {
270
+ const h = headerWriter()
271
+ .str('sourceId', chunk.sourceId)
272
+ .str('mediaType', chunk.mediaType)
273
+ .str('title', chunk.title)
274
+ .str('filename', chunk.filename)
275
+ .json('providerMetadata', chunk.providerMetadata)
276
+ .build();
277
+ await this._core.publishDiscrete({ name: 'source-document', data: '', headers: h }, perWrite);
278
+ break;
279
+ }
280
+
281
+ case 'message-metadata': {
282
+ const h = headerWriter().json('messageMetadata', chunk.messageMetadata).build();
283
+ await this._core.publishDiscrete({ name: 'message-metadata', data: '', headers: h }, perWrite);
284
+ break;
285
+ }
286
+
287
+ // -- Discrete: data-* custom chunks -----------------------------------
288
+
289
+ default: {
290
+ if (chunk.type.startsWith('data-')) {
291
+ const h = headerWriter().str('id', chunk.id).bool('transient', chunk.transient).build();
292
+ const ephemeral = chunk.transient === true;
293
+ await this._core.publishDiscrete({ name: chunk.type, data: chunk.data, headers: h, ephemeral }, perWrite);
294
+ }
295
+ break;
296
+ }
297
+ }
298
+ }
299
+
300
+ async writeEvent(chunk: AI.UIMessageChunk, perWrite?: WriteOptions): Promise<Ably.PublishResult> {
301
+ if (!chunk.type.startsWith('data-')) {
302
+ throw new Ably.ErrorInfo(
303
+ `unable to write event; only data-* chunk types are supported, got '${chunk.type}'`,
304
+ ErrorCode.InvalidArgument,
305
+ 400,
306
+ );
307
+ }
308
+ const h = headerWriter()
309
+ .str('id', 'id' in chunk ? chunk.id : undefined)
310
+ .bool('transient', 'transient' in chunk ? chunk.transient : undefined)
311
+ .build();
312
+ const ephemeral = 'transient' in chunk && chunk.transient === true;
313
+ return this._core.publishDiscrete(
314
+ { name: chunk.type, data: 'data' in chunk ? chunk.data : undefined, headers: h, ephemeral },
315
+ perWrite,
316
+ );
317
+ }
318
+
319
+ async writeMessages(messages: AI.UIMessage[], perWrite?: WriteOptions): Promise<Ably.PublishResult> {
320
+ const payloads = messages.flatMap((msg) => encodeMessagePayloads(msg));
321
+ return this._core.publishDiscreteBatch(payloads, perWrite);
322
+ }
323
+
324
+ async abort(reason?: string): Promise<void> {
325
+ if (this._aborted) return;
326
+ this._aborted = true;
327
+ await this._core.abortAllStreams();
328
+ await this._core.publishDiscrete({
329
+ name: 'abort',
330
+ data: reason ?? '',
331
+ headers: { [HEADER_STATUS]: 'aborted' },
332
+ });
333
+ }
334
+
335
+ async close(): Promise<void> {
336
+ await this._core.close();
337
+ }
338
+ }
339
+
340
+ // ---------------------------------------------------------------------------
341
+ // Message payload encoding (stateless helper)
342
+ // ---------------------------------------------------------------------------
343
+
344
+ const encodeMessagePayloads = (message: AI.UIMessage): MessagePayload[] => {
345
+ const messageId = message.id;
346
+ const payloads: MessagePayload[] = [];
347
+
348
+ for (const part of message.parts) {
349
+ switch (part.type) {
350
+ case 'text': {
351
+ payloads.push({ name: 'text', data: part.text, headers: headerWriter().str('messageId', messageId).build() });
352
+ break;
353
+ }
354
+ case 'file': {
355
+ payloads.push({
356
+ name: 'file',
357
+ data: part.url,
358
+ headers: headerWriter().str('messageId', messageId).str('mediaType', part.mediaType).build(),
359
+ });
360
+ break;
361
+ }
362
+ default: {
363
+ if (isDataUIPart(part)) {
364
+ payloads.push({
365
+ name: part.type,
366
+ data: part.data,
367
+ headers: headerWriter().str('messageId', messageId).str('id', part.id).build(),
368
+ });
369
+ }
370
+ break;
371
+ }
372
+ }
373
+ }
374
+
375
+ if (payloads.length === 0) {
376
+ payloads.push({ name: 'text', data: '', headers: headerWriter().str('messageId', messageId).build() });
377
+ }
378
+
379
+ return payloads;
380
+ };
381
+
382
+ // ---------------------------------------------------------------------------
383
+ // Factory
384
+ // ---------------------------------------------------------------------------
385
+
386
+ /**
387
+ * Create a Vercel AI SDK encoder that maps UIMessageChunk events to Ably
388
+ * channel operations via the encoder core.
389
+ * @param writer - The channel writer to publish messages through.
390
+ * @param options - Encoder configuration (clientId, extras, hooks, logger).
391
+ * @returns A {@link StreamEncoder} for UIMessageChunk/UIMessage.
392
+ */
393
+ export const createEncoder = (
394
+ writer: ChannelWriter,
395
+ options: EncoderCoreOptions = {},
396
+ ): StreamEncoder<AI.UIMessageChunk, AI.UIMessage> => new DefaultUIMessageEncoder(writer, options);
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Vercel AI SDK codec — maps UIMessageChunk events and UIMessage objects
3
+ * to/from native Ably message primitives (publish, append, update, delete).
4
+ *
5
+ * ```ts
6
+ * import { UIMessageCodec } from '@ably/ai-transport/vercel';
7
+ *
8
+ * const encoder = UIMessageCodec.createEncoder(writer, options);
9
+ * const decoder = UIMessageCodec.createDecoder();
10
+ * const accumulator = UIMessageCodec.createAccumulator();
11
+ * ```
12
+ */
13
+
14
+ import type * as AI from 'ai';
15
+
16
+ import type { Codec } from '../../core/codec/types.js';
17
+ import { createAccumulator } from './accumulator.js';
18
+ import { createDecoder } from './decoder.js';
19
+ import { createEncoder } from './encoder.js';
20
+
21
+ /**
22
+ * Vercel AI SDK codec implementing `Codec<UIMessageChunk, UIMessage>`.
23
+ *
24
+ * Provides factory methods for creating encoders, decoders, and accumulators
25
+ * that map between Vercel's UIMessageChunk/UIMessage types and Ably's native
26
+ * message primitives.
27
+ */
28
+ export const UIMessageCodec: Codec<AI.UIMessageChunk, AI.UIMessage> = {
29
+ createEncoder,
30
+ createDecoder,
31
+ createAccumulator,
32
+
33
+ getMessageKey: (message: AI.UIMessage): string => message.id,
34
+
35
+ isTerminal: (event: AI.UIMessageChunk): boolean =>
36
+ event.type === 'finish' || event.type === 'error' || event.type === 'abort',
37
+ };
@@ -0,0 +1,12 @@
1
+ // Vercel AI SDK codec
2
+ export { UIMessageCodec } from './codec/index.js';
3
+
4
+ // Vercel AI SDK transport wrappers (pre-bound to UIMessageCodec)
5
+ export type {
6
+ ChatTransport,
7
+ ChatTransportOptions,
8
+ SendMessagesRequestContext,
9
+ VercelClientTransportOptions,
10
+ VercelServerTransportOptions,
11
+ } from './transport/index.js';
12
+ export { createChatTransport, createClientTransport, createServerTransport } from './transport/index.js';
@@ -0,0 +1,4 @@
1
+ // Vercel-specific React hooks
2
+ export type { ChatTransport } from '../transport/chat-transport.js';
3
+ export { useChatTransport } from './use-chat-transport.js';
4
+ export { useMessageSync } from './use-message-sync.js';
@@ -0,0 +1,60 @@
1
+ /**
2
+ * useChatTransport: wraps a core ClientTransport into the ChatTransport
3
+ * shape that Vercel's useChat expects.
4
+ *
5
+ * Accepts either an existing ClientTransport or options to create one:
6
+ * - From an existing ClientTransport — wraps it directly
7
+ * - From options — creates a ClientTransport with UIMessageCodec and wraps it
8
+ *
9
+ * Both forms accept an optional second argument for ChatTransportOptions
10
+ * (e.g. prepareSendMessagesRequest for the persistence pattern).
11
+ *
12
+ * The hook does NOT auto-close the transport on unmount. Channel lifecycle is
13
+ * managed by the Ably provider (useChannel). Auto-closing would break React
14
+ * Strict Mode. Call chatTransport.close() explicitly if needed.
15
+ */
16
+
17
+ import type * as AI from 'ai';
18
+ import { useRef } from 'react';
19
+
20
+ import type { ClientTransport } from '../../core/transport/types.js';
21
+ import type { ChatTransport, ChatTransportOptions } from '../transport/chat-transport.js';
22
+ import { createChatTransport } from '../transport/chat-transport.js';
23
+ import type { VercelClientTransportOptions } from '../transport/index.js';
24
+ import { createClientTransport as createCoreClientTransport } from '../transport/index.js';
25
+
26
+ /**
27
+ * Type guard: distinguish an existing ClientTransport from options.
28
+ * @param x - Either a transport instance or options object.
29
+ * @returns True if the argument is a ClientTransport instance.
30
+ */
31
+ const isClientTransport = (
32
+ x: ClientTransport<AI.UIMessageChunk, AI.UIMessage> | VercelClientTransportOptions,
33
+ ): x is ClientTransport<AI.UIMessageChunk, AI.UIMessage> => 'send' in x && typeof x.send === 'function';
34
+
35
+ /**
36
+ * Create and memoize a {@link ChatTransport} for Vercel's useChat hook.
37
+ *
38
+ * Pass an existing `ClientTransport` to wrap it, or pass
39
+ * `VercelClientTransportOptions` to create one internally with UIMessageCodec.
40
+ * @param transportOrOptions - An existing ClientTransport, or options to create one.
41
+ * @param chatOptions - Optional hooks for customizing request construction.
42
+ * @returns A {@link ChatTransport} compatible with Vercel's useChat hook.
43
+ */
44
+ export const useChatTransport = (
45
+ transportOrOptions: ClientTransport<AI.UIMessageChunk, AI.UIMessage> | VercelClientTransportOptions,
46
+ chatOptions?: ChatTransportOptions,
47
+ ): ChatTransport => {
48
+ const chatTransportRef = useRef<ChatTransport | null>(null);
49
+
50
+ if (chatTransportRef.current === null) {
51
+ if (isClientTransport(transportOrOptions)) {
52
+ chatTransportRef.current = createChatTransport(transportOrOptions, chatOptions);
53
+ } else {
54
+ const transport = createCoreClientTransport(transportOrOptions);
55
+ chatTransportRef.current = createChatTransport(transport, chatOptions);
56
+ }
57
+ }
58
+
59
+ return chatTransportRef.current;
60
+ };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * useMessageSync: wires transport message lifecycle events into useChat's setMessages.
3
+ *
4
+ * Subscribes to the transport's 'message' event and replaces messages state
5
+ * with the transport's authoritative message list. Events fire immediately
6
+ * on every store update (including during active streaming), so this hook
7
+ * keeps React state in sync in real time.
8
+ *
9
+ * Returns the unsubscribe function in the useEffect cleanup so handlers
10
+ * are removed on unmount or when dependencies change.
11
+ */
12
+
13
+ import type * as AI from 'ai';
14
+ import { useEffect } from 'react';
15
+
16
+ import type { ClientTransport } from '../../core/transport/types.js';
17
+
18
+ /**
19
+ * Wire transport message updates into useChat's `setMessages` updater.
20
+ * @param transport - The client transport to observe, or null/undefined if not yet available.
21
+ * @param setMessages - The `setMessages` updater function from useChat.
22
+ */
23
+ export const useMessageSync = (
24
+ transport: ClientTransport<unknown, AI.UIMessage> | null | undefined,
25
+ setMessages: (updater: (prev: AI.UIMessage[]) => AI.UIMessage[]) => void,
26
+ ): void => {
27
+ useEffect(() => {
28
+ if (!transport) return;
29
+ const unsubscribe = transport.on('message', () => {
30
+ setMessages(() => transport.getMessages());
31
+ });
32
+ return unsubscribe;
33
+ }, [transport, setMessages]);
34
+ };
@@ -0,0 +1,33 @@
1
+ import { resolve } from 'path';
2
+ import { defineConfig } from 'vite';
3
+ import dts from 'vite-plugin-dts';
4
+
5
+ export default defineConfig({
6
+ root: resolve(__dirname, '.'),
7
+ plugins: [
8
+ dts({
9
+ entryRoot: resolve(__dirname, '.'),
10
+ insertTypesEntry: true,
11
+ }),
12
+ ],
13
+ build: {
14
+ outDir: '../../../dist/vercel/react',
15
+ lib: {
16
+ entry: resolve(__dirname, 'index.ts'),
17
+ name: 'AblyAiTransportVercelReact',
18
+ fileName: 'ably-ai-transport-vercel-react',
19
+ formats: ['es', 'umd'],
20
+ },
21
+ rollupOptions: {
22
+ external: ['ably', 'ai', 'react'],
23
+ output: {
24
+ globals: {
25
+ ably: 'Ably',
26
+ ai: 'AI',
27
+ react: 'React',
28
+ },
29
+ },
30
+ },
31
+ sourcemap: true,
32
+ },
33
+ });