@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,402 @@
1
+ /**
2
+ * Decoder core — action dispatch and serial tracking machinery.
3
+ *
4
+ * Handles the Ably message action patterns (create, append, update, delete)
5
+ * and delegates to domain-specific hooks for event building and discrete
6
+ * event decoding.
7
+ *
8
+ * Domain decoders call `createDecoderCore(hooks, options)` and provide hooks
9
+ * for stream classification, event building, and discrete decoding.
10
+ */
11
+
12
+ import type * as Ably from 'ably';
13
+
14
+ import { HEADER_MSG_ID, HEADER_STATUS, HEADER_STREAM, HEADER_STREAM_ID } from '../../constants.js';
15
+ import type { Logger } from '../../logger.js';
16
+ import { getHeaders } from '../../utils.js';
17
+ import type { DecoderOutput, MessagePayload, StreamTrackerState } from './types.js';
18
+
19
+ /**
20
+ * Wrap a domain event as a single-element decoder output array.
21
+ * @param event - The domain event to wrap.
22
+ * @returns A single-element array containing the event as a decoder output.
23
+ */
24
+ export const eventOutput = <TEvent, TMessage>(event: TEvent): DecoderOutput<TEvent, TMessage>[] => [
25
+ { kind: 'event', event },
26
+ ];
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Options
30
+ // ---------------------------------------------------------------------------
31
+
32
+ /** Options for creating a decoder core. */
33
+ export interface DecoderCoreOptions {
34
+ /** Called when a tracked stream is replaced (non-prefix update). Receives the tracker with updated state. */
35
+ onStreamUpdate?: (tracker: StreamTrackerState) => void;
36
+ /** Called when a message is deleted. Receives the serial and tracker (if one exists). */
37
+ onStreamDelete?: (serial: string, tracker: StreamTrackerState | undefined) => void;
38
+ /** Logger instance for diagnostic output. */
39
+ logger?: Logger;
40
+ }
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Domain hooks
44
+ // ---------------------------------------------------------------------------
45
+
46
+ /** Hooks that a domain codec provides to the decoder core for stream classification and event building. */
47
+ export interface DecoderCoreHooks<TEvent, TMessage> {
48
+ /**
49
+ * Build domain events emitted when a new stream starts. May return multiple
50
+ * events (e.g. a start event and a start-step event).
51
+ */
52
+ buildStartEvents(tracker: StreamTrackerState): DecoderOutput<TEvent, TMessage>[];
53
+
54
+ /** Build domain events for a text delta received on a stream. */
55
+ buildDeltaEvents(tracker: StreamTrackerState, delta: string): DecoderOutput<TEvent, TMessage>[];
56
+
57
+ /**
58
+ * Build domain events emitted when a stream finishes (x-ably-status:finished).
59
+ * Not called for aborted streams. The closing headers may differ from
60
+ * tracker.headers if the closing append carried updated headers.
61
+ */
62
+ buildEndEvents(
63
+ tracker: StreamTrackerState,
64
+ closingHeaders: Record<string, string>,
65
+ ): DecoderOutput<TEvent, TMessage>[];
66
+
67
+ /**
68
+ * Decode a discrete message (message.create where x-ably-stream is "false",
69
+ * or a non-streamable first-contact update). Handles user messages, lifecycle
70
+ * events, tool lifecycle, data-*, etc.
71
+ */
72
+ decodeDiscrete(input: MessagePayload): DecoderOutput<TEvent, TMessage>[];
73
+ }
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // Interface
77
+ // ---------------------------------------------------------------------------
78
+
79
+ /** The decoder core returned by {@link createDecoderCore}. */
80
+ export interface DecoderCore<TEvent, TMessage> {
81
+ /** Decode a single Ably message into zero or more domain outputs. */
82
+ decode(message: Ably.InboundMessage): DecoderOutput<TEvent, TMessage>[];
83
+ }
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // Default implementation
87
+ // ---------------------------------------------------------------------------
88
+
89
+ // Spec: AIT-CD7
90
+ class DefaultDecoderCore<TEvent, TMessage> implements DecoderCore<TEvent, TMessage> {
91
+ private readonly _hooks: DecoderCoreHooks<TEvent, TMessage>;
92
+ private readonly _logger: Logger | undefined;
93
+ private readonly _onStreamUpdate: ((tracker: StreamTrackerState) => void) | undefined;
94
+ private readonly _onStreamDelete: ((serial: string, tracker: StreamTrackerState | undefined) => void) | undefined;
95
+ private readonly _serialState = new Map<string, StreamTrackerState>();
96
+
97
+ constructor(hooks: DecoderCoreHooks<TEvent, TMessage>, options: DecoderCoreOptions = {}) {
98
+ this._hooks = hooks;
99
+ this._onStreamUpdate = options.onStreamUpdate;
100
+ this._onStreamDelete = options.onStreamDelete;
101
+ this._logger = options.logger?.withContext({ component: 'DecoderCore' });
102
+ }
103
+
104
+ decode(message: Ably.InboundMessage): DecoderOutput<TEvent, TMessage>[] {
105
+ const action = message.action;
106
+
107
+ this._logger?.trace('DefaultDecoderCore.decode();', { action, serial: message.serial, name: message.name });
108
+
109
+ let outputs: DecoderOutput<TEvent, TMessage>[];
110
+
111
+ switch (action) {
112
+ // Spec: AIT-CD7a
113
+ case 'message.create': {
114
+ const payload = this._toPayload(message);
115
+
116
+ outputs =
117
+ payload.headers?.[HEADER_STREAM] === 'true'
118
+ ? this._decodeStreamedCreate(payload, message.serial)
119
+ : this._hooks.decodeDiscrete(payload);
120
+ break;
121
+ }
122
+
123
+ case 'message.append': {
124
+ outputs = this._decodeAppend(message);
125
+ break;
126
+ }
127
+
128
+ case 'message.update': {
129
+ outputs = this._decodeUpdate(message);
130
+ break;
131
+ }
132
+
133
+ case 'message.delete': {
134
+ outputs = this._decodeDelete(message);
135
+ break;
136
+ }
137
+
138
+ default: {
139
+ return [];
140
+ }
141
+ }
142
+
143
+ // Tag all event outputs with the message ID from x-ably-msg-id for accumulator correlation.
144
+ const messageId = getHeaders(message)[HEADER_MSG_ID];
145
+ if (messageId) {
146
+ for (const output of outputs) {
147
+ if (output.kind === 'event') {
148
+ output.messageId = messageId;
149
+ }
150
+ }
151
+ }
152
+
153
+ return outputs;
154
+ }
155
+
156
+ // -------------------------------------------------------------------------
157
+ // Private: extract MessagePayload
158
+ // -------------------------------------------------------------------------
159
+
160
+ private _toPayload(message: Ably.InboundMessage): MessagePayload {
161
+ return {
162
+ name: message.name ?? '',
163
+ // CAST: Ably SDK types `data` as `any`; cast to unknown is the safe boundary type.
164
+ data: message.data as unknown,
165
+ headers: getHeaders(message),
166
+ };
167
+ }
168
+
169
+ /**
170
+ * Extract string data from an Ably message, for stream accumulation paths.
171
+ * @param message - The Ably message to extract string data from.
172
+ * @returns The string data, or empty string if data is not a string.
173
+ */
174
+ private _stringData(message: Ably.InboundMessage): string {
175
+ return typeof message.data === 'string' ? message.data : '';
176
+ }
177
+
178
+ // -------------------------------------------------------------------------
179
+ // Private: safe callback invocation
180
+ // -------------------------------------------------------------------------
181
+
182
+ private _invokeOnStreamUpdate(tracker: StreamTrackerState): void {
183
+ if (!this._onStreamUpdate) return;
184
+ try {
185
+ this._onStreamUpdate(tracker);
186
+ } catch (error) {
187
+ this._logger?.error('DefaultDecoderCore._invokeOnStreamUpdate(); callback threw', { error });
188
+ }
189
+ }
190
+
191
+ private _invokeOnStreamDelete(serial: string, tracker: StreamTrackerState | undefined): void {
192
+ if (!this._onStreamDelete) return;
193
+ try {
194
+ this._onStreamDelete(serial, tracker);
195
+ } catch (error) {
196
+ this._logger?.error('DefaultDecoderCore._invokeOnStreamDelete(); callback threw', { error });
197
+ }
198
+ }
199
+
200
+ // -------------------------------------------------------------------------
201
+ // Private: streamed message create
202
+ // -------------------------------------------------------------------------
203
+
204
+ private _decodeStreamedCreate(
205
+ payload: MessagePayload,
206
+ serial: string | undefined,
207
+ ): DecoderOutput<TEvent, TMessage>[] {
208
+ if (!serial) return [];
209
+
210
+ const streamId = payload.headers?.[HEADER_STREAM_ID] ?? '';
211
+ const h = payload.headers ?? {};
212
+
213
+ const tracker: StreamTrackerState = {
214
+ name: payload.name,
215
+ streamId,
216
+ accumulated: '',
217
+ headers: { ...h },
218
+ closed: false,
219
+ };
220
+ this._serialState.set(serial, tracker);
221
+
222
+ this._logger?.debug('DefaultDecoderCore._decodeStreamedCreate(); new stream', {
223
+ name: payload.name,
224
+ streamId,
225
+ serial,
226
+ });
227
+
228
+ return this._hooks.buildStartEvents(tracker);
229
+ }
230
+
231
+ // -------------------------------------------------------------------------
232
+ // Private: append handling
233
+ // -------------------------------------------------------------------------
234
+
235
+ // Spec: AIT-CD8
236
+ private _decodeAppend(message: Ably.InboundMessage): DecoderOutput<TEvent, TMessage>[] {
237
+ const serial = message.serial;
238
+ if (!serial) return [];
239
+
240
+ const tracker = this._serialState.get(serial);
241
+ if (!tracker) {
242
+ // Unknown serial on append — treat as first-contact update
243
+ return this._decodeUpdate(message);
244
+ }
245
+
246
+ const h = getHeaders(message);
247
+ const delta = typeof message.data === 'string' ? message.data : '';
248
+ const status = h[HEADER_STATUS];
249
+ const outputs: DecoderOutput<TEvent, TMessage>[] = [];
250
+
251
+ if (delta.length > 0) {
252
+ tracker.accumulated += delta;
253
+ outputs.push(...this._hooks.buildDeltaEvents(tracker, delta));
254
+ }
255
+
256
+ if (status === 'finished' && !tracker.closed) {
257
+ tracker.closed = true;
258
+ outputs.push(...this._hooks.buildEndEvents(tracker, h));
259
+ this._logger?.debug('DefaultDecoderCore._decodeAppend(); stream finished', { streamId: tracker.streamId });
260
+ } else if (status === 'aborted' && !tracker.closed) {
261
+ tracker.closed = true;
262
+ this._logger?.debug('DefaultDecoderCore._decodeAppend(); stream aborted', { streamId: tracker.streamId });
263
+ }
264
+
265
+ return outputs;
266
+ }
267
+
268
+ // -------------------------------------------------------------------------
269
+ // Private: update handling (first-contact, prefix-match, replacement)
270
+ // -------------------------------------------------------------------------
271
+
272
+ // Spec: AIT-CD9
273
+ private _decodeUpdate(message: Ably.InboundMessage): DecoderOutput<TEvent, TMessage>[] {
274
+ const serial = message.serial;
275
+ if (!serial) return [];
276
+
277
+ const payload = this._toPayload(message);
278
+ const h = payload.headers ?? {};
279
+ const isStreamed = h[HEADER_STREAM] === 'true';
280
+ const status = h[HEADER_STATUS];
281
+
282
+ const tracker = this._serialState.get(serial);
283
+
284
+ if (!tracker) {
285
+ return this._decodeFirstContact(payload, isStreamed, status, serial);
286
+ }
287
+
288
+ // Updates to tracked streams use string data for prefix-match accumulation
289
+ const data = this._stringData(message);
290
+
291
+ // --- Tracker exists: prefix-match or replacement ---
292
+ if (data.startsWith(tracker.accumulated)) {
293
+ const delta = data.slice(tracker.accumulated.length);
294
+ const outputs: DecoderOutput<TEvent, TMessage>[] = [];
295
+
296
+ if (delta.length > 0) {
297
+ tracker.accumulated = data;
298
+ outputs.push(...this._hooks.buildDeltaEvents(tracker, delta));
299
+ }
300
+
301
+ if (status === 'finished' && !tracker.closed) {
302
+ tracker.closed = true;
303
+ outputs.push(...this._hooks.buildEndEvents(tracker, h));
304
+ } else if (status === 'aborted' && !tracker.closed) {
305
+ tracker.closed = true;
306
+ }
307
+
308
+ return outputs;
309
+ }
310
+
311
+ // --- Replacement (NOT a prefix match) ---
312
+ tracker.accumulated = data;
313
+ tracker.headers = { ...h };
314
+
315
+ this._invokeOnStreamUpdate(tracker);
316
+
317
+ return [];
318
+ }
319
+
320
+ private _decodeFirstContact(
321
+ payload: MessagePayload,
322
+ isStreamed: boolean,
323
+ status: string | undefined,
324
+ serial: string,
325
+ ): DecoderOutput<TEvent, TMessage>[] {
326
+ // Non-streamed messages are discrete
327
+ if (!isStreamed) {
328
+ return this._hooks.decodeDiscrete(payload);
329
+ }
330
+
331
+ const streamId = payload.headers?.[HEADER_STREAM_ID] ?? '';
332
+ const h = payload.headers ?? {};
333
+ const data = typeof payload.data === 'string' ? payload.data : '';
334
+
335
+ this._logger?.debug('DefaultDecoderCore._decodeFirstContact(); first-contact stream', {
336
+ name: payload.name,
337
+ streamId,
338
+ serial,
339
+ });
340
+
341
+ // Create tracker
342
+ const newTracker: StreamTrackerState = {
343
+ name: payload.name,
344
+ streamId,
345
+ accumulated: data,
346
+ headers: { ...h },
347
+ closed: status === 'finished' || status === 'aborted',
348
+ };
349
+ this._serialState.set(serial, newTracker);
350
+
351
+ // Emit start + delta (if any) + end (if finished)
352
+ const outputs = this._hooks.buildStartEvents(newTracker);
353
+
354
+ if (data.length > 0) {
355
+ outputs.push(...this._hooks.buildDeltaEvents(newTracker, data));
356
+ }
357
+
358
+ if (status === 'finished') {
359
+ outputs.push(...this._hooks.buildEndEvents(newTracker, h));
360
+ }
361
+
362
+ return outputs;
363
+ }
364
+
365
+ // -------------------------------------------------------------------------
366
+ // Private: delete handling
367
+ // -------------------------------------------------------------------------
368
+
369
+ // Spec: AIT-CD10
370
+ private _decodeDelete(message: Ably.InboundMessage): DecoderOutput<TEvent, TMessage>[] {
371
+ const serial = message.serial;
372
+ if (!serial) return [];
373
+
374
+ const tracker = this._serialState.get(serial);
375
+
376
+ this._invokeOnStreamDelete(serial, tracker);
377
+
378
+ if (tracker) {
379
+ tracker.accumulated = '';
380
+ tracker.closed = true;
381
+ }
382
+
383
+ this._logger?.debug('DefaultDecoderCore._decodeDelete();', { serial });
384
+
385
+ return [];
386
+ }
387
+ }
388
+
389
+ // ---------------------------------------------------------------------------
390
+ // Factory
391
+ // ---------------------------------------------------------------------------
392
+
393
+ /**
394
+ * Create a decoder core with the given domain hooks.
395
+ * @param hooks - Domain-specific hooks for stream classification, event building, and discrete decoding.
396
+ * @param options - Decoder configuration (callbacks, logger).
397
+ * @returns A new {@link DecoderCore} instance.
398
+ */
399
+ export const createDecoderCore = <TEvent, TMessage>(
400
+ hooks: DecoderCoreHooks<TEvent, TMessage>,
401
+ options: DecoderCoreOptions = {},
402
+ ): DecoderCore<TEvent, TMessage> => new DefaultDecoderCore(hooks, options);