@ably/ai-transport 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/README.md +10 -19
  2. package/dist/ably-ai-transport.js +1790 -1091
  3. package/dist/ably-ai-transport.js.map +1 -1
  4. package/dist/ably-ai-transport.umd.cjs +1 -1
  5. package/dist/ably-ai-transport.umd.cjs.map +1 -1
  6. package/dist/constants.d.ts +2 -2
  7. package/dist/core/agent.d.ts +20 -5
  8. package/dist/core/channel-options.d.ts +57 -0
  9. package/dist/core/codec/codec-event.d.ts +9 -0
  10. package/dist/core/codec/decoder.d.ts +4 -1
  11. package/dist/core/codec/define-codec.d.ts +100 -0
  12. package/dist/core/codec/encoder.d.ts +2 -7
  13. package/dist/core/codec/field-bag.d.ts +85 -0
  14. package/dist/core/codec/fields.d.ts +141 -0
  15. package/dist/core/codec/index.d.ts +8 -1
  16. package/dist/core/codec/input-descriptor-decoder.d.ts +19 -0
  17. package/dist/core/codec/input-descriptor-encoder.d.ts +22 -0
  18. package/dist/core/codec/input-descriptors.d.ts +281 -0
  19. package/dist/core/codec/output-descriptor-decoder.d.ts +29 -0
  20. package/dist/core/codec/output-descriptor-encoder.d.ts +31 -0
  21. package/dist/core/codec/output-descriptors.d.ts +237 -0
  22. package/dist/core/codec/types.d.ts +95 -36
  23. package/dist/core/codec/well-known-inputs.d.ts +52 -0
  24. package/dist/core/transport/agent-view.d.ts +296 -0
  25. package/dist/core/transport/decode-fold.d.ts +40 -32
  26. package/dist/core/transport/headers.d.ts +30 -1
  27. package/dist/core/transport/index.d.ts +1 -1
  28. package/dist/core/transport/invocation.d.ts +1 -1
  29. package/dist/core/transport/load-history-pages.d.ts +71 -0
  30. package/dist/core/transport/load-history.d.ts +21 -16
  31. package/dist/core/transport/run-manager.d.ts +9 -11
  32. package/dist/core/transport/session-support.d.ts +55 -0
  33. package/dist/core/transport/tree.d.ts +165 -15
  34. package/dist/core/transport/types/agent.d.ts +120 -98
  35. package/dist/core/transport/types/client.d.ts +45 -12
  36. package/dist/core/transport/types/tree.d.ts +52 -10
  37. package/dist/core/transport/types/view.d.ts +55 -28
  38. package/dist/core/transport/view.d.ts +176 -58
  39. package/dist/core/transport/wire-log.d.ts +102 -0
  40. package/dist/errors.d.ts +10 -4
  41. package/dist/index.d.ts +6 -5
  42. package/dist/react/ably-ai-transport-react.js +784 -415
  43. package/dist/react/ably-ai-transport-react.js.map +1 -1
  44. package/dist/react/ably-ai-transport-react.umd.cjs +1 -1
  45. package/dist/react/ably-ai-transport-react.umd.cjs.map +1 -1
  46. package/dist/react/contexts/client-session-context.d.ts +2 -1
  47. package/dist/react/contexts/client-session-provider.d.ts +3 -0
  48. package/dist/react/index.d.ts +2 -1
  49. package/dist/react/internal/skipped-session.d.ts +8 -0
  50. package/dist/react/use-view.d.ts +3 -3
  51. package/dist/utils.d.ts +22 -54
  52. package/dist/vercel/ably-ai-transport-vercel.js +2297 -2026
  53. package/dist/vercel/ably-ai-transport-vercel.js.map +1 -1
  54. package/dist/vercel/ably-ai-transport-vercel.umd.cjs +1 -1
  55. package/dist/vercel/ably-ai-transport-vercel.umd.cjs.map +1 -1
  56. package/dist/vercel/codec/decode-lifecycle.d.ts +9 -0
  57. package/dist/vercel/codec/events.d.ts +1 -2
  58. package/dist/vercel/codec/fields.d.ts +44 -0
  59. package/dist/vercel/codec/fold-content.d.ts +16 -0
  60. package/dist/vercel/codec/fold-data.d.ts +16 -0
  61. package/dist/vercel/codec/fold-input.d.ts +67 -0
  62. package/dist/vercel/codec/fold-lifecycle.d.ts +16 -0
  63. package/dist/vercel/codec/fold-text.d.ts +16 -0
  64. package/dist/vercel/codec/fold-tool-input.d.ts +17 -0
  65. package/dist/vercel/codec/fold-tool-output.d.ts +16 -0
  66. package/dist/vercel/codec/index.d.ts +5 -30
  67. package/dist/vercel/codec/inputs.d.ts +11 -0
  68. package/dist/vercel/codec/outputs.d.ts +11 -0
  69. package/dist/vercel/codec/reducer-state.d.ts +121 -0
  70. package/dist/vercel/codec/reducer.d.ts +20 -102
  71. package/dist/vercel/codec/tool-transitions.d.ts +0 -6
  72. package/dist/vercel/codec/wire-data.d.ts +34 -0
  73. package/dist/vercel/index.d.ts +1 -0
  74. package/dist/vercel/react/ably-ai-transport-vercel-react.js +2013 -9500
  75. package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
  76. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +1 -70
  77. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
  78. package/dist/vercel/react/contexts/chat-transport-context.d.ts +2 -1
  79. package/dist/vercel/run-end-reason.d.ts +66 -11
  80. package/dist/vercel/tool-part.d.ts +21 -0
  81. package/dist/vercel/transport/chat-transport.d.ts +0 -2
  82. package/dist/vercel/transport/index.d.ts +1 -1
  83. package/dist/vercel/transport/run-output-stream.d.ts +6 -8
  84. package/dist/version.d.ts +1 -1
  85. package/package.json +2 -2
  86. package/src/constants.ts +2 -2
  87. package/src/core/agent.ts +43 -19
  88. package/src/core/channel-options.ts +89 -0
  89. package/src/core/codec/codec-event.ts +27 -0
  90. package/src/core/codec/decoder.ts +145 -21
  91. package/src/core/codec/define-codec.ts +432 -0
  92. package/src/core/codec/encoder.ts +13 -54
  93. package/src/core/codec/field-bag.ts +142 -0
  94. package/src/core/codec/fields.ts +193 -0
  95. package/src/core/codec/index.ts +43 -0
  96. package/src/core/codec/input-descriptor-decoder.ts +97 -0
  97. package/src/core/codec/input-descriptor-encoder.ts +150 -0
  98. package/src/core/codec/input-descriptors.ts +373 -0
  99. package/src/core/codec/output-descriptor-decoder.ts +139 -0
  100. package/src/core/codec/output-descriptor-encoder.ts +101 -0
  101. package/src/core/codec/output-descriptors.ts +307 -0
  102. package/src/core/codec/types.ts +99 -36
  103. package/src/core/codec/well-known-inputs.ts +96 -0
  104. package/src/core/transport/agent-session.ts +330 -589
  105. package/src/core/transport/agent-view.ts +738 -0
  106. package/src/core/transport/client-session.ts +74 -69
  107. package/src/core/transport/decode-fold.ts +57 -47
  108. package/src/core/transport/headers.ts +57 -4
  109. package/src/core/transport/index.ts +2 -1
  110. package/src/core/transport/invocation.ts +1 -1
  111. package/src/core/transport/load-history-pages.ts +220 -0
  112. package/src/core/transport/load-history.ts +63 -61
  113. package/src/core/transport/pipe-stream.ts +10 -1
  114. package/src/core/transport/run-manager.ts +25 -31
  115. package/src/core/transport/session-support.ts +96 -0
  116. package/src/core/transport/tree.ts +414 -47
  117. package/src/core/transport/types/agent.ts +129 -102
  118. package/src/core/transport/types/client.ts +49 -13
  119. package/src/core/transport/types/tree.ts +61 -12
  120. package/src/core/transport/types/view.ts +57 -28
  121. package/src/core/transport/view.ts +520 -172
  122. package/src/core/transport/wire-log.ts +189 -0
  123. package/src/errors.ts +10 -3
  124. package/src/index.ts +44 -11
  125. package/src/react/contexts/client-session-context.ts +1 -1
  126. package/src/react/contexts/client-session-provider.tsx +38 -2
  127. package/src/react/index.ts +2 -1
  128. package/src/react/internal/skipped-session.ts +62 -0
  129. package/src/react/use-client-session.ts +7 -30
  130. package/src/react/use-view.ts +3 -3
  131. package/src/utils.ts +31 -97
  132. package/src/vercel/codec/decode-lifecycle.ts +70 -0
  133. package/src/vercel/codec/events.ts +1 -3
  134. package/src/vercel/codec/fields.ts +58 -0
  135. package/src/vercel/codec/fold-content.ts +54 -0
  136. package/src/vercel/codec/fold-data.ts +46 -0
  137. package/src/vercel/codec/fold-input.ts +255 -0
  138. package/src/vercel/codec/fold-lifecycle.ts +85 -0
  139. package/src/vercel/codec/fold-text.ts +55 -0
  140. package/src/vercel/codec/fold-tool-input.ts +86 -0
  141. package/src/vercel/codec/fold-tool-output.ts +79 -0
  142. package/src/vercel/codec/index.ts +23 -63
  143. package/src/vercel/codec/inputs.ts +116 -0
  144. package/src/vercel/codec/outputs.ts +207 -0
  145. package/src/vercel/codec/reducer-state.ts +169 -0
  146. package/src/vercel/codec/reducer.ts +52 -838
  147. package/src/vercel/codec/tool-transitions.ts +1 -12
  148. package/src/vercel/codec/wire-data.ts +64 -0
  149. package/src/vercel/index.ts +1 -0
  150. package/src/vercel/react/contexts/chat-transport-context.ts +1 -1
  151. package/src/vercel/react/use-chat-transport.ts +8 -28
  152. package/src/vercel/react/use-message-sync.ts +5 -10
  153. package/src/vercel/run-end-reason.ts +95 -16
  154. package/src/vercel/tool-part.ts +25 -0
  155. package/src/vercel/transport/chat-transport.ts +10 -22
  156. package/src/vercel/transport/index.ts +1 -1
  157. package/src/vercel/transport/run-output-stream.ts +7 -8
  158. package/src/version.ts +1 -1
  159. package/dist/core/transport/branch-chain.d.ts +0 -43
  160. package/dist/core/transport/load-conversation.d.ts +0 -128
  161. package/dist/vercel/codec/decoder.d.ts +0 -9
  162. package/dist/vercel/codec/encoder.d.ts +0 -11
  163. package/src/core/transport/branch-chain.ts +0 -58
  164. package/src/core/transport/load-conversation.ts +0 -355
  165. package/src/vercel/codec/decoder.ts +0 -696
  166. package/src/vercel/codec/encoder.ts +0 -548
@@ -3,7 +3,10 @@
3
3
  *
4
4
  * Handles the Ably message action patterns (create, append, update, delete)
5
5
  * and delegates to domain-specific hooks for event building and discrete
6
- * event decoding.
6
+ * event decoding. Stream trackers are version-guarded: a delivery whose
7
+ * `Message.version.serial` the tracker has already incorporated decodes to
8
+ * nothing, so the same decoder instance can serve both the live
9
+ * subscription and history hydration without double-decoding.
7
10
  *
8
11
  * Domain decoders call `createDecoderCore(hooks, options)` and provide hooks
9
12
  * for stream classification, event building, and discrete decoding. Hooks
@@ -102,7 +105,7 @@ class DefaultDecoderCore<TEvent> implements DecoderCore<TEvent> {
102
105
  case 'message.create': {
103
106
  const payload = this._toPayload(message);
104
107
  return payload.transportHeaders?.[HEADER_STREAM] === 'true'
105
- ? this._decodeStreamedCreate(payload, message.serial)
108
+ ? this._decodeStreamedCreate(payload, message.serial, message.version.serial)
106
109
  : this._hooks.decodeDiscrete(payload);
107
110
  }
108
111
 
@@ -169,13 +172,120 @@ class DefaultDecoderCore<TEvent> implements DecoderCore<TEvent> {
169
172
  }
170
173
  }
171
174
 
175
+ // -------------------------------------------------------------------------
176
+ // Private: version guard
177
+ // -------------------------------------------------------------------------
178
+
179
+ /**
180
+ * Whether a delivery is already incorporated into (or out of contract for)
181
+ * an existing tracker, and so must decode to nothing. Covers two cases:
182
+ *
183
+ * - The delivery carries a `version.serial` at or below the tracker's —
184
+ * the mutation it describes is already incorporated (a history aggregate
185
+ * covered by live deltas, a resume retransmission, a whole-wire replay).
186
+ * - The tracker is closed — the stream has ended and its accumulated text
187
+ * has been dropped, so nothing further can fold into it. In-contract
188
+ * replays are already covered by the version check; this catches
189
+ * out-of-contract version-less deliveries for an ended stream.
190
+ *
191
+ * A version-bearing delivery that passes advances the tracker's version.
192
+ * @param method - Calling method name, for log messages.
193
+ * @param serial - The message serial (the tracker's key).
194
+ * @param tracker - The existing tracker for the serial.
195
+ * @param version - The delivery's `Message.version.serial`, if present.
196
+ * @returns True when the delivery must decode to nothing.
197
+ */
198
+ private _alreadyIncorporated(
199
+ method: string,
200
+ serial: string,
201
+ tracker: StreamTrackerState,
202
+ version: string | undefined,
203
+ ): boolean {
204
+ if (version !== undefined && version <= tracker.version) {
205
+ this._logger?.debug(`DefaultDecoderCore.${method}(); delivery already incorporated`, {
206
+ serial,
207
+ version,
208
+ trackerVersion: tracker.version,
209
+ });
210
+ return true;
211
+ }
212
+ if (tracker.closed) {
213
+ this._logger?.debug(`DefaultDecoderCore.${method}(); stream closed, dropping delivery`, { serial, version });
214
+ return true;
215
+ }
216
+ if (version !== undefined) tracker.version = version;
217
+ return false;
218
+ }
219
+
220
+ /**
221
+ * Close a tracker, dropping its accumulated text. What remains is a
222
+ * `{version, closed}` tombstone: enough to recognise covered replays and
223
+ * out-of-contract post-close deliveries, without retaining the stream's
224
+ * full content for the decoder's lifetime.
225
+ * @param tracker - The tracker to close.
226
+ */
227
+ private _closeTracker(tracker: StreamTrackerState): void {
228
+ tracker.closed = true;
229
+ tracker.accumulated = '';
230
+ }
231
+
232
+ // -------------------------------------------------------------------------
233
+ // Private: terminal-status transition
234
+ // -------------------------------------------------------------------------
235
+
236
+ /**
237
+ * Apply a stream's terminal status (complete / cancelled) to a tracker. On
238
+ * `complete` it emits end events (read before the tracker is closed) and
239
+ * then closes the tracker; on `cancelled` it closes silently. Both the
240
+ * append and prefix-match update paths funnel through here so they can't
241
+ * diverge. Covered replays and post-close deliveries are filtered upstream
242
+ * by `_alreadyIncorporated`, so no closed-once guard is needed here.
243
+ * Returns whether a terminal transition fired (so callers can log it).
244
+ * @param tracker - The stream tracker to close.
245
+ * @param status - The status header value from the message (may be undefined).
246
+ * @param closingCodecHeaders - Codec headers from the closing message, passed to buildEndEvents.
247
+ * @param outputs - The output array end events are pushed into.
248
+ * @returns True when this call closed the tracker; false otherwise.
249
+ */
250
+ private _applyTerminalStatus(
251
+ tracker: StreamTrackerState,
252
+ status: string | undefined,
253
+ closingCodecHeaders: Record<string, string>,
254
+ outputs: TEvent[],
255
+ ): boolean {
256
+ if (status === 'complete') {
257
+ outputs.push(...this._hooks.buildEndEvents(tracker, closingCodecHeaders));
258
+ this._closeTracker(tracker);
259
+ return true;
260
+ }
261
+ if (status === 'cancelled') {
262
+ this._closeTracker(tracker);
263
+ return true;
264
+ }
265
+ return false;
266
+ }
267
+
172
268
  // -------------------------------------------------------------------------
173
269
  // Private: streamed message create
174
270
  // -------------------------------------------------------------------------
175
271
 
176
- private _decodeStreamedCreate(payload: MessagePayload, serial: string | undefined): TEvent[] {
272
+ private _decodeStreamedCreate(
273
+ payload: MessagePayload,
274
+ serial: string | undefined,
275
+ version: string | undefined,
276
+ ): TEvent[] {
177
277
  if (!serial) return [];
178
278
 
279
+ const existing = this._serialState.get(serial);
280
+ if (existing) {
281
+ // A create is the message's first version, so a tracker for this serial
282
+ // has already incorporated it (resume retransmission, whole-wire replay).
283
+ this._logger?.debug('DefaultDecoderCore._decodeStreamedCreate(); duplicate create for tracked stream', {
284
+ serial,
285
+ });
286
+ return [];
287
+ }
288
+
179
289
  const streamId = payload.transportHeaders?.[HEADER_STREAM_ID] ?? '';
180
290
 
181
291
  const tracker: StreamTrackerState = {
@@ -184,6 +294,7 @@ class DefaultDecoderCore<TEvent> implements DecoderCore<TEvent> {
184
294
  accumulated: '',
185
295
  codecHeaders: { ...payload.codecHeaders },
186
296
  transportHeaders: { ...payload.transportHeaders },
297
+ version: version ?? serial,
187
298
  closed: false,
188
299
  };
189
300
  this._serialState.set(serial, tracker);
@@ -208,10 +319,18 @@ class DefaultDecoderCore<TEvent> implements DecoderCore<TEvent> {
208
319
 
209
320
  const tracker = this._serialState.get(serial);
210
321
  if (!tracker) {
211
- // Unknown serial on append treat as first-contact update
322
+ // Out of contract: the platform converts the first post-attach append
323
+ // of an in-flight message into a full-contents update, so an append
324
+ // should never be a stream's first contact. Keep the first-contact
325
+ // heuristic as a defensive fallback.
326
+ this._logger?.warn('DefaultDecoderCore._decodeAppend(); append with no tracker, treating as first contact', {
327
+ serial,
328
+ });
212
329
  return this._decodeUpdate(message);
213
330
  }
214
331
 
332
+ if (this._alreadyIncorporated('_decodeAppend', serial, tracker, message.version.serial)) return [];
333
+
215
334
  const transport = getTransportHeaders(message);
216
335
  const closingCodec = getCodecHeaders(message);
217
336
  const delta = typeof message.data === 'string' ? message.data : '';
@@ -223,13 +342,13 @@ class DefaultDecoderCore<TEvent> implements DecoderCore<TEvent> {
223
342
  outputs.push(...this._hooks.buildDeltaEvents(tracker, delta));
224
343
  }
225
344
 
226
- if (status === 'complete' && !tracker.closed) {
227
- tracker.closed = true;
228
- outputs.push(...this._hooks.buildEndEvents(tracker, closingCodec));
229
- this._logger?.debug('DefaultDecoderCore._decodeAppend(); stream complete', { streamId: tracker.streamId });
230
- } else if (status === 'cancelled' && !tracker.closed) {
231
- tracker.closed = true;
232
- this._logger?.debug('DefaultDecoderCore._decodeAppend(); stream cancelled', { streamId: tracker.streamId });
345
+ if (this._applyTerminalStatus(tracker, status, closingCodec, outputs)) {
346
+ this._logger?.debug(
347
+ `DefaultDecoderCore._decodeAppend(); stream ${status === 'complete' ? 'complete' : 'cancelled'}`,
348
+ {
349
+ streamId: tracker.streamId,
350
+ },
351
+ );
233
352
  }
234
353
 
235
354
  return outputs;
@@ -253,9 +372,11 @@ class DefaultDecoderCore<TEvent> implements DecoderCore<TEvent> {
253
372
  const tracker = this._serialState.get(serial);
254
373
 
255
374
  if (!tracker) {
256
- return this._decodeFirstContact(payload, isStreamed, status, serial);
375
+ return this._decodeFirstContact(payload, isStreamed, status, serial, message.version.serial);
257
376
  }
258
377
 
378
+ if (this._alreadyIncorporated('_decodeUpdate', serial, tracker, message.version.serial)) return [];
379
+
259
380
  // Updates to tracked streams use string data for prefix-match accumulation
260
381
  const data = this._stringData(message);
261
382
 
@@ -269,12 +390,7 @@ class DefaultDecoderCore<TEvent> implements DecoderCore<TEvent> {
269
390
  outputs.push(...this._hooks.buildDeltaEvents(tracker, delta));
270
391
  }
271
392
 
272
- if (status === 'complete' && !tracker.closed) {
273
- tracker.closed = true;
274
- outputs.push(...this._hooks.buildEndEvents(tracker, codec));
275
- } else if (status === 'cancelled' && !tracker.closed) {
276
- tracker.closed = true;
277
- }
393
+ this._applyTerminalStatus(tracker, status, codec, outputs);
278
394
 
279
395
  return outputs;
280
396
  }
@@ -294,6 +410,7 @@ class DefaultDecoderCore<TEvent> implements DecoderCore<TEvent> {
294
410
  isStreamed: boolean,
295
411
  status: string | undefined,
296
412
  serial: string,
413
+ version: string | undefined,
297
414
  ): TEvent[] {
298
415
  // Non-streamed messages are discrete
299
416
  if (!isStreamed) {
@@ -317,7 +434,8 @@ class DefaultDecoderCore<TEvent> implements DecoderCore<TEvent> {
317
434
  accumulated: data,
318
435
  codecHeaders: { ...codec },
319
436
  transportHeaders: { ...payload.transportHeaders },
320
- closed: status === 'complete' || status === 'cancelled',
437
+ version: version ?? serial,
438
+ closed: false,
321
439
  };
322
440
  this._serialState.set(serial, newTracker);
323
441
 
@@ -332,6 +450,10 @@ class DefaultDecoderCore<TEvent> implements DecoderCore<TEvent> {
332
450
  outputs.push(...this._hooks.buildEndEvents(newTracker, codec));
333
451
  }
334
452
 
453
+ if (status === 'complete' || status === 'cancelled') {
454
+ this._closeTracker(newTracker);
455
+ }
456
+
335
457
  return outputs;
336
458
  }
337
459
 
@@ -349,8 +471,10 @@ class DefaultDecoderCore<TEvent> implements DecoderCore<TEvent> {
349
471
  this._invokeOnStreamDelete(serial, tracker);
350
472
 
351
473
  if (tracker) {
352
- tracker.accumulated = '';
353
- tracker.closed = true;
474
+ // No need to advance the tracker's version here: `_closeTracker` leaves a
475
+ // closed tombstone, and `_alreadyIncorporated`'s closed check drops every
476
+ // later delivery regardless of version.
477
+ this._closeTracker(tracker);
354
478
  }
355
479
 
356
480
  this._logger?.debug('DefaultDecoderCore._decodeDelete();', { serial });