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