@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
@@ -0,0 +1,307 @@
1
+ /**
2
+ * Declarative output descriptors — the single source of truth for a codec's
3
+ * `ai-output` wire mapping, the output-side sibling of {@link import('./input-descriptors.js')}.
4
+ *
5
+ * A codec declares each ordinary output event once, as a descriptor built on the
6
+ * typed header-field bindings ({@link HeaderField}). The generic encode/decode
7
+ * drivers consume the descriptor set, so adding an ordinary event is one
8
+ * descriptor entry instead of three hand-synchronised switch arms (encoder,
9
+ * decoder, stream reconstruction).
10
+ *
11
+ * Authoring is cast-free: the {@link outputBuilder} factory hands the codec an
12
+ * `{ event, stream }` pair curried on the codec's output union, so every `data` /
13
+ * `encode` / `decode` callback receives the exact narrowed member. The descriptors
14
+ * are then erased to a heterogeneous {@link OutputDescriptor} via a single
15
+ * documented cast at each constructor boundary — never in author code.
16
+ */
17
+
18
+ import type * as Ably from 'ably';
19
+
20
+ import { wildcardMatcher } from './field-bag.js';
21
+ import type { DataCodec, FieldFor, HeaderField } from './fields.js';
22
+ import type { MessagePayload, StreamPayload, WriteOptions } from './types.js';
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Type helpers
26
+ // ---------------------------------------------------------------------------
27
+
28
+ /** The string-valued keys of `C` — the only keys `idField`/`deltaField` may name. */
29
+ export type StringKeyOf<C> = { [K in keyof C]-?: C[K] extends string ? K : never }[keyof C];
30
+
31
+ /**
32
+ * Resolve the union member a descriptor `type` literal selects. An exact match
33
+ * wins; a wildcard literal (`'data-*'`) resolves to the template member
34
+ * (`data-${string}`), so wildcard descriptors still narrow to the real member.
35
+ */
36
+ export type ResolveType<U extends { type: string }, T extends string> =
37
+ Extract<U, { type: T }> extends never
38
+ ? T extends `${infer P}-*`
39
+ ? Extract<U, { type: `${P}-${string}` }>
40
+ : never
41
+ : Extract<U, { type: T }>;
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Escape-hatch core surface
45
+ // ---------------------------------------------------------------------------
46
+
47
+ /**
48
+ * The narrowed view of the encoder core that escape-hatch `encode` functions
49
+ * receive — only the publish/stream operations a hatch legitimately needs. The
50
+ * full internal `EncoderCore` satisfies this structurally.
51
+ */
52
+ export interface EscapeHatchCore {
53
+ /** Publish a single discrete message. */
54
+ publishDiscrete(payload: MessagePayload, opts?: WriteOptions): Promise<Ably.PublishResult>;
55
+ /** Start a streamed message. */
56
+ startStream(streamId: string, payload: StreamPayload, opts?: WriteOptions): Promise<void>;
57
+ /** Append a fragment to an in-flight stream (fire-and-forget). */
58
+ appendStream(streamId: string, data: string): void;
59
+ /** Close a streamed message. */
60
+ closeStream(streamId: string, payload: StreamPayload): Promise<void>;
61
+ /** Cancel all in-progress streams. */
62
+ cancelAllStreams(opts?: WriteOptions): Promise<void>;
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Header builder + contexts
67
+ // ---------------------------------------------------------------------------
68
+
69
+ /**
70
+ * Builds a codec headers record from a chunk through the descriptor's declared
71
+ * fields, stamping the dispatch `type` plus each field read off `chunk`. An
72
+ * optional `keys` subset restricts which declared fields are written; the keys
73
+ * are checked against the chunk so the imperative path can't drift.
74
+ * @template C - The narrowed chunk member.
75
+ */
76
+ export type HeaderBuilder<C> = <K extends keyof C & string>(chunk: C, keys?: readonly K[]) => Record<string, string>;
77
+
78
+ /**
79
+ * Context passed to an escape-hatch `encode` function.
80
+ * @template C - The narrowed chunk member.
81
+ */
82
+ export interface OutputEncodeHatchContext<C> {
83
+ /** Header builder bound to the descriptor's declared fields. */
84
+ h: HeaderBuilder<C>;
85
+ /** The wire message name for this direction (`ai-output` / `ai-input`). */
86
+ name: string;
87
+ /** The encoder's configured fallback message id, if any. */
88
+ messageId: string | undefined;
89
+ /** Per-write overrides to thread into the hatch's publish/cancel calls. */
90
+ opts: WriteOptions | undefined;
91
+ }
92
+
93
+ /** Context passed to a discrete escape-hatch `decode` function. */
94
+ export interface OutputDecodeContext {
95
+ /** The codec `kind` header value the message dispatched on (mirrors the input context's `codecKind`). */
96
+ codecKind: string;
97
+ /** The inbound codec-tier headers. */
98
+ codecHeaders: Record<string, string>;
99
+ /** The inbound transport-tier headers. */
100
+ transportHeaders: Record<string, string>;
101
+ /** The inbound message data. */
102
+ data: unknown;
103
+ }
104
+
105
+ /** Context passed to a stream descriptor's `decodeEnd` escape hatch. */
106
+ export interface OutputStreamEndContext {
107
+ /** The stream identifier (e.g. chunk id, toolCallId). */
108
+ streamId: string;
109
+ /** The full accumulated stream text. */
110
+ accumulated: string;
111
+ /** The stream's persistent (start) codec headers. */
112
+ codecHeaders: Record<string, string>;
113
+ /** The codec headers carried on close (may differ from the start headers). */
114
+ closingCodecHeaders: Record<string, string>;
115
+ }
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // Data codec
119
+ // ---------------------------------------------------------------------------
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // Author-facing specs (narrowed)
123
+ // ---------------------------------------------------------------------------
124
+
125
+ /**
126
+ * A discrete (non-streaming) output event descriptor spec, narrowed to chunk member `C`.
127
+ * @template C - The narrowed chunk member.
128
+ */
129
+ export interface OutputEventSpec<C> {
130
+ /**
131
+ * Declared header fields, written on encode and read on decode. Each field's
132
+ * key names both the wire header and the chunk property it carries (see
133
+ * {@link FieldFor}). Omit for a header-less event.
134
+ */
135
+ fields?: readonly FieldFor<C>[];
136
+ /** Wire `data` codec. Omit when the event carries no data (`data: ''`). */
137
+ data?: DataCodec<C>;
138
+ /** Whether the publish is ephemeral (not persisted). Default false. */
139
+ ephemeral?: (chunk: C) => boolean;
140
+ /** Escape-hatch encode — overrides the default discrete publish. */
141
+ encode?: (chunk: C, core: EscapeHatchCore, ctx: OutputEncodeHatchContext<C>) => Promise<void>;
142
+ }
143
+
144
+ /**
145
+ * A streamed-family descriptor spec. `start`/`delta`/`end` are the domain chunk
146
+ * `type` literals; the family id (the {@link OutputBuilder.stream} first argument)
147
+ * is the wire `kind` header all three phases stamp.
148
+ * @template U - The codec's event union.
149
+ * @template S - The start chunk `type` literal.
150
+ * @template D - The delta chunk `type` literal.
151
+ * @template E - The end chunk `type` literal.
152
+ */
153
+ export interface OutputStreamSpec<
154
+ U extends { type: string },
155
+ S extends U['type'],
156
+ D extends U['type'],
157
+ E extends U['type'],
158
+ > {
159
+ /** The start chunk `type` literal. */
160
+ start: S;
161
+ /** The delta chunk `type` literal. */
162
+ delta: D;
163
+ /** The end chunk `type` literal. */
164
+ end: E;
165
+ /** The string-valued chunk key carrying the stream id (e.g. `id`, `toolCallId`). */
166
+ idField: StringKeyOf<ResolveType<U, S>> & StringKeyOf<ResolveType<U, D>> & StringKeyOf<ResolveType<U, E>>;
167
+ /** The string-valued delta chunk key carrying the appended fragment. */
168
+ deltaField: StringKeyOf<ResolveType<U, D>>;
169
+ /**
170
+ * Declared header fields written/read on the start and end chunks. Each
171
+ * field's key names both the wire header and the chunk property (see
172
+ * {@link FieldFor}); a field may bind a property carried by either phase.
173
+ */
174
+ fields: readonly (FieldFor<ResolveType<U, S>> | FieldFor<ResolveType<U, E>>)[];
175
+ /** Escape-hatch override for the stream-close step only (e.g. close-or-discrete fallback). */
176
+ onEnd?: (
177
+ chunk: ResolveType<U, E>,
178
+ core: EscapeHatchCore,
179
+ ctx: OutputEncodeHatchContext<ResolveType<U, E>>,
180
+ ) => Promise<void>;
181
+ /** Escape-hatch override for the end-chunk rebuild (e.g. input from accumulated text). */
182
+ decodeEnd?: (ctx: OutputStreamEndContext) => ResolveType<U, E>[];
183
+ /**
184
+ * Escape-hatch decode for when the family arrives as a discrete (non-streamed)
185
+ * message — the wire `kind` equals the family id but the wire wasn't streamed
186
+ * (e.g. history compaction). Reconstructs the start/end chunk pair.
187
+ */
188
+ decodeDiscrete?: (ctx: OutputDecodeContext) => ResolveType<U, S | E>[];
189
+ }
190
+
191
+ // ---------------------------------------------------------------------------
192
+ // Erased descriptor (heterogeneous array element)
193
+ // ---------------------------------------------------------------------------
194
+
195
+ /** A discrete output event descriptor erased to the codec's union `U`. */
196
+ export interface OutputEventDescriptor<U> {
197
+ /** Discriminator — the construct this descriptor was built with. */
198
+ construct: 'event';
199
+ /** The dispatch `type` literal (or wildcard sentinel), stamped as the wire `kind` header. */
200
+ type: string;
201
+ /** Declared header fields. */
202
+ fields: readonly HeaderField<unknown>[];
203
+ /** Wire `data` codec, if any. */
204
+ data?: DataCodec<U>;
205
+ /** Ephemeral predicate, if any. */
206
+ ephemeral?: (chunk: U) => boolean;
207
+ /** Wildcard dispatch predicate (both directions), derived by the builder from a `-*` type literal. */
208
+ match?: (type: string) => boolean;
209
+ /** Escape-hatch encode, if any. */
210
+ encode?: (chunk: U, core: EscapeHatchCore, ctx: OutputEncodeHatchContext<U>) => Promise<void>;
211
+ }
212
+
213
+ /** A streamed-family descriptor erased to the codec's union `U`. */
214
+ export interface OutputStreamDescriptor<U> {
215
+ /** Discriminator — the construct this descriptor was built with. */
216
+ construct: 'stream';
217
+ /** The stream family id, stamped as the wire `kind` header on every phase. */
218
+ kind: string;
219
+ /** The start chunk `type`. */
220
+ start: string;
221
+ /** The delta chunk `type`. */
222
+ delta: string;
223
+ /** The end chunk `type`. */
224
+ end: string;
225
+ /** The chunk key carrying the stream id. */
226
+ idField: string;
227
+ /** The delta chunk key carrying the appended fragment. */
228
+ deltaField: string;
229
+ /** Declared header fields. */
230
+ fields: readonly HeaderField<unknown>[];
231
+ /** Escape-hatch close override, if any. */
232
+ onEnd?: (chunk: U, core: EscapeHatchCore, ctx: OutputEncodeHatchContext<U>) => Promise<void>;
233
+ /** Escape-hatch end-rebuild override, if any. */
234
+ decodeEnd?: (ctx: OutputStreamEndContext) => U[];
235
+ /** Escape-hatch non-streamed decode, if any. */
236
+ decodeDiscrete?: (ctx: OutputDecodeContext) => U[];
237
+ }
238
+
239
+ /** An erased output descriptor — a discrete event or a streamed family. */
240
+ export type OutputDescriptor<U> = OutputEventDescriptor<U> | OutputStreamDescriptor<U>;
241
+
242
+ // ---------------------------------------------------------------------------
243
+ // Builder factory
244
+ // ---------------------------------------------------------------------------
245
+
246
+ /**
247
+ * The direction-scoped output builder `defineCodec` injects into the `output`
248
+ * config function — `event` (single discrete) and `stream` (streamed family),
249
+ * both curried on the codec's output union so author entries narrow cast-free.
250
+ * @template U - The codec's output union.
251
+ */
252
+ export interface OutputBuilder<U extends { type: string }> {
253
+ /**
254
+ * Declare a single discrete output event. Curried on the output union; narrows
255
+ * `spec` to the member the `type` literal selects. The `type` literal is stamped
256
+ * as the wire `kind` dispatch header.
257
+ * @param type - The event's `type` literal (or a `*-*` wildcard); stamped as the wire `kind` header.
258
+ * @param spec - The narrowed output event spec. Omit for a header-less event with no data.
259
+ * @returns An erased {@link OutputDescriptor}.
260
+ */
261
+ event: <T extends U['type'] | `${string}-*`>(
262
+ type: T,
263
+ spec?: OutputEventSpec<ResolveType<U, T>>,
264
+ ) => OutputDescriptor<U>;
265
+ /**
266
+ * Declare a streamed output family (start / delta / end). `start`/`delta`/`end`
267
+ * are domain chunk `type` literals; the first argument is the family id, stamped
268
+ * as the wire `kind` dispatch header on every phase.
269
+ * @param kind - The stream family id, stamped as the wire `kind` header on every phase.
270
+ * @param spec - The narrowed stream spec.
271
+ * @returns An erased {@link OutputDescriptor}.
272
+ */
273
+ stream: <S extends U['type'], D extends U['type'], E extends U['type']>(
274
+ kind: string,
275
+ spec: OutputStreamSpec<U, S, D, E>,
276
+ ) => OutputDescriptor<U>;
277
+ }
278
+
279
+ /**
280
+ * Build the curried `{ event, stream }` output builder for a codec's output union.
281
+ * `defineCodec` calls this once and hands the result to the `output` config
282
+ * function; mirrors the input side's {@link import('./input-descriptors.js').inputBuilder}.
283
+ * @template U - The codec's output union.
284
+ * @returns The direction-scoped {@link OutputBuilder}.
285
+ */
286
+ export const outputBuilder = <U extends { type: string }>(): OutputBuilder<U> => ({
287
+ event: (type, spec) =>
288
+ // CAST: `spec` is narrowed to the member selected by `type`; the descriptor
289
+ // erases that to the codec's union `U` so heterogeneous descriptors share one
290
+ // array type. The drivers only ever invoke a descriptor's callbacks with the
291
+ // matching member, so the erasure is sound by construction. `fields` defaults
292
+ // to `[]` so a header-less event needs no spec (mirrors the input `event` builder).
293
+ ({
294
+ construct: 'event',
295
+ type,
296
+ fields: spec?.fields ?? [],
297
+ data: spec?.data,
298
+ ephemeral: spec?.ephemeral,
299
+ // A `-*` literal declares a wildcard family; the dispatch predicate is
300
+ // derived from the literal so the two can never disagree (see wildcardMatcher).
301
+ match: wildcardMatcher(type),
302
+ encode: spec?.encode,
303
+ }) as unknown as OutputDescriptor<U>,
304
+ stream: (kind, spec) =>
305
+ // CAST: see `event` — the narrowed stream spec erases to `OutputDescriptor<U>`.
306
+ ({ construct: 'stream', kind, ...spec }) as unknown as OutputDescriptor<U>,
307
+ });
@@ -51,8 +51,6 @@ export interface Extras {
51
51
 
52
52
  /** Per-write overrides for encoder operations. */
53
53
  export interface WriteOptions {
54
- /** Override the default clientId for this write. */
55
- clientId?: string;
56
54
  /** Override the default extras for this write. */
57
55
  extras?: Extras;
58
56
  /** Message identity for projection routing. Stamped as `codec-message-id`. */
@@ -66,7 +64,7 @@ export interface WriteOptions {
66
64
  /**
67
65
  * A codec-agnostic description of a discrete Ably message. Used on both sides:
68
66
  * - **Encode:** the domain encoder describes what to publish; the encoder core
69
- * handles header merging, clientId resolution, and the actual publish.
67
+ * handles header merging and the actual publish.
70
68
  * - **Decode:** the decoder core extracts these fields from an
71
69
  * `Ably.InboundMessage` before calling domain hooks, keeping hooks free of
72
70
  * Ably SDK types.
@@ -75,7 +73,7 @@ export interface WriteOptions {
75
73
  * (strings, objects, etc.) — Ably handles serialization natively.
76
74
  */
77
75
  export interface MessagePayload {
78
- /** Ably message name (e.g. "text", "tool-input", "user-message"). */
76
+ /** Ably message name — the wire direction (`ai-output` / `ai-input`). */
79
77
  name: string;
80
78
  /** Message data. Ably handles serialization — strings, objects, and arrays are all valid. */
81
79
  data: unknown;
@@ -97,7 +95,7 @@ export interface MessagePayload {
97
95
  * concatenated for recovery and prefix-matching on the decoder.
98
96
  */
99
97
  export interface StreamPayload {
100
- /** Ably message name (e.g. "text", "reasoning", "tool-input"). */
98
+ /** Ably message name — `ai-output` (only outputs stream); not the codec `kind` / stream family. */
101
99
  name: string;
102
100
  /** Initial or closing data for the stream. Must be a string for append/accumulate semantics. */
103
101
  data: string;
@@ -119,7 +117,7 @@ export interface StreamPayload {
119
117
  * Accumulates text across appends and tracks lifecycle (open/closed).
120
118
  */
121
119
  export interface StreamTrackerState {
122
- /** Ably message name (e.g. "text", "reasoning", "tool-input"). */
120
+ /** Ably message name — `ai-output` (only outputs stream); not the codec `kind` / stream family. */
123
121
  name: string;
124
122
  /** Stream identifier (e.g. chunk.id for text, toolCallId for tool-input). */
125
123
  streamId: string;
@@ -135,6 +133,16 @@ export interface StreamTrackerState {
135
133
  * Initially set from the first publish, but may be replaced on update.
136
134
  */
137
135
  transportHeaders: Record<string, string>;
136
+ /**
137
+ * Highest `Message.version.serial` incorporated into this tracker.
138
+ * Versions are lexicographically comparable within one message serial, so
139
+ * a delivery carrying a version at or below this value is already
140
+ * incorporated and decodes to nothing. Stamped at first contact (a
141
+ * never-mutated message's version serial equals the message serial, which
142
+ * is also the fallback when the version carries no serial) and advanced by
143
+ * each version-bearing delivery.
144
+ */
145
+ version: string;
138
146
  /** Whether this stream has been closed (complete or cancelled). */
139
147
  closed: boolean;
140
148
  }
@@ -149,9 +157,12 @@ export interface StreamTrackerState {
149
157
  */
150
158
  export interface ReducerMeta {
151
159
  /**
152
- * Ably channel serial of the message that produced this event. The reducer
153
- * uses this for idempotency / dedup: events at or below the projection's
154
- * high-water-mark serial must be skipped (no-op return).
160
+ * Ably channel serial of the wire message that produced this event, or `''`
161
+ * for a not-yet-sequenced optimistic (local) fold. Ordering context only:
162
+ * the transport invokes `fold` in canonical serial order and exactly once
163
+ * per event, so the reducer must not treat a same-or-lower serial as a
164
+ * replay to skip — ordering and dedup are the transport's job, not the
165
+ * reducer's.
155
166
  */
156
167
  serial: string;
157
168
  /**
@@ -168,9 +179,14 @@ export interface ReducerMeta {
168
179
  * result every time — `fold` is a pure function and the reducer holds no
169
180
  * instance state.
170
181
  *
171
- * Idempotency: re-folding an event whose serial has already been incorporated
172
- * must be a no-op. The reducer is free to store a high-water-mark inside the
173
- * projection.
182
+ * Ordering, deduplication, and replay are the transport's responsibility, not
183
+ * the reducer's. The transport invokes `fold` exactly once per event, in
184
+ * canonical order — wire messages ascending by serial, events within a wire in
185
+ * decode order — refolding a node from a fresh `init` when a late wire would
186
+ * otherwise land out of order. The reducer therefore folds unconditionally: it
187
+ * must not keep a serial high-water-mark or skip "already-seen" events.
188
+ * Last-writer-wins for events competing over the same state falls out of fold
189
+ * order, since the highest-serial event folds last.
174
190
  *
175
191
  * Mutation: `fold` is allowed to mutate the projection passed in and return
176
192
  * it. The caller treats the projection as single-owner and never retains a
@@ -180,31 +196,56 @@ export interface Reducer<TEvent, TProjection> {
180
196
  /**
181
197
  * Build an empty initial projection. Called once per conversation node — a
182
198
  * Run node or a run-less input node — before any of that node's events are
183
- * folded.
199
+ * folded, and again on every refold of that node.
184
200
  */
185
201
  init(): TProjection;
186
202
  /**
187
203
  * Fold one TEvent into the projection and return the updated projection.
188
- * The reducer may mutate `state` in place.
204
+ * Invoked exactly once per event, in canonical order; the reducer may mutate
205
+ * `state` in place.
189
206
  */
190
207
  fold(state: TProjection, event: TEvent, meta: ReducerMeta): TProjection;
191
208
  }
192
209
 
210
+ /**
211
+ * A decoded event tagged with the wire direction it arrived on. The reducer
212
+ * folds this union (not a bare `TInput | TOutput`) so it can dispatch on
213
+ * `direction` rather than inspecting the event's shape. Direction is derived
214
+ * once, from the Ably message name, at decode time (see `toCodecEvents`) — the
215
+ * authoritative signal, since a single message is either `ai-input` or
216
+ * `ai-output` but never both.
217
+ * @template TInput - The codec's input union.
218
+ * @template TOutput - The codec's output union.
219
+ */
220
+ export type CodecEvent<TInput, TOutput> =
221
+ | {
222
+ /** The event arrived on the `ai-input` wire. */
223
+ readonly direction: 'input';
224
+ /** The decoded input event. */
225
+ readonly event: TInput;
226
+ }
227
+ | {
228
+ /** The event arrived on the `ai-output` wire. */
229
+ readonly direction: 'output';
230
+ /** The decoded output event. */
231
+ readonly event: TOutput;
232
+ };
233
+
193
234
  // ---------------------------------------------------------------------------
194
235
  // Encoder — direction-typed publication API
195
236
  // ---------------------------------------------------------------------------
196
237
 
197
238
  /** Options passed to a codec's `createEncoder` factory. */
198
239
  export interface EncoderOptions {
199
- /** Default clientId for all writes. */
200
- clientId?: string;
201
240
  /** Default extras (e.g. headers) merged into every Ably message. */
202
241
  extras?: Extras;
203
242
  /** Hook called before each Ably message is published. Mutate the message in place to add transport-level headers under `extras.ai`. */
204
243
  onMessage?: (message: Ably.Message) => void;
205
244
  /**
206
- * Default `codec-message-id` for messages where the event payload doesn't
207
- * supply one. Overridden by `WriteOptions.messageId` per-publish.
245
+ * Fallback domain message id surfaced to output escape hatches as
246
+ * `ctx.messageId` (e.g. the Vercel `start` hatch injects it when a chunk
247
+ * carries no `messageId` of its own). Unrelated to the wire
248
+ * codec-message-id transport header, which `WriteOptions.messageId` stamps.
208
249
  */
209
250
  messageId?: string;
210
251
  }
@@ -219,24 +260,24 @@ export interface EncoderOptions {
219
260
  export interface Encoder<TInput extends CodecInputEvent, TOutput extends CodecOutputEvent> {
220
261
  /**
221
262
  * Encode and publish a single client input on the `ai-input` wire.
222
- * Throws synchronously if the codec cannot encode the given input
263
+ * Rejects if the codec cannot encode the given input
223
264
  * variant.
224
265
  */
225
266
  publishInput(input: TInput, options?: WriteOptions): Promise<void>;
226
267
  /**
227
268
  * Encode and publish a single agent output on the `ai-output` wire.
228
- * Throws synchronously if the codec cannot encode the given output
269
+ * Rejects if the codec cannot encode the given output
229
270
  * variant.
230
271
  */
231
272
  publishOutput(output: TOutput, options?: WriteOptions): Promise<void>;
232
273
  /**
233
- * Cancel any in-progress streams and emit a codec-specific cancel signal.
234
- * Idempotent across repeated calls a second `cancel` is a no-op. Must not
235
- * be called after `close`; doing so throws because the encoder is already
236
- * closed.
237
- * @param reason - Optional reason string for the cancellation (e.g. 'cancelled').
274
+ * Close all in-progress streamed messages as cancelled (status:cancelled) and
275
+ * flush pending appends. Pure transport mechanics emits no codec output.
276
+ * Idempotent: streams already cancelled are not re-appended. Must not be
277
+ * called after `close`; doing so throws because the encoder is already closed.
278
+ * Run termination is signalled separately by the transport `ai-run-end` event.
238
279
  */
239
- cancel(reason?: string): Promise<void>;
280
+ cancelStreams(): Promise<void>;
240
281
  /** Flush pending appends and release encoder resources. */
241
282
  close(): Promise<void>;
242
283
  }
@@ -263,6 +304,12 @@ export interface DecodedMessage<TInput extends CodecInputEvent, TOutput extends
263
304
  * compaction, partial-history page boundary, rewind miss) synthesizes any
264
305
  * missing start events before deltas reach the SDK — the reducer always
265
306
  * sees a clean `(start, delta*, end)` sequence.
307
+ *
308
+ * Trackers are version-guarded: a delivery whose `Message.version.serial`
309
+ * is at or below the version already incorporated decodes to nothing. One
310
+ * decoder instance can therefore be shared by the live subscription and
311
+ * history hydration — whichever route delivers a message's content first
312
+ * wins, and the other route's covered deliveries are no-ops.
266
313
  */
267
314
  export interface Decoder<TInput extends CodecInputEvent, TOutput extends CodecOutputEvent> {
268
315
  /** Decode one Ably inbound message into the input/output halves. */
@@ -302,10 +349,11 @@ export interface CodecInputEvent {
302
349
  * Pointer to another codec-message this input references. The semantic
303
350
  * depends on `kind` — for `regenerate`, the assistant codec-message to
304
351
  * regenerate; codec-specific `kind`s may give it other meanings. The
305
- * input event itself does not create a fork — it requests one. The fork
352
+ * input event itself does not create a fork — it requests one: the
353
+ * transport reads `target` off the input (e.g. the client session maps a
354
+ * regenerate's target into its transport headers) and the fork
306
355
  * relationship is established on the agent's response (and on
307
- * `ai-run-start`), which the codec encoder maps `target` to via the
308
- * wire's `fork-of` header.
356
+ * `ai-run-start`).
309
357
  */
310
358
  target?: string;
311
359
  /**
@@ -359,7 +407,7 @@ export interface Regenerate extends CodecInputEvent {
359
407
  * The core is domain-independent: it knows only that this input amends the
360
408
  * assistant at `codecMessageId` and carries a codec-defined `payload`. The
361
409
  * shape of `payload` (e.g. the tool-call id and output value) is supplied
362
- * by the codec via `TPayload` see the Vercel layer's payload type.
410
+ * by the codec via `TPayload` (e.g. a tool-call id and output value).
363
411
  *
364
412
  * Codecs opt in to client-side tool resolution by including this variant
365
413
  * in their `TInput` union. Codecs whose domain model doesn't natively
@@ -382,7 +430,7 @@ export interface ToolResult<TPayload> extends CodecInputEvent {
382
430
  * Well-known input variant: client-published tool result (failure). The
383
431
  * tool ran and failed. Mutates the assistant codec-message addressed by
384
432
  * `codecMessageId`. The failure detail (e.g. tool-call id and error text)
385
- * is the codec's domain `payload` — see the Vercel layer's payload type.
433
+ * is the codec's domain `payload`.
386
434
  * @template TPayload - The codec's domain payload for a tool-result failure.
387
435
  */
388
436
  export interface ToolResultError<TPayload> extends CodecInputEvent {
@@ -400,7 +448,7 @@ export interface ToolResultError<TPayload> extends CodecInputEvent {
400
448
  * codec-message addressed by `codecMessageId` — flipping the targeted
401
449
  * tool call from pending-approval to approved or denied. The decision
402
450
  * detail (e.g. tool-call id, approved flag, reason) is the codec's domain
403
- * `payload` — see the Vercel layer's payload type.
451
+ * `payload`.
404
452
  *
405
453
  * Codecs may layer approval semantics on top of domain models that don't
406
454
  * natively support gating tool execution behind an approval — the codec
@@ -439,6 +487,16 @@ export type ToolResultErrorPayloadOf<TInput> = TInput extends ToolResultError<in
439
487
  */
440
488
  export type ToolApprovalResponsePayloadOf<TInput> = TInput extends ToolApprovalResponse<infer P> ? P : never;
441
489
 
490
+ /**
491
+ * Extract the domain message type (`TMessage`) carried by a codec's
492
+ * {@link UserMessage} member from its `TInput` union, or `never` if the codec
493
+ * has no user-message variant. Lets the core well-known input factories type
494
+ * `createUserMessage` from `TInput` alone, without a separate message type
495
+ * parameter.
496
+ * @template TInput - The codec's input union.
497
+ */
498
+ export type UserMessageOf<TInput> = TInput extends UserMessage<infer M> ? M : never;
499
+
442
500
  // ---------------------------------------------------------------------------
443
501
  // Codec output events — base shape for the output side
444
502
  // ---------------------------------------------------------------------------
@@ -451,9 +509,8 @@ export type ToolApprovalResponsePayloadOf<TInput> = TInput extends ToolApprovalR
451
509
  * fields on outputs without a breaking generic-arity change.
452
510
  *
453
511
  * The `type` discriminator is required on every variant so the codec's
454
- * reducer can switch on it. `AI.UIMessageChunk` already satisfies this
455
- * constraint structurally — its `type` literal is assignable to
456
- * `string` — so the Vercel codec needs no implementation changes.
512
+ * reducer can switch on it any domain union whose members carry a `type`
513
+ * string literal satisfies it structurally.
457
514
  *
458
515
  * No routing fields today: outputs carry no per-event `codecMessageId` /
459
516
  * `parent` / `forkOf` overrides. Those move onto this base when a concrete
@@ -512,7 +569,13 @@ export interface Codec<
512
569
  TOutput extends CodecOutputEvent,
513
570
  TProjection,
514
571
  TMessage,
515
- > extends Reducer<TInput | TOutput, TProjection> {
572
+ > extends Reducer<CodecEvent<TInput, TOutput>, TProjection> {
573
+ /**
574
+ * Optional Ably-Agent identifier. When present, the agent-registration path
575
+ * registers it on the channel (so traffic is attributed to this codec); when
576
+ * absent, the codec opts out of registration. Read directly by `registerAgent`.
577
+ */
578
+ readonly adapterTag?: string;
516
579
  /** Create a stateful encoder bound to the given channel. */
517
580
  createEncoder(channel: ChannelWriter, options?: EncoderOptions): Encoder<TInput, TOutput>;
518
581
  /** Create a stateful decoder for converting Ably inbound messages into typed inputs and outputs. */