@ably/ai-transport 0.0.1

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 (118) hide show
  1. package/LICENSE +176 -0
  2. package/README.md +426 -0
  3. package/dist/ably-ai-transport.js +1388 -0
  4. package/dist/ably-ai-transport.js.map +1 -0
  5. package/dist/ably-ai-transport.umd.cjs +2 -0
  6. package/dist/ably-ai-transport.umd.cjs.map +1 -0
  7. package/dist/constants.d.ts +50 -0
  8. package/dist/core/codec/decoder.d.ts +62 -0
  9. package/dist/core/codec/encoder.d.ts +56 -0
  10. package/dist/core/codec/index.d.ts +8 -0
  11. package/dist/core/codec/lifecycle-tracker.d.ts +74 -0
  12. package/dist/core/codec/types.d.ts +188 -0
  13. package/dist/core/transport/client-transport.d.ts +10 -0
  14. package/dist/core/transport/conversation-tree.d.ts +9 -0
  15. package/dist/core/transport/decode-history.d.ts +41 -0
  16. package/dist/core/transport/headers.d.ts +26 -0
  17. package/dist/core/transport/index.d.ts +4 -0
  18. package/dist/core/transport/pipe-stream.d.ts +16 -0
  19. package/dist/core/transport/server-transport.d.ts +7 -0
  20. package/dist/core/transport/stream-router.d.ts +19 -0
  21. package/dist/core/transport/turn-manager.d.ts +34 -0
  22. package/dist/core/transport/types.d.ts +407 -0
  23. package/dist/errors.d.ts +46 -0
  24. package/dist/event-emitter.d.ts +65 -0
  25. package/dist/index.d.ts +11 -0
  26. package/dist/logger.d.ts +103 -0
  27. package/dist/react/ably-ai-transport-react.js +823 -0
  28. package/dist/react/ably-ai-transport-react.js.map +1 -0
  29. package/dist/react/ably-ai-transport-react.umd.cjs +2 -0
  30. package/dist/react/ably-ai-transport-react.umd.cjs.map +1 -0
  31. package/dist/react/index.d.ts +11 -0
  32. package/dist/react/use-ably-messages.d.ts +18 -0
  33. package/dist/react/use-active-turns.d.ts +8 -0
  34. package/dist/react/use-client-transport.d.ts +7 -0
  35. package/dist/react/use-conversation-tree.d.ts +20 -0
  36. package/dist/react/use-edit.d.ts +7 -0
  37. package/dist/react/use-history.d.ts +19 -0
  38. package/dist/react/use-messages.d.ts +7 -0
  39. package/dist/react/use-regenerate.d.ts +7 -0
  40. package/dist/react/use-send.d.ts +7 -0
  41. package/dist/utils.d.ts +127 -0
  42. package/dist/vercel/ably-ai-transport-vercel.js +2331 -0
  43. package/dist/vercel/ably-ai-transport-vercel.js.map +1 -0
  44. package/dist/vercel/ably-ai-transport-vercel.umd.cjs +2 -0
  45. package/dist/vercel/ably-ai-transport-vercel.umd.cjs.map +1 -0
  46. package/dist/vercel/codec/accumulator.d.ts +21 -0
  47. package/dist/vercel/codec/decoder.d.ts +22 -0
  48. package/dist/vercel/codec/encoder.d.ts +41 -0
  49. package/dist/vercel/codec/index.d.ts +22 -0
  50. package/dist/vercel/index.d.ts +3 -0
  51. package/dist/vercel/react/ably-ai-transport-vercel-react.js +2082 -0
  52. package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -0
  53. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +2 -0
  54. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -0
  55. package/dist/vercel/react/index.d.ts +3 -0
  56. package/dist/vercel/react/use-chat-transport.d.ts +29 -0
  57. package/dist/vercel/react/use-message-sync.d.ts +19 -0
  58. package/dist/vercel/transport/chat-transport.d.ts +118 -0
  59. package/dist/vercel/transport/index.d.ts +36 -0
  60. package/package.json +123 -0
  61. package/react/README.md +3 -0
  62. package/react/index.d.ts +1 -0
  63. package/react/index.js +1 -0
  64. package/react/index.umd.cjs +1 -0
  65. package/src/constants.ts +98 -0
  66. package/src/core/codec/decoder.ts +402 -0
  67. package/src/core/codec/encoder.ts +470 -0
  68. package/src/core/codec/index.ts +28 -0
  69. package/src/core/codec/lifecycle-tracker.ts +140 -0
  70. package/src/core/codec/types.ts +249 -0
  71. package/src/core/transport/client-transport.ts +959 -0
  72. package/src/core/transport/conversation-tree.ts +434 -0
  73. package/src/core/transport/decode-history.ts +337 -0
  74. package/src/core/transport/headers.ts +46 -0
  75. package/src/core/transport/index.ts +34 -0
  76. package/src/core/transport/pipe-stream.ts +95 -0
  77. package/src/core/transport/server-transport.ts +458 -0
  78. package/src/core/transport/stream-router.ts +118 -0
  79. package/src/core/transport/turn-manager.ts +147 -0
  80. package/src/core/transport/types.ts +533 -0
  81. package/src/errors.ts +58 -0
  82. package/src/event-emitter.ts +103 -0
  83. package/src/index.ts +89 -0
  84. package/src/logger.ts +241 -0
  85. package/src/react/index.ts +11 -0
  86. package/src/react/use-ably-messages.ts +37 -0
  87. package/src/react/use-active-turns.ts +61 -0
  88. package/src/react/use-client-transport.ts +37 -0
  89. package/src/react/use-conversation-tree.ts +71 -0
  90. package/src/react/use-edit.ts +24 -0
  91. package/src/react/use-history.ts +111 -0
  92. package/src/react/use-messages.ts +32 -0
  93. package/src/react/use-regenerate.ts +24 -0
  94. package/src/react/use-send.ts +25 -0
  95. package/src/react/vite.config.ts +32 -0
  96. package/src/tsconfig.json +25 -0
  97. package/src/utils.ts +230 -0
  98. package/src/vercel/codec/accumulator.ts +603 -0
  99. package/src/vercel/codec/decoder.ts +615 -0
  100. package/src/vercel/codec/encoder.ts +396 -0
  101. package/src/vercel/codec/index.ts +37 -0
  102. package/src/vercel/index.ts +12 -0
  103. package/src/vercel/react/index.ts +4 -0
  104. package/src/vercel/react/use-chat-transport.ts +60 -0
  105. package/src/vercel/react/use-message-sync.ts +34 -0
  106. package/src/vercel/react/vite.config.ts +33 -0
  107. package/src/vercel/transport/chat-transport.ts +278 -0
  108. package/src/vercel/transport/index.ts +56 -0
  109. package/src/vercel/vite.config.ts +33 -0
  110. package/src/vite.config.ts +31 -0
  111. package/vercel/README.md +3 -0
  112. package/vercel/index.d.ts +1 -0
  113. package/vercel/index.js +1 -0
  114. package/vercel/index.umd.cjs +1 -0
  115. package/vercel/react/README.md +3 -0
  116. package/vercel/react/index.d.ts +1 -0
  117. package/vercel/react/index.js +1 -0
  118. package/vercel/react/index.umd.cjs +1 -0
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Server-side turn state management and lifecycle event publishing.
3
+ *
4
+ * Owns the authoritative turn lifecycle. Tracks active turns with their
5
+ * AbortControllers and clientIds. Publishes turn-start and turn-end events
6
+ * on the Ably channel so all clients can react to turn state changes.
7
+ */
8
+
9
+ import type * as Ably from 'ably';
10
+
11
+ import {
12
+ EVENT_TURN_END,
13
+ EVENT_TURN_START,
14
+ HEADER_TURN_CLIENT_ID,
15
+ HEADER_TURN_ID,
16
+ HEADER_TURN_REASON,
17
+ } from '../../constants.js';
18
+ import type { Logger } from '../../logger.js';
19
+ import type { TurnEndReason } from './types.js';
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Interface
23
+ // ---------------------------------------------------------------------------
24
+
25
+ /** Manages active turns and publishes turn lifecycle events on the channel. */
26
+ export interface TurnManager {
27
+ /** Register a new turn. Publishes turn-start on the channel. Returns AbortSignal. */
28
+ startTurn(turnId: string, clientId?: string, controller?: AbortController): Promise<AbortSignal>;
29
+ /** End a turn. Publishes turn-end on the channel. Cleans up internal state. */
30
+ endTurn(turnId: string, reason: TurnEndReason): Promise<void>;
31
+ /** Get the AbortSignal for a turn. */
32
+ getSignal(turnId: string): AbortSignal | undefined;
33
+ /** Get the clientId that owns a turn. */
34
+ getClientId(turnId: string): string | undefined;
35
+ /** Abort the signal for a turn. */
36
+ abort(turnId: string): void;
37
+ /** Get all active turn IDs. */
38
+ getActiveTurnIds(): string[];
39
+ /** Abort all active turns and clear state. */
40
+ close(): void;
41
+ }
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Internal state
45
+ // ---------------------------------------------------------------------------
46
+
47
+ interface TurnState {
48
+ controller: AbortController;
49
+ clientId: string;
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Implementation
54
+ // ---------------------------------------------------------------------------
55
+
56
+ class DefaultTurnManager implements TurnManager {
57
+ private readonly _channel: Ably.RealtimeChannel;
58
+ private readonly _logger: Logger | undefined;
59
+ private readonly _activeTurns = new Map<string, TurnState>();
60
+
61
+ constructor(channel: Ably.RealtimeChannel, logger?: Logger) {
62
+ this._channel = channel;
63
+ this._logger = logger?.withContext({ component: 'TurnManager' });
64
+ }
65
+
66
+ async startTurn(turnId: string, clientId?: string, externalController?: AbortController): Promise<AbortSignal> {
67
+ this._logger?.trace('DefaultTurnManager.startTurn();', { turnId, clientId });
68
+
69
+ const controller = externalController ?? new AbortController();
70
+ const resolvedClientId = clientId ?? '';
71
+ this._activeTurns.set(turnId, { controller, clientId: resolvedClientId });
72
+
73
+ await this._channel.publish({
74
+ name: EVENT_TURN_START,
75
+ extras: {
76
+ headers: {
77
+ [HEADER_TURN_ID]: turnId,
78
+ [HEADER_TURN_CLIENT_ID]: resolvedClientId,
79
+ },
80
+ },
81
+ });
82
+
83
+ this._logger?.debug('DefaultTurnManager.startTurn(); turn started', { turnId });
84
+ return controller.signal;
85
+ }
86
+
87
+ async endTurn(turnId: string, reason: TurnEndReason): Promise<void> {
88
+ this._logger?.trace('DefaultTurnManager.endTurn();', { turnId, reason });
89
+
90
+ const state = this._activeTurns.get(turnId);
91
+ const resolvedClientId = state?.clientId ?? '';
92
+
93
+ // Publish before deleting local state so that if publish fails,
94
+ // the turn remains in the active set and can be retried or cleaned up.
95
+ await this._channel.publish({
96
+ name: EVENT_TURN_END,
97
+ extras: {
98
+ headers: {
99
+ [HEADER_TURN_ID]: turnId,
100
+ [HEADER_TURN_CLIENT_ID]: resolvedClientId,
101
+ [HEADER_TURN_REASON]: reason,
102
+ },
103
+ },
104
+ });
105
+
106
+ this._activeTurns.delete(turnId);
107
+ this._logger?.debug('DefaultTurnManager.endTurn(); turn ended', { turnId, reason });
108
+ }
109
+
110
+ getSignal(turnId: string): AbortSignal | undefined {
111
+ return this._activeTurns.get(turnId)?.controller.signal;
112
+ }
113
+
114
+ getClientId(turnId: string): string | undefined {
115
+ return this._activeTurns.get(turnId)?.clientId;
116
+ }
117
+
118
+ abort(turnId: string): void {
119
+ this._logger?.debug('DefaultTurnManager.abort();', { turnId });
120
+ this._activeTurns.get(turnId)?.controller.abort();
121
+ }
122
+
123
+ getActiveTurnIds(): string[] {
124
+ return [...this._activeTurns.keys()];
125
+ }
126
+
127
+ close(): void {
128
+ this._logger?.trace('DefaultTurnManager.close();', { activeTurns: this._activeTurns.size });
129
+ for (const state of this._activeTurns.values()) {
130
+ state.controller.abort();
131
+ }
132
+ this._activeTurns.clear();
133
+ }
134
+ }
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // Factory
138
+ // ---------------------------------------------------------------------------
139
+
140
+ /**
141
+ * Create a turn manager bound to the given channel.
142
+ * @param channel - The Ably channel to publish lifecycle events on.
143
+ * @param logger - Optional logger for diagnostic output.
144
+ * @returns A new {@link TurnManager} instance.
145
+ */
146
+ export const createTurnManager = (channel: Ably.RealtimeChannel, logger?: Logger): TurnManager =>
147
+ new DefaultTurnManager(channel, logger);
@@ -0,0 +1,533 @@
1
+ /**
2
+ * Core transport types, parameterized by codec event and message types.
3
+ *
4
+ * These types define the contract for both client and server transport
5
+ * implementations, independent of which codec (Vercel AI SDK, etc.) is used.
6
+ */
7
+
8
+ import type * as Ably from 'ably';
9
+
10
+ import type { Logger } from '../../logger.js';
11
+ import type { Codec } from '../codec/types.js';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Shared types
15
+ // ---------------------------------------------------------------------------
16
+
17
+ /** Why a turn ended. */
18
+ export type TurnEndReason = 'complete' | 'cancelled' | 'error';
19
+
20
+ /** Filter for cancel operations. At most one field should be set. */
21
+ export interface CancelFilter {
22
+ /** Cancel a specific turn by ID. */
23
+ turnId?: string;
24
+ /** Cancel all turns belonging to the sender's clientId. */
25
+ own?: boolean;
26
+ /** Cancel all turns belonging to a specific clientId. */
27
+ clientId?: string;
28
+ /** Cancel all turns on the channel. */
29
+ all?: boolean;
30
+ }
31
+
32
+ /**
33
+ * Passed to the server's `onCancel` hook for authorization decisions.
34
+ * The hook inspects the incoming cancel message and decides whether to
35
+ * allow each matched turn to be aborted.
36
+ */
37
+ export interface CancelRequest {
38
+ /** The raw Ably message that carried the cancel signal. */
39
+ message: Ably.InboundMessage;
40
+ /** The parsed cancel scope from the message headers. */
41
+ filter: CancelFilter;
42
+ /** Which active turnIds would be cancelled if allowed. */
43
+ matchedTurnIds: string[];
44
+ /** Map of turnId to the ownerClientId for the matched turns. */
45
+ turnOwners: Map<string, string>;
46
+ }
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Message with headers
50
+ // ---------------------------------------------------------------------------
51
+
52
+ /** A domain message paired with its Ably transport headers. Used on the read path to snapshot conversation state (e.g. for HTTP POST bodies). */
53
+ export interface MessageWithHeaders<TMessage> {
54
+ /** The domain message. */
55
+ message: TMessage;
56
+ /** Ably headers associated with this message (transport metadata, domain headers). */
57
+ headers?: Record<string, string>;
58
+ }
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Server transport options
62
+ // ---------------------------------------------------------------------------
63
+
64
+ /** Options for creating a server transport. */
65
+ export interface ServerTransportOptions<TEvent, TMessage> {
66
+ /** The Ably channel to publish to. Must match the client's channel. */
67
+ channel: Ably.RealtimeChannel;
68
+ /** The codec to use for encoding events and messages. */
69
+ codec: Codec<TEvent, TMessage>;
70
+ /** Logger instance for diagnostic output. */
71
+ logger?: Logger;
72
+ /**
73
+ * Called with non-fatal transport-level errors not scoped to any turn.
74
+ * Examples: cancel listener subscription failure, channel attach errors.
75
+ */
76
+ onError?: (error: Ably.ErrorInfo) => void;
77
+ }
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Turn options
81
+ // ---------------------------------------------------------------------------
82
+
83
+ /** Options for addMessages — per-operation overrides for message identity and branching. */
84
+ export interface AddMessageOptions {
85
+ /** The user's clientId for attribution. */
86
+ clientId?: string;
87
+ /** The msg-id of the immediately preceding message in this branch. */
88
+ parent?: string | null;
89
+ /** The msg-id of the message this one replaces (creates a fork). */
90
+ forkOf?: string;
91
+ }
92
+
93
+ /** Result of publishing user messages via addMessages. */
94
+ export interface AddMessagesResult {
95
+ /** The `x-ably-msg-id` of each published message, in order. */
96
+ msgIds: string[];
97
+ }
98
+
99
+ /** Options for streamResponse — per-operation overrides for the assistant message. */
100
+ export interface StreamResponseOptions {
101
+ /** The msg-id of the immediately preceding message in this branch. */
102
+ parent?: string | null;
103
+ /** The msg-id of the message this response replaces (for regeneration). */
104
+ forkOf?: string;
105
+ }
106
+
107
+ /** The result of streaming a response through the encoder. */
108
+ export interface StreamResult {
109
+ /** Why the stream ended. */
110
+ reason: TurnEndReason;
111
+ }
112
+
113
+ /** Options passed to newTurn for configuring the turn lifecycle. */
114
+ export interface NewTurnOptions<TEvent> {
115
+ /** The turn identifier (generated by the client transport or the server). */
116
+ turnId: string;
117
+
118
+ /** The user's clientId for attribution. */
119
+ clientId?: string;
120
+
121
+ /**
122
+ * The msg-id of the immediately preceding message in this branch.
123
+ * Used as the default parent for user messages (via addMessages) and
124
+ * assistant messages (via streamResponse) when not overridden per-operation.
125
+ */
126
+ parent?: string | null;
127
+
128
+ /**
129
+ * The msg-id of the message this turn replaces (creates a fork).
130
+ * Stamped on user messages (for edits) or assistant messages
131
+ * (for regeneration).
132
+ */
133
+ forkOf?: string;
134
+
135
+ /**
136
+ * Called before each Ably message is published in this turn.
137
+ * Mutate the Ably message in place to add custom extras.headers.
138
+ */
139
+ onMessage?: (message: Ably.Message) => void;
140
+
141
+ /**
142
+ * Called when the turn's stream is aborted (by cancel or server).
143
+ * Receives a write function to publish final events before the abort finalises.
144
+ */
145
+ onAbort?: (write: (event: TEvent) => Promise<void>) => void | Promise<void>;
146
+
147
+ /**
148
+ * Called when a cancel message arrives matching this turn.
149
+ * Return true to allow cancellation (fires abortSignal, stream aborts).
150
+ * Return false to reject (cancel ignored, stream continues).
151
+ * If not provided, all cancels are accepted.
152
+ */
153
+ onCancel?: (request: CancelRequest) => Promise<boolean>;
154
+
155
+ /**
156
+ * Called with non-fatal errors scoped to this turn. Examples: turn-start
157
+ * publish failure, encoder recovery failure, stream encoding errors.
158
+ */
159
+ onError?: (error: Ably.ErrorInfo) => void;
160
+ }
161
+
162
+ // ---------------------------------------------------------------------------
163
+ // Turn interface
164
+ // ---------------------------------------------------------------------------
165
+
166
+ /** A server-side turn with explicit lifecycle methods. */
167
+ export interface Turn<TEvent, TMessage> {
168
+ /** The turn's unique identifier. */
169
+ readonly turnId: string;
170
+
171
+ /** Abort signal scoped to this turn. Fires when a cancel event arrives for this turnId. */
172
+ readonly abortSignal: AbortSignal;
173
+
174
+ /** Publish turn-start event to the channel. Must be called before addMessages or streamResponse. */
175
+ start(): Promise<void>;
176
+
177
+ /**
178
+ * Publish user messages to the channel, scoped to this turn.
179
+ * Each message is published with its own headers (including `x-ably-msg-id`
180
+ * for optimistic reconciliation with the client's inserts). Per-message
181
+ * headers from `MessageWithHeaders` override transport-generated defaults.
182
+ * @returns The msg-ids of all published messages, in order.
183
+ */
184
+ addMessages(messages: MessageWithHeaders<TMessage>[], options?: AddMessageOptions): Promise<AddMessagesResult>;
185
+
186
+ /**
187
+ * Pipe a ReadableStream through the encoder to the channel.
188
+ * Returns when the stream completes, is cancelled, or errors.
189
+ * Does NOT call end() — the caller must call end() after streamResponse returns.
190
+ */
191
+ streamResponse(stream: ReadableStream<TEvent>, options?: StreamResponseOptions): Promise<StreamResult>;
192
+
193
+ /** Publish turn-end event to the channel and clean up. */
194
+ end(reason: TurnEndReason): Promise<void>;
195
+ }
196
+
197
+ // ---------------------------------------------------------------------------
198
+ // Server transport interface
199
+ // ---------------------------------------------------------------------------
200
+
201
+ /** Server-side transport that manages turn lifecycles over an Ably channel. */
202
+ export interface ServerTransport<TEvent, TMessage> {
203
+ /**
204
+ * Create a new turn. Synchronous — no channel activity until start() is called.
205
+ * The turn is registered for cancel routing immediately so that early cancels
206
+ * fire the abort signal.
207
+ */
208
+ newTurn(options: NewTurnOptions<TEvent>): Turn<TEvent, TMessage>;
209
+
210
+ /** Unsubscribe from cancel messages, abort all active turns, and clean up. */
211
+ close(): void;
212
+ }
213
+
214
+ // ---------------------------------------------------------------------------
215
+ // Client transport options
216
+ // ---------------------------------------------------------------------------
217
+
218
+ /** Options for creating a client transport. */
219
+ export interface ClientTransportOptions<TEvent, TMessage> {
220
+ /** The Ably channel to receive responses on and publish cancel signals to. */
221
+ channel: Ably.RealtimeChannel;
222
+
223
+ /** The codec to use for encoding/decoding. */
224
+ codec: Codec<TEvent, TMessage>;
225
+
226
+ /** The client's identity. Sent to the server in the POST body. */
227
+ clientId?: string;
228
+
229
+ /** Server endpoint URL for the HTTP POST. Defaults to `"/api/chat"`. */
230
+ api?: string;
231
+
232
+ /** Headers for the HTTP POST. Function form for dynamic values (e.g. auth tokens). */
233
+ headers?: Record<string, string> | (() => Record<string, string>);
234
+
235
+ /** Additional body fields merged into the HTTP POST. Function form for dynamic values. */
236
+ body?: Record<string, unknown> | (() => Record<string, unknown>);
237
+
238
+ /** Fetch credentials mode for the HTTP POST. */
239
+ credentials?: RequestCredentials;
240
+
241
+ /** Custom fetch implementation. Defaults to `globalThis.fetch`. */
242
+ fetch?: typeof globalThis.fetch;
243
+
244
+ /** Initial messages to seed the conversation tree with. Forms a linear chain. */
245
+ messages?: TMessage[];
246
+
247
+ /** Logger instance for diagnostic output. */
248
+ logger?: Logger;
249
+ }
250
+
251
+ // ---------------------------------------------------------------------------
252
+ // Send options
253
+ // ---------------------------------------------------------------------------
254
+
255
+ /** Per-send options for customizing the HTTP POST and branching metadata. */
256
+ export interface SendOptions {
257
+ /** Additional fields merged into the HTTP POST body. */
258
+ body?: Record<string, unknown>;
259
+ /** Additional headers for the HTTP POST. */
260
+ headers?: Record<string, string>;
261
+ /**
262
+ * The msg-id of the message this send replaces (fork).
263
+ * Set for regeneration (forkOf an assistant message) or
264
+ * edit (forkOf a user message).
265
+ */
266
+ forkOf?: string;
267
+ /**
268
+ * The msg-id of the message that precedes this one in the
269
+ * conversation thread. Null means the message is a root.
270
+ * If omitted, auto-computed from the last message in the tree.
271
+ */
272
+ parent?: string | null;
273
+ }
274
+
275
+ // ---------------------------------------------------------------------------
276
+ // Turn lifecycle events
277
+ // ---------------------------------------------------------------------------
278
+
279
+ /** A structured event describing a turn starting or ending. */
280
+ export type TurnLifecycleEvent =
281
+ | { type: 'x-ably-turn-start'; turnId: string; clientId: string }
282
+ | { type: 'x-ably-turn-end'; turnId: string; clientId: string; reason: TurnEndReason };
283
+
284
+ // ---------------------------------------------------------------------------
285
+ // Active turn handle
286
+ // ---------------------------------------------------------------------------
287
+
288
+ /** A handle to an active client-side turn, returned by `send()`, `regenerate()`, and `edit()`. */
289
+ export interface ActiveTurn<TEvent> {
290
+ /** The decoded event stream for this turn. */
291
+ stream: ReadableStream<TEvent>;
292
+ /** The turn's unique identifier. */
293
+ turnId: string;
294
+ /** Cancel this specific turn. Publishes a cancel message and closes the local stream. */
295
+ cancel(): Promise<void>;
296
+ }
297
+
298
+ // ---------------------------------------------------------------------------
299
+ // Close options
300
+ // ---------------------------------------------------------------------------
301
+
302
+ /** Options for closing a client transport. */
303
+ export interface CloseOptions {
304
+ /** Cancel in-progress turns before closing. Publishes a cancel message to the channel. */
305
+ cancel?: CancelFilter;
306
+ }
307
+
308
+ // ---------------------------------------------------------------------------
309
+ // History / pagination
310
+ // ---------------------------------------------------------------------------
311
+
312
+ /** A page of decoded messages from channel history. */
313
+ export interface PaginatedMessages<TMessage> {
314
+ /** Decoded messages in chronological order (oldest first). */
315
+ items: TMessage[];
316
+ /** Headers for each item, parallel to `items`. Used by the transport to populate the tree. */
317
+ itemHeaders?: Record<string, string>[];
318
+ /** Ably serial for each item, parallel to `items`. Used by the transport for tree ordering. */
319
+ itemSerials?: string[];
320
+ /** Raw Ably messages that produced this page, in chronological order. */
321
+ rawMessages?: Ably.InboundMessage[];
322
+ /** Whether there are older pages available. */
323
+ hasNext(): boolean;
324
+ /** Fetch the next (older) page. Returns undefined if no more pages. */
325
+ next(): Promise<PaginatedMessages<TMessage> | undefined>;
326
+ }
327
+
328
+ /** Options for loading channel history. */
329
+ export interface LoadHistoryOptions {
330
+ /** Max messages per page. Default: 100. */
331
+ limit?: number;
332
+ }
333
+
334
+ // ---------------------------------------------------------------------------
335
+ // Conversation tree (branching history)
336
+ // ---------------------------------------------------------------------------
337
+
338
+ /** A node in the conversation tree, representing a single domain message. */
339
+ export interface ConversationNode<TMessage> {
340
+ /** The domain message. */
341
+ message: TMessage;
342
+ /** The x-ably-msg-id of this node — primary key in the tree. */
343
+ msgId: string;
344
+ /** Parent node's msg-id (x-ably-parent), or undefined for root messages. */
345
+ parentId: string | undefined;
346
+ /** The msg-id this node forks from (x-ably-fork-of), or undefined if first version. */
347
+ forkOf: string | undefined;
348
+ /** Full Ably headers for this message. */
349
+ headers: Record<string, string>;
350
+ /**
351
+ * Ably serial for this message. Lexicographically comparable for total order.
352
+ * Used to sort siblings deterministically regardless of delivery/history order.
353
+ * Absent for optimistic messages (set when the server relay arrives).
354
+ */
355
+ serial: string | undefined;
356
+ }
357
+
358
+ /**
359
+ * Materializes a branching conversation tree from a flat oplog.
360
+ *
361
+ * Owns the conversation state — `flatten()` returns the linear message list
362
+ * for the currently selected branches. The transport's `getMessages()` delegates
363
+ * to `flatten()`.
364
+ */
365
+ export interface ConversationTree<TMessage> {
366
+ /**
367
+ * Flatten the tree along the currently selected branches into
368
+ * a linear message list. This is what getMessages() returns.
369
+ */
370
+ flatten(): TMessage[];
371
+
372
+ /**
373
+ * Get all messages that are siblings (alternatives) at a given
374
+ * fork point. Returns an array ordered chronologically by serial.
375
+ * The message identified by msgId is always included.
376
+ */
377
+ getSiblings(msgId: string): TMessage[];
378
+
379
+ /** Whether a message has sibling alternatives (i.e., show navigation arrows). */
380
+ hasSiblings(msgId: string): boolean;
381
+
382
+ /** Get the index of the currently selected sibling at a fork point. */
383
+ getSelectedIndex(msgId: string): number;
384
+
385
+ /**
386
+ * Select a sibling at a fork point by index. Updates the active branch.
387
+ * Calling flatten() after this returns the new linear thread.
388
+ * Index is clamped to `[0, siblings.length - 1]`.
389
+ */
390
+ select(msgId: string, index: number): void;
391
+
392
+ /** Get a node by msgId, or undefined if not found. */
393
+ getNode(msgId: string): ConversationNode<TMessage> | undefined;
394
+
395
+ /**
396
+ * Get a node by codec message key (e.g. UIMessage.id), or undefined if
397
+ * not found. Uses a secondary index since the tree is keyed by x-ably-msg-id.
398
+ */
399
+ getNodeByKey(key: string): ConversationNode<TMessage> | undefined;
400
+
401
+ /** Get the stored headers for a node by msgId, or undefined if not found. */
402
+ getHeaders(msgId: string): Record<string, string> | undefined;
403
+
404
+ // --- Mutation (used by the transport, not the UI) ---
405
+
406
+ /**
407
+ * Insert or update a message in the tree. Reads parent/forkOf from the
408
+ * provided headers. If the message already exists (by msgId), updates
409
+ * it in place. The optional serial is the Ably message serial used for
410
+ * deterministic sibling ordering.
411
+ */
412
+ upsert(msgId: string, message: TMessage, headers: Record<string, string>, serial?: string): void;
413
+
414
+ /** Remove a message from the tree. */
415
+ delete(msgId: string): void;
416
+ }
417
+
418
+ // ---------------------------------------------------------------------------
419
+ // Internal sub-component types
420
+ // ---------------------------------------------------------------------------
421
+
422
+ /** Entry in the StreamRouter's turn map. Not part of the public API. */
423
+ export interface TurnEntry<TEvent> {
424
+ /** The ReadableStream controller for this turn. */
425
+ controller: ReadableStreamDefaultController<TEvent>;
426
+ /** The turn's unique identifier. */
427
+ turnId: string;
428
+ }
429
+
430
+ // ---------------------------------------------------------------------------
431
+ // Client transport interface
432
+ // ---------------------------------------------------------------------------
433
+
434
+ /** Client-side transport that manages conversation state over an Ably channel. */
435
+ export interface ClientTransport<TEvent, TMessage> {
436
+ /**
437
+ * Send one or more messages and start a new turn. Returns a handle to the
438
+ * active turn with the decoded event stream and a cancel function.
439
+ *
440
+ * The HTTP POST is fire-and-forget — the returned stream is available
441
+ * immediately. If the POST fails, the error is surfaced via `on("error")`.
442
+ */
443
+ send(messages: TMessage | TMessage[], options?: SendOptions): Promise<ActiveTurn<TEvent>>;
444
+
445
+ /**
446
+ * Regenerate an assistant message. Creates a new turn that forks the
447
+ * target message with no new user messages. Automatically computes
448
+ * `forkOf`, `parent`, and truncated `history` from the tree.
449
+ *
450
+ * Pass `options.body.history` to override the default truncated history.
451
+ */
452
+ regenerate(messageId: string, options?: SendOptions): Promise<ActiveTurn<TEvent>>;
453
+
454
+ /**
455
+ * Edit a user message. Creates a new turn that forks the target message
456
+ * with replacement content. Automatically computes `forkOf`, `parent`,
457
+ * and `history` from the tree.
458
+ */
459
+ edit(messageId: string, newMessages: TMessage | TMessage[], options?: SendOptions): Promise<ActiveTurn<TEvent>>;
460
+
461
+ /**
462
+ * Access the conversation tree for branch navigation.
463
+ * The tree is updated in real-time by the transport's channel subscription.
464
+ */
465
+ getTree(): ConversationTree<TMessage>;
466
+
467
+ /** Cancel turns matching the filter. Defaults to `{ own: true }` (all own turns). */
468
+ cancel(filter?: CancelFilter): Promise<void>;
469
+
470
+ /**
471
+ * Returns a promise that resolves when all active turns matching the filter
472
+ * have completed. Resolves immediately if no matching turns are active.
473
+ * Defaults to `{ own: true }`.
474
+ */
475
+ waitForTurn(filter?: CancelFilter): Promise<void>;
476
+
477
+ /**
478
+ * Subscribe to message store changes or raw Ably message additions.
479
+ * The handler is called with no arguments — call `getMessages()` or
480
+ * `getAblyMessages()` for the current state. Returns an unsubscribe function.
481
+ */
482
+ on(event: 'message' | 'ably-message', handler: () => void): () => void;
483
+
484
+ /** Subscribe to turn lifecycle events (start, end). Returns an unsubscribe function. */
485
+ on(event: 'turn', handler: (event: TurnLifecycleEvent) => void): () => void;
486
+
487
+ /**
488
+ * Subscribe to non-fatal transport errors. These indicate something went
489
+ * wrong but the transport is still operational. Returns an unsubscribe function.
490
+ */
491
+ on(event: 'error', handler: (error: Ably.ErrorInfo) => void): () => void;
492
+
493
+ /**
494
+ * Get the accumulated raw Ably messages, in chronological order.
495
+ * Includes both live messages and history-loaded messages.
496
+ */
497
+ getAblyMessages(): Ably.InboundMessage[];
498
+
499
+ /** Get all currently active turns, keyed by clientId. */
500
+ getActiveTurnIds(): Map<string, Set<string>>;
501
+
502
+ /** Get Ably headers associated with a message via the conversation tree. */
503
+ getMessageHeaders(message: TMessage): Record<string, string> | undefined;
504
+
505
+ /** Get the current message list (follows selected branches). Updated by message lifecycle events. */
506
+ getMessages(): TMessage[];
507
+
508
+ /**
509
+ * Snapshot the current message list as message + headers pairs.
510
+ * Convenience for building the `history` body field in HTTP POSTs.
511
+ */
512
+ getMessagesWithHeaders(): MessageWithHeaders<TMessage>[];
513
+
514
+ /**
515
+ * Load a page of conversation history from the channel, decoded through
516
+ * the transport's codec. Uses `untilAttach` for gapless continuity with
517
+ * the live subscription.
518
+ *
519
+ * History messages are inserted into the conversation tree and trigger
520
+ * a notification. Returns a PaginatedMessages handle — call `next()`
521
+ * for older pages.
522
+ */
523
+ history(options?: LoadHistoryOptions): Promise<PaginatedMessages<TMessage>>;
524
+
525
+ /**
526
+ * Tear down the transport: unsubscribe from the channel, close active
527
+ * streams, clear all handlers, and prevent further operations.
528
+ *
529
+ * Pass `cancel` to publish a cancel message before closing. Without it,
530
+ * only local state is torn down (the server keeps streaming).
531
+ */
532
+ close(options?: CloseOptions): Promise<void>;
533
+ }