@ably/ai-transport 0.0.1 → 0.1.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 (110) hide show
  1. package/README.md +54 -47
  2. package/dist/ably-ai-transport.js +1006 -539
  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 +4 -0
  7. package/dist/core/codec/types.d.ts +19 -2
  8. package/dist/core/transport/decode-history.d.ts +8 -6
  9. package/dist/core/transport/headers.d.ts +4 -2
  10. package/dist/core/transport/index.d.ts +4 -1
  11. package/dist/core/transport/pipe-stream.d.ts +3 -2
  12. package/dist/core/transport/stream-router.d.ts +11 -1
  13. package/dist/core/transport/tree.d.ts +171 -0
  14. package/dist/core/transport/turn-manager.d.ts +4 -1
  15. package/dist/core/transport/types.d.ts +270 -119
  16. package/dist/core/transport/view.d.ts +166 -0
  17. package/dist/errors.d.ts +19 -2
  18. package/dist/index.d.ts +3 -1
  19. package/dist/react/ably-ai-transport-react.js +1019 -486
  20. package/dist/react/ably-ai-transport-react.js.map +1 -1
  21. package/dist/react/ably-ai-transport-react.umd.cjs +1 -1
  22. package/dist/react/ably-ai-transport-react.umd.cjs.map +1 -1
  23. package/dist/react/contexts/transport-context.d.ts +31 -0
  24. package/dist/react/contexts/transport-provider.d.ts +49 -0
  25. package/dist/react/create-transport-hooks.d.ts +124 -0
  26. package/dist/react/index.d.ts +14 -8
  27. package/dist/react/use-ably-messages.d.ts +14 -8
  28. package/dist/react/use-active-turns.d.ts +7 -3
  29. package/dist/react/use-client-transport.d.ts +78 -5
  30. package/dist/react/use-create-view.d.ts +22 -0
  31. package/dist/react/use-tree.d.ts +20 -0
  32. package/dist/react/use-view.d.ts +79 -0
  33. package/dist/vercel/ably-ai-transport-vercel.js +1478 -842
  34. package/dist/vercel/ably-ai-transport-vercel.js.map +1 -1
  35. package/dist/vercel/ably-ai-transport-vercel.umd.cjs +1 -1
  36. package/dist/vercel/ably-ai-transport-vercel.umd.cjs.map +1 -1
  37. package/dist/vercel/codec/tool-transitions.d.ts +50 -0
  38. package/dist/vercel/index.d.ts +3 -0
  39. package/dist/vercel/react/ably-ai-transport-vercel-react.js +9099 -852
  40. package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
  41. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +45 -1
  42. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
  43. package/dist/vercel/react/contexts/chat-transport-context.d.ts +32 -0
  44. package/dist/vercel/react/contexts/chat-transport-provider.d.ts +84 -0
  45. package/dist/vercel/react/index.d.ts +5 -0
  46. package/dist/vercel/react/use-chat-transport.d.ts +61 -20
  47. package/dist/vercel/react/use-message-sync.d.ts +41 -9
  48. package/dist/vercel/react/use-staged-add-tool-approval-response.d.ts +30 -0
  49. package/dist/vercel/tool-approvals.d.ts +124 -0
  50. package/dist/vercel/tool-events.d.ts +26 -0
  51. package/dist/vercel/transport/chat-transport.d.ts +33 -11
  52. package/dist/vercel/transport/index.d.ts +5 -2
  53. package/package.json +23 -17
  54. package/src/constants.ts +6 -0
  55. package/src/core/codec/encoder.ts +10 -1
  56. package/src/core/codec/types.ts +19 -3
  57. package/src/core/transport/client-transport.ts +382 -364
  58. package/src/core/transport/decode-history.ts +229 -81
  59. package/src/core/transport/headers.ts +6 -2
  60. package/src/core/transport/index.ts +13 -5
  61. package/src/core/transport/pipe-stream.ts +8 -5
  62. package/src/core/transport/server-transport.ts +212 -58
  63. package/src/core/transport/stream-router.ts +21 -3
  64. package/src/core/transport/{conversation-tree.ts → tree.ts} +192 -77
  65. package/src/core/transport/turn-manager.ts +28 -10
  66. package/src/core/transport/types.ts +318 -139
  67. package/src/core/transport/view.ts +840 -0
  68. package/src/errors.ts +21 -1
  69. package/src/index.ts +10 -5
  70. package/src/react/contexts/transport-context.ts +37 -0
  71. package/src/react/contexts/transport-provider.tsx +164 -0
  72. package/src/react/create-transport-hooks.ts +144 -0
  73. package/src/react/index.ts +15 -8
  74. package/src/react/use-ably-messages.ts +34 -16
  75. package/src/react/use-active-turns.ts +28 -17
  76. package/src/react/use-client-transport.ts +184 -24
  77. package/src/react/use-create-view.ts +68 -0
  78. package/src/react/use-tree.ts +53 -0
  79. package/src/react/use-view.ts +233 -0
  80. package/src/react/vite.config.ts +4 -1
  81. package/src/vercel/codec/accumulator.ts +64 -79
  82. package/src/vercel/codec/decoder.ts +11 -8
  83. package/src/vercel/codec/encoder.ts +68 -54
  84. package/src/vercel/codec/index.ts +0 -2
  85. package/src/vercel/codec/tool-transitions.ts +122 -0
  86. package/src/vercel/index.ts +17 -0
  87. package/src/vercel/react/contexts/chat-transport-context.ts +40 -0
  88. package/src/vercel/react/contexts/chat-transport-provider.tsx +122 -0
  89. package/src/vercel/react/index.ts +14 -0
  90. package/src/vercel/react/use-chat-transport.ts +164 -42
  91. package/src/vercel/react/use-message-sync.ts +77 -19
  92. package/src/vercel/react/use-staged-add-tool-approval-response.ts +87 -0
  93. package/src/vercel/react/vite.config.ts +4 -2
  94. package/src/vercel/tool-approvals.ts +380 -0
  95. package/src/vercel/tool-events.ts +53 -0
  96. package/src/vercel/transport/chat-transport.ts +225 -79
  97. package/src/vercel/transport/index.ts +14 -3
  98. package/dist/core/transport/conversation-tree.d.ts +0 -9
  99. package/dist/react/use-conversation-tree.d.ts +0 -20
  100. package/dist/react/use-edit.d.ts +0 -7
  101. package/dist/react/use-history.d.ts +0 -19
  102. package/dist/react/use-messages.d.ts +0 -7
  103. package/dist/react/use-regenerate.d.ts +0 -7
  104. package/dist/react/use-send.d.ts +0 -7
  105. package/src/react/use-conversation-tree.ts +0 -71
  106. package/src/react/use-edit.ts +0 -24
  107. package/src/react/use-history.ts +0 -111
  108. package/src/react/use-messages.ts +0 -32
  109. package/src/react/use-regenerate.ts +0 -24
  110. package/src/react/use-send.ts +0 -25
@@ -8,7 +8,7 @@
8
8
  import type * as Ably from 'ably';
9
9
 
10
10
  import type { Logger } from '../../logger.js';
11
- import type { Codec } from '../codec/types.js';
11
+ import type { Codec, WriteOptions } from '../codec/types.js';
12
12
 
13
13
  // ---------------------------------------------------------------------------
14
14
  // Shared types
@@ -45,18 +45,6 @@ export interface CancelRequest {
45
45
  turnOwners: Map<string, string>;
46
46
  }
47
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
48
  // ---------------------------------------------------------------------------
61
49
  // Server transport options
62
50
  // ---------------------------------------------------------------------------
@@ -71,7 +59,9 @@ export interface ServerTransportOptions<TEvent, TMessage> {
71
59
  logger?: Logger;
72
60
  /**
73
61
  * Called with non-fatal transport-level errors not scoped to any turn.
74
- * Examples: cancel listener subscription failure, channel attach errors.
62
+ * Examples: cancel listener subscription failure, channel attach errors,
63
+ * channel continuity loss (FAILED/SUSPENDED/DETACHED or re-attach with
64
+ * `resumed: false`).
75
65
  */
76
66
  onError?: (error: Ably.ErrorInfo) => void;
77
67
  }
@@ -80,14 +70,10 @@ export interface ServerTransportOptions<TEvent, TMessage> {
80
70
  // Turn options
81
71
  // ---------------------------------------------------------------------------
82
72
 
83
- /** Options for addMessages — per-operation overrides for message identity and branching. */
73
+ /** Options for addMessages — per-operation overrides for attribution. */
84
74
  export interface AddMessageOptions {
85
75
  /** The user's clientId for attribution. */
86
76
  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
77
  }
92
78
 
93
79
  /** Result of publishing user messages via addMessages. */
@@ -96,18 +82,63 @@ export interface AddMessagesResult {
96
82
  msgIds: string[];
97
83
  }
98
84
 
99
- /** Options for streamResponse — per-operation overrides for the assistant message. */
100
- export interface StreamResponseOptions {
85
+ /**
86
+ * A batch of events targeting an existing message.
87
+ * Each node specifies the target message and the events to apply to it.
88
+ * Used for cross-turn updates such as tool result delivery.
89
+ */
90
+ export interface EventsNode<TEvent> {
91
+ /** Discriminator — identifies this as an events node. */
92
+ kind: 'event';
93
+ /** The `x-ably-msg-id` of the existing message to update. */
94
+ msgId: string;
95
+ /** Events to apply to the target message. */
96
+ events: TEvent[];
97
+ }
98
+
99
+ /** @deprecated Use {@link EventsNode} instead. */
100
+ export type EventNode<TEvent> = EventsNode<TEvent>;
101
+
102
+ /**
103
+ * Options for streamResponse — per-operation overrides for the assistant message.
104
+ * @template TEvent - The codec event type carried by the stream; used by the `resolveWriteOptions` hook.
105
+ */
106
+ export interface StreamResponseOptions<TEvent> {
101
107
  /** The msg-id of the immediately preceding message in this branch. */
102
- parent?: string | null;
108
+ parent?: string;
103
109
  /** The msg-id of the message this response replaces (for regeneration). */
104
110
  forkOf?: string;
111
+ /**
112
+ * Optional per-event hook invoked before each event is encoded. The
113
+ * returned {@link WriteOptions} (if any) override the stream's default
114
+ * headers and `msgId` for that one encode call only; return `undefined`
115
+ * to use the stream defaults.
116
+ *
117
+ * Used to carry a subset of events within the stream to a different
118
+ * message (e.g. `tool-output-available` chunks that belong on a prior
119
+ * assistant message, stamped with `x-ably-amend`). Must not be used
120
+ * for events that participate in the encoder's stream-append pipeline
121
+ * — streaming state (stream tracker, append ordering) is anchored to
122
+ * the stream's default identity and is not affected by per-event
123
+ * overrides.
124
+ * @param event - The event about to be encoded.
125
+ * @returns Per-write overrides for this event, or undefined.
126
+ */
127
+ resolveWriteOptions?: (event: TEvent) => WriteOptions | undefined;
105
128
  }
106
129
 
107
130
  /** The result of streaming a response through the encoder. */
108
131
  export interface StreamResult {
109
132
  /** Why the stream ended. */
110
133
  reason: TurnEndReason;
134
+ /**
135
+ * The error that caused the stream to fail, present when `reason` is
136
+ * `'error'`. This is the original error (e.g. from the LLM provider)
137
+ * preserved so the caller can inspect provider-specific fields. The
138
+ * turn's `onError` callback also fires with a wrapped `Ably.ErrorInfo`
139
+ * (code `StreamError`) for standardized observability.
140
+ */
141
+ error?: Error;
111
142
  }
112
143
 
113
144
  /** Options passed to newTurn for configuring the turn lifecycle. */
@@ -123,7 +154,7 @@ export interface NewTurnOptions<TEvent> {
123
154
  * Used as the default parent for user messages (via addMessages) and
124
155
  * assistant messages (via streamResponse) when not overridden per-operation.
125
156
  */
126
- parent?: string | null;
157
+ parent?: string;
127
158
 
128
159
  /**
129
160
  * The msg-id of the message this turn replaces (creates a fork).
@@ -153,10 +184,33 @@ export interface NewTurnOptions<TEvent> {
153
184
  onCancel?: (request: CancelRequest) => Promise<boolean>;
154
185
 
155
186
  /**
156
- * Called with non-fatal errors scoped to this turn. Examples: turn-start
157
- * publish failure, encoder recovery failure, stream encoding errors.
187
+ * Called with non-fatal turn-scoped errors that have no other delivery
188
+ * path. Fires in two scenarios:
189
+ * - Stream failures in `streamResponse` — the underlying error is also
190
+ * returned on `StreamResult.error`, but this callback delivers it
191
+ * wrapped as an `Ably.ErrorInfo` (code `StreamError`) for standardized
192
+ * observability.
193
+ * - Failures in the `onCancel` handler.
194
+ *
195
+ * Publish failures in `start`, `addMessages`, `addEvents`, and `end`
196
+ * are not delivered here — those methods reject their returned promise
197
+ * with an `Ably.ErrorInfo`, and the caller should handle it at the await
198
+ * site. Turn errors never render the transport unusable, but the turn
199
+ * may be in an inconsistent state; the caller should typically `end` it
200
+ * with reason `'error'`.
201
+ *
202
+ * Channel-wide events (e.g. continuity loss) are delivered via the
203
+ * transport-level `onError` on {@link ServerTransportOptions}, not here.
158
204
  */
159
205
  onError?: (error: Ably.ErrorInfo) => void;
206
+
207
+ /**
208
+ * An external abort signal (typically the HTTP request's `req.signal`) that,
209
+ * when fired, aborts this turn. This allows platform-level cancellation —
210
+ * request cancellation, serverless function timeout — to stop LLM generation
211
+ * and stream piping gracefully.
212
+ */
213
+ signal?: AbortSignal;
160
214
  }
161
215
 
162
216
  // ---------------------------------------------------------------------------
@@ -176,19 +230,31 @@ export interface Turn<TEvent, TMessage> {
176
230
 
177
231
  /**
178
232
  * 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.
233
+ * Each node's `msgId`, `parentId`, and `forkOf` are used for message identity
234
+ * and branching. The node's `headers` override transport-generated defaults
235
+ * (e.g. for optimistic reconciliation with the client's inserts).
182
236
  * @returns The msg-ids of all published messages, in order.
183
237
  */
184
- addMessages(messages: MessageWithHeaders<TMessage>[], options?: AddMessageOptions): Promise<AddMessagesResult>;
238
+ addMessages(messages: MessageNode<TMessage>[], options?: AddMessageOptions): Promise<AddMessagesResult>;
185
239
 
186
240
  /**
187
241
  * Pipe a ReadableStream through the encoder to the channel.
188
242
  * Returns when the stream completes, is cancelled, or errors.
189
243
  * Does NOT call end() — the caller must call end() after streamResponse returns.
190
244
  */
191
- streamResponse(stream: ReadableStream<TEvent>, options?: StreamResponseOptions): Promise<StreamResult>;
245
+ streamResponse(stream: ReadableStream<TEvent>, options?: StreamResponseOptions<TEvent>): Promise<StreamResult>;
246
+
247
+ /**
248
+ * Publish events targeting existing messages in the tree. Each node
249
+ * specifies a target message (by `msgId`) and the events to apply.
250
+ * Events are encoded and published with the target's `x-ably-msg-id`,
251
+ * so receiving clients apply them to the existing node rather than
252
+ * creating a new one.
253
+ *
254
+ * Used for cross-turn updates such as tool result delivery after
255
+ * approval or client-side tool execution.
256
+ */
257
+ addEvents(nodes: EventsNode<TEvent>[]): Promise<void>;
192
258
 
193
259
  /** Publish turn-end event to the channel and clean up. */
194
260
  end(reason: TurnEndReason): Promise<void>;
@@ -226,8 +292,8 @@ export interface ClientTransportOptions<TEvent, TMessage> {
226
292
  /** The client's identity. Sent to the server in the POST body. */
227
293
  clientId?: string;
228
294
 
229
- /** Server endpoint URL for the HTTP POST. Defaults to `"/api/chat"`. */
230
- api?: string;
295
+ /** Server endpoint URL for the HTTP POST. */
296
+ api: string;
231
297
 
232
298
  /** Headers for the HTTP POST. Function form for dynamic values (e.g. auth tokens). */
233
299
  headers?: Record<string, string> | (() => Record<string, string>);
@@ -266,10 +332,10 @@ export interface SendOptions {
266
332
  forkOf?: string;
267
333
  /**
268
334
  * 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.
335
+ * conversation thread. If omitted, auto-computed from the last
336
+ * message in the view.
271
337
  */
272
- parent?: string | null;
338
+ parent?: string;
273
339
  }
274
340
 
275
341
  // ---------------------------------------------------------------------------
@@ -278,7 +344,15 @@ export interface SendOptions {
278
344
 
279
345
  /** A structured event describing a turn starting or ending. */
280
346
  export type TurnLifecycleEvent =
281
- | { type: 'x-ably-turn-start'; turnId: string; clientId: string }
347
+ | {
348
+ type: 'x-ably-turn-start';
349
+ turnId: string;
350
+ clientId: string;
351
+ /** The msg-id of the parent message, if known. Omitted for root turns. */
352
+ parent?: string;
353
+ /** The msg-id being forked/replaced, if this is a regeneration or edit. */
354
+ forkOf?: string;
355
+ }
282
356
  | { type: 'x-ably-turn-end'; turnId: string; clientId: string; reason: TurnEndReason };
283
357
 
284
358
  // ---------------------------------------------------------------------------
@@ -287,12 +361,18 @@ export type TurnLifecycleEvent =
287
361
 
288
362
  /** A handle to an active client-side turn, returned by `send()`, `regenerate()`, and `edit()`. */
289
363
  export interface ActiveTurn<TEvent> {
290
- /** The decoded event stream for this turn. */
364
+ /** The decoded event stream for this turn. May error if the delivery guarantee is broken (e.g. POST failure, channel continuity loss). */
291
365
  stream: ReadableStream<TEvent>;
292
366
  /** The turn's unique identifier. */
293
367
  turnId: string;
294
368
  /** Cancel this specific turn. Publishes a cancel message and closes the local stream. */
295
369
  cancel(): Promise<void>;
370
+ /**
371
+ * The msg-ids of optimistically inserted user messages, in order.
372
+ * Present when the send included user messages (edit); empty for
373
+ * regeneration (no user messages to insert optimistically).
374
+ */
375
+ optimisticMsgIds: string[];
296
376
  }
297
377
 
298
378
  // ---------------------------------------------------------------------------
@@ -309,20 +389,26 @@ export interface CloseOptions {
309
389
  // History / pagination
310
390
  // ---------------------------------------------------------------------------
311
391
 
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[];
392
+ /** A single decoded history item with its transport metadata. */
393
+ export interface HistoryItem<TMessage> {
394
+ /** The decoded domain message. */
395
+ message: TMessage;
396
+ /** Transport headers for tree identity and ordering. */
397
+ headers: Record<string, string>;
398
+ /** Ably serial for tree ordering. */
399
+ serial: string;
400
+ }
401
+
402
+ /** A page of decoded history from the channel. Internal to View/decodeHistory. */
403
+ export interface HistoryPage<TMessage> {
404
+ /** Decoded items in chronological order (oldest first). */
405
+ items: HistoryItem<TMessage>[];
320
406
  /** Raw Ably messages that produced this page, in chronological order. */
321
- rawMessages?: Ably.InboundMessage[];
407
+ rawMessages: Ably.InboundMessage[];
322
408
  /** Whether there are older pages available. */
323
409
  hasNext(): boolean;
324
410
  /** Fetch the next (older) page. Returns undefined if no more pages. */
325
- next(): Promise<PaginatedMessages<TMessage> | undefined>;
411
+ next(): Promise<HistoryPage<TMessage> | undefined>;
326
412
  }
327
413
 
328
414
  /** Options for loading channel history. */
@@ -336,7 +422,9 @@ export interface LoadHistoryOptions {
336
422
  // ---------------------------------------------------------------------------
337
423
 
338
424
  /** A node in the conversation tree, representing a single domain message. */
339
- export interface ConversationNode<TMessage> {
425
+ export interface MessageNode<TMessage> {
426
+ /** Discriminator — identifies this as a message node. */
427
+ kind: 'message';
340
428
  /** The domain message. */
341
429
  message: TMessage;
342
430
  /** The x-ably-msg-id of this node — primary key in the tree. */
@@ -355,20 +443,17 @@ export interface ConversationNode<TMessage> {
355
443
  serial: string | undefined;
356
444
  }
357
445
 
446
+ /** @deprecated Use {@link MessageNode} instead. */
447
+ export type TreeNode<TMessage> = MessageNode<TMessage>;
448
+
358
449
  /**
359
450
  * Materializes a branching conversation tree from a flat oplog.
360
451
  *
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()`.
452
+ * Owns the complete conversation state — every node from live messages and
453
+ * history. `flattenNodes()` returns the linear message list for the currently
454
+ * selected branches. Events fire for any change across the full tree.
364
455
  */
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
-
456
+ export interface Tree<TMessage> {
372
457
  /**
373
458
  * Get all messages that are siblings (alternatives) at a given
374
459
  * fork point. Returns an array ordered chronologically by serial.
@@ -379,24 +464,8 @@ export interface ConversationTree<TMessage> {
379
464
  /** Whether a message has sibling alternatives (i.e., show navigation arrows). */
380
465
  hasSiblings(msgId: string): boolean;
381
466
 
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
467
  /** 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;
468
+ getNode(msgId: string): MessageNode<TMessage> | undefined;
400
469
 
401
470
  /** Get the stored headers for a node by msgId, or undefined if not found. */
402
471
  getHeaders(msgId: string): Record<string, string> | undefined;
@@ -413,114 +482,224 @@ export interface ConversationTree<TMessage> {
413
482
 
414
483
  /** Remove a message from the tree. */
415
484
  delete(msgId: string): void;
416
- }
417
485
 
418
- // ---------------------------------------------------------------------------
419
- // Internal sub-component types
420
- // ---------------------------------------------------------------------------
486
+ // --- Events ---
421
487
 
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;
488
+ /** Active turn IDs grouped by clientId (all turns, not just visible). */
489
+ getActiveTurnIds(): Map<string, Set<string>>;
490
+
491
+ /** Subscribe to tree structure changes (insert, update, delete). */
492
+ on(event: 'update', handler: () => void): () => void;
493
+
494
+ /** Subscribe to raw Ably messages arriving on the channel. */
495
+ on(event: 'ably-message', handler: (msg: Ably.InboundMessage) => void): () => void;
496
+
497
+ /** Subscribe to turn lifecycle events (start and end). */
498
+ on(event: 'turn', handler: (event: TurnLifecycleEvent) => void): () => void;
428
499
  }
429
500
 
430
501
  // ---------------------------------------------------------------------------
431
- // Client transport interface
502
+ // View windowed projection over the tree
432
503
  // ---------------------------------------------------------------------------
433
504
 
434
- /** Client-side transport that manages conversation state over an Ably channel. */
435
- export interface ClientTransport<TEvent, TMessage> {
505
+ /**
506
+ * A paginated, branch-aware projection of the conversation tree.
507
+ *
508
+ * Returns only the visible portion of the selected branch. New live messages
509
+ * appear immediately; older messages are revealed progressively via
510
+ * `loadOlder()`. Events are scoped to the visible window — subscribers
511
+ * are only notified when the visible output changes.
512
+ */
513
+ export interface View<TEvent, TMessage> {
514
+ /** The visible domain messages along the selected branch. Shorthand for `flattenNodes().map(n => n.message)`. */
515
+ getMessages(): TMessage[];
516
+
517
+ /** Visible nodes along the selected branch, filtered by the pagination window. */
518
+ flattenNodes(): MessageNode<TMessage>[];
519
+
520
+ /** Whether there are older messages that can be loaded or revealed. */
521
+ hasOlder(): boolean;
522
+
436
523
  /**
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
- *
524
+ * Reveal older messages. Loads from channel history if the tree doesn't
525
+ * have enough, then advances the window to show up to `limit` more messages.
526
+ * Emits 'update' when the visible list changes.
527
+ * @param limit - Maximum number of older messages to reveal. Defaults to 100.
528
+ */
529
+ loadOlder(limit?: number): Promise<void>;
530
+
531
+ // --- Branch navigation ---
532
+
533
+ /**
534
+ * Select a sibling at a fork point by index. Updates this view's
535
+ * branch selection. Index is clamped to `[0, siblings.length - 1]`.
536
+ * Emits 'update' when the visible output changes.
537
+ */
538
+ select(msgId: string, index: number): void;
539
+
540
+ /** Get the index of the currently selected sibling at a fork point. */
541
+ getSelectedIndex(msgId: string): number;
542
+
543
+ /**
544
+ * Get all messages that are siblings (alternatives) at a given
545
+ * fork point. Returns an array ordered chronologically by serial.
546
+ */
547
+ getSiblings(msgId: string): TMessage[];
548
+
549
+ /** Whether a message has sibling alternatives (i.e., show navigation arrows). */
550
+ hasSiblings(msgId: string): boolean;
551
+
552
+ /** Get a node by msgId, or undefined if not found. */
553
+ getNode(msgId: string): MessageNode<TMessage> | undefined;
554
+
555
+ // --- Write operations ---
556
+
557
+ /**
558
+ * Send one or more messages and start a new turn. The parent is
559
+ * auto-computed from this view's selected branch unless overridden.
440
560
  * 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")`.
561
+ * immediately. If the POST fails, the error is surfaced via the
562
+ * transport's `on("error")` and the stream is errored.
442
563
  */
443
564
  send(messages: TMessage | TMessage[], options?: SendOptions): Promise<ActiveTurn<TEvent>>;
444
565
 
445
566
  /**
446
567
  * Regenerate an assistant message. Creates a new turn that forks the
447
568
  * 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.
569
+ * `forkOf`, `parent`, and truncated `history` from this view's branch.
451
570
  */
452
571
  regenerate(messageId: string, options?: SendOptions): Promise<ActiveTurn<TEvent>>;
453
572
 
454
573
  /**
455
574
  * Edit a user message. Creates a new turn that forks the target message
456
575
  * with replacement content. Automatically computes `forkOf`, `parent`,
457
- * and `history` from the tree.
576
+ * and `history` from this view's branch.
458
577
  */
459
578
  edit(messageId: string, newMessages: TMessage | TMessage[], options?: SendOptions): Promise<ActiveTurn<TEvent>>;
460
579
 
461
580
  /**
462
- * Access the conversation tree for branch navigation.
463
- * The tree is updated in real-time by the transport's channel subscription.
581
+ * Update an existing message and start a continuation turn.
582
+ * The local tree is updated optimistically, then the events are sent
583
+ * to the server in the POST body. The server publishes them to the channel
584
+ * and streams a continuation response.
585
+ * @param msgId - The `x-ably-msg-id` of the existing message to amend.
586
+ * @param events - Events to apply to the target message (e.g. tool output).
587
+ * @param options - Optional send options (body, headers).
588
+ * @returns An active turn with the continuation response stream.
464
589
  */
465
- getTree(): ConversationTree<TMessage>;
590
+ update(msgId: string, events: TEvent[], options?: SendOptions): Promise<ActiveTurn<TEvent>>;
466
591
 
467
- /** Cancel turns matching the filter. Defaults to `{ own: true }` (all own turns). */
468
- cancel(filter?: CancelFilter): Promise<void>;
592
+ // --- Observation ---
469
593
 
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>;
594
+ /** Active turn IDs for turns with visible messages, grouped by clientId. */
595
+ getActiveTurnIds(): Map<string, Set<string>>;
476
596
 
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;
597
+ /** The visible message list changed (new visible node, branch switch, window shift). */
598
+ on(event: 'update', handler: () => void): () => void;
483
599
 
484
- /** Subscribe to turn lifecycle events (start, end). Returns an unsubscribe function. */
600
+ /** A raw Ably message arrived that corresponds to a visible node. */
601
+ on(event: 'ably-message', handler: (msg: Ably.InboundMessage) => void): () => void;
602
+
603
+ /** A turn event occurred for a turn with visible messages in the window. */
485
604
  on(event: 'turn', handler: (event: TurnLifecycleEvent) => void): () => void;
486
605
 
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;
606
+ /** Tear down the view — unsubscribe from tree events and clear internal state. */
607
+ close(): void;
608
+ }
609
+
610
+ // ---------------------------------------------------------------------------
611
+ // Internal sub-component types
612
+ // ---------------------------------------------------------------------------
613
+
614
+ /** Entry in the StreamRouter's turn map. Not part of the public API. */
615
+ export interface TurnEntry<TEvent> {
616
+ /** The ReadableStream controller for this turn. */
617
+ controller: ReadableStreamDefaultController<TEvent>;
618
+ /** The turn's unique identifier. */
619
+ turnId: string;
620
+ }
621
+
622
+ // ---------------------------------------------------------------------------
623
+ // Client transport interface
624
+ // ---------------------------------------------------------------------------
625
+
626
+ /** Client-side transport that manages conversation state over an Ably channel. */
627
+ export interface ClientTransport<TEvent, TMessage> {
628
+ /** The complete conversation tree — all known nodes, events for any change. */
629
+ readonly tree: Tree<TMessage>;
630
+
631
+ /** The default paginated, branch-aware view for rendering — events scoped to visible messages. */
632
+ readonly view: View<TEvent, TMessage>;
492
633
 
493
634
  /**
494
- * Get the accumulated raw Ably messages, in chronological order.
495
- * Includes both live messages and history-loaded messages.
635
+ * Create an additional view over the same conversation tree.
636
+ * Each view has independent branch selections and pagination state.
637
+ * The caller is responsible for calling `close()` on the returned view
638
+ * when it is no longer needed, or it will be closed when the transport closes.
496
639
  */
497
- getAblyMessages(): Ably.InboundMessage[];
640
+ createView(): View<TEvent, TMessage>;
498
641
 
499
- /** Get all currently active turns, keyed by clientId. */
500
- getActiveTurnIds(): Map<string, Set<string>>;
642
+ /** Cancel turns matching the filter. Defaults to `{ own: true }` (all own turns). */
643
+ cancel(filter?: CancelFilter): Promise<void>;
501
644
 
502
- /** Get Ably headers associated with a message via the conversation tree. */
503
- getMessageHeaders(message: TMessage): Record<string, string> | undefined;
645
+ /**
646
+ * Apply events to an existing tree message locally and queue them for
647
+ * delivery on the next send.
648
+ *
649
+ * Use for cross-turn updates where the event value is produced on the
650
+ * client (e.g. after `addToolResult` resolves a client-executed tool) and
651
+ * must appear in the tree immediately so downstream observers — such as a
652
+ * destructive `setMessages(...)` mirror — cannot wipe it before it lands
653
+ * on the wire.
654
+ *
655
+ * The events are applied to the tree via the codec's accumulator
656
+ * (tree `update` fires once with the merged message) and queued on the
657
+ * transport. The next send operation flushes the queue into the POST
658
+ * body's `events` field so the server can republish them over the channel.
659
+ *
660
+ * If `msgId` is not present in the tree, the call is a no-op and a
661
+ * warning is logged.
662
+ * @param msgId - The x-ably-msg-id of the existing message to amend.
663
+ * @param events - Events to apply and later ship.
664
+ */
665
+ stageEvents(msgId: string, events: TEvent[]): void;
504
666
 
505
- /** Get the current message list (follows selected branches). Updated by message lifecycle events. */
506
- getMessages(): TMessage[];
667
+ /**
668
+ * Replace the tree's copy of an existing message with a caller-provided
669
+ * version, preserving headers and serial.
670
+ *
671
+ * Use for useChat-style state transitions the codec can't express as
672
+ * chunks — the canonical example is `addToolApprovalResponse`, which
673
+ * sets `state: 'approval-responded'` on a `dynamic-tool` part directly
674
+ * on the UIMessage and has no corresponding chunk variant.
675
+ *
676
+ * Unlike {@link stageEvents}, staged messages are NOT queued for the
677
+ * next send: the tree is authoritative for the POST body's history,
678
+ * so updating it is sufficient.
679
+ *
680
+ * Runs synchronously. Subsequent tree observers (e.g. useMessageSync)
681
+ * see the patched state on the next tick, so an interleaved
682
+ * observer-turn sync can't clobber it back.
683
+ *
684
+ * If `msgId` is not present in the tree, the call is a no-op and a
685
+ * warning is logged.
686
+ * @param msgId - The x-ably-msg-id of the existing message to replace.
687
+ * @param message - The patched message to store.
688
+ */
689
+ stageMessage(msgId: string, message: TMessage): void;
507
690
 
508
691
  /**
509
- * Snapshot the current message list as message + headers pairs.
510
- * Convenience for building the `history` body field in HTTP POSTs.
692
+ * Returns a promise that resolves when all active turns matching the filter
693
+ * have completed. Resolves immediately if no matching turns are active.
694
+ * Defaults to `{ own: true }`.
511
695
  */
512
- getMessagesWithHeaders(): MessageWithHeaders<TMessage>[];
696
+ waitForTurn(filter?: CancelFilter): Promise<void>;
513
697
 
514
698
  /**
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.
699
+ * Subscribe to non-fatal transport errors. These indicate something went
700
+ * wrong but the transport is still operational. Returns an unsubscribe function.
522
701
  */
523
- history(options?: LoadHistoryOptions): Promise<PaginatedMessages<TMessage>>;
702
+ on(event: 'error', handler: (error: Ably.ErrorInfo) => void): () => void;
524
703
 
525
704
  /**
526
705
  * Tear down the transport: unsubscribe from the channel, close active