@ably/ai-transport 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/README.md +91 -100
  2. package/dist/ably-ai-transport.js +1553 -1238
  3. package/dist/ably-ai-transport.js.map +1 -1
  4. package/dist/ably-ai-transport.umd.cjs +1 -1
  5. package/dist/ably-ai-transport.umd.cjs.map +1 -1
  6. package/dist/constants.d.ts +116 -42
  7. package/dist/core/agent.d.ts +29 -0
  8. package/dist/core/codec/decoder.d.ts +20 -23
  9. package/dist/core/codec/encoder.d.ts +11 -8
  10. package/dist/core/codec/index.d.ts +1 -2
  11. package/dist/core/codec/lifecycle-tracker.d.ts +10 -9
  12. package/dist/core/codec/types.d.ts +407 -115
  13. package/dist/core/transport/agent-session.d.ts +10 -0
  14. package/dist/core/transport/branch-chain.d.ts +43 -0
  15. package/dist/core/transport/client-session.d.ts +13 -0
  16. package/dist/core/transport/decode-fold.d.ts +47 -0
  17. package/dist/core/transport/headers.d.ts +96 -18
  18. package/dist/core/transport/index.d.ts +5 -6
  19. package/dist/core/transport/internal/bounded-map.d.ts +20 -0
  20. package/dist/core/transport/invocation.d.ts +74 -0
  21. package/dist/core/transport/load-conversation.d.ts +128 -0
  22. package/dist/core/transport/load-history.d.ts +39 -0
  23. package/dist/core/transport/pipe-stream.d.ts +9 -9
  24. package/dist/core/transport/run-manager.d.ts +78 -0
  25. package/dist/core/transport/tree.d.ts +373 -109
  26. package/dist/core/transport/types/agent.d.ts +353 -0
  27. package/dist/core/transport/types/client.d.ts +168 -0
  28. package/dist/core/transport/types/shared.d.ts +24 -0
  29. package/dist/core/transport/types/tree.d.ts +315 -0
  30. package/dist/core/transport/types/view.d.ts +222 -0
  31. package/dist/core/transport/types.d.ts +13 -553
  32. package/dist/core/transport/view.d.ts +272 -84
  33. package/dist/errors.d.ts +21 -10
  34. package/dist/index.d.ts +6 -8
  35. package/dist/logger.d.ts +12 -0
  36. package/dist/react/ably-ai-transport-react.js +976 -990
  37. package/dist/react/ably-ai-transport-react.js.map +1 -1
  38. package/dist/react/ably-ai-transport-react.umd.cjs +1 -1
  39. package/dist/react/ably-ai-transport-react.umd.cjs.map +1 -1
  40. package/dist/react/contexts/client-session-context.d.ts +36 -0
  41. package/dist/react/contexts/client-session-provider.d.ts +53 -0
  42. package/dist/react/create-session-hooks.d.ts +116 -0
  43. package/dist/react/index.d.ts +12 -12
  44. package/dist/react/internal/use-resolved-session.d.ts +36 -0
  45. package/dist/react/use-ably-messages.d.ts +17 -14
  46. package/dist/react/use-client-session.d.ts +81 -0
  47. package/dist/react/use-create-view.d.ts +14 -13
  48. package/dist/react/use-tree.d.ts +30 -15
  49. package/dist/react/use-view.d.ts +82 -51
  50. package/dist/utils.d.ts +32 -23
  51. package/dist/vercel/ably-ai-transport-vercel.js +2573 -2086
  52. package/dist/vercel/ably-ai-transport-vercel.js.map +1 -1
  53. package/dist/vercel/ably-ai-transport-vercel.umd.cjs +1 -1
  54. package/dist/vercel/ably-ai-transport-vercel.umd.cjs.map +1 -1
  55. package/dist/vercel/codec/decoder.d.ts +5 -18
  56. package/dist/vercel/codec/encoder.d.ts +6 -36
  57. package/dist/vercel/codec/events.d.ts +51 -0
  58. package/dist/vercel/codec/index.d.ts +24 -12
  59. package/dist/vercel/codec/reducer.d.ts +144 -0
  60. package/dist/vercel/codec/tool-transitions.d.ts +2 -2
  61. package/dist/vercel/index.d.ts +4 -5
  62. package/dist/vercel/react/ably-ai-transport-vercel-react.js +3907 -3266
  63. package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
  64. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +33 -8
  65. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
  66. package/dist/vercel/react/contexts/chat-transport-context.d.ts +7 -6
  67. package/dist/vercel/react/contexts/chat-transport-provider.d.ts +53 -41
  68. package/dist/vercel/react/index.d.ts +1 -2
  69. package/dist/vercel/react/use-chat-transport.d.ts +30 -26
  70. package/dist/vercel/react/use-message-sync.d.ts +17 -30
  71. package/dist/vercel/run-end-reason.d.ts +29 -0
  72. package/dist/vercel/transport/chat-transport.d.ts +43 -24
  73. package/dist/vercel/transport/index.d.ts +25 -21
  74. package/dist/vercel/transport/run-output-stream.d.ts +56 -0
  75. package/dist/version.d.ts +2 -0
  76. package/package.json +30 -23
  77. package/src/constants.ts +124 -51
  78. package/src/core/agent.ts +68 -0
  79. package/src/core/codec/decoder.ts +71 -98
  80. package/src/core/codec/encoder.ts +113 -65
  81. package/src/core/codec/index.ts +13 -6
  82. package/src/core/codec/lifecycle-tracker.ts +10 -9
  83. package/src/core/codec/types.ts +436 -120
  84. package/src/core/transport/agent-session.ts +1344 -0
  85. package/src/core/transport/branch-chain.ts +58 -0
  86. package/src/core/transport/client-session.ts +775 -0
  87. package/src/core/transport/decode-fold.ts +91 -0
  88. package/src/core/transport/headers.ts +181 -22
  89. package/src/core/transport/index.ts +25 -26
  90. package/src/core/transport/internal/bounded-map.ts +27 -0
  91. package/src/core/transport/invocation.ts +98 -0
  92. package/src/core/transport/load-conversation.ts +355 -0
  93. package/src/core/transport/load-history.ts +269 -0
  94. package/src/core/transport/pipe-stream.ts +54 -39
  95. package/src/core/transport/run-manager.ts +249 -0
  96. package/src/core/transport/tree.ts +926 -308
  97. package/src/core/transport/types/agent.ts +407 -0
  98. package/src/core/transport/types/client.ts +211 -0
  99. package/src/core/transport/types/shared.ts +27 -0
  100. package/src/core/transport/types/tree.ts +344 -0
  101. package/src/core/transport/types/view.ts +259 -0
  102. package/src/core/transport/types.ts +13 -706
  103. package/src/core/transport/view.ts +864 -433
  104. package/src/errors.ts +22 -9
  105. package/src/event-emitter.ts +3 -2
  106. package/src/index.ts +52 -41
  107. package/src/logger.ts +14 -1
  108. package/src/react/contexts/client-session-context.ts +41 -0
  109. package/src/react/contexts/client-session-provider.tsx +186 -0
  110. package/src/react/create-session-hooks.ts +141 -0
  111. package/src/react/index.ts +23 -13
  112. package/src/react/internal/use-resolved-session.ts +63 -0
  113. package/src/react/use-ably-messages.ts +32 -22
  114. package/src/react/use-client-session.ts +201 -0
  115. package/src/react/use-create-view.ts +33 -29
  116. package/src/react/use-tree.ts +61 -30
  117. package/src/react/use-view.ts +139 -97
  118. package/src/utils.ts +63 -45
  119. package/src/vercel/codec/decoder.ts +336 -258
  120. package/src/vercel/codec/encoder.ts +343 -205
  121. package/src/vercel/codec/events.ts +87 -0
  122. package/src/vercel/codec/index.ts +60 -13
  123. package/src/vercel/codec/reducer.ts +977 -0
  124. package/src/vercel/codec/tool-transitions.ts +2 -2
  125. package/src/vercel/index.ts +6 -19
  126. package/src/vercel/react/contexts/chat-transport-context.ts +7 -6
  127. package/src/vercel/react/contexts/chat-transport-provider.tsx +87 -59
  128. package/src/vercel/react/index.ts +3 -5
  129. package/src/vercel/react/use-chat-transport.ts +47 -49
  130. package/src/vercel/react/use-message-sync.ts +80 -39
  131. package/src/vercel/run-end-reason.ts +78 -0
  132. package/src/vercel/transport/chat-transport.ts +392 -98
  133. package/src/vercel/transport/index.ts +39 -38
  134. package/src/vercel/transport/run-output-stream.ts +170 -0
  135. package/src/version.ts +2 -0
  136. package/dist/core/transport/client-transport.d.ts +0 -10
  137. package/dist/core/transport/decode-history.d.ts +0 -43
  138. package/dist/core/transport/server-transport.d.ts +0 -7
  139. package/dist/core/transport/stream-router.d.ts +0 -29
  140. package/dist/core/transport/turn-manager.d.ts +0 -37
  141. package/dist/react/contexts/transport-context.d.ts +0 -31
  142. package/dist/react/contexts/transport-provider.d.ts +0 -49
  143. package/dist/react/create-transport-hooks.d.ts +0 -124
  144. package/dist/react/use-active-turns.d.ts +0 -12
  145. package/dist/react/use-client-transport.d.ts +0 -80
  146. package/dist/vercel/codec/accumulator.d.ts +0 -21
  147. package/dist/vercel/react/use-staged-add-tool-approval-response.d.ts +0 -30
  148. package/dist/vercel/tool-approvals.d.ts +0 -124
  149. package/dist/vercel/tool-events.d.ts +0 -26
  150. package/src/core/transport/client-transport.ts +0 -977
  151. package/src/core/transport/decode-history.ts +0 -485
  152. package/src/core/transport/server-transport.ts +0 -612
  153. package/src/core/transport/stream-router.ts +0 -136
  154. package/src/core/transport/turn-manager.ts +0 -165
  155. package/src/react/contexts/transport-context.ts +0 -37
  156. package/src/react/contexts/transport-provider.tsx +0 -164
  157. package/src/react/create-transport-hooks.ts +0 -144
  158. package/src/react/use-active-turns.ts +0 -72
  159. package/src/react/use-client-transport.ts +0 -197
  160. package/src/vercel/codec/accumulator.ts +0 -588
  161. package/src/vercel/react/use-staged-add-tool-approval-response.ts +0 -87
  162. package/src/vercel/tool-approvals.ts +0 -380
  163. package/src/vercel/tool-events.ts +0 -53
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Encoder core — message append lifecycle machinery.
3
3
  *
4
- * Provides Ably primitives (publish, append, close, abort, flush) that
4
+ * Provides Ably primitives (publish, append, close, cancel, flush) that
5
5
  * domain-specific encoders wire their event types to.
6
6
  *
7
7
  * Domain encoders call `createEncoderCore(writer, options)` and use the
@@ -11,7 +11,13 @@
11
11
 
12
12
  import * as Ably from 'ably';
13
13
 
14
- import { HEADER_DISCRETE, HEADER_MSG_ID, HEADER_STATUS, HEADER_STREAM, HEADER_STREAM_ID } from '../../constants.js';
14
+ import {
15
+ HEADER_CODEC_MESSAGE_ID,
16
+ HEADER_DISCRETE,
17
+ HEADER_STATUS,
18
+ HEADER_STREAM,
19
+ HEADER_STREAM_ID,
20
+ } from '../../constants.js';
15
21
  import { ErrorCode } from '../../errors.js';
16
22
  import type { Logger } from '../../logger.js';
17
23
  import { mergeHeaders } from '../../utils.js';
@@ -36,8 +42,20 @@ interface StreamState {
36
42
  name: string;
37
43
  streamId: string;
38
44
  accumulated: string;
39
- persistentHeaders: Record<string, string>;
40
- aborted: boolean;
45
+ /** Transport-tier headers repeated on every append (`extras.ai.transport`). */
46
+ persistentTransport: Record<string, string>;
47
+ /** Codec-tier headers repeated on every append (`extras.ai.codec`). */
48
+ persistentCodec: Record<string, string>;
49
+ cancelled: boolean;
50
+ }
51
+
52
+ /**
53
+ * The SDK's `extras.ai` namespace as written to the wire: a `transport` tier
54
+ * (always present on SDK-published messages) and an optional `codec` tier.
55
+ */
56
+ interface AiExtras {
57
+ transport: Record<string, string>;
58
+ codec?: Record<string, string>;
41
59
  }
42
60
 
43
61
  interface PendingAppend {
@@ -57,32 +75,35 @@ export interface EncoderCore {
57
75
  /** Publish multiple discrete messages atomically in a single channel publish. */
58
76
  publishDiscreteBatch(payloads: MessagePayload[], opts?: WriteOptions): Promise<Ably.PublishResult>;
59
77
 
60
- /** Start a streamed message with x-ably-status:streaming. */
78
+ /** Start a streamed message with status:streaming. */
61
79
  startStream(streamId: string, payload: StreamPayload, opts?: WriteOptions): Promise<void>;
62
80
 
63
81
  /**
64
82
  * Append data to an in-flight streamed message. Fire-and-forget: errors are
65
- * collected internally and surfaced by {@link closeStream} or {@link close}.
83
+ * collected internally and surfaced by {@link closeStream}, {@link cancelStream},
84
+ * {@link cancelAllStreams} or {@link close}.
85
+ * @throws {Ably.ErrorInfo} InvalidArgument if there is no active stream for `streamId` or the core is closed.
66
86
  */
67
87
  appendStream(streamId: string, data: string): void;
68
88
 
69
89
  /**
70
- * Close a streamed message with x-ably-status:finished. Flushes all pending
90
+ * Close a streamed message with status:complete. Flushes all pending
71
91
  * appends for recovery before returning. Repeats persistent and payload headers.
92
+ * @throws {Ably.ErrorInfo} InvalidArgument if there is no active stream for `streamId`, or the encoder has been closed; EncoderRecoveryFailed if a failed append cannot be recovered during the flush.
72
93
  */
73
94
  closeStream(streamId: string, payload: StreamPayload): Promise<void>;
74
95
 
75
96
  /**
76
- * Abort a single in-progress stream (x-ably-status:aborted) and flush all
97
+ * Cancel a single in-progress stream (status:cancelled) and flush all
77
98
  * pending appends for recovery before returning.
78
99
  */
79
- abortStream(streamId: string, opts?: WriteOptions): Promise<void>;
100
+ cancelStream(streamId: string, opts?: WriteOptions): Promise<void>;
80
101
 
81
102
  /**
82
- * Abort all in-progress streams (x-ably-status:aborted) and flush all
103
+ * Cancel all in-progress streams (status:cancelled) and flush all
83
104
  * pending appends for recovery before returning.
84
105
  */
85
- abortAllStreams(opts?: WriteOptions): Promise<void>;
106
+ cancelAllStreams(opts?: WriteOptions): Promise<void>;
86
107
 
87
108
  /** Flush + clear trackers. Idempotent. */
88
109
  close(): Promise<void>;
@@ -128,14 +149,7 @@ class DefaultEncoderCore implements EncoderCore {
128
149
  async publishDiscreteBatch(payloads: MessagePayload[], opts?: WriteOptions): Promise<Ably.PublishResult> {
129
150
  this._assertNotClosed();
130
151
  this._logger?.trace('DefaultEncoderCore.publishDiscreteBatch();', { count: payloads.length });
131
- const msgs = payloads.map((p) => this._buildDiscreteMessage(p, opts));
132
- // Mark batch-published payloads as discrete message parts (from writeMessages).
133
- // The decoder relies on this header to distinguish message parts from lifecycle
134
- // events that also happen to be discrete (x-ably-stream: false).
135
- for (const msg of msgs) {
136
- // CAST: extras is built by _buildDiscreteMessage with a known { headers } shape.
137
- (msg.extras as { headers: Record<string, string> }).headers[HEADER_DISCRETE] = 'true';
138
- }
152
+ const msgs = payloads.map((p) => this._buildDiscreteMessage(p, opts, true));
139
153
  return this._writer.publish(msgs);
140
154
  }
141
155
 
@@ -144,16 +158,17 @@ class DefaultEncoderCore implements EncoderCore {
144
158
  this._assertNotClosed();
145
159
  this._logger?.trace('DefaultEncoderCore.startStream();', { name: payload.name, streamId });
146
160
 
147
- const allHeaders = this._buildHeaders(payload.headers ?? {}, opts);
148
- allHeaders[HEADER_STREAM] = 'true';
149
- allHeaders[HEADER_STATUS] = 'streaming';
150
- allHeaders[HEADER_STREAM_ID] = streamId;
161
+ const transport = this._buildTransport(payload.transportHeaders, opts);
162
+ transport[HEADER_STREAM] = 'true';
163
+ transport[HEADER_STATUS] = 'streaming';
164
+ transport[HEADER_STREAM_ID] = streamId;
165
+ const codec = payload.codecHeaders ?? {};
151
166
 
152
167
  const clientId = this._resolveClientId(opts);
153
168
  const msg: Ably.Message = {
154
169
  name: payload.name,
155
170
  data: payload.data,
156
- extras: { headers: allHeaders },
171
+ extras: { ai: this._aiExtras(transport, codec) },
157
172
  ...(clientId ? { clientId } : {}),
158
173
  };
159
174
 
@@ -175,8 +190,9 @@ class DefaultEncoderCore implements EncoderCore {
175
190
  name: payload.name,
176
191
  streamId,
177
192
  accumulated: payload.data,
178
- persistentHeaders: allHeaders,
179
- aborted: false,
193
+ persistentTransport: transport,
194
+ persistentCodec: codec,
195
+ cancelled: false,
180
196
  });
181
197
 
182
198
  this._logger?.debug('DefaultEncoderCore.startStream(); stream started', {
@@ -204,7 +220,7 @@ class DefaultEncoderCore implements EncoderCore {
204
220
  const appendMsg: Ably.Message = {
205
221
  serial: tracker.serial,
206
222
  data,
207
- extras: { headers: { ...tracker.persistentHeaders } },
223
+ extras: { ai: this._aiExtras({ ...tracker.persistentTransport }, { ...tracker.persistentCodec }) },
208
224
  };
209
225
 
210
226
  this._invokeOnMessage(appendMsg);
@@ -229,13 +245,13 @@ class DefaultEncoderCore implements EncoderCore {
229
245
  // Accumulate closing data so recovery has the full content
230
246
  tracker.accumulated += payload.data;
231
247
 
232
- const allHeaders = this._buildClosingHeaders(tracker, payload.headers ?? {});
233
- allHeaders[HEADER_STATUS] = 'finished';
248
+ const { transport, codec } = this._buildClosing(tracker, payload);
249
+ transport[HEADER_STATUS] = 'complete';
234
250
 
235
251
  const msg: Ably.Message = {
236
252
  serial: tracker.serial,
237
253
  data: payload.data,
238
- extras: { headers: allHeaders },
254
+ extras: { ai: this._aiExtras(transport, codec) },
239
255
  };
240
256
 
241
257
  this._invokeOnMessage(msg);
@@ -248,28 +264,28 @@ class DefaultEncoderCore implements EncoderCore {
248
264
  }
249
265
 
250
266
  // Spec: AIT-CD5, AIT-CD5b
251
- async abortStream(streamId: string, opts?: WriteOptions): Promise<void> {
267
+ async cancelStream(streamId: string, opts?: WriteOptions): Promise<void> {
252
268
  this._assertNotClosed();
253
- this._logger?.trace('DefaultEncoderCore.abortStream();', { streamId });
269
+ this._logger?.trace('DefaultEncoderCore.cancelStream();', { streamId });
254
270
 
255
271
  const tracker = this._trackers.get(streamId);
256
272
  if (!tracker) {
257
273
  throw new Ably.ErrorInfo(
258
- `unable to abort stream; no active stream for streamId '${streamId}'`,
274
+ `unable to cancel stream; no active stream for streamId '${streamId}'`,
259
275
  ErrorCode.InvalidArgument,
260
276
  400,
261
277
  );
262
278
  }
263
279
 
264
- tracker.aborted = true;
280
+ tracker.cancelled = true;
265
281
 
266
- const allHeaders = this._buildClosingHeaders(tracker, {}, opts);
267
- allHeaders[HEADER_STATUS] = 'aborted';
282
+ const { transport, codec } = this._buildClosing(tracker, undefined, opts);
283
+ transport[HEADER_STATUS] = 'cancelled';
268
284
 
269
285
  const msg: Ably.Message = {
270
286
  serial: tracker.serial,
271
287
  data: '',
272
- extras: { headers: allHeaders },
288
+ extras: { ai: this._aiExtras(transport, codec) },
273
289
  };
274
290
 
275
291
  this._invokeOnMessage(msg);
@@ -278,24 +294,24 @@ class DefaultEncoderCore implements EncoderCore {
278
294
 
279
295
  await this._flushPending();
280
296
 
281
- this._logger?.debug('DefaultEncoderCore.abortStream(); stream aborted', { streamId });
297
+ this._logger?.debug('DefaultEncoderCore.cancelStream(); stream cancelled', { streamId });
282
298
  }
283
299
 
284
300
  // Spec: AIT-CD5a
285
- async abortAllStreams(opts?: WriteOptions): Promise<void> {
301
+ async cancelAllStreams(opts?: WriteOptions): Promise<void> {
286
302
  this._assertNotClosed();
287
- this._logger?.trace('DefaultEncoderCore.abortAllStreams();', { streamCount: this._trackers.size });
303
+ this._logger?.trace('DefaultEncoderCore.cancelAllStreams();', { streamCount: this._trackers.size });
288
304
 
289
305
  for (const tracker of this._trackers.values()) {
290
- tracker.aborted = true;
306
+ tracker.cancelled = true;
291
307
 
292
- const allHeaders = this._buildClosingHeaders(tracker, {}, opts);
293
- allHeaders[HEADER_STATUS] = 'aborted';
308
+ const { transport, codec } = this._buildClosing(tracker, undefined, opts);
309
+ transport[HEADER_STATUS] = 'cancelled';
294
310
 
295
311
  const msg: Ably.Message = {
296
312
  serial: tracker.serial,
297
313
  data: '',
298
- extras: { headers: allHeaders },
314
+ extras: { ai: this._aiExtras(transport, codec) },
299
315
  };
300
316
 
301
317
  this._invokeOnMessage(msg);
@@ -354,11 +370,16 @@ class DefaultEncoderCore implements EncoderCore {
354
370
  const tracker = this._trackers.get(streamId);
355
371
  if (!tracker) continue;
356
372
 
357
- const recoveryStatus = tracker.aborted ? 'aborted' : 'finished';
373
+ const recoveryStatus = tracker.cancelled ? 'cancelled' : 'complete';
358
374
  const msg: Ably.Message = {
359
375
  serial: tracker.serial,
360
376
  data: tracker.accumulated,
361
- extras: { headers: { ...tracker.persistentHeaders, [HEADER_STATUS]: recoveryStatus } },
377
+ extras: {
378
+ ai: this._aiExtras(
379
+ { ...tracker.persistentTransport, [HEADER_STATUS]: recoveryStatus },
380
+ { ...tracker.persistentCodec },
381
+ ),
382
+ },
362
383
  };
363
384
 
364
385
  try {
@@ -415,25 +436,53 @@ class DefaultEncoderCore implements EncoderCore {
415
436
  return opts?.clientId ?? this._defaultClientId;
416
437
  }
417
438
 
418
- private _buildHeaders(codecHeaders: Record<string, string>, opts?: WriteOptions): Record<string, string> {
439
+ /**
440
+ * Build the transport-tier header record for a message: caller-configured
441
+ * transport headers (default extras + per-write overrides) layered with any
442
+ * transport headers the codec payload stamps directly, plus the message-id.
443
+ * @param payloadTransport - Transport headers carried on the codec payload.
444
+ * @param opts - Optional per-write overrides.
445
+ * @returns The transport-tier headers record (`extras.ai.transport`).
446
+ */
447
+ private _buildTransport(
448
+ payloadTransport: Record<string, string> | undefined,
449
+ opts?: WriteOptions,
450
+ ): Record<string, string> {
419
451
  const callerHeaders = mergeHeaders(this._defaultExtras?.headers, opts?.extras?.headers);
420
- const merged = { ...callerHeaders, ...codecHeaders };
452
+ const transport = { ...callerHeaders, ...payloadTransport };
421
453
  if (opts?.messageId !== undefined) {
422
- merged[HEADER_MSG_ID] = opts.messageId;
454
+ transport[HEADER_CODEC_MESSAGE_ID] = opts.messageId;
423
455
  }
424
- return merged;
456
+ return transport;
457
+ }
458
+
459
+ /**
460
+ * Assemble the `extras.ai` namespace from its two tiers, omitting the codec
461
+ * tier when empty.
462
+ * @param transport - Transport-tier headers (always present on SDK messages).
463
+ * @param codec - Codec-tier headers; omitted from the wire when empty.
464
+ * @returns The `extras.ai` object.
465
+ */
466
+ private _aiExtras(transport: Record<string, string>, codec: Record<string, string>): AiExtras {
467
+ return Object.keys(codec).length > 0 ? { transport, codec } : { transport };
425
468
  }
426
469
 
427
- private _buildDiscreteMessage(payload: MessagePayload, opts?: WriteOptions): Ably.Message {
428
- const headers = this._buildHeaders(payload.headers ?? {}, opts);
429
- headers[HEADER_STREAM] = 'false';
470
+ private _buildDiscreteMessage(payload: MessagePayload, opts?: WriteOptions, discrete = false): Ably.Message {
471
+ const transport = this._buildTransport(payload.transportHeaders, opts);
472
+ transport[HEADER_STREAM] = 'false';
473
+ if (discrete) {
474
+ // Mark batch-published payloads as discrete message parts (from writeMessages).
475
+ // The decoder relies on this header to distinguish message parts from lifecycle
476
+ // events that also happen to be discrete (stream: false).
477
+ transport[HEADER_DISCRETE] = 'true';
478
+ }
430
479
  const clientId = this._resolveClientId(opts);
431
480
 
432
481
  const msg: Ably.Message = {
433
482
  name: payload.name,
434
483
  data: payload.data,
435
484
  extras: {
436
- headers,
485
+ ai: this._aiExtras(transport, payload.codecHeaders ?? {}),
437
486
  ...(payload.ephemeral ? { ephemeral: true } : {}),
438
487
  },
439
488
  ...(clientId ? { clientId } : {}),
@@ -444,24 +493,23 @@ class DefaultEncoderCore implements EncoderCore {
444
493
  }
445
494
 
446
495
  /**
447
- * Build headers for a closing append. Closing appends must repeat ALL
448
- * persistent headers (Ably replaces the entire extras object on append).
496
+ * Build both header tiers for a closing append. Closing appends must repeat
497
+ * ALL persistent headers (Ably replaces the entire extras object on append).
449
498
  * Then layer caller and codec overrides.
450
499
  * @param tracker - The stream tracker with persistent headers.
451
- * @param codecHeaders - Codec-layer headers to merge.
500
+ * @param payload - The closing stream payload (codec + transport headers).
452
501
  * @param opts - Optional per-write overrides.
453
- * @returns Merged headers for the closing append.
502
+ * @returns The two tiers for the closing append.
454
503
  */
455
- private _buildClosingHeaders(
504
+ private _buildClosing(
456
505
  tracker: StreamState,
457
- codecHeaders: Record<string, string>,
506
+ payload: StreamPayload | undefined,
458
507
  opts?: WriteOptions,
459
- ): Record<string, string> {
460
- const h = { ...tracker.persistentHeaders };
508
+ ): { transport: Record<string, string>; codec: Record<string, string> } {
461
509
  const callerHeaders = mergeHeaders(this._defaultExtras?.headers, opts?.extras?.headers);
462
- Object.assign(h, callerHeaders);
463
- Object.assign(h, codecHeaders);
464
- return h;
510
+ const transport = { ...tracker.persistentTransport, ...callerHeaders, ...payload?.transportHeaders };
511
+ const codec = { ...tracker.persistentCodec, ...payload?.codecHeaders };
512
+ return { transport, codec };
465
513
  }
466
514
  }
467
515
 
@@ -1,17 +1,24 @@
1
- export { eventOutput } from './decoder.js';
2
1
  export type {
3
2
  ChannelWriter,
4
3
  Codec,
5
- DecoderOutput,
6
- DiscreteEncoder,
4
+ CodecInputEvent,
5
+ CodecMessage,
6
+ CodecOutputEvent,
7
+ DecodedMessage,
8
+ Decoder,
9
+ Encoder,
7
10
  EncoderOptions,
8
11
  Extras,
9
- MessageAccumulator,
10
12
  MessagePayload,
11
- StreamDecoder,
12
- StreamEncoder,
13
+ Reducer,
14
+ ReducerMeta,
15
+ Regenerate,
13
16
  StreamPayload,
14
17
  StreamTrackerState,
18
+ ToolApprovalResponse,
19
+ ToolResult,
20
+ ToolResultError,
21
+ UserMessage,
15
22
  WriteOptions,
16
23
  } from './types.js';
17
24
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Generic lifecycle tracker for codec decoders.
3
3
  *
4
- * Manages per-scope (typically per-turn) tracking of lifecycle phases that
4
+ * Manages per-scope (typically per-run) tracking of lifecycle phases that
5
5
  * must be emitted before content events. When a phase has not been emitted
6
6
  * (e.g. mid-stream join), the tracker synthesizes the missing events using
7
7
  * codec-provided build functions.
@@ -40,15 +40,16 @@ export interface PhaseConfig<TEvent> {
40
40
  * Per-scope lifecycle tracker that ensures required phases are emitted
41
41
  * before content events, synthesizing missing ones for mid-stream joins.
42
42
  *
43
- * Scoped by an arbitrary string key (typically a turn ID). Each scope
43
+ * Scoped by an arbitrary string key (typically a run ID). Each scope
44
44
  * tracks independently which phases have been emitted.
45
45
  */
46
46
  export interface LifecycleTracker<TEvent> {
47
47
  /**
48
48
  * Ensure all configured phases have been emitted for the given scope.
49
- * Returns synthetic events for any phases not yet marked as emitted,
50
- * then marks them. Returns an empty array if all phases are current.
51
- * @param scopeId - The scope to check (e.g. turn ID).
49
+ * Synthesizes and returns the events for any phases not yet marked as
50
+ * emitted, marking each as emitted so it is not synthesized again.
51
+ * Returns an empty array if all phases are already emitted.
52
+ * @param scopeId - The scope to check (e.g. run ID).
52
53
  * @param context - Key-value pairs passed through to phase build functions.
53
54
  * @returns Synthetic events for missing phases, in configuration order.
54
55
  */
@@ -57,7 +58,7 @@ export interface LifecycleTracker<TEvent> {
57
58
  /**
58
59
  * Mark a phase as emitted from the wire (not synthetic). Call this
59
60
  * when the real event arrives so the tracker does not re-synthesize it.
60
- * @param scopeId - The scope (e.g. turn ID).
61
+ * @param scopeId - The scope (e.g. run ID).
61
62
  * @param phaseKey - The phase key to mark.
62
63
  */
63
64
  markEmitted(scopeId: string, phaseKey: string): void;
@@ -66,14 +67,14 @@ export interface LifecycleTracker<TEvent> {
66
67
  * Reset a phase so it will be re-synthesized on the next
67
68
  * {@link ensurePhases} call. Used for repeating phases (e.g. "start-step"
68
69
  * resets after "finish-step").
69
- * @param scopeId - The scope (e.g. turn ID).
70
+ * @param scopeId - The scope (e.g. run ID).
70
71
  * @param phaseKey - The phase key to reset.
71
72
  */
72
73
  resetPhase(scopeId: string, phaseKey: string): void;
73
74
 
74
75
  /**
75
- * Remove all tracking state for a scope. Call on turn completion
76
- * (finish, abort) to free memory.
76
+ * Remove all tracking state for a scope. Call on run completion
77
+ * (finish, cancel) to free memory.
77
78
  * @param scopeId - The scope to clear.
78
79
  */
79
80
  clearScope(scopeId: string): void;