@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
@@ -1,12 +1,14 @@
1
1
  /** Agent (server-side) session types: options, run runtime, and the Run / AgentSession contracts. */
2
2
 
3
3
  import type * as Ably from 'ably';
4
+ // Also augments RealtimeChannel with `.object` (ably/liveobjects side-effect).
5
+ import type * as AblyObjects from 'ably/liveobjects';
4
6
 
5
7
  import type { Logger } from '../../../logger.js';
6
8
  import type { Codec, CodecInputEvent, CodecOutputEvent, WriteOptions } from '../../codec/types.js';
7
9
  import type { Invocation } from '../invocation.js';
8
10
  import type { CancelRequest, RunEndReason } from './shared.js';
9
- import type { MessageNode } from './tree.js';
11
+ import type { MessageNode, Tree } from './tree.js';
10
12
 
11
13
  // ---------------------------------------------------------------------------
12
14
  // Agent session options
@@ -43,8 +45,9 @@ export interface AgentSessionOptions<
43
45
 
44
46
  /**
45
47
  * How long `Run.start()` will wait for the input event(s) tagged with
46
- * the run's `invocationId` to arrive on the channel (rewind + live wait)
47
- * before rejecting with `InputEventNotFound`. The rejection bubbles up to the
48
+ * the run's `invocationId` to arrive on the channel across both the
49
+ * post-attach live subscription and the bounded history scan before
50
+ * rejecting with `InputEventNotFound`. The rejection bubbles up to the
48
51
  * developer's HTTP handler, which should surface it as a non-2xx response
49
52
  * so the client's pending send fails.
50
53
  * Default: 30000 (30 seconds).
@@ -52,60 +55,41 @@ export interface AgentSessionOptions<
52
55
  inputEventLookupTimeoutMs?: number;
53
56
 
54
57
  /**
55
- * Maximum number of distinct invocation-ids whose input events
56
- * may be buffered while waiting for `Run.start()` to register a lookup
57
- * listener. Channel rewind on attach can replay input events before any
58
- * run has been created for them; this buffer holds those events so
59
- * that subsequent `start()` calls can drain them on registration.
58
+ * How far back in time `Run.start()` scans channel history for the
59
+ * triggering input event. Implements the lookback bound for the
60
+ * input-event scan anything older than `Date.now() - inputEventLookbackMs`
61
+ * is treated as outside the lookup window.
60
62
  *
61
- * Each entry corresponds to one invocation-id regardless of how many
62
- * events that invocation buffered. When the limit is exceeded the
63
- * oldest invocation entry (and all its buffered events) is FIFO-evicted
64
- * — the client whose input was dropped will fail their lookup with
65
- * `InputEventNotFound`. The eviction is logged at warn level so operators
66
- * can correlate capacity pressure with `InputEventNotFound` errors.
63
+ * History is fetched with `untilAttach: true` so the scan composes
64
+ * with the live subscription by serial boundary together they cover
65
+ * every message within `inputEventLookbackMs` of attach.
67
66
  *
68
- * Default: 200.
67
+ * Increase this for long-suspended runs whose continuation may arrive
68
+ * many minutes after the original publish. Decrease it for stricter
69
+ * recency.
70
+ *
71
+ * Default: 120000 (2 minutes).
69
72
  */
70
- inputEventBufferLimit?: number;
73
+ inputEventLookbackMs?: number;
71
74
 
72
75
  /**
73
- * The channel rewind applied when the agent attaches. Replays the whole
74
- * channel subscription on attach (not just input events) so the lookup
75
- * can catch input events published before the session attached. Passed
76
- * through verbatim to Ably's `params.rewind` channel parameter accepts
77
- * duration strings (`"2m"`, `"30s"`) or a count of messages as a string
78
- * (e.g. `"50"`). Malformed values surface as a channel attach error from
79
- * Ably; the SDK does not pre-validate.
80
- *
81
- * A longer window improves the chances of catching an input event for an
82
- * agent that takes a while to come up after the client published, but
83
- * also increases the buffer pressure on `inputEventBufferLimit` because
84
- * more events may be replayed on attach.
76
+ * Extra Ably channel modes to request on the session's channel, on top of the
77
+ * modes AI Transport always needs. Pass `OBJECT_MODES` (or
78
+ * `['OBJECT_SUBSCRIBE', 'OBJECT_PUBLISH']`) to use Ably LiveObjects via
79
+ * {@link AgentSession.object}. Omit to attach with the default mode set.
85
80
  *
86
- * Default: `"2m"`.
81
+ * The session requests the union of these modes with the modes it always
82
+ * needs, so passing extra modes never drops the SDK's required modes. The
83
+ * connection's token/key capability must permit the requested operations,
84
+ * otherwise the server grants only the permitted subset.
87
85
  */
88
- rewindWindow?: string;
86
+ channelModes?: readonly Ably.ChannelMode[];
89
87
  }
90
88
 
91
89
  // ---------------------------------------------------------------------------
92
90
  // Run options
93
91
  // ---------------------------------------------------------------------------
94
92
 
95
- /**
96
- * A batch of events targeting an existing message.
97
- * Each node specifies the target message and the events to apply to it.
98
- * Used for cross-run updates such as tool result delivery.
99
- */
100
- export interface EventsNode<TOutput extends CodecOutputEvent> {
101
- /** Discriminator — identifies this as an events node. */
102
- kind: 'event';
103
- /** The `codec-message-id` of the existing message to update. */
104
- codecMessageId: string;
105
- /** Outputs to apply to the target message. */
106
- events: TOutput[];
107
- }
108
-
109
93
  /**
110
94
  * Options for `Run.pipe` — per-operation overrides for the assistant message.
111
95
  * @template TOutput - The codec output type carried by the stream; used by the `resolveWriteOptions` hook.
@@ -206,7 +190,7 @@ export interface RunRuntime<TOutput extends CodecOutputEvent> {
206
190
  * `Ably.ErrorInfo` (code `StreamError`) for standardized observability.
207
191
  * - Failures in the `onCancel` handler.
208
192
  *
209
- * Publish failures in `start`, `addEvents`, and `end`
193
+ * Publish failures in `start` and `end`
210
194
  * are not delivered here — those methods reject their returned promise
211
195
  * with an `Ably.ErrorInfo`, and the caller should handle it at the await
212
196
  * site. Run errors never render the session unusable, but the run may
@@ -240,23 +224,47 @@ export interface RunView<TMessage> {
240
224
  /** Options for {@link Run.loadConversation}. */
241
225
  export interface LoadConversationOptions {
242
226
  /**
243
- * Number of wire messages to request per history page.
244
- * Default: 200.
245
- */
246
- pageLimit?: number;
247
- /**
248
- * Maximum total wire messages to collect across all pages before
249
- * stopping pagination. A safety bound so a long-lived channel
250
- * doesn't exhaust memory.
251
- * Default: 2000.
227
+ * Maximum number of ANCESTOR reply RunNodes to walk back through the
228
+ * chain. Input nodes encountered alongside don't count toward the bound,
229
+ * and neither does the current run's own node (it is the conversation
230
+ * tail, not ancestor context). Default unbounded (walks to the
231
+ * conversation root).
232
+ *
233
+ * Set this to bound the LLM context window — `maxRuns: 5` returns the
234
+ * 5 most-recent prior reply runs and their associated input nodes
235
+ * (each bounded run's triggering input included, so the chain never
236
+ * starts assistant-first), in chronological order.
252
237
  */
253
- maxMessages?: number;
238
+ maxRuns?: number;
254
239
  }
255
240
 
241
+ /**
242
+ * How a run terminates, passed to {@link Run.end}. Discriminated on `reason`:
243
+ * an `'error'` end may carry a terminal `error`; any other reason carries none.
244
+ */
245
+ export type RunEndParams =
246
+ | {
247
+ /** Why the run ended — any terminal reason other than `'error'`. */
248
+ reason: Exclude<RunEndReason, 'error'>;
249
+ }
250
+ | {
251
+ /** The run ended in error. */
252
+ reason: 'error';
253
+ /**
254
+ * Optional terminal error to surface to clients. Omit to end in error
255
+ * without detail.
256
+ */
257
+ error?: Ably.ErrorInfo;
258
+ };
259
+
256
260
  /**
257
261
  * A server-side run with explicit lifecycle methods. Generic over the codec's
258
- * output, projection, and message types.
262
+ * output, projection, and message types. `TProjection` is retained for
263
+ * parameter symmetry with {@link AgentSession.createRun}; it does not
264
+ * appear in the Run's public surface today but keeps the type slot
265
+ * available for future per-Run projection accessors.
259
266
  */
267
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- see JSDoc
260
268
  export interface Run<TOutput extends CodecOutputEvent, TProjection, TMessage> {
261
269
  /** The run's unique identifier. */
262
270
  readonly runId: string;
@@ -295,7 +303,7 @@ export interface Run<TOutput extends CodecOutputEvent, TProjection, TMessage> {
295
303
  /**
296
304
  * Publish the run's opening lifecycle event to the channel (run-start, or
297
305
  * run-resume for a continuation). Must be called before any other run method
298
- * (pipe, addEvents, suspend, end).
306
+ * (pipe, suspend, end).
299
307
  */
300
308
  start(): Promise<void>;
301
309
 
@@ -306,47 +314,26 @@ export interface Run<TOutput extends CodecOutputEvent, TProjection, TMessage> {
306
314
  */
307
315
  pipe(stream: ReadableStream<TOutput>, options?: PipeOptions<TOutput>): Promise<StreamResult>;
308
316
 
309
- /**
310
- * Publish events targeting existing messages in the tree. Each node
311
- * specifies a target message (by `codecMessageId`) and the events to apply.
312
- * Events are encoded and published with the target's `codec-message-id`,
313
- * so receiving clients apply them to the existing node rather than
314
- * creating a new one.
315
- *
316
- * Used for cross-run updates such as tool result delivery after
317
- * approval or client-side tool execution.
318
- */
319
- addEvents(nodes: EventsNode<TOutput>[]): Promise<void>;
320
-
321
- /**
322
- * Fetch every channel message bound to this run and fold them through
323
- * the codec into a single projection. Used by the agent to reconstruct
324
- * the run's full state — including client-published tool-output amends
325
- * the agent didn't observe live — when resuming a suspended run.
326
- *
327
- * Uses `channel.history()` (no `untilAttach`) so messages published
328
- * after the channel was originally attached are still included. Each
329
- * call paginates until either there are no more pages or an internal
330
- * safety bound is reached.
331
- * @returns The TProjection produced by folding every event for this run
332
- * in serial order. The caller extracts what they need via
333
- * {@link Codec.getMessages}.
334
- */
335
- loadProjection(): Promise<TProjection>;
336
-
337
317
  /**
338
318
  * Reconstruct the full multi-turn conversation by walking the ancestor
339
- * run chain and concatenating each run's messages, oldest turn first.
319
+ * run chain over the session's Tree, concatenating each ancestor's
320
+ * projection (oldest turn first) plus the current run's projection.
340
321
  *
341
- * Performs a single `channel.history()` scan and builds projections for
342
- * all ancestor runs plus the current run. After this call:
343
- * - {@link Run.messages} returns the complete conversation (all ancestor
344
- * turns followed by the current run's messages), making it ready to
345
- * pass directly to the LLM.
346
- * - The current run's projection is cached so {@link Run.pipe} works
347
- * correctly without a separate {@link Run.loadProjection} call.
348
- * @param options - Optional tuning for history pagination.
349
- * @returns The same message list now accessible via {@link Run.messages}.
322
+ * Hydrates the Tree as needed from channel history if the chain from
323
+ * the run's structural-parent anchor isn't already fully present;
324
+ * subsequent reads of {@link Run.messages} re-walk the same Tree and
325
+ * reflect any further folds (e.g. live arrivals from concurrent runs).
326
+ * No cache: every call computes a fresh snapshot from the live Tree.
327
+ *
328
+ * Walks to the conversation root by default; bound the walk via the
329
+ * optional {@link LoadConversationOptions.maxRuns} cap. If channel
330
+ * retention has expired older turns, the walk stops at what is available.
331
+ * @param options - Optional walk bounds.
332
+ * @returns The conversation messages in chronological order, ready to pass to an LLM.
333
+ * @throws {Ably.ErrorInfo} `HistoryFetchFailed` — or the underlying Ably
334
+ * code when the failure carried one — when the history fetch fails after
335
+ * retries (the conversation is never silently truncated on fetch
336
+ * failure); `InvalidArgument` when the run's signal aborts.
350
337
  */
351
338
  loadConversation(options?: LoadConversationOptions): Promise<TMessage[]>;
352
339
 
@@ -362,8 +349,11 @@ export interface Run<TOutput extends CodecOutputEvent, TProjection, TMessage> {
362
349
  */
363
350
  suspend(): Promise<void>;
364
351
 
365
- /** Publish run-end event to the channel and clean up. Terminal. */
366
- end(reason: RunEndReason): Promise<void>;
352
+ /**
353
+ * Publish run-end event to the channel and clean up. Terminal.
354
+ * @param params - How the run ended; see {@link RunEndParams}.
355
+ */
356
+ end(params: RunEndParams): Promise<void>;
367
357
  }
368
358
 
369
359
  // ---------------------------------------------------------------------------
@@ -372,13 +362,50 @@ export interface Run<TOutput extends CodecOutputEvent, TProjection, TMessage> {
372
362
 
373
363
  /** Server-side session that manages run lifecycles over an Ably channel. */
374
364
  export interface AgentSession<TOutput extends CodecOutputEvent, TProjection, TMessage> {
365
+ /**
366
+ * The Ably presence object for this session's channel.
367
+ *
368
+ * Exposed as a convenience so the agent can track and publish presence
369
+ * (`enter`/`leave`/`update`/`get`/`subscribe`) — for example, to detect
370
+ * whether the requesting user is still connected — without obtaining the
371
+ * channel separately. This is the same `Ably.RealtimePresence` instance the
372
+ * underlying channel exposes; the session applies no additional semantics.
373
+ * Presence operations implicitly attach the channel and do not require
374
+ * {@link connect} to have been called first.
375
+ */
376
+ readonly presence: Ably.RealtimePresence;
377
+
378
+ /**
379
+ * The Ably LiveObjects entry point for this session's channel.
380
+ *
381
+ * Exposed as a convenience so the agent can read and mutate shared objects
382
+ * (LiveMap / LiveCounter) on the same channel the session uses, without
383
+ * obtaining the channel separately. This is the same `RealtimeObject`
384
+ * instance the underlying channel exposes; the session applies no additional
385
+ * semantics. Operating on it requires (a) the Realtime client to have been
386
+ * constructed with the `LiveObjects` plugin from `ably/liveobjects` and
387
+ * (b) the object channel modes to have been requested via
388
+ * {@link AgentSessionOptions.channelModes}. When either is absent the
389
+ * underlying SDK throws; the session does not suppress the error.
390
+ */
391
+ readonly object: AblyObjects.RealtimeObject;
392
+
393
+ /**
394
+ * The session's materialisation tree. Every Ably message received on the channel
395
+ * (live + history) folds into this tree; consumers can introspect hydrated
396
+ * conversation state via {@link Tree.getNodeByCodecMessageId} /
397
+ * {@link Tree.getRunNode} etc. Mirrors `ClientSession.tree` so both
398
+ * sessions share one materialisation engine.
399
+ */
400
+ readonly tree: Tree<TOutput, TProjection>;
401
+
375
402
  /**
376
403
  * Subscribe (unfiltered) to the shared channel and (implicitly) attach. The
377
- * subscribe is deliberately unfiltered so channel-rewind-replayed input
378
- * events also reach the dispatcher, which routes by name (cancel vs. input
379
- * event). Idempotent — subsequent calls return the same promise. All run
380
- * methods (`start`, `addEvents`, `pipe`, `loadProjection`,
381
- * `loadConversation`, `suspend`, `end`) throw `InvalidArgument` until
404
+ * subscribe is deliberately unfiltered so channel-history-replayed input
405
+ * events reach the materialisation engine, which the input-event lookup
406
+ * queries via the Tree. Idempotent — subsequent calls return the same
407
+ * promise. All run methods (`start`, `pipe`, `loadConversation`,
408
+ * `suspend`, `end`) throw `InvalidArgument` until
382
409
  * `connect()` has been *called*; once it has, they await the in-flight
383
410
  * connect promise rather than throwing.
384
411
  */
@@ -1,6 +1,8 @@
1
1
  /** Client session types: options, send options, the ActiveRun handle, and the ClientSession contract. */
2
2
 
3
3
  import type * as Ably from 'ably';
4
+ // Also augments RealtimeChannel with `.object` (ably/liveobjects side-effect).
5
+ import type * as AblyObjects from 'ably/liveobjects';
4
6
 
5
7
  import type { Logger } from '../../../logger.js';
6
8
  import type { Codec, CodecInputEvent, CodecOutputEvent } from '../../codec/types.js';
@@ -22,6 +24,12 @@ export interface ClientSessionOptions<
22
24
  /**
23
25
  * The Ably Realtime client. The caller owns its lifecycle —
24
26
  * `session.close()` does not close the client.
27
+ *
28
+ * The session's identity is taken from this client's `auth.clientId` (set
29
+ * via the Ably token or `ClientOptions.clientId`) — it is read at publish
30
+ * time and stamped on the wire as the run/input client id so other clients
31
+ * can attribute messages. A connection without a concrete clientId
32
+ * (anonymous, or a wildcard `*` token) publishes without one.
25
33
  */
26
34
  client: Ably.Realtime;
27
35
 
@@ -35,16 +43,22 @@ export interface ClientSessionOptions<
35
43
  /** The codec to use for encoding/decoding. */
36
44
  codec: Codec<TInput, TOutput, TProjection, TMessage>;
37
45
 
38
- /**
39
- * The client's identity, used as the Ably publisher `clientId` on
40
- * everything this session publishes. Surfaces on the wire as the
41
- * run/input client id so other clients can attribute messages.
42
- */
43
- clientId?: string;
44
-
45
46
  /** Initial messages to seed the conversation tree with. Forms a linear chain. */
46
47
  messages?: TMessage[];
47
48
 
49
+ /**
50
+ * Extra Ably channel modes to request on the session's channel, on top of the
51
+ * modes AI Transport always needs. Pass `OBJECT_MODES` (or
52
+ * `['OBJECT_SUBSCRIBE', 'OBJECT_PUBLISH']`) to use Ably LiveObjects via
53
+ * {@link ClientSession.object}. Omit to attach with the default mode set.
54
+ *
55
+ * The session requests the union of these modes with the modes it always
56
+ * needs, so passing extra modes never drops the SDK's required modes. The
57
+ * connection's token/key capability must permit the requested operations,
58
+ * otherwise the server grants only the permitted subset.
59
+ */
60
+ channelModes?: readonly Ably.ChannelMode[];
61
+
48
62
  /** Logger instance for diagnostic output. */
49
63
  logger?: Logger;
50
64
  }
@@ -75,12 +89,6 @@ export interface SendOptions {
75
89
  * agent, which mints a distinct `invocationId` per HTTP request.
76
90
  */
77
91
  runId?: string;
78
- /**
79
- * Currently non-functional: the send path always mints each input's
80
- * `inputEventId` with `crypto.randomUUID()` (no override is read), so a
81
- * value supplied here has no effect.
82
- */
83
- inputEventId?: string;
84
92
  }
85
93
 
86
94
  // ---------------------------------------------------------------------------
@@ -171,6 +179,34 @@ export interface ClientSession<
171
179
  /** The default paginated, branch-aware view for rendering — events scoped to visible messages. */
172
180
  readonly view: View<TInput, TMessage>;
173
181
 
182
+ /**
183
+ * The Ably presence object for this session's channel.
184
+ *
185
+ * Exposed as a convenience so callers can track and publish presence
186
+ * (`enter`/`leave`/`update`/`get`/`subscribe`) — for example, to detect
187
+ * whether an agent is online — without obtaining the channel separately.
188
+ * This is the same `Ably.RealtimePresence` instance the underlying channel
189
+ * exposes; the session applies no additional semantics. Presence operations
190
+ * implicitly attach the channel and do not require {@link connect} to have
191
+ * been called first.
192
+ */
193
+ readonly presence: Ably.RealtimePresence;
194
+
195
+ /**
196
+ * The Ably LiveObjects entry point for this session's channel.
197
+ *
198
+ * Exposed as a convenience so callers can read and mutate shared objects
199
+ * (LiveMap / LiveCounter) on the same channel the session uses, without
200
+ * obtaining the channel separately. This is the same `RealtimeObject`
201
+ * instance the underlying channel exposes; the session applies no additional
202
+ * semantics. Operating on it requires (a) the Realtime client to have been
203
+ * constructed with the `LiveObjects` plugin from `ably/liveobjects` and
204
+ * (b) the object channel modes to have been requested via
205
+ * {@link ClientSessionOptions.channelModes}. When either is absent the
206
+ * underlying SDK throws; the session does not suppress the error.
207
+ */
208
+ readonly object: AblyObjects.RealtimeObject;
209
+
174
210
  /**
175
211
  * Subscribe to the channel and (implicitly) attach. Idempotent —
176
212
  * subsequent calls return the same promise. The View's write operations
@@ -25,6 +25,12 @@ interface RunLifecycleBase {
25
25
  * Empty string if the wire didn't carry an invocation-id.
26
26
  */
27
27
  invocationId: string;
28
+ /**
29
+ * Ably server timestamp (epoch ms) of the lifecycle message; absent for an
30
+ * optimistic local event. Advances the Tree's event-log retention clock and
31
+ * the target run's last-activity time.
32
+ */
33
+ timestamp?: number;
28
34
  }
29
35
 
30
36
  /**
@@ -84,12 +90,23 @@ export type RunLifecycleEvent =
84
90
  * optimistic local event. The Tree reads it to set the Run's endSerial.
85
91
  */
86
92
  serial: string | undefined;
87
- /**
88
- * Why the run ended — the terminal reason the Tree records as the
89
- * RunNode's status: `complete`, `cancelled`, or `error`.
90
- */
91
- reason: RunEndReason;
92
- });
93
+ } & (
94
+ | {
95
+ /** Why the run ended any terminal reason other than `'error'`. */
96
+ reason: Exclude<RunEndReason, 'error'>;
97
+ }
98
+ | {
99
+ /** The run ended in error. */
100
+ reason: 'error';
101
+ /**
102
+ * Terminal error detail, reconstructed from the run-end's
103
+ * `error-code` / `error-message` headers (or a generic fallback
104
+ * when the run ended in error without detail). The Tree records it
105
+ * on the RunNode and exposes it via `RunInfo.error`.
106
+ */
107
+ error: Ably.ErrorInfo;
108
+ }
109
+ ));
93
110
 
94
111
  // ---------------------------------------------------------------------------
95
112
  // Conversation tree (branching history)
@@ -117,6 +134,27 @@ export interface MessageNode<TMessage> {
117
134
  serial: string | undefined;
118
135
  }
119
136
 
137
+ /**
138
+ * A Run's lifecycle state, modelled as one discriminated value so the terminal
139
+ * `error` is carried exactly when `status` is `'error'`. A RunNode is mutated
140
+ * in place, so status and its dependent error move together — transitions
141
+ * reassign `node.state` wholesale rather than setting fields individually.
142
+ */
143
+ export type RunNodeState =
144
+ | {
145
+ /** `'active'` (streaming), `'suspended'` (paused), or a non-error terminal reason. */
146
+ status: 'active' | 'suspended' | Exclude<RunEndReason, 'error'>;
147
+ }
148
+ | {
149
+ /** Terminal error status. */
150
+ status: 'error';
151
+ /**
152
+ * The run-end's stamped error (or a generic fallback). Exposed to
153
+ * consumers via `RunInfo.error`.
154
+ */
155
+ error: Ably.ErrorInfo;
156
+ };
157
+
120
158
  /**
121
159
  * A node in the conversation tree, representing a single Run.
122
160
  *
@@ -173,13 +211,12 @@ export interface RunNode<TProjection> {
173
211
  */
174
212
  clientId: string;
175
213
  /**
176
- * Run lifecycle status.
177
- * - `'active'` run-start observed, no terminal event yet.
178
- * - `'suspended'` run-suspend observed; the run is paused awaiting input
179
- * and stays live (a continuation re-activates it). Not terminal.
180
- * - {@link RunEndReason} — terminal state reflecting the run-end reason.
214
+ * Run lifecycle state — see {@link RunNodeState}. `'active'` until a terminal
215
+ * event; `'suspended'` while paused (a continuation re-activates it);
216
+ * otherwise the run-end reason, carrying `error` when that reason is
217
+ * `'error'`.
181
218
  */
182
- status: 'active' | 'suspended' | RunEndReason;
219
+ state: RunNodeState;
183
220
  /** Per-Run codec projection. Folded by the Tree from every event published under this run-id. */
184
221
  projection: TProjection;
185
222
  /**
@@ -317,6 +354,18 @@ export interface Tree<TOutput extends CodecOutputEvent, TProjection> {
317
354
  */
318
355
  getSiblingNodes(key: string): ConversationNode<TProjection>[];
319
356
 
357
+ /**
358
+ * Look up the raw Ably message that carried the given `event-id` header,
359
+ * if the Tree has observed it. Populated incrementally as messages arrive
360
+ * through the Tree's `ably-message` channel; not bounded except by the
361
+ * Tree's lifetime. Used by the agent's input-event lookup to find a
362
+ * triggering input message by id without scanning a separate buffer.
363
+ * @param eventId - The `event-id` header value to look up.
364
+ * @returns The matching raw Ably message, or undefined when the Tree has
365
+ * not observed an event with that id.
366
+ */
367
+ findAblyMessageByEventId(eventId: string): Ably.InboundMessage | undefined;
368
+
320
369
  // --- Events ---
321
370
 
322
371
  /**
@@ -31,16 +31,8 @@ export interface LoadHistoryOptions {
31
31
  // View — windowed projection over the tree
32
32
  // ---------------------------------------------------------------------------
33
33
 
34
- /**
35
- * Projection-free, View-facing snapshot of a Run.
36
- *
37
- * Exposes the Run facts a UI consumer needs (`runId`, owner `clientId`,
38
- * lifecycle `status`, `invocationId`) without leaking the codec's
39
- * opaque per-Run projection or the Tree's structural fields. Callers
40
- * that need the full Run record (parent / fork relationships, serials,
41
- * projection) reach `session.tree.getRunNode(runId)` directly.
42
- */
43
- export interface RunInfo {
34
+ /** Fields common to every {@link RunInfo} arm. */
35
+ interface RunInfoBase {
44
36
  /** The Run's unique identifier. */
45
37
  runId: string;
46
38
  /**
@@ -48,14 +40,6 @@ export interface RunInfo {
48
40
  * when the wire didn't carry an owner client id.
49
41
  */
50
42
  clientId: string;
51
- /**
52
- * Run lifecycle status. `'active'` while the Run is streaming;
53
- * `'suspended'` while it is paused awaiting input (still live, a
54
- * continuation re-activates it); otherwise the {@link RunEndReason} the Run
55
- * terminated with. Literal lifecycle vocabulary — UIs that want `'streaming'`
56
- * rendering language translate at the component boundary.
57
- */
58
- status: 'active' | 'suspended' | RunEndReason;
59
43
  /**
60
44
  * The agent-minted `invocationId` observed for this Run, adopted from the
61
45
  * wire `ai-run-start`. Stable across the Run's lifecycle once observed.
@@ -66,6 +50,48 @@ export interface RunInfo {
66
50
  invocationId: string;
67
51
  }
68
52
 
53
+ /**
54
+ * Projection-free, View-facing snapshot of a Run.
55
+ *
56
+ * Exposes the Run facts a UI consumer needs (`runId`, owner `clientId`,
57
+ * lifecycle `status`, `invocationId`, and — only when it failed — the terminal
58
+ * `error`) without leaking the codec's opaque per-Run projection or the Tree's
59
+ * structural fields. Callers that need the full Run record (parent / fork
60
+ * relationships, serials, projection) reach `session.tree.getRunNode(runId)`
61
+ * directly.
62
+ *
63
+ * Discriminated on `status`: a Run with `status: 'error'` carries the terminal
64
+ * `error`; every other status has no `error`. So `info.error` is defined
65
+ * exactly when `info.status === 'error'`.
66
+ */
67
+ export type RunInfo =
68
+ | (RunInfoBase & {
69
+ /**
70
+ * Run lifecycle status. `'active'` while the Run is streaming;
71
+ * `'suspended'` while it is paused awaiting input (still live, a
72
+ * continuation re-activates it); otherwise the non-error terminal
73
+ * {@link RunEndReason} (`'complete'` or `'cancelled'`). Literal lifecycle
74
+ * vocabulary — UIs that want `'streaming'` rendering language translate
75
+ * at the component boundary. The `'error'` terminal status lives on the
76
+ * other arm of this union, where it is paired with the terminal `error`.
77
+ */
78
+ status: 'active' | 'suspended' | Exclude<RunEndReason, 'error'>;
79
+ /** Never present for a non-error status. */
80
+ error?: never;
81
+ })
82
+ | (RunInfoBase & {
83
+ /** Terminal error status — the Run ended with {@link RunEndReason} `'error'`, carrying the terminal `error` below. */
84
+ status: 'error';
85
+ /**
86
+ * The terminal error. Carries the agent-stamped `error-code` /
87
+ * `error-message` detail (or a generic fallback when the run ended in
88
+ * error without detail), so a UI can show *why* a run failed alongside
89
+ * its `'error'` status. Mirrors the `Ably.ErrorInfo` delivered via
90
+ * `ClientSession.on('error')`.
91
+ */
92
+ error: Ably.ErrorInfo;
93
+ });
94
+
69
95
  /**
70
96
  * Bundle returned by {@link View.branchSelection} describing the
71
97
  * sibling group anchored at a given codec-message-id.
@@ -138,20 +164,23 @@ export interface View<TInput extends CodecInputEvent, TMessage> {
138
164
  */
139
165
  runs(): RunInfo[];
140
166
 
141
- /** Whether there are older Runs that can be loaded or revealed. */
167
+ /** Whether there are older messages that can be loaded or revealed. */
142
168
  hasOlder(): boolean;
143
169
 
144
170
  /**
145
- * Reveal older Runs. Loads from channel history if the tree doesn't have
146
- * enough, then advances the pagination window by up to `limit` Runs.
147
- * Emits 'update' when the visible list changes.
171
+ * Reveal exactly `limit` older codecMessages fewer only when channel history
172
+ * is exhausted. Loads from channel history when the tree doesn't already hold
173
+ * `limit` hidden messages, then advances the pagination window. Emits 'update'
174
+ * when the visible list changes.
148
175
  *
149
- * The pagination unit is the **Run**, not the message. A single Run
150
- * typically contributes more than one message to the flat list returned
151
- * by {@link View.getMessages} (e.g. a user prompt + assistant reply
152
- * pair). Revealing `limit` Runs may add 1..N messages each to the
153
- * visible window.
154
- * @param limit - Maximum number of older Runs to reveal. Defaults to 100.
176
+ * The pagination unit is the **codecMessage**. A node (a user prompt, or a
177
+ * reply Run) contributes 1..N messages to the flat list returned by
178
+ * {@link View.getMessages}; the window counts those messages, so a node
179
+ * straddling the boundary is **partially revealed** only its newest messages
180
+ * enter the window — and the page lands exactly on `limit` rather than on a
181
+ * node boundary. Such a partially-revealed run still appears in
182
+ * {@link View.runs} and is event-scoped.
183
+ * @param limit - Number of older codecMessages to reveal. Defaults to 10.
155
184
  */
156
185
  loadOlder(limit?: number): Promise<void>;
157
186