@ably/ai-transport 0.0.1 → 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 (167) hide show
  1. package/README.md +114 -116
  2. package/dist/ably-ai-transport.js +1743 -961
  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 +117 -39
  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 +410 -101
  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 +97 -17
  18. package/dist/core/transport/index.d.ts +5 -3
  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 -8
  24. package/dist/core/transport/run-manager.d.ts +78 -0
  25. package/dist/core/transport/tree.d.ts +435 -0
  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 -402
  32. package/dist/core/transport/view.d.ts +354 -0
  33. package/dist/errors.d.ts +37 -9
  34. package/dist/index.d.ts +6 -6
  35. package/dist/logger.d.ts +12 -0
  36. package/dist/react/ably-ai-transport-react.js +1164 -645
  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 +16 -10
  44. package/dist/react/internal/use-resolved-session.d.ts +36 -0
  45. package/dist/react/use-ably-messages.d.ts +20 -11
  46. package/dist/react/use-client-session.d.ts +81 -0
  47. package/dist/react/use-create-view.d.ts +23 -0
  48. package/dist/react/use-tree.d.ts +35 -0
  49. package/dist/react/use-view.d.ts +110 -0
  50. package/dist/utils.d.ts +32 -23
  51. package/dist/vercel/ably-ai-transport-vercel.js +2748 -1625
  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 +50 -0
  61. package/dist/vercel/index.d.ts +4 -2
  62. package/dist/vercel/react/ably-ai-transport-vercel-react.js +10298 -1410
  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 +70 -1
  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 +33 -0
  67. package/dist/vercel/react/contexts/chat-transport-provider.d.ts +96 -0
  68. package/dist/vercel/react/index.d.ts +4 -0
  69. package/dist/vercel/react/use-chat-transport.d.ts +66 -21
  70. package/dist/vercel/react/use-message-sync.d.ts +31 -12
  71. package/dist/vercel/run-end-reason.d.ts +29 -0
  72. package/dist/vercel/transport/chat-transport.d.ts +71 -30
  73. package/dist/vercel/transport/index.d.ts +25 -18
  74. package/dist/vercel/transport/run-output-stream.d.ts +56 -0
  75. package/dist/version.d.ts +2 -0
  76. package/package.json +47 -34
  77. package/src/constants.ts +126 -47
  78. package/src/core/agent.ts +68 -0
  79. package/src/core/codec/decoder.ts +71 -98
  80. package/src/core/codec/encoder.ts +115 -58
  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 +438 -106
  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 +182 -19
  89. package/src/core/transport/index.ts +29 -22
  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 +58 -40
  95. package/src/core/transport/run-manager.ts +249 -0
  96. package/src/core/transport/tree.ts +1167 -0
  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 -527
  103. package/src/core/transport/view.ts +1271 -0
  104. package/src/errors.ts +42 -9
  105. package/src/event-emitter.ts +3 -2
  106. package/src/index.ts +55 -39
  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 +27 -10
  112. package/src/react/internal/use-resolved-session.ts +63 -0
  113. package/src/react/use-ably-messages.ts +47 -19
  114. package/src/react/use-client-session.ts +201 -0
  115. package/src/react/use-create-view.ts +72 -0
  116. package/src/react/use-tree.ts +84 -0
  117. package/src/react/use-view.ts +275 -0
  118. package/src/react/vite.config.ts +4 -1
  119. package/src/utils.ts +63 -45
  120. package/src/vercel/codec/decoder.ts +336 -255
  121. package/src/vercel/codec/encoder.ts +348 -196
  122. package/src/vercel/codec/events.ts +87 -0
  123. package/src/vercel/codec/index.ts +59 -14
  124. package/src/vercel/codec/reducer.ts +977 -0
  125. package/src/vercel/codec/tool-transitions.ts +122 -0
  126. package/src/vercel/index.ts +7 -3
  127. package/src/vercel/react/contexts/chat-transport-context.ts +41 -0
  128. package/src/vercel/react/contexts/chat-transport-provider.tsx +150 -0
  129. package/src/vercel/react/index.ts +13 -1
  130. package/src/vercel/react/use-chat-transport.ts +162 -42
  131. package/src/vercel/react/use-message-sync.ts +121 -22
  132. package/src/vercel/react/vite.config.ts +4 -2
  133. package/src/vercel/run-end-reason.ts +78 -0
  134. package/src/vercel/transport/chat-transport.ts +553 -113
  135. package/src/vercel/transport/index.ts +40 -28
  136. package/src/vercel/transport/run-output-stream.ts +170 -0
  137. package/src/version.ts +2 -0
  138. package/dist/core/transport/client-transport.d.ts +0 -10
  139. package/dist/core/transport/conversation-tree.d.ts +0 -9
  140. package/dist/core/transport/decode-history.d.ts +0 -41
  141. package/dist/core/transport/server-transport.d.ts +0 -7
  142. package/dist/core/transport/stream-router.d.ts +0 -19
  143. package/dist/core/transport/turn-manager.d.ts +0 -34
  144. package/dist/react/use-active-turns.d.ts +0 -8
  145. package/dist/react/use-client-transport.d.ts +0 -7
  146. package/dist/react/use-conversation-tree.d.ts +0 -20
  147. package/dist/react/use-edit.d.ts +0 -7
  148. package/dist/react/use-history.d.ts +0 -19
  149. package/dist/react/use-messages.d.ts +0 -7
  150. package/dist/react/use-regenerate.d.ts +0 -7
  151. package/dist/react/use-send.d.ts +0 -7
  152. package/dist/vercel/codec/accumulator.d.ts +0 -21
  153. package/src/core/transport/client-transport.ts +0 -959
  154. package/src/core/transport/conversation-tree.ts +0 -434
  155. package/src/core/transport/decode-history.ts +0 -337
  156. package/src/core/transport/server-transport.ts +0 -458
  157. package/src/core/transport/stream-router.ts +0 -118
  158. package/src/core/transport/turn-manager.ts +0 -147
  159. package/src/react/use-active-turns.ts +0 -61
  160. package/src/react/use-client-transport.ts +0 -37
  161. package/src/react/use-conversation-tree.ts +0 -71
  162. package/src/react/use-edit.ts +0 -24
  163. package/src/react/use-history.ts +0 -111
  164. package/src/react/use-messages.ts +0 -32
  165. package/src/react/use-regenerate.ts +0 -24
  166. package/src/react/use-send.ts +0 -25
  167. package/src/vercel/codec/accumulator.ts +0 -603
@@ -0,0 +1,407 @@
1
+ /** Agent (server-side) session types: options, run runtime, and the Run / AgentSession contracts. */
2
+
3
+ import type * as Ably from 'ably';
4
+
5
+ import type { Logger } from '../../../logger.js';
6
+ import type { Codec, CodecInputEvent, CodecOutputEvent, WriteOptions } from '../../codec/types.js';
7
+ import type { Invocation } from '../invocation.js';
8
+ import type { CancelRequest, RunEndReason } from './shared.js';
9
+ import type { MessageNode } from './tree.js';
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Agent session options
13
+ // ---------------------------------------------------------------------------
14
+
15
+ /** Options for creating an agent session. */
16
+ export interface AgentSessionOptions<
17
+ TInput extends CodecInputEvent,
18
+ TOutput extends CodecOutputEvent,
19
+ TProjection,
20
+ TMessage,
21
+ > {
22
+ /**
23
+ * The Ably Realtime client. The caller owns its lifecycle —
24
+ * `session.close()` does not close the client.
25
+ */
26
+ client: Ably.Realtime;
27
+ /**
28
+ * The name of the channel to publish to. The session owns this channel —
29
+ * do not also resolve it elsewhere with conflicting channel options.
30
+ */
31
+ channelName: string;
32
+ /** The codec to use for encoding events and messages. */
33
+ codec: Codec<TInput, TOutput, TProjection, TMessage>;
34
+ /** Logger instance for diagnostic output. */
35
+ logger?: Logger;
36
+ /**
37
+ * Called with non-fatal session-level errors not scoped to any run.
38
+ * Examples: cancel listener subscription failure, channel attach errors,
39
+ * channel continuity loss (FAILED/SUSPENDED/DETACHED or re-attach with
40
+ * `resumed: false`).
41
+ */
42
+ onError?: (error: Ably.ErrorInfo) => void;
43
+
44
+ /**
45
+ * 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
+ * developer's HTTP handler, which should surface it as a non-2xx response
49
+ * so the client's pending send fails.
50
+ * Default: 30000 (30 seconds).
51
+ */
52
+ inputEventLookupTimeoutMs?: number;
53
+
54
+ /**
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.
60
+ *
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.
67
+ *
68
+ * Default: 200.
69
+ */
70
+ inputEventBufferLimit?: number;
71
+
72
+ /**
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.
85
+ *
86
+ * Default: `"2m"`.
87
+ */
88
+ rewindWindow?: string;
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Run options
93
+ // ---------------------------------------------------------------------------
94
+
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
+ /**
110
+ * Options for `Run.pipe` — per-operation overrides for the assistant message.
111
+ * @template TOutput - The codec output type carried by the stream; used by the `resolveWriteOptions` hook.
112
+ */
113
+ export interface PipeOptions<TOutput extends CodecOutputEvent> {
114
+ /** The codec-message-id of the immediately preceding message in this branch. */
115
+ parent?: string;
116
+ /** The codec-message-id of the message this response replaces (for regeneration). */
117
+ forkOf?: string;
118
+ /**
119
+ * Optional per-output hook invoked before each output is encoded. The
120
+ * returned {@link WriteOptions} (if any) override the stream's default
121
+ * headers and `codecMessageId` for that one encode call only; return `undefined`
122
+ * to use the stream defaults.
123
+ *
124
+ * Used to carry a subset of outputs within the stream to a different
125
+ * message (e.g. `tool-output-available` chunks that belong on a prior
126
+ * assistant message, stamped with `amend`). Must not be used
127
+ * for outputs that participate in the encoder's stream-append pipeline
128
+ * — streaming state (stream tracker, append ordering) is anchored to
129
+ * the stream's default identity and is not affected by per-output
130
+ * overrides.
131
+ * @param output - The output about to be encoded.
132
+ * @returns Per-write overrides for this output, or undefined.
133
+ */
134
+ resolveWriteOptions?: (output: TOutput) => WriteOptions | undefined;
135
+ }
136
+
137
+ /** The result of streaming a response through the encoder. */
138
+ export interface StreamResult {
139
+ /** Why the stream ended. */
140
+ reason: RunEndReason;
141
+ /**
142
+ * The error that caused the stream to fail, present when `reason` is
143
+ * `'error'`. This is the original error (e.g. from the LLM provider)
144
+ * preserved so the caller can inspect provider-specific fields. The
145
+ * run's `onError` callback also fires with a wrapped `Ably.ErrorInfo`
146
+ * (code `StreamError`) for standardized observability.
147
+ */
148
+ error?: Error;
149
+ }
150
+
151
+ /** Per-run runtime hooks, signal, and overrides supplied at `createRun()` time. */
152
+ export interface RunRuntime<TOutput extends CodecOutputEvent> {
153
+ /**
154
+ * Override the invocation id for this run. When omitted, the agent mints a
155
+ * fresh `crypto.randomUUID()` — the normal path (one per HTTP request).
156
+ * Supply a non-empty fixed value for deterministic ids in tests or
157
+ * in-process drivers; the empty string is not a valid override (it is the
158
+ * "unset" sentinel and does not fall through to minting).
159
+ */
160
+ invocationId?: string;
161
+
162
+ /**
163
+ * Override the run id for a FRESH run. When omitted, the agent mints a fresh
164
+ * `crypto.randomUUID()` — the normal path. A continuation IGNORES this: its
165
+ * run id is read from the triggering input event's wire headers, since a
166
+ * continuation re-enters a run that already exists. Supply a non-empty fixed
167
+ * value for deterministic ids in tests or in-process drivers; the empty
168
+ * string is not a valid override (it is the "unset" sentinel and does not
169
+ * fall through to minting).
170
+ */
171
+ runId?: string;
172
+
173
+ /**
174
+ * An external AbortSignal (typically the HTTP request's `req.signal`) that,
175
+ * when fired, cancels this run. This allows platform-level cancellation —
176
+ * request cancellation, serverless function timeout — to stop LLM generation
177
+ * and stream piping gracefully.
178
+ */
179
+ signal?: AbortSignal;
180
+
181
+ /**
182
+ * Called before each Ably message is published in this run.
183
+ * Mutate the Ably message in place to add custom headers under extras.ai.
184
+ */
185
+ onMessage?: (message: Ably.Message) => void;
186
+
187
+ /**
188
+ * Called when the run's stream is cancelled (by client cancel or server).
189
+ * Receives a write function to publish final outputs before the cancellation finalises.
190
+ */
191
+ onCancelled?: (write: (output: TOutput) => Promise<void>) => void | Promise<void>;
192
+
193
+ /**
194
+ * Called when a cancel message arrives matching this run.
195
+ * Return true to allow cancellation (fires `abortSignal`, stream cancels).
196
+ * Return false to reject (cancel ignored, stream continues).
197
+ * If not provided, all cancels are accepted.
198
+ */
199
+ onCancel?: (request: CancelRequest) => Promise<boolean>;
200
+
201
+ /**
202
+ * Called with non-fatal run-scoped errors that have no other delivery
203
+ * path. Fires in two scenarios:
204
+ * - Stream failures in `pipe` — the underlying error is also returned on
205
+ * `StreamResult.error`, but this callback delivers it wrapped as an
206
+ * `Ably.ErrorInfo` (code `StreamError`) for standardized observability.
207
+ * - Failures in the `onCancel` handler.
208
+ *
209
+ * Publish failures in `start`, `addEvents`, and `end`
210
+ * are not delivered here — those methods reject their returned promise
211
+ * with an `Ably.ErrorInfo`, and the caller should handle it at the await
212
+ * site. Run errors never render the session unusable, but the run may
213
+ * be in an inconsistent state; the caller should typically `end` it
214
+ * with reason `'error'`.
215
+ *
216
+ * Channel-wide events (e.g. continuity loss) are delivered via the
217
+ * session-level `onError` on {@link AgentSessionOptions}, not here.
218
+ */
219
+ onError?: (error: Ably.ErrorInfo) => void;
220
+ }
221
+
222
+ // ---------------------------------------------------------------------------
223
+ // Run interface
224
+ // ---------------------------------------------------------------------------
225
+
226
+ /**
227
+ * Read-only view exposed on a {@link Run} of the conversation messages
228
+ * this run was created with.
229
+ *
230
+ * TODO(AIT-771): when the agent rebuilds full conversation history from
231
+ * the channel, this should expose `RunNode[]`-shaped data to match the
232
+ * client side. Today it carries the flat input messages handed to the
233
+ * invocation.
234
+ */
235
+ export interface RunView<TMessage> {
236
+ /** Invocation input messages handed to this run; no branch awareness today. */
237
+ readonly messages: MessageNode<TMessage>[];
238
+ }
239
+
240
+ /** Options for {@link Run.loadConversation}. */
241
+ export interface LoadConversationOptions {
242
+ /**
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.
252
+ */
253
+ maxMessages?: number;
254
+ }
255
+
256
+ /**
257
+ * A server-side run with explicit lifecycle methods. Generic over the codec's
258
+ * output, projection, and message types.
259
+ */
260
+ export interface Run<TOutput extends CodecOutputEvent, TProjection, TMessage> {
261
+ /** The run's unique identifier. */
262
+ readonly runId: string;
263
+
264
+ /**
265
+ * The invocation's unique identifier, minted by the agent when the run is
266
+ * created (one per HTTP request that invokes the agent). Readable
267
+ * synchronously — the application returns it on the HTTP response so the
268
+ * caller can observe it. The agent stamps it on every event it publishes
269
+ * for this invocation (run lifecycle + outputs).
270
+ */
271
+ readonly invocationId: string;
272
+
273
+ /** AbortSignal scoped to this run. Fires when a cancel event arrives for this runId. */
274
+ readonly abortSignal: AbortSignal;
275
+
276
+ /** Read-only view of the conversation messages associated with this run. */
277
+ readonly view: RunView<TMessage>;
278
+
279
+ /**
280
+ * The conversation messages this run should feed to the model.
281
+ *
282
+ * - Before {@link start} resolves: empty (no view contribution yet).
283
+ * - After {@link start}: the user-prompt messages looked up on the
284
+ * channel for this invocation.
285
+ * - After {@link loadConversation}: the full multi-turn conversation —
286
+ * all ancestor run messages followed by the current run's messages,
287
+ * oldest turn first. This is the value to pass to the LLM when the
288
+ * agent handles a reply in an ongoing conversation.
289
+ *
290
+ * Each access returns a fresh array — safe to mutate without affecting
291
+ * internal Run state.
292
+ */
293
+ readonly messages: TMessage[];
294
+
295
+ /**
296
+ * Publish the run's opening lifecycle event to the channel (run-start, or
297
+ * run-resume for a continuation). Must be called before any other run method
298
+ * (pipe, addEvents, suspend, end).
299
+ */
300
+ start(): Promise<void>;
301
+
302
+ /**
303
+ * Pipe a ReadableStream through the encoder to the channel.
304
+ * Returns when the stream completes, is cancelled, or errors.
305
+ * Does NOT call end() — the caller must call end() after pipe returns.
306
+ */
307
+ pipe(stream: ReadableStream<TOutput>, options?: PipeOptions<TOutput>): Promise<StreamResult>;
308
+
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
+ /**
338
+ * Reconstruct the full multi-turn conversation by walking the ancestor
339
+ * run chain and concatenating each run's messages, oldest turn first.
340
+ *
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}.
350
+ */
351
+ loadConversation(options?: LoadConversationOptions): Promise<TMessage[]>;
352
+
353
+ /**
354
+ * Publish a run-suspend event to the channel and clean up, pausing the run
355
+ * without ending it. Call this instead of {@link Run.end} when the run is
356
+ * waiting on participant input (e.g. a client-side tool execution or a
357
+ * server-side tool approval): the run stays live and a later invocation can
358
+ * resume it under the same `runId`. Like {@link Run.end}, it is terminal for
359
+ * this Run instance — the resuming invocation builds a fresh Run. Must be
360
+ * called after {@link Run.start}; a no-op if the run has already ended or
361
+ * suspended.
362
+ */
363
+ suspend(): Promise<void>;
364
+
365
+ /** Publish run-end event to the channel and clean up. Terminal. */
366
+ end(reason: RunEndReason): Promise<void>;
367
+ }
368
+
369
+ // ---------------------------------------------------------------------------
370
+ // Agent session interface
371
+ // ---------------------------------------------------------------------------
372
+
373
+ /** Server-side session that manages run lifecycles over an Ably channel. */
374
+ export interface AgentSession<TOutput extends CodecOutputEvent, TProjection, TMessage> {
375
+ /**
376
+ * 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
382
+ * `connect()` has been *called*; once it has, they await the in-flight
383
+ * connect promise rather than throwing.
384
+ */
385
+ connect(): Promise<void>;
386
+
387
+ /**
388
+ * Create a new run from an invocation. Synchronous — no channel activity
389
+ * until start() is called. The run is registered for cancel routing
390
+ * immediately so that early cancels fire the AbortSignal.
391
+ * @param invocation - The {@link Invocation} carrying run identity and
392
+ * conversation messages.
393
+ * @param runtime - Optional runtime hooks and external AbortSignal
394
+ * (e.g. the HTTP request's `req.signal`).
395
+ */
396
+ createRun(invocation: Invocation, runtime?: RunRuntime<TOutput>): Run<TOutput, TProjection, TMessage>;
397
+
398
+ /**
399
+ * Unsubscribe from cancel messages, cancel all active runs, detach the
400
+ * channel this session attached, and clean up.
401
+ *
402
+ * Resolves once the detach completes. The detach is best-effort:
403
+ * a failure (e.g. the channel is already FAILED) is swallowed
404
+ * and does not reject. Idempotent.
405
+ */
406
+ close(): Promise<void>;
407
+ }
@@ -0,0 +1,211 @@
1
+ /** Client session types: options, send options, the ActiveRun handle, and the ClientSession contract. */
2
+
3
+ import type * as Ably from 'ably';
4
+
5
+ import type { Logger } from '../../../logger.js';
6
+ import type { Codec, CodecInputEvent, CodecOutputEvent } from '../../codec/types.js';
7
+ import type { Invocation } from '../invocation.js';
8
+ import type { Tree } from './tree.js';
9
+ import type { View } from './view.js';
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Client session options
13
+ // ---------------------------------------------------------------------------
14
+
15
+ /** Options for creating a client session. */
16
+ export interface ClientSessionOptions<
17
+ TInput extends CodecInputEvent,
18
+ TOutput extends CodecOutputEvent,
19
+ TProjection,
20
+ TMessage,
21
+ > {
22
+ /**
23
+ * The Ably Realtime client. The caller owns its lifecycle —
24
+ * `session.close()` does not close the client.
25
+ */
26
+ client: Ably.Realtime;
27
+
28
+ /**
29
+ * The name of the channel to subscribe to and publish cancel signals on.
30
+ * The session owns this channel — do not also resolve it elsewhere with
31
+ * conflicting channel options.
32
+ */
33
+ channelName: string;
34
+
35
+ /** The codec to use for encoding/decoding. */
36
+ codec: Codec<TInput, TOutput, TProjection, TMessage>;
37
+
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
+ /** Initial messages to seed the conversation tree with. Forms a linear chain. */
46
+ messages?: TMessage[];
47
+
48
+ /** Logger instance for diagnostic output. */
49
+ logger?: Logger;
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Send options
54
+ // ---------------------------------------------------------------------------
55
+
56
+ /** Per-send options for branching metadata and run identity. */
57
+ export interface SendOptions {
58
+ /**
59
+ * The codec-message-id of the message this send replaces (fork).
60
+ * Set for regeneration (forkOf an assistant message) or
61
+ * edit (forkOf a user message).
62
+ */
63
+ forkOf?: string;
64
+ /**
65
+ * The codec-message-id of the message that precedes this one in the
66
+ * conversation thread. If omitted, auto-computed from the last
67
+ * message in the view.
68
+ */
69
+ parent?: string;
70
+ /**
71
+ * Reuse an existing `runId` (e.g. resume a suspended run). When set,
72
+ * the send is treated as a continuation: the run's existing observer
73
+ * state (router stream, tree run-tracking) is reused; no fresh
74
+ * `crypto.randomUUID()` is minted. Each continuation POST is woken by the
75
+ * agent, which mints a distinct `invocationId` per HTTP request.
76
+ */
77
+ 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
+ }
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // Active run handle
88
+ // ---------------------------------------------------------------------------
89
+
90
+ /**
91
+ * A handle to an active client-side run, returned by `send()`,
92
+ * `regenerate()`, and `edit()`.
93
+ *
94
+ * The core does not expose a per-run output stream — streaming is a
95
+ * consumer-layer concern (e.g. the Vercel ChatTransport builds a stream from
96
+ * the Tree's `output` events). The handle carries only run identity and
97
+ * control, so it is not parameterized by the codec output type.
98
+ */
99
+ export interface ActiveRun {
100
+ /**
101
+ * The synchronous routing handle for this send: the triggering input's
102
+ * codec-message-id, which the client owns the moment it publishes and the
103
+ * agent echoes back as `input-codec-message-id`. Stream routing and cancel
104
+ * key on this — it is known immediately, unlike {@link runId}, which the
105
+ * agent mints.
106
+ */
107
+ inputCodecMessageId: string;
108
+ /**
109
+ * The run's unique identifier, resolved when the agent's `ai-run-start` for
110
+ * this send is observed on the channel. The agent mints the run-id, so it is
111
+ * not known synchronously: `await run.runId`
112
+ * to learn it (this also tells you the agent has picked up the run). There is
113
+ * no built-in deadline — race it against your own timeout if you need one.
114
+ * Rejects only if the session is closed before run-start arrives. A
115
+ * continuation does not resolve any sooner: its run-id promise resolves when
116
+ * the agent's run-resume (`event.type === 'resume'`) for this send is observed
117
+ * on the channel, not synchronously from the run-id the caller passed in.
118
+ */
119
+ runId: Promise<string>;
120
+ /**
121
+ * The input event's unique identifier. Stamped on the primary input event
122
+ * published to the channel and forwarded in the HTTP POST body so the
123
+ * agent can locate the exact triggering event.
124
+ */
125
+ inputEventId: string;
126
+ /**
127
+ * Cancel this specific run. Publishes a cancel signal synchronously — keyed
128
+ * by the triggering input's codec-message-id ({@link inputCodecMessageId}),
129
+ * which the client owns the moment it publishes, so a cancel issued before
130
+ * the agent mints the run-id is still honoured (the agent buffers it and
131
+ * fires it once its input-event lookup resolves the input to the run). A
132
+ * continuation also carries its known run-id. Resolves once the cancel is
133
+ * published; it does not wait for {@link runId}.
134
+ */
135
+ cancel(): Promise<void>;
136
+ /**
137
+ * The codec-message-id of every input in this send, in input order. Includes
138
+ * wire-only inputs (regenerate, tool resolutions), which are NOT folded into
139
+ * the local projection — so an entry here does not by itself mean an
140
+ * optimistic insert occurred.
141
+ */
142
+ optimisticCodecMessageIds: string[];
143
+ /**
144
+ * Build the {@link Invocation} pointer for this run — only `inputEventId` and
145
+ * the session's channel name as `sessionName`. The body carries no run-id: a
146
+ * fresh run's run-id is minted by the agent, and a continuation's run-id is
147
+ * read off the triggering input event's wire headers, so run identity always
148
+ * lives on the channel rather than the invocation body. The
149
+ * application POSTs `run.toInvocation().toJSON()` to its agent endpoint to
150
+ * wake the agent; the agent rebuilds it via {@link Invocation.fromJSON} and
151
+ * mints the `invocationId` (and a fresh `runId`) itself. The conversation
152
+ * itself is read from the channel, so the pointer carries only identifiers.
153
+ */
154
+ toInvocation(): Invocation;
155
+ }
156
+
157
+ // ---------------------------------------------------------------------------
158
+ // Client session interface
159
+ // ---------------------------------------------------------------------------
160
+
161
+ /** Client-side session that manages conversation state over an Ably channel. */
162
+ export interface ClientSession<
163
+ TInput extends CodecInputEvent,
164
+ TOutput extends CodecOutputEvent,
165
+ TProjection,
166
+ TMessage,
167
+ > {
168
+ /** The complete conversation tree — all known Run nodes, events for any change. */
169
+ readonly tree: Tree<TOutput, TProjection>;
170
+
171
+ /** The default paginated, branch-aware view for rendering — events scoped to visible messages. */
172
+ readonly view: View<TInput, TMessage>;
173
+
174
+ /**
175
+ * Subscribe to the channel and (implicitly) attach. Idempotent —
176
+ * subsequent calls return the same promise. The View's write operations
177
+ * (`send()`, `regenerate()`, `edit()`) and this session's `cancel()` throw
178
+ * `InvalidArgument` until `connect()` resolves.
179
+ */
180
+ connect(): Promise<void>;
181
+
182
+ /**
183
+ * Create an additional view over the same conversation tree.
184
+ * Each view has independent branch selections and pagination state.
185
+ * The caller is responsible for calling `close()` on the returned view
186
+ * when it is no longer needed, or it will be closed when the session closes.
187
+ */
188
+ createView(): View<TInput, TMessage>;
189
+
190
+ /** Cancel the specified run by publishing an `ai-cancel` signal on the channel. The core does not own a per-run stream; closing any consumer-built stream is the responsibility of the layer that built it (e.g. the Vercel ChatTransport). */
191
+ cancel(runId: string): Promise<void>;
192
+
193
+ /**
194
+ * Subscribe to non-fatal session errors. These indicate something went
195
+ * wrong but the session is still operational. Returns an unsubscribe function.
196
+ * Once the session is CLOSED this is a no-op: the handler is not registered and
197
+ * the returned function does nothing.
198
+ */
199
+ on(event: 'error', handler: (error: Ably.ErrorInfo) => void): () => void;
200
+
201
+ /**
202
+ * Tear down the session: unsubscribe from the channel, close active views,
203
+ * clear all handlers, and detach the channel this session attached.
204
+ *
205
+ * Detaching stops the session from receiving further channel messages;
206
+ * the server keeps streaming until its runs end on their own. To stop
207
+ * in-progress runs, call {@link cancel} for each before `close()`. The
208
+ * detach is best-effort: a failure is swallowed and does not reject.
209
+ */
210
+ close(): Promise<void>;
211
+ }
@@ -0,0 +1,27 @@
1
+ /** Types shared across the client, agent, tree, and view layers. */
2
+
3
+ import type * as Ably from 'ably';
4
+
5
+ /**
6
+ * Why a run ended.
7
+ *
8
+ * A run-end is terminal — a run that merely pauses awaiting input publishes
9
+ * `ai-run-suspend` instead (see {@link Run.suspend}).
10
+ *
11
+ * - `complete` — the run finished naturally.
12
+ * - `cancelled` — the run was cancelled by a client.
13
+ * - `error` — the run errored.
14
+ */
15
+ export type RunEndReason = 'complete' | 'cancelled' | 'error';
16
+
17
+ /**
18
+ * Passed to a run's `onCancel` hook for authorization decisions.
19
+ * The hook inspects the incoming cancel message and decides whether to
20
+ * allow the targeted run to be cancelled.
21
+ */
22
+ export interface CancelRequest {
23
+ /** The raw Ably message that carried the cancel signal. */
24
+ message: Ably.InboundMessage;
25
+ /** The runId being cancelled. */
26
+ runId: string;
27
+ }