@ably/ai-transport 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/README.md +10 -19
  2. package/dist/ably-ai-transport.js +1790 -1091
  3. package/dist/ably-ai-transport.js.map +1 -1
  4. package/dist/ably-ai-transport.umd.cjs +1 -1
  5. package/dist/ably-ai-transport.umd.cjs.map +1 -1
  6. package/dist/constants.d.ts +2 -2
  7. package/dist/core/agent.d.ts +20 -5
  8. package/dist/core/channel-options.d.ts +57 -0
  9. package/dist/core/codec/codec-event.d.ts +9 -0
  10. package/dist/core/codec/decoder.d.ts +4 -1
  11. package/dist/core/codec/define-codec.d.ts +100 -0
  12. package/dist/core/codec/encoder.d.ts +2 -7
  13. package/dist/core/codec/field-bag.d.ts +85 -0
  14. package/dist/core/codec/fields.d.ts +141 -0
  15. package/dist/core/codec/index.d.ts +8 -1
  16. package/dist/core/codec/input-descriptor-decoder.d.ts +19 -0
  17. package/dist/core/codec/input-descriptor-encoder.d.ts +22 -0
  18. package/dist/core/codec/input-descriptors.d.ts +281 -0
  19. package/dist/core/codec/output-descriptor-decoder.d.ts +29 -0
  20. package/dist/core/codec/output-descriptor-encoder.d.ts +31 -0
  21. package/dist/core/codec/output-descriptors.d.ts +237 -0
  22. package/dist/core/codec/types.d.ts +95 -36
  23. package/dist/core/codec/well-known-inputs.d.ts +52 -0
  24. package/dist/core/transport/agent-view.d.ts +296 -0
  25. package/dist/core/transport/decode-fold.d.ts +40 -32
  26. package/dist/core/transport/headers.d.ts +30 -1
  27. package/dist/core/transport/index.d.ts +1 -1
  28. package/dist/core/transport/invocation.d.ts +1 -1
  29. package/dist/core/transport/load-history-pages.d.ts +71 -0
  30. package/dist/core/transport/load-history.d.ts +21 -16
  31. package/dist/core/transport/run-manager.d.ts +9 -11
  32. package/dist/core/transport/session-support.d.ts +55 -0
  33. package/dist/core/transport/tree.d.ts +165 -15
  34. package/dist/core/transport/types/agent.d.ts +120 -98
  35. package/dist/core/transport/types/client.d.ts +45 -12
  36. package/dist/core/transport/types/tree.d.ts +52 -10
  37. package/dist/core/transport/types/view.d.ts +55 -28
  38. package/dist/core/transport/view.d.ts +176 -58
  39. package/dist/core/transport/wire-log.d.ts +102 -0
  40. package/dist/errors.d.ts +10 -4
  41. package/dist/index.d.ts +6 -5
  42. package/dist/react/ably-ai-transport-react.js +784 -415
  43. package/dist/react/ably-ai-transport-react.js.map +1 -1
  44. package/dist/react/ably-ai-transport-react.umd.cjs +1 -1
  45. package/dist/react/ably-ai-transport-react.umd.cjs.map +1 -1
  46. package/dist/react/contexts/client-session-context.d.ts +2 -1
  47. package/dist/react/contexts/client-session-provider.d.ts +3 -0
  48. package/dist/react/index.d.ts +2 -1
  49. package/dist/react/internal/skipped-session.d.ts +8 -0
  50. package/dist/react/use-view.d.ts +3 -3
  51. package/dist/utils.d.ts +22 -54
  52. package/dist/vercel/ably-ai-transport-vercel.js +2297 -2026
  53. package/dist/vercel/ably-ai-transport-vercel.js.map +1 -1
  54. package/dist/vercel/ably-ai-transport-vercel.umd.cjs +1 -1
  55. package/dist/vercel/ably-ai-transport-vercel.umd.cjs.map +1 -1
  56. package/dist/vercel/codec/decode-lifecycle.d.ts +9 -0
  57. package/dist/vercel/codec/events.d.ts +1 -2
  58. package/dist/vercel/codec/fields.d.ts +44 -0
  59. package/dist/vercel/codec/fold-content.d.ts +16 -0
  60. package/dist/vercel/codec/fold-data.d.ts +16 -0
  61. package/dist/vercel/codec/fold-input.d.ts +67 -0
  62. package/dist/vercel/codec/fold-lifecycle.d.ts +16 -0
  63. package/dist/vercel/codec/fold-text.d.ts +16 -0
  64. package/dist/vercel/codec/fold-tool-input.d.ts +17 -0
  65. package/dist/vercel/codec/fold-tool-output.d.ts +16 -0
  66. package/dist/vercel/codec/index.d.ts +5 -30
  67. package/dist/vercel/codec/inputs.d.ts +11 -0
  68. package/dist/vercel/codec/outputs.d.ts +11 -0
  69. package/dist/vercel/codec/reducer-state.d.ts +121 -0
  70. package/dist/vercel/codec/reducer.d.ts +20 -102
  71. package/dist/vercel/codec/tool-transitions.d.ts +0 -6
  72. package/dist/vercel/codec/wire-data.d.ts +34 -0
  73. package/dist/vercel/index.d.ts +1 -0
  74. package/dist/vercel/react/ably-ai-transport-vercel-react.js +2013 -9500
  75. package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
  76. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +1 -70
  77. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
  78. package/dist/vercel/react/contexts/chat-transport-context.d.ts +2 -1
  79. package/dist/vercel/run-end-reason.d.ts +66 -11
  80. package/dist/vercel/tool-part.d.ts +21 -0
  81. package/dist/vercel/transport/chat-transport.d.ts +0 -2
  82. package/dist/vercel/transport/index.d.ts +1 -1
  83. package/dist/vercel/transport/run-output-stream.d.ts +6 -8
  84. package/dist/version.d.ts +1 -1
  85. package/package.json +2 -2
  86. package/src/constants.ts +2 -2
  87. package/src/core/agent.ts +43 -19
  88. package/src/core/channel-options.ts +89 -0
  89. package/src/core/codec/codec-event.ts +27 -0
  90. package/src/core/codec/decoder.ts +145 -21
  91. package/src/core/codec/define-codec.ts +432 -0
  92. package/src/core/codec/encoder.ts +13 -54
  93. package/src/core/codec/field-bag.ts +142 -0
  94. package/src/core/codec/fields.ts +193 -0
  95. package/src/core/codec/index.ts +43 -0
  96. package/src/core/codec/input-descriptor-decoder.ts +97 -0
  97. package/src/core/codec/input-descriptor-encoder.ts +150 -0
  98. package/src/core/codec/input-descriptors.ts +373 -0
  99. package/src/core/codec/output-descriptor-decoder.ts +139 -0
  100. package/src/core/codec/output-descriptor-encoder.ts +101 -0
  101. package/src/core/codec/output-descriptors.ts +307 -0
  102. package/src/core/codec/types.ts +99 -36
  103. package/src/core/codec/well-known-inputs.ts +96 -0
  104. package/src/core/transport/agent-session.ts +330 -589
  105. package/src/core/transport/agent-view.ts +738 -0
  106. package/src/core/transport/client-session.ts +74 -69
  107. package/src/core/transport/decode-fold.ts +57 -47
  108. package/src/core/transport/headers.ts +57 -4
  109. package/src/core/transport/index.ts +2 -1
  110. package/src/core/transport/invocation.ts +1 -1
  111. package/src/core/transport/load-history-pages.ts +220 -0
  112. package/src/core/transport/load-history.ts +63 -61
  113. package/src/core/transport/pipe-stream.ts +10 -1
  114. package/src/core/transport/run-manager.ts +25 -31
  115. package/src/core/transport/session-support.ts +96 -0
  116. package/src/core/transport/tree.ts +414 -47
  117. package/src/core/transport/types/agent.ts +129 -102
  118. package/src/core/transport/types/client.ts +49 -13
  119. package/src/core/transport/types/tree.ts +61 -12
  120. package/src/core/transport/types/view.ts +57 -28
  121. package/src/core/transport/view.ts +520 -172
  122. package/src/core/transport/wire-log.ts +189 -0
  123. package/src/errors.ts +10 -3
  124. package/src/index.ts +44 -11
  125. package/src/react/contexts/client-session-context.ts +1 -1
  126. package/src/react/contexts/client-session-provider.tsx +38 -2
  127. package/src/react/index.ts +2 -1
  128. package/src/react/internal/skipped-session.ts +62 -0
  129. package/src/react/use-client-session.ts +7 -30
  130. package/src/react/use-view.ts +3 -3
  131. package/src/utils.ts +31 -97
  132. package/src/vercel/codec/decode-lifecycle.ts +70 -0
  133. package/src/vercel/codec/events.ts +1 -3
  134. package/src/vercel/codec/fields.ts +58 -0
  135. package/src/vercel/codec/fold-content.ts +54 -0
  136. package/src/vercel/codec/fold-data.ts +46 -0
  137. package/src/vercel/codec/fold-input.ts +255 -0
  138. package/src/vercel/codec/fold-lifecycle.ts +85 -0
  139. package/src/vercel/codec/fold-text.ts +55 -0
  140. package/src/vercel/codec/fold-tool-input.ts +86 -0
  141. package/src/vercel/codec/fold-tool-output.ts +79 -0
  142. package/src/vercel/codec/index.ts +23 -63
  143. package/src/vercel/codec/inputs.ts +116 -0
  144. package/src/vercel/codec/outputs.ts +207 -0
  145. package/src/vercel/codec/reducer-state.ts +169 -0
  146. package/src/vercel/codec/reducer.ts +52 -838
  147. package/src/vercel/codec/tool-transitions.ts +1 -12
  148. package/src/vercel/codec/wire-data.ts +64 -0
  149. package/src/vercel/index.ts +1 -0
  150. package/src/vercel/react/contexts/chat-transport-context.ts +1 -1
  151. package/src/vercel/react/use-chat-transport.ts +8 -28
  152. package/src/vercel/react/use-message-sync.ts +5 -10
  153. package/src/vercel/run-end-reason.ts +95 -16
  154. package/src/vercel/tool-part.ts +25 -0
  155. package/src/vercel/transport/chat-transport.ts +10 -22
  156. package/src/vercel/transport/index.ts +1 -1
  157. package/src/vercel/transport/run-output-stream.ts +7 -8
  158. package/src/version.ts +1 -1
  159. package/dist/core/transport/branch-chain.d.ts +0 -43
  160. package/dist/core/transport/load-conversation.d.ts +0 -128
  161. package/dist/vercel/codec/decoder.d.ts +0 -9
  162. package/dist/vercel/codec/encoder.d.ts +0 -11
  163. package/src/core/transport/branch-chain.ts +0 -58
  164. package/src/core/transport/load-conversation.ts +0 -355
  165. package/src/vercel/codec/decoder.ts +0 -696
  166. package/src/vercel/codec/encoder.ts +0 -548
@@ -29,8 +29,6 @@ export interface Extras {
29
29
  }
30
30
  /** Per-write overrides for encoder operations. */
31
31
  export interface WriteOptions {
32
- /** Override the default clientId for this write. */
33
- clientId?: string;
34
32
  /** Override the default extras for this write. */
35
33
  extras?: Extras;
36
34
  /** Message identity for projection routing. Stamped as `codec-message-id`. */
@@ -39,7 +37,7 @@ export interface WriteOptions {
39
37
  /**
40
38
  * A codec-agnostic description of a discrete Ably message. Used on both sides:
41
39
  * - **Encode:** the domain encoder describes what to publish; the encoder core
42
- * handles header merging, clientId resolution, and the actual publish.
40
+ * handles header merging and the actual publish.
43
41
  * - **Decode:** the decoder core extracts these fields from an
44
42
  * `Ably.InboundMessage` before calling domain hooks, keeping hooks free of
45
43
  * Ably SDK types.
@@ -48,7 +46,7 @@ export interface WriteOptions {
48
46
  * (strings, objects, etc.) — Ably handles serialization natively.
49
47
  */
50
48
  export interface MessagePayload {
51
- /** Ably message name (e.g. "text", "tool-input", "user-message"). */
49
+ /** Ably message name — the wire direction (`ai-output` / `ai-input`). */
52
50
  name: string;
53
51
  /** Message data. Ably handles serialization — strings, objects, and arrays are all valid. */
54
52
  data: unknown;
@@ -69,7 +67,7 @@ export interface MessagePayload {
69
67
  * concatenated for recovery and prefix-matching on the decoder.
70
68
  */
71
69
  export interface StreamPayload {
72
- /** Ably message name (e.g. "text", "reasoning", "tool-input"). */
70
+ /** Ably message name — `ai-output` (only outputs stream); not the codec `kind` / stream family. */
73
71
  name: string;
74
72
  /** Initial or closing data for the stream. Must be a string for append/accumulate semantics. */
75
73
  data: string;
@@ -86,7 +84,7 @@ export interface StreamPayload {
86
84
  * Accumulates text across appends and tracks lifecycle (open/closed).
87
85
  */
88
86
  export interface StreamTrackerState {
89
- /** Ably message name (e.g. "text", "reasoning", "tool-input"). */
87
+ /** Ably message name — `ai-output` (only outputs stream); not the codec `kind` / stream family. */
90
88
  name: string;
91
89
  /** Stream identifier (e.g. chunk.id for text, toolCallId for tool-input). */
92
90
  streamId: string;
@@ -102,6 +100,16 @@ export interface StreamTrackerState {
102
100
  * Initially set from the first publish, but may be replaced on update.
103
101
  */
104
102
  transportHeaders: Record<string, string>;
103
+ /**
104
+ * Highest `Message.version.serial` incorporated into this tracker.
105
+ * Versions are lexicographically comparable within one message serial, so
106
+ * a delivery carrying a version at or below this value is already
107
+ * incorporated and decodes to nothing. Stamped at first contact (a
108
+ * never-mutated message's version serial equals the message serial, which
109
+ * is also the fallback when the version carries no serial) and advanced by
110
+ * each version-bearing delivery.
111
+ */
112
+ version: string;
105
113
  /** Whether this stream has been closed (complete or cancelled). */
106
114
  closed: boolean;
107
115
  }
@@ -111,9 +119,12 @@ export interface StreamTrackerState {
111
119
  */
112
120
  export interface ReducerMeta {
113
121
  /**
114
- * Ably channel serial of the message that produced this event. The reducer
115
- * uses this for idempotency / dedup: events at or below the projection's
116
- * high-water-mark serial must be skipped (no-op return).
122
+ * Ably channel serial of the wire message that produced this event, or `''`
123
+ * for a not-yet-sequenced optimistic (local) fold. Ordering context only:
124
+ * the transport invokes `fold` in canonical serial order and exactly once
125
+ * per event, so the reducer must not treat a same-or-lower serial as a
126
+ * replay to skip — ordering and dedup are the transport's job, not the
127
+ * reducer's.
117
128
  */
118
129
  serial: string;
119
130
  /**
@@ -129,9 +140,14 @@ export interface ReducerMeta {
129
140
  * result every time — `fold` is a pure function and the reducer holds no
130
141
  * instance state.
131
142
  *
132
- * Idempotency: re-folding an event whose serial has already been incorporated
133
- * must be a no-op. The reducer is free to store a high-water-mark inside the
134
- * projection.
143
+ * Ordering, deduplication, and replay are the transport's responsibility, not
144
+ * the reducer's. The transport invokes `fold` exactly once per event, in
145
+ * canonical order — wire messages ascending by serial, events within a wire in
146
+ * decode order — refolding a node from a fresh `init` when a late wire would
147
+ * otherwise land out of order. The reducer therefore folds unconditionally: it
148
+ * must not keep a serial high-water-mark or skip "already-seen" events.
149
+ * Last-writer-wins for events competing over the same state falls out of fold
150
+ * order, since the highest-serial event folds last.
135
151
  *
136
152
  * Mutation: `fold` is allowed to mutate the projection passed in and return
137
153
  * it. The caller treats the projection as single-owner and never retains a
@@ -141,26 +157,48 @@ export interface Reducer<TEvent, TProjection> {
141
157
  /**
142
158
  * Build an empty initial projection. Called once per conversation node — a
143
159
  * Run node or a run-less input node — before any of that node's events are
144
- * folded.
160
+ * folded, and again on every refold of that node.
145
161
  */
146
162
  init(): TProjection;
147
163
  /**
148
164
  * Fold one TEvent into the projection and return the updated projection.
149
- * The reducer may mutate `state` in place.
165
+ * Invoked exactly once per event, in canonical order; the reducer may mutate
166
+ * `state` in place.
150
167
  */
151
168
  fold(state: TProjection, event: TEvent, meta: ReducerMeta): TProjection;
152
169
  }
170
+ /**
171
+ * A decoded event tagged with the wire direction it arrived on. The reducer
172
+ * folds this union (not a bare `TInput | TOutput`) so it can dispatch on
173
+ * `direction` rather than inspecting the event's shape. Direction is derived
174
+ * once, from the Ably message name, at decode time (see `toCodecEvents`) — the
175
+ * authoritative signal, since a single message is either `ai-input` or
176
+ * `ai-output` but never both.
177
+ * @template TInput - The codec's input union.
178
+ * @template TOutput - The codec's output union.
179
+ */
180
+ export type CodecEvent<TInput, TOutput> = {
181
+ /** The event arrived on the `ai-input` wire. */
182
+ readonly direction: 'input';
183
+ /** The decoded input event. */
184
+ readonly event: TInput;
185
+ } | {
186
+ /** The event arrived on the `ai-output` wire. */
187
+ readonly direction: 'output';
188
+ /** The decoded output event. */
189
+ readonly event: TOutput;
190
+ };
153
191
  /** Options passed to a codec's `createEncoder` factory. */
154
192
  export interface EncoderOptions {
155
- /** Default clientId for all writes. */
156
- clientId?: string;
157
193
  /** Default extras (e.g. headers) merged into every Ably message. */
158
194
  extras?: Extras;
159
195
  /** Hook called before each Ably message is published. Mutate the message in place to add transport-level headers under `extras.ai`. */
160
196
  onMessage?: (message: Ably.Message) => void;
161
197
  /**
162
- * Default `codec-message-id` for messages where the event payload doesn't
163
- * supply one. Overridden by `WriteOptions.messageId` per-publish.
198
+ * Fallback domain message id surfaced to output escape hatches as
199
+ * `ctx.messageId` (e.g. the Vercel `start` hatch injects it when a chunk
200
+ * carries no `messageId` of its own). Unrelated to the wire
201
+ * codec-message-id transport header, which `WriteOptions.messageId` stamps.
164
202
  */
165
203
  messageId?: string;
166
204
  }
@@ -174,24 +212,24 @@ export interface EncoderOptions {
174
212
  export interface Encoder<TInput extends CodecInputEvent, TOutput extends CodecOutputEvent> {
175
213
  /**
176
214
  * Encode and publish a single client input on the `ai-input` wire.
177
- * Throws synchronously if the codec cannot encode the given input
215
+ * Rejects if the codec cannot encode the given input
178
216
  * variant.
179
217
  */
180
218
  publishInput(input: TInput, options?: WriteOptions): Promise<void>;
181
219
  /**
182
220
  * Encode and publish a single agent output on the `ai-output` wire.
183
- * Throws synchronously if the codec cannot encode the given output
221
+ * Rejects if the codec cannot encode the given output
184
222
  * variant.
185
223
  */
186
224
  publishOutput(output: TOutput, options?: WriteOptions): Promise<void>;
187
225
  /**
188
- * Cancel any in-progress streams and emit a codec-specific cancel signal.
189
- * Idempotent across repeated calls a second `cancel` is a no-op. Must not
190
- * be called after `close`; doing so throws because the encoder is already
191
- * closed.
192
- * @param reason - Optional reason string for the cancellation (e.g. 'cancelled').
226
+ * Close all in-progress streamed messages as cancelled (status:cancelled) and
227
+ * flush pending appends. Pure transport mechanics emits no codec output.
228
+ * Idempotent: streams already cancelled are not re-appended. Must not be
229
+ * called after `close`; doing so throws because the encoder is already closed.
230
+ * Run termination is signalled separately by the transport `ai-run-end` event.
193
231
  */
194
- cancel(reason?: string): Promise<void>;
232
+ cancelStreams(): Promise<void>;
195
233
  /** Flush pending appends and release encoder resources. */
196
234
  close(): Promise<void>;
197
235
  }
@@ -212,6 +250,12 @@ export interface DecodedMessage<TInput extends CodecInputEvent, TOutput extends
212
250
  * compaction, partial-history page boundary, rewind miss) synthesizes any
213
251
  * missing start events before deltas reach the SDK — the reducer always
214
252
  * sees a clean `(start, delta*, end)` sequence.
253
+ *
254
+ * Trackers are version-guarded: a delivery whose `Message.version.serial`
255
+ * is at or below the version already incorporated decodes to nothing. One
256
+ * decoder instance can therefore be shared by the live subscription and
257
+ * history hydration — whichever route delivers a message's content first
258
+ * wins, and the other route's covered deliveries are no-ops.
215
259
  */
216
260
  export interface Decoder<TInput extends CodecInputEvent, TOutput extends CodecOutputEvent> {
217
261
  /** Decode one Ably inbound message into the input/output halves. */
@@ -246,10 +290,11 @@ export interface CodecInputEvent {
246
290
  * Pointer to another codec-message this input references. The semantic
247
291
  * depends on `kind` — for `regenerate`, the assistant codec-message to
248
292
  * regenerate; codec-specific `kind`s may give it other meanings. The
249
- * input event itself does not create a fork — it requests one. The fork
293
+ * input event itself does not create a fork — it requests one: the
294
+ * transport reads `target` off the input (e.g. the client session maps a
295
+ * regenerate's target into its transport headers) and the fork
250
296
  * relationship is established on the agent's response (and on
251
- * `ai-run-start`), which the codec encoder maps `target` to via the
252
- * wire's `fork-of` header.
297
+ * `ai-run-start`).
253
298
  */
254
299
  target?: string;
255
300
  /**
@@ -300,7 +345,7 @@ export interface Regenerate extends CodecInputEvent {
300
345
  * The core is domain-independent: it knows only that this input amends the
301
346
  * assistant at `codecMessageId` and carries a codec-defined `payload`. The
302
347
  * shape of `payload` (e.g. the tool-call id and output value) is supplied
303
- * by the codec via `TPayload` see the Vercel layer's payload type.
348
+ * by the codec via `TPayload` (e.g. a tool-call id and output value).
304
349
  *
305
350
  * Codecs opt in to client-side tool resolution by including this variant
306
351
  * in their `TInput` union. Codecs whose domain model doesn't natively
@@ -322,7 +367,7 @@ export interface ToolResult<TPayload> extends CodecInputEvent {
322
367
  * Well-known input variant: client-published tool result (failure). The
323
368
  * tool ran and failed. Mutates the assistant codec-message addressed by
324
369
  * `codecMessageId`. The failure detail (e.g. tool-call id and error text)
325
- * is the codec's domain `payload` — see the Vercel layer's payload type.
370
+ * is the codec's domain `payload`.
326
371
  * @template TPayload - The codec's domain payload for a tool-result failure.
327
372
  */
328
373
  export interface ToolResultError<TPayload> extends CodecInputEvent {
@@ -339,7 +384,7 @@ export interface ToolResultError<TPayload> extends CodecInputEvent {
339
384
  * codec-message addressed by `codecMessageId` — flipping the targeted
340
385
  * tool call from pending-approval to approved or denied. The decision
341
386
  * detail (e.g. tool-call id, approved flag, reason) is the codec's domain
342
- * `payload` — see the Vercel layer's payload type.
387
+ * `payload`.
343
388
  *
344
389
  * Codecs may layer approval semantics on top of domain models that don't
345
390
  * natively support gating tool execution behind an approval — the codec
@@ -374,6 +419,15 @@ export type ToolResultErrorPayloadOf<TInput> = TInput extends ToolResultError<in
374
419
  * @template TInput - The codec's input union.
375
420
  */
376
421
  export type ToolApprovalResponsePayloadOf<TInput> = TInput extends ToolApprovalResponse<infer P> ? P : never;
422
+ /**
423
+ * Extract the domain message type (`TMessage`) carried by a codec's
424
+ * {@link UserMessage} member from its `TInput` union, or `never` if the codec
425
+ * has no user-message variant. Lets the core well-known input factories type
426
+ * `createUserMessage` from `TInput` alone, without a separate message type
427
+ * parameter.
428
+ * @template TInput - The codec's input union.
429
+ */
430
+ export type UserMessageOf<TInput> = TInput extends UserMessage<infer M> ? M : never;
377
431
  /**
378
432
  * Structural base every codec output variant must satisfy. The output
379
433
  * counterpart of {@link CodecInputEvent}: pins a `type` discriminator so
@@ -382,9 +436,8 @@ export type ToolApprovalResponsePayloadOf<TInput> = TInput extends ToolApprovalR
382
436
  * fields on outputs without a breaking generic-arity change.
383
437
  *
384
438
  * The `type` discriminator is required on every variant so the codec's
385
- * reducer can switch on it. `AI.UIMessageChunk` already satisfies this
386
- * constraint structurally — its `type` literal is assignable to
387
- * `string` — so the Vercel codec needs no implementation changes.
439
+ * reducer can switch on it any domain union whose members carry a `type`
440
+ * string literal satisfies it structurally.
388
441
  *
389
442
  * No routing fields today: outputs carry no per-event `codecMessageId` /
390
443
  * `parent` / `forkOf` overrides. Those move onto this base when a concrete
@@ -428,7 +481,13 @@ export interface CodecMessage<TMessage> {
428
481
  * - `TMessage` — the per-message shape consumed by the Tree. Returned from
429
482
  * {@link Codec.getMessages}.
430
483
  */
431
- export interface Codec<TInput extends CodecInputEvent, TOutput extends CodecOutputEvent, TProjection, TMessage> extends Reducer<TInput | TOutput, TProjection> {
484
+ export interface Codec<TInput extends CodecInputEvent, TOutput extends CodecOutputEvent, TProjection, TMessage> extends Reducer<CodecEvent<TInput, TOutput>, TProjection> {
485
+ /**
486
+ * Optional Ably-Agent identifier. When present, the agent-registration path
487
+ * registers it on the channel (so traffic is attributed to this codec); when
488
+ * absent, the codec opts out of registration. Read directly by `registerAgent`.
489
+ */
490
+ readonly adapterTag?: string;
432
491
  /** Create a stateful encoder bound to the given channel. */
433
492
  createEncoder(channel: ChannelWriter, options?: EncoderOptions): Encoder<TInput, TOutput>;
434
493
  /** Create a stateful decoder for converting Ably inbound messages into typed inputs and outputs. */
@@ -0,0 +1,52 @@
1
+ import { CodecInputEvent, Regenerate, ToolApprovalResponse, ToolApprovalResponsePayloadOf, ToolResult, ToolResultError, ToolResultErrorPayloadOf, ToolResultPayloadOf, UserMessage, UserMessageOf } from './types.js';
2
+ /**
3
+ * The well-known input factory functions, payload-typed for a codec's `TInput`
4
+ * union. A codec spreads these into its definition rather than re-implementing
5
+ * the variant-wrapping boilerplate. Each factory returns the specific variant
6
+ * it builds — a member of the codec's `TInput` union.
7
+ * @template TInput - The codec's input union.
8
+ */
9
+ export interface WellKnownInputFactories<TInput extends CodecInputEvent> {
10
+ /**
11
+ * Wrap a domain message as the codec's {@link UserMessage} variant.
12
+ * @param message - The message in the codec's domain representation.
13
+ * @returns The user-message input.
14
+ */
15
+ createUserMessage(message: UserMessageOf<TInput>): UserMessage<UserMessageOf<TInput>>;
16
+ /**
17
+ * Build a {@link Regenerate} input.
18
+ * @param target - The codec-message-id of the assistant message to regenerate.
19
+ * @param parent - The codec-message-id of the parent user message the new assistant threads under.
20
+ * @returns The regenerate input.
21
+ */
22
+ createRegenerate(target: string, parent: string): Regenerate;
23
+ /**
24
+ * Build a {@link ToolResult} input addressing an assistant codec-message.
25
+ * @param codecMessageId - The assistant codec-message carrying the tool call.
26
+ * @param payload - The codec's domain payload describing the tool result.
27
+ * @returns The tool-result input.
28
+ */
29
+ createToolResult(codecMessageId: string, payload: ToolResultPayloadOf<TInput>): ToolResult<ToolResultPayloadOf<TInput>>;
30
+ /**
31
+ * Build a {@link ToolResultError} input addressing an assistant codec-message.
32
+ * @param codecMessageId - The assistant codec-message carrying the tool call.
33
+ * @param payload - The codec's domain payload describing the failure.
34
+ * @returns The tool-result-error input.
35
+ */
36
+ createToolResultError(codecMessageId: string, payload: ToolResultErrorPayloadOf<TInput>): ToolResultError<ToolResultErrorPayloadOf<TInput>>;
37
+ /**
38
+ * Build a {@link ToolApprovalResponse} input addressing an assistant codec-message.
39
+ * @param codecMessageId - The assistant codec-message carrying the tool call.
40
+ * @param payload - The codec's domain payload describing the approval decision.
41
+ * @returns The tool-approval-response input.
42
+ */
43
+ createToolApprovalResponse(codecMessageId: string, payload: ToolApprovalResponsePayloadOf<TInput>): ToolApprovalResponse<ToolApprovalResponsePayloadOf<TInput>>;
44
+ }
45
+ /**
46
+ * Build the {@link WellKnownInputFactories} for a codec's `TInput` union. The
47
+ * returned factories wrap domain values into the well-known input variants and
48
+ * are typically spread into a codec definition.
49
+ * @template TInput - The codec's input union.
50
+ * @returns The well-known input factory functions, payload-typed to `TInput`.
51
+ */
52
+ export declare const wellKnownInputs: <TInput extends CodecInputEvent>() => WellKnownInputFactories<TInput>;
@@ -0,0 +1,296 @@
1
+ import { Logger } from '../../logger.js';
2
+ import { Codec, CodecInputEvent, CodecOutputEvent } from '../codec/types.js';
3
+ import { WireApplier } from './decode-fold.js';
4
+ import { TreeInternal } from './tree.js';
5
+ import { ConversationNode, Tree } from './types.js';
6
+ /**
7
+ * AgentView — internal, server-side message-loading + input-event lookup for
8
+ * AgentSession.
9
+ *
10
+ * Encapsulates everything the agent needs to read conversation state off the
11
+ * channel: locating the triggering input event before `run-start`
12
+ * ({@link AgentView.findInputEvent}), and reconstructing the ancestor chain for
13
+ * an LLM prompt ({@link AgentView.loadConversation} / {@link AgentView.messages}).
14
+ *
15
+ * It does NOT own the materialisation Tree — AgentSession owns the Tree and the
16
+ * applier (and swaps them on channel continuity loss) and injects them here as
17
+ * `readonly` fields, the same way ClientSession wires `DefaultView`. Because
18
+ * AgentSession swaps the Tree, it RECREATES the AgentView on continuity loss
19
+ * (a fresh instance bound to the fresh Tree/applier) rather than mutating it —
20
+ * so this class never needs a tree accessor or a reset hook.
21
+ *
22
+ * This is deliberately internal: it is not exported from any entry point and
23
+ * does NOT implement the public `View` interface (that is the client-side
24
+ * `DefaultView`, unrelated to this class).
25
+ *
26
+ * Both `findInputEvent` and `loadConversation` drive ONE history-walk mechanism
27
+ * — the single-flight chain in {@link AgentView._driveHistoryChain} — so a
28
+ * `start()` input scan and a concurrent `loadConversation` share folded pages
29
+ * instead of each scanning the channel.
30
+ */
31
+ import * as Ably from 'ably';
32
+ /**
33
+ * Result of {@link AgentView.findInputEvent}. The lookup races the session's
34
+ * Tree (`findAblyMessageByEventId` pre-scan + `'ably-message'` event for live
35
+ * arrivals) against a bounded history scan; resolves with the matched messages
36
+ * sorted by Ably `serial` ascending.
37
+ *
38
+ * Run.start reads `firstHeaders` / `firstClientId` from the smallest-serial
39
+ * matched message to derive per-run metadata (run-id, parent, forkOf,
40
+ * continuation flag, publisher clientId). The Tree has already folded each
41
+ * message by the time the lookup resolves, so callers do NOT need to decode the
42
+ * raw matched messages themselves.
43
+ */
44
+ export interface InputEventLookupResult {
45
+ /** Raw Ably messages matched by the lookup, sorted by serial ascending. */
46
+ rawMessages: Ably.InboundMessage[];
47
+ /** Transport headers of the smallest-serial matched message (run metadata). */
48
+ firstHeaders?: Record<string, string>;
49
+ /** Publisher's Ably channel-level `clientId` from the smallest-serial message. */
50
+ firstClientId?: string;
51
+ }
52
+ /**
53
+ * Walk parent pointers from an anchor codec-message-id back through the
54
+ * Tree to the conversation root, returning nodes in root-first order. When
55
+ * `maxRuns` is set, the walk stops before the RunNode that would exceed the
56
+ * bound, so the bounding run's own input node(s) are still included (input
57
+ * nodes never count toward the bound). The chain therefore starts with the
58
+ * input that triggered its oldest run, never with an assistant reply.
59
+ *
60
+ * Returns an empty array when the anchor isn't in the Tree.
61
+ * @param tree - The materialisation tree to walk.
62
+ * @param anchor - The codec-message-id to start from (typically the current run's input).
63
+ * @param maxRuns - Optional bound on the number of ancestor reply RunNodes in the chain.
64
+ * @param currentRunId - The current run's id. Its own RunNode (reachable when
65
+ * the anchor's wire carried the run-id) is conversation tail, not ancestor
66
+ * context, so it never counts toward `maxRuns`.
67
+ * @returns Nodes from root to anchor in chronological order.
68
+ */
69
+ export declare const walkAncestorChain: <TOutput extends CodecOutputEvent, TProjection>(tree: Tree<TOutput, TProjection>, anchor: string | undefined, maxRuns?: number, currentRunId?: string) => readonly ConversationNode<TProjection>[];
70
+ /**
71
+ * Constructor dependencies for {@link AgentView}, injected by AgentSession.
72
+ *
73
+ * AgentView holds `tree` + `applier` directly (like `DefaultView`). AgentSession
74
+ * owns them and, because it SWAPS the Tree on continuity loss, recreates the
75
+ * AgentView with the fresh Tree/applier rather than mutating them in place.
76
+ */
77
+ export interface AgentViewOptions<TInput extends CodecInputEvent, TOutput extends CodecOutputEvent, TProjection, TMessage> {
78
+ /** The session's materialisation Tree (read for walks; folded into by history). */
79
+ tree: TreeInternal<TInput, TOutput, TProjection>;
80
+ /** The Ably channel to read history from. */
81
+ channel: Ably.RealtimeChannel;
82
+ /** Codec used to project per-node messages. */
83
+ codec: Codec<TInput, TOutput, TProjection, TMessage>;
84
+ /** The Tree's decode-and-apply engine; history pages fold through it. */
85
+ applier: WireApplier;
86
+ /** Logger for diagnostic output. */
87
+ logger?: Logger;
88
+ /**
89
+ * Age bound for the input-event scan: the scan gives up paging once it
90
+ * crosses `Date.now() - inputEventLookbackMs`. Applied only to
91
+ * `findInputEvent`, never to the ancestor-hydration walk.
92
+ */
93
+ inputEventLookbackMs: number;
94
+ }
95
+ /**
96
+ * Internal server-side view: input-event lookup + conversation loading over the
97
+ * session Tree. See the file header for the ownership boundary.
98
+ */
99
+ export declare class AgentView<TInput extends CodecInputEvent, TOutput extends CodecOutputEvent, TProjection, TMessage> {
100
+ private readonly _tree;
101
+ private readonly _channel;
102
+ private readonly _codec;
103
+ private readonly _applier;
104
+ private readonly _logger?;
105
+ private readonly _inputEventLookbackMs;
106
+ /**
107
+ * Tail of the single-flight history-hydration chain. Each walk links behind
108
+ * the current tail and becomes the new tail, so concurrent calls serialise
109
+ * and share each other's folded pages instead of each scanning the channel.
110
+ * A link never rejects (it records its error locally), so a follower awaiting
111
+ * the tail is isolated from a prior link's failure.
112
+ */
113
+ private _hydrationMutex;
114
+ /**
115
+ * Shared history-walk cursor for this AgentView's attach epoch — ONE backward
116
+ * `untilAttach` pagination that both `findInputEvent` and `loadConversation`
117
+ * advance. `findInputEvent` pages it until the trigger is found (or its
118
+ * lookback give-up point) and pauses; `loadConversation` resumes from that
119
+ * position instead of re-paging from newest, so the channel is walked once.
120
+ * Created lazily on first use (no per-caller signal, so it outlives any one
121
+ * caller; no lookback, so it can reach attach). The single-flight chain
122
+ * (`_hydrationMutex`) serialises access so it is never paged concurrently. A
123
+ * continuity-loss swap recreates the whole AgentView, so there is no in-place
124
+ * reset.
125
+ */
126
+ private _cursor;
127
+ /**
128
+ * True once the shared cursor reached attach (channel exhausted). Because the
129
+ * cursor carries no lookback, its exhaustion is always genuine (never a
130
+ * lookback boundary), so either caller may record it; a lookback-bounded
131
+ * `findInputEvent` scan stops via an early `break` that leaves the cursor
132
+ * non-exhausted, so it never sets this.
133
+ */
134
+ private _historyExhausted;
135
+ constructor(options: AgentViewOptions<TInput, TOutput, TProjection, TMessage>);
136
+ /**
137
+ * Fold a single wire message into the Tree: decode-and-apply via the applier,
138
+ * then notify Tree subscribers and populate the event-id index. Mirrors
139
+ * AgentSession's live `_foldWire`; history pages fold through this.
140
+ * @param wire - The inbound Ably message to fold.
141
+ */
142
+ private _foldWire;
143
+ /**
144
+ * Find every message whose `event-id` matches one of `expectedEventIds`,
145
+ * racing three sources:
146
+ *
147
+ * 1. A pre-scan of the Tree via `findAblyMessageByEventId` for messages already
148
+ * folded into it from prior live arrivals.
149
+ * 2. A live listener on the Tree's `ably-message` event for new arrivals
150
+ * during the call.
151
+ * 3. The shared history walk (lookback-bounded) — pages fold into the Tree
152
+ * and surface through the same `ably-message` event.
153
+ *
154
+ * Resolves when every expected event-id has been matched. Per-id race
155
+ * resolution — whichever source surfaces a matched message first wins
156
+ * (dedup by serial). On timeout: cancels the in-flight history scan and
157
+ * rejects with `InputEventNotFound`, wrapping any history-scan failure as
158
+ * `cause` so a broken history fetch isn't masked behind the timeout. On
159
+ * signal abort: rejects with `InvalidArgument`.
160
+ *
161
+ * `firstHeaders` and `firstClientId` are read from the matched message with
162
+ * the smallest serial (`compareBySerial`), giving stable run-level
163
+ * metadata regardless of arrival ordering across sources.
164
+ * @param opts - Lookup parameters.
165
+ * @param opts.invocationId - The invocation id this lookup is for (logging / error messages).
166
+ * @param opts.runId - The run id this lookup is for (logging / error messages).
167
+ * @param opts.expectedEventIds - The set of `event-id`s the lookup must observe before resolving.
168
+ * @param opts.timeoutMs - Maximum total wait across live + history sources.
169
+ * @param opts.signal - AbortSignal that aborts the lookup if the run is cancelled.
170
+ * @returns Raw matched Ably messages sorted by serial ascending, plus the
171
+ * smallest-serial message's headers and clientId for downstream metadata.
172
+ */
173
+ findInputEvent(opts: {
174
+ invocationId: string;
175
+ runId: string;
176
+ expectedEventIds: readonly string[];
177
+ timeoutMs: number;
178
+ signal: AbortSignal;
179
+ }): Promise<InputEventLookupResult>;
180
+ /**
181
+ * Reconstruct the conversation by walking the parent chain from the run's
182
+ * input node back to the conversation root, reading already-folded
183
+ * projections off the Tree's nodes.
184
+ *
185
+ * Hydrates the Tree as needed via the shared history walk
186
+ * ({@link AgentView._hydrateAncestors}), then concatenates
187
+ * `codec.getMessages(node.projection)` per node (root first) and appends the
188
+ * current run's projection at the tail.
189
+ * @param runId - The current run's id (for the tail run's projection lookup).
190
+ * @param assistantParentFallback - The current run's input node codec-message-id.
191
+ * @param signal - AbortSignal; rejects with InvalidArgument when aborted.
192
+ * @param maxRuns - Optional bound on the parent walk; counts reply RunNodes.
193
+ * @param runIdAdopted - True when the run-id came from outside (runtime
194
+ * override or continuation), so its node may exist in channel history;
195
+ * false for agent-minted ids, whose run-start only ever arrives via the
196
+ * live echo.
197
+ * @param regenerateTarget - The codec-message-id being regenerated, or
198
+ * undefined; the run that owns it is flattened only up to that message so
199
+ * the reconstructed history stops before the assistant message being
200
+ * replaced (which the model would otherwise reject).
201
+ * @returns The branch's messages (root-first) and the current run's projection.
202
+ */
203
+ loadConversation(runId: string, assistantParentFallback: string | undefined, signal: AbortSignal, maxRuns: number | undefined, runIdAdopted: boolean, regenerateTarget?: string): Promise<{
204
+ messages: TMessage[];
205
+ projection: TProjection;
206
+ }>;
207
+ /**
208
+ * Walk the parent chain from `anchor` over the current Tree and concatenate
209
+ * each node's projected messages (root-first), then append the current run's
210
+ * own messages when its RunNode isn't already on the chain. Shared by
211
+ * {@link AgentView.loadConversation} and {@link AgentView.messages}. Pure read
212
+ * over whatever is currently folded — no fetching.
213
+ * @param runId - The current run's id (for the tail run's projection lookup).
214
+ * @param anchor - The current run's input node codec-message-id.
215
+ * @param maxRuns - Optional bound on the ancestor walk (counts reply runs).
216
+ * @param regenerateTarget - The codec-message-id being regenerated; when set,
217
+ * the walk stops before that message (a regenerate of a non-head message
218
+ * anchors at the target's predecessor, so flattening its run whole would
219
+ * re-emit the target and end the history on the message being replaced).
220
+ * @returns The conversation messages (root-first) and the current run's
221
+ * projection (the codec's empty init when the run has no node yet).
222
+ */
223
+ private _collectConversation;
224
+ /**
225
+ * Synchronous live read of the conversation messages for `Run.messages`:
226
+ * walk the parent chain from `anchor` (no `maxRuns` bound), concatenate each
227
+ * ancestor's projection, then append the current run's messages if its node
228
+ * isn't already on the chain. No I/O — reflects whatever is currently folded.
229
+ * @param runId - The current run's id (for the tail run's projection lookup).
230
+ * @param anchor - The current run's input node codec-message-id (assistantParentFallback).
231
+ * @param regenerateTarget - The codec-message-id being regenerated; when set,
232
+ * the walk stops before it (see {@link AgentView._collectConversation}).
233
+ * @returns The conversation messages, root-first.
234
+ */
235
+ messages(runId: string, anchor: string | undefined, regenerateTarget?: string): TMessage[];
236
+ /**
237
+ * Single-flight chain entry shared by `findInputEvent` and `loadConversation`.
238
+ * Serialises behind any in-flight walk so the shared cursor is advanced by one
239
+ * caller at a time (never paged concurrently), then runs one
240
+ * {@link AgentView._walkSharedHistory}. A link never rejects (it records its
241
+ * error locally), so a follower awaiting the chain tail is isolated from a
242
+ * prior link's failure; this method rethrows the wrapped error from its own
243
+ * frame after awaiting.
244
+ *
245
+ * Returns `exhausted` but never records `_historyExhausted`; the caller records
246
+ * it (both callers may, since the shared cursor's exhaustion is always genuine
247
+ * — see {@link AgentView._historyExhausted}).
248
+ * @param shouldStop - Polled before each page; true pauses this walk.
249
+ * @param signal - Per-call abort signal (checked between pages).
250
+ * @param lookbackMs - Optional give-up bound for the input scan (early break).
251
+ * @param operationLabel - Verb for the wrapped error message.
252
+ * @returns `{ exhausted }` — true only when the shared cursor reached attach.
253
+ */
254
+ private _driveHistoryChain;
255
+ /**
256
+ * Advance the SHARED history cursor (lazily opening it once per attach epoch)
257
+ * and fold each page into the session Tree via the injected `fold`, stopping
258
+ * when `shouldStop()` returns true, the channel is exhausted, the signal
259
+ * aborts, a continuity-loss Tree swap abandons the walk, or — when `lookbackMs`
260
+ * is given — the walk pages past the lookback window. The cursor is NOT closed
261
+ * on stop: it stays paused at its current position so a later caller resumes
262
+ * from there rather than re-paging from newest. Throws (caller-wrapped) on a
263
+ * fetch failure after `loadHistoryPages`' per-page retries.
264
+ * @param shouldStop - Polled before each page; true pauses the walk.
265
+ * @param signal - Per-call abort signal (checked between pages; the shared cursor carries none).
266
+ * @param lookbackMs - Optional give-up bound: stop paging once a page's oldest
267
+ * message predates `Date.now() - lookbackMs`. An early `break`, NOT a cursor
268
+ * bound, so the cursor stays resumable and exhaustion is never reported here.
269
+ * @returns True only when the cursor genuinely reached attach — NOT when
270
+ * paused by the predicate / lookback, a Tree swap, or signal abort.
271
+ */
272
+ private _walkSharedHistory;
273
+ /**
274
+ * Populate the Tree with enough ancestor coverage to walk from `anchor` to
275
+ * root (or `maxRuns` reply runs back) by driving the shared history walk.
276
+ * Records `_historyExhausted` only when a FULL (no-lookback) walk genuinely
277
+ * exhausts the channel.
278
+ * @param runId - The current run's id (when adopted, its node must be present in the Tree before the walk is complete).
279
+ * @param anchor - The input codec-message-id to walk from. Undefined means no walk is needed (current run only).
280
+ * @param signal - AbortSignal.
281
+ * @param maxRuns - Optional bound on the ancestor walk.
282
+ * @param runIdAdopted - Whether the run-id came from outside (override or continuation) and so may name a run present in channel history.
283
+ * @throws {Ably.ErrorInfo} `InvalidArgument` when `signal` aborts;
284
+ * `HistoryFetchFailed` — or the underlying Ably code when the failure
285
+ * carried one — (original as `cause`) when this caller's own history
286
+ * fetch fails after retries.
287
+ */
288
+ private _hydrateAncestors;
289
+ }
290
+ /**
291
+ * Create an {@link AgentView}. Factory entry point mirroring `createTree`;
292
+ * AgentSession never calls `new AgentView` directly.
293
+ * @param options - Injected dependencies.
294
+ * @returns A new AgentView.
295
+ */
296
+ export declare const createAgentView: <TInput extends CodecInputEvent, TOutput extends CodecOutputEvent, TProjection, TMessage>(options: AgentViewOptions<TInput, TOutput, TProjection, TMessage>) => AgentView<TInput, TOutput, TProjection, TMessage>;