@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,142 @@
1
+ /**
2
+ * Shared header-field bag helpers.
3
+ *
4
+ * The wire dispatch discriminator (`kind`) plus the symmetric field↔headers
5
+ * write and read used by the descriptor drivers. Centralised so encode and
6
+ * decode operate on the same record shape and the dispatch key has one home —
7
+ * and so the input drivers can reuse the same primitives as the output drivers.
8
+ */
9
+
10
+ import type { HeaderField } from './fields.js';
11
+ import type { OutputDescriptor, OutputEventDescriptor } from './output-descriptors.js';
12
+
13
+ /** The codec header carrying the SDK-controlled dispatch kind / stream family id. */
14
+ export const KIND_HEADER = 'kind';
15
+
16
+ /** The sentinel suffix marking a descriptor literal as a wildcard family. */
17
+ const WILDCARD_SUFFIX = '-*';
18
+
19
+ /**
20
+ * Derive a wildcard dispatch predicate from a descriptor literal: a literal
21
+ * ending in `-*` matches any value sharing its prefix, so the literal and its
22
+ * predicate can never disagree. Returns `undefined` for an exact literal.
23
+ * Shared by the output event builder and the input part builder so the `-*`
24
+ * sentinel rule lives in one place, next to the {@link partFor} that consumes it.
25
+ * @param literal - The declared descriptor literal (`type` / `partType`).
26
+ * @returns A prefix-match predicate for a wildcard literal, else `undefined`.
27
+ */
28
+ export const wildcardMatcher = (literal: string): ((value: string) => boolean) | undefined =>
29
+ literal.endsWith(WILDCARD_SUFFIX) ? (value: string): boolean => value.startsWith(literal.slice(0, -1)) : undefined;
30
+
31
+ /**
32
+ * The codec header carrying a batch part's sub-discriminator. A batch stamps it
33
+ * on every exploded part on encode; the decoder reads it back to resolve the
34
+ * matching part descriptor. Centralised so the key has one home across the
35
+ * input encode and decode drivers and cannot drift between them.
36
+ */
37
+ export const PART_TYPE_HEADER = 'partType';
38
+
39
+ /**
40
+ * Read the value at a declared field key off a source object.
41
+ * @param source - The object to index (a chunk, or a lensed sub-object such as a payload).
42
+ * @param key - The declared field key.
43
+ * @returns The value at `key`, typed `unknown`.
44
+ */
45
+ // CAST: a descriptor indexes a source object's props by a declared key. The
46
+ // source's indexed type isn't statically known here, but a descriptor only ever
47
+ // runs against the member it matches, so the value has the field's type at runtime.
48
+ export const prop = (source: object, key: string): unknown => (source as Record<string, unknown>)[key];
49
+
50
+ /**
51
+ * Build a codec-headers record from a source object through declared fields,
52
+ * seeded with the dispatch `kind`. Each field writes the value at its key on
53
+ * `source`; an optional `keys` subset restricts which fields are written.
54
+ * @param fields - The declared header fields.
55
+ * @param kindValue - The dispatch kind / stream family id to seed under {@link KIND_HEADER}.
56
+ * @param source - The object to read field values from (a chunk, or a lensed payload).
57
+ * @param keys - Optional subset of field keys to write; omit to write all.
58
+ * @returns The codec-headers record.
59
+ */
60
+ export const writeFields = (
61
+ fields: readonly HeaderField<unknown>[],
62
+ kindValue: string,
63
+ source: object,
64
+ keys?: readonly string[],
65
+ ): Record<string, string> => {
66
+ const rec: Record<string, string> = { [KIND_HEADER]: kindValue };
67
+ for (const field of fields) {
68
+ if (keys && !keys.includes(field.key)) continue;
69
+ field.write(rec, prop(source, field.key));
70
+ }
71
+ return rec;
72
+ };
73
+
74
+ /**
75
+ * Read declared fields out of a codec-headers record into a bag keyed by field key.
76
+ * A field that reads `undefined` (absent, with no default) contributes no key — the
77
+ * bag carries only the values that are actually present.
78
+ * @param fields - The declared header fields.
79
+ * @param headers - The inbound codec-tier headers.
80
+ * @returns A bag of the present field values, keyed by each field's key.
81
+ */
82
+ /** The structural slice of a part descriptor {@link partFor} dispatches on. */
83
+ interface PartDispatch {
84
+ /** The exact `partType` literal, or the `-*` wildcard literal for a family. */
85
+ partType: string;
86
+ /** Wildcard dispatch predicate; absent for an exact part. */
87
+ match?: (partType: string) => boolean;
88
+ }
89
+
90
+ /**
91
+ * Resolve the part descriptor for a `partType`: an exact non-wildcard match,
92
+ * else a wildcard whose derived predicate accepts it. Wildcards are excluded
93
+ * from the exact pass — only their predicate may route to them. Shared by the
94
+ * input encode and decode drivers.
95
+ * @param parts - The batch's part descriptor sub-table.
96
+ * @param partType - The `partType` to resolve (from `partTypeOf` on encode, the wire header on decode).
97
+ * @returns The matching part descriptor, or undefined when none matches.
98
+ */
99
+ export const partFor = <P extends PartDispatch>(parts: readonly P[], partType: string): P | undefined =>
100
+ parts.find((part) => !part.match && part.partType === partType) ?? parts.find((part) => part.match?.(partType));
101
+
102
+ /** An output descriptor set's event descriptors, split for dispatch. */
103
+ export interface OutputEventDispatch<U> {
104
+ /** Exact (non-wildcard) event descriptors, keyed by `type`. */
105
+ discreteByType: Map<string, OutputEventDescriptor<U>>;
106
+ /** Wildcard event descriptors, dispatched by their `match` predicate. */
107
+ wildcards: OutputEventDescriptor<U>[];
108
+ }
109
+
110
+ /**
111
+ * Partition an output descriptor set's `event` descriptors into an exact-type
112
+ * map and a wildcard list. Stream descriptors are skipped — each driver indexes
113
+ * those by its own key (phase on encode, kind on decode). Shared by the output
114
+ * encode and decode drivers so the exact-vs-wildcard split has one home.
115
+ * @template U - The codec's event union.
116
+ * @param descriptors - The full descriptor set (events + streamed families).
117
+ * @returns The event descriptors split into {@link OutputEventDispatch}.
118
+ */
119
+ export const partitionOutputEvents = <U extends { type: string }>(
120
+ descriptors: readonly OutputDescriptor<U>[],
121
+ ): OutputEventDispatch<U> => {
122
+ const discreteByType = new Map<string, OutputEventDescriptor<U>>();
123
+ const wildcards: OutputEventDescriptor<U>[] = [];
124
+ for (const descriptor of descriptors) {
125
+ if (descriptor.construct !== 'event') continue;
126
+ if (descriptor.match) wildcards.push(descriptor);
127
+ else discreteByType.set(descriptor.type, descriptor);
128
+ }
129
+ return { discreteByType, wildcards };
130
+ };
131
+
132
+ export const readFields = (
133
+ fields: readonly HeaderField<unknown>[],
134
+ headers: Record<string, string>,
135
+ ): Record<string, unknown> => {
136
+ const bag: Record<string, unknown> = {};
137
+ for (const field of fields) {
138
+ const value = field.read(headers);
139
+ if (value !== undefined) bag[field.key] = value;
140
+ }
141
+ return bag;
142
+ };
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Typed header-field bindings.
3
+ *
4
+ * A {@link HeaderField} binds a codec header key to its value type **once**,
5
+ * and exposes a symmetric {@link HeaderField.read | read} / {@link
6
+ * HeaderField.write | write} pair over a raw `Record<string, string>` headers
7
+ * record. Because a single binding drives both the encode and decode side, the
8
+ * header key and its value type stay in lockstep across directions — a key
9
+ * cannot be misspelled on one side and silently read as absent on the other.
10
+ *
11
+ * This is deliberately **not** a schema library: it is a thin bidirectional
12
+ * string (de)serializer over the headers record. The SDK ships no hard runtime
13
+ * dependencies, and a schema approach would force re-declaring peer-SDK-owned
14
+ * types. The four constructors cover every header value shape the codecs use:
15
+ *
16
+ * - {@link strField} — string values, optional default.
17
+ * - {@link boolField} — `"true"`/`"false"` booleans, optional default.
18
+ * - {@link jsonField} — JSON-serialized structured values.
19
+ * - {@link enumField} — string values validated against an allow-list with a
20
+ * fallback (e.g. a finish reason).
21
+ *
22
+ * Passing a default to `strField`/`boolField` makes the field **total**: its
23
+ * `read` returns `V` rather than `V | undefined`, for required headers that
24
+ * should always decode to a concrete value.
25
+ */
26
+
27
+ import { parseBool, parseJson } from '../../utils.js';
28
+
29
+ /**
30
+ * A header key bound to its value type, with symmetric read/write over a raw
31
+ * headers record. Created via {@link strField}, {@link boolField}, {@link
32
+ * jsonField}, or {@link enumField}.
33
+ *
34
+ * The `key` plays a dual role in descriptor `fields` tables: it is the wire
35
+ * header key AND the property name the drivers read off the source object on
36
+ * encode and write back into the rebuilt object on decode. {@link FieldFor}
37
+ * enforces this — a declared field's key must name a real property of the
38
+ * member it lenses onto.
39
+ * @template V - The decoded value type this field reads and writes.
40
+ * @template K - The header key literal (preserved so {@link FieldFor} can match it against the member's property names).
41
+ */
42
+ export interface HeaderField<V, K extends string = string> {
43
+ /** The raw header key this field reads from and writes to — also the source/rebuilt property name in descriptor tables. */
44
+ readonly key: K;
45
+ /**
46
+ * Read and decode this field's value from a headers record.
47
+ * @param headers - The raw codec headers record to read from.
48
+ * @returns The decoded value. For defaulted/validated fields this is total
49
+ * (the default/fallback is returned when the header is absent or invalid);
50
+ * otherwise `undefined` when the header is absent.
51
+ */
52
+ read(headers: Record<string, string>): V;
53
+ /**
54
+ * Encode and write this field's value into a headers record, mutating it in
55
+ * place. `undefined` (and `null`, for JSON values), and values whose runtime
56
+ * type doesn't match the field, are skipped — the key is left unset rather
57
+ * than written. The parameter is `unknown` (not `V`) so a field keeps `V` in
58
+ * a single covariant position (`read`); this lets heterogeneous fields share a
59
+ * `HeaderField<unknown>[]` array, which the descriptor drivers rely on. At a
60
+ * typed call site the caller still passes a `V`.
61
+ * @param headers - The headers record to mutate.
62
+ * @param value - The value to encode and set.
63
+ */
64
+ write(headers: Record<string, string>, value: unknown): void;
65
+ }
66
+
67
+ /**
68
+ * Symmetric codec for a descriptor's wire `data`. Many wire payloads are object
69
+ * envelopes a decode reads several chunk props out of (e.g. `{ errorText, input }`),
70
+ * so a single field can't model them. `encode` produces the wire data from the
71
+ * chunk; `decode` returns the chunk props the envelope contributes, merged into
72
+ * the rebuilt chunk by the driver.
73
+ * @template C - The narrowed chunk member.
74
+ */
75
+ export interface DataCodec<C> {
76
+ /** Produce the wire `data` from the chunk. */
77
+ encode: (chunk: C) => unknown;
78
+ /**
79
+ * Extract the chunk props this envelope contributes from the wire `data`.
80
+ * Undefined-valued props are stripped when the driver rebuilds the object —
81
+ * every rebuild seam (output chunk, input payload, batch part) applies the
82
+ * same rule, since absent and undefined are indistinguishable on the wire.
83
+ */
84
+ decode: (data: unknown) => Partial<C>;
85
+ }
86
+
87
+ /**
88
+ * The header fields a descriptor may declare against member `C`. For each
89
+ * string-keyed property of `C`, a field is acceptable when its key IS that
90
+ * property name and its value type can hold the property. A mistyped key or a
91
+ * wrong-typed field (e.g. a `boolField` on a string property) is a compile
92
+ * error instead of a silently absent header.
93
+ * @template C - The member (chunk, payload, or part) the fields lens onto.
94
+ */
95
+ export type FieldFor<C> = {
96
+ [K in keyof C & string]-?: HeaderField<C[K] | undefined, K>;
97
+ }[keyof C & string];
98
+
99
+ /**
100
+ * Bind a string-valued header field.
101
+ * @param key - The header key (and source property name in descriptor tables).
102
+ * @returns A field whose `read` yields `string | undefined` (absent → `undefined`).
103
+ */
104
+ export function strField<K extends string>(key: K): HeaderField<string | undefined, K>;
105
+ /**
106
+ * Bind a string-valued header field with a default, making it total.
107
+ * @param key - The header key (and source property name in descriptor tables).
108
+ * @param fallback - Value returned by `read` when the header is absent.
109
+ * @returns A field whose `read` yields `string` (absent → `fallback`).
110
+ */
111
+ export function strField<K extends string>(key: K, fallback: string): HeaderField<string, K>;
112
+ export function strField<K extends string>(key: K, fallback?: string): HeaderField<string | undefined, K> {
113
+ return {
114
+ key,
115
+ read: (headers) => headers[key] ?? fallback,
116
+ write: (headers, value) => {
117
+ if (typeof value === 'string') headers[key] = value;
118
+ },
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Bind a boolean-valued header field, serialized as `"true"`/`"false"`.
124
+ * @param key - The header key (and source property name in descriptor tables).
125
+ * @returns A field whose `read` yields `boolean | undefined` (absent → `undefined`).
126
+ */
127
+ export function boolField<K extends string>(key: K): HeaderField<boolean | undefined, K>;
128
+ /**
129
+ * Bind a boolean-valued header field with a default, making it total.
130
+ * @param key - The header key (and source property name in descriptor tables).
131
+ * @param fallback - Value returned by `read` when the header is absent.
132
+ * @returns A field whose `read` yields `boolean` (absent → `fallback`).
133
+ */
134
+ export function boolField<K extends string>(key: K, fallback: boolean): HeaderField<boolean, K>;
135
+ export function boolField<K extends string>(key: K, fallback?: boolean): HeaderField<boolean | undefined, K> {
136
+ return {
137
+ key,
138
+ read: (headers) => parseBool(headers[key]) ?? fallback,
139
+ write: (headers, value) => {
140
+ if (typeof value === 'boolean') headers[key] = String(value);
141
+ },
142
+ };
143
+ }
144
+
145
+ /**
146
+ * Bind a JSON-serialized header field. The value is written with
147
+ * `JSON.stringify` and read back with `JSON.parse`; malformed JSON reads as
148
+ * `undefined`. The decoded shape is a trust boundary — the caller asserts it
149
+ * via the `V` type parameter.
150
+ * @template V - The expected decoded shape of the JSON value.
151
+ * @template K - The header key literal. Inferred when `V` is omitted; pass it explicitly alongside `V` when the field participates in a typed descriptor `fields` table.
152
+ * @param key - The header key (and source property name in descriptor tables).
153
+ * @returns A field whose `read` yields `V | undefined` (absent or malformed → `undefined`).
154
+ */
155
+ export const jsonField = <V, K extends string = string>(key: K): HeaderField<V | undefined, K> => ({
156
+ key,
157
+ // CAST: header values are wire data parsed via JSON.parse — a trust
158
+ // boundary. The caller declares the expected shape through `V`; malformed
159
+ // JSON reads back as `undefined` (parseJson swallows the parse error).
160
+ read: (headers) => parseJson(headers[key]) as V | undefined,
161
+ write: (headers, value) => {
162
+ // Skip undefined and null so an absent value leaves the key unset rather
163
+ // than serializing to "null".
164
+ if (value !== undefined && value !== null) headers[key] = JSON.stringify(value);
165
+ },
166
+ });
167
+
168
+ /**
169
+ * Bind a string-valued header field validated against a fixed allow-list,
170
+ * falling back to a given value when the header is absent or unrecognized. Use
171
+ * for headers with a small closed set of valid values (e.g. a finish reason).
172
+ * @template T - The union of allowed string literals, inferred from `allowed`.
173
+ * @template K - The header key literal, inferred from `key`.
174
+ * @param key - The header key (and source property name in descriptor tables).
175
+ * @param allowed - The exhaustive list of valid values.
176
+ * @param fallback - Value returned by `read` when the header is absent or not in `allowed`.
177
+ * @returns A total field whose `read` yields one of the allowed literals.
178
+ */
179
+ export const enumField = <const T extends string, K extends string>(
180
+ key: K,
181
+ allowed: readonly T[],
182
+ fallback: NoInfer<T>,
183
+ ): HeaderField<T, K> => ({
184
+ key,
185
+ read: (headers) => {
186
+ const raw = headers[key];
187
+ // find returns the matched literal (typed T) or undefined — no cast needed.
188
+ return allowed.find((candidate) => candidate === raw) ?? fallback;
189
+ },
190
+ write: (headers, value) => {
191
+ if (typeof value === 'string') headers[key] = value;
192
+ },
193
+ });
@@ -1,6 +1,7 @@
1
1
  export type {
2
2
  ChannelWriter,
3
3
  Codec,
4
+ CodecEvent,
4
5
  CodecInputEvent,
5
6
  CodecMessage,
6
7
  CodecOutputEvent,
@@ -33,3 +34,45 @@ export { createDecoderCore } from './decoder.js';
33
34
  // Lifecycle tracker
34
35
  export type { LifecycleTracker, PhaseConfig } from './lifecycle-tracker.js';
35
36
  export { createLifecycleTracker } from './lifecycle-tracker.js';
37
+
38
+ // Typed header-field bindings
39
+ export type { DataCodec, FieldFor, HeaderField } from './fields.js';
40
+ export { boolField, enumField, jsonField, strField } from './fields.js';
41
+
42
+ // Well-known input factories (merged into every codec by defineCodec)
43
+ export type { WellKnownInputFactories } from './well-known-inputs.js';
44
+
45
+ // Output descriptor authoring surface
46
+ export type {
47
+ EscapeHatchCore,
48
+ HeaderBuilder,
49
+ OutputDecodeContext,
50
+ OutputDescriptor,
51
+ OutputEncodeHatchContext,
52
+ OutputEventSpec,
53
+ OutputStreamEndContext,
54
+ OutputStreamSpec,
55
+ } from './output-descriptors.js';
56
+
57
+ // Input descriptor authoring surface
58
+ export type {
59
+ BatchAssembleContext,
60
+ BatchMessageHeaders,
61
+ BatchSpec,
62
+ InputDescriptor,
63
+ InputEventSpec,
64
+ PartBuilder,
65
+ PartSpec,
66
+ } from './input-descriptors.js';
67
+
68
+ // Codec composition factory
69
+ export type {
70
+ CodecReducer,
71
+ DefineCodecConfig,
72
+ DefinedCodec,
73
+ InputBuilder,
74
+ LifecycleDiscreteContext,
75
+ LifecyclePolicy,
76
+ OutputBuilder,
77
+ } from './define-codec.js';
78
+ export { defineCodec } from './define-codec.js';
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Generic input decode driver over an input descriptor set — the input-side
3
+ * sibling of {@link import('./output-descriptor-decoder.js')}.
4
+ *
5
+ * Rebuilds inputs from one inbound `ai-input` message, dispatching on the codec
6
+ * `kind` header. A single `event` rebuilds its field bag (and `data`) and wraps
7
+ * it into the `{ kind, codecMessageId, payload }` envelope; `wireOnly` events
8
+ * decode to `[]`. A
9
+ * `batch` reads the `partType` sub-discriminator, rebuilds the part via its
10
+ * sub-table, `assemble`s it into a one-part input, and the driver stamps the
11
+ * `kind` plus the codec-message-id reconstructed from the transport header.
12
+ *
13
+ * Returns bare `TInput[]`, never `CodecEvent[]` — direction tagging is
14
+ * core-owned, downstream at the decode→fold seam.
15
+ */
16
+
17
+ import { HEADER_CODEC_MESSAGE_ID } from '../../constants.js';
18
+ import { stripUndefined } from '../../utils.js';
19
+ import { PART_TYPE_HEADER, partFor, readFields } from './field-bag.js';
20
+ import type {
21
+ BatchDescriptor,
22
+ InputDecodeContext,
23
+ InputDescriptor,
24
+ InputEventDescriptor,
25
+ } from './input-descriptors.js';
26
+
27
+ /** Decodes inbound `ai-input` messages of union `U` from an input descriptor set. */
28
+ export interface InputDescriptorDecoder<U> {
29
+ /**
30
+ * Rebuild zero or more inputs from one inbound `ai-input` message.
31
+ * @param ctx - The inbound message context (codec kind, data, header tiers).
32
+ * @returns The decoded inputs (empty when no descriptor matches or the input is wire-only).
33
+ */
34
+ decode(ctx: InputDecodeContext): U[];
35
+ }
36
+
37
+ /**
38
+ * Build an input decode driver for an input descriptor set.
39
+ * @template U - The codec's input union.
40
+ * @param descriptors - The input descriptor set (events + batches).
41
+ * @returns An {@link InputDescriptorDecoder} that reconstructs inputs from the wire.
42
+ */
43
+ export const createInputDescriptorDecoder = <U extends { kind: string }>(
44
+ descriptors: readonly InputDescriptor<U>[],
45
+ ): InputDescriptorDecoder<U> => {
46
+ const byKind = new Map<string, InputDescriptor<U>>();
47
+ for (const descriptor of descriptors) byKind.set(descriptor.kind, descriptor);
48
+
49
+ const decodeEvent = (descriptor: InputEventDescriptor, ctx: InputDecodeContext): U[] => {
50
+ if (descriptor.wireOnly) return [];
51
+
52
+ const bag = readFields(descriptor.fields, ctx.codecHeaders);
53
+ if (descriptor.data) Object.assign(bag, descriptor.data.decode(ctx.data));
54
+
55
+ const codecMessageId = ctx.transportHeaders[HEADER_CODEC_MESSAGE_ID] ?? '';
56
+ // The payload bag is stripped of undefined-valued props — the same rule
57
+ // every rebuild seam applies to its innermost bag (absent and undefined
58
+ // are indistinguishable on the wire). The envelope keys are always defined.
59
+ // CAST: the rebuild seam — `bag` is assembled from the descriptor's declared
60
+ // fields and data codec onto the payload, so the `{ kind, codecMessageId, payload }`
61
+ // envelope conforms to the matched member by construction.
62
+ return [{ kind: descriptor.kind, codecMessageId, payload: stripUndefined(bag) } as unknown as U];
63
+ };
64
+
65
+ const decodeBatch = (descriptor: BatchDescriptor<U>, ctx: InputDecodeContext): U[] => {
66
+ const partType = ctx.codecHeaders[PART_TYPE_HEADER] ?? '';
67
+ const partDesc = partFor(descriptor.parts, partType);
68
+ if (!partDesc) return [];
69
+
70
+ const bag = readFields(partDesc.fields, ctx.codecHeaders);
71
+ if (partDesc.data) Object.assign(bag, partDesc.data.decode(ctx.data));
72
+ bag.type = partType;
73
+
74
+ // `assemble` takes the erased part (`unknown`); `bag` is the part rebuilt from
75
+ // its declared fields/data plus the `partType` written to the domain `type`
76
+ // field. The header tiers carry the per-message metadata (id, role, …) the
77
+ // batch stamped on every part, so `assemble` can rebuild the message envelope.
78
+ const partial = descriptor.assemble(stripUndefined(bag), {
79
+ codecHeaders: ctx.codecHeaders,
80
+ transportHeaders: ctx.transportHeaders,
81
+ });
82
+ // CAST: the driver stamps the shared `kind` onto the assembled one-part input; together
83
+ // they complete the matched member. A batch creates a new message (not addressed by a
84
+ // codec-message-id, unlike single `event`s), so none is stamped — the per-message
85
+ // identity rides the transport header and is recovered by `assemble` when needed.
86
+ return [{ kind: descriptor.kind, ...partial } as unknown as U];
87
+ };
88
+
89
+ return {
90
+ decode: (ctx) => {
91
+ const descriptor = byKind.get(ctx.codecKind);
92
+ if (!descriptor) return [];
93
+ if (descriptor.construct === 'event') return decodeEvent(descriptor, ctx);
94
+ return decodeBatch(descriptor, ctx);
95
+ },
96
+ };
97
+ };
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Generic input encode driver over an input descriptor set — the input-side
3
+ * sibling of {@link import('./output-descriptor-encoder.js')}.
4
+ *
5
+ * Builds a `kind`→descriptor registry once, then routes each input: a single
6
+ * `event` publishes one discrete message (fields/data lensed onto the member's
7
+ * `payload`, or kind-only when `wireOnly`); a `batch` explodes the domain
8
+ * message into one wire event per part and publishes them atomically, with a
9
+ * built-in ≥1-event guarantee so the codec-message-id and role survive an empty
10
+ * decomposition. Headers are always built through the descriptor's declared
11
+ * fields ({@link writeFields}), so the imperative paths can't drift.
12
+ */
13
+
14
+ import * as Ably from 'ably';
15
+
16
+ import { ErrorCode } from '../../errors.js';
17
+ import { KIND_HEADER, PART_TYPE_HEADER, partFor, prop, writeFields } from './field-bag.js';
18
+ import type {
19
+ BatchDescriptor,
20
+ BatchMessageHeaders,
21
+ InputDescriptor,
22
+ InputEncodeContext,
23
+ InputEncoderCore,
24
+ InputEventDescriptor,
25
+ } from './input-descriptors.js';
26
+ import type { MessagePayload } from './types.js';
27
+
28
+ /** Encodes inputs of union `U` to channel operations via an input descriptor set. */
29
+ export interface InputDescriptorEncoder<U> {
30
+ /**
31
+ * Encode one input through its descriptor.
32
+ * @param input - The input to encode.
33
+ * @param core - The input encoder core to publish through.
34
+ * @param ctx - Per-write context (write options, carrying the codec-message-id).
35
+ * @returns A promise resolving when the publish operation completes.
36
+ */
37
+ encode(input: U, core: InputEncoderCore, ctx: InputEncodeContext): Promise<void>;
38
+ }
39
+
40
+ // Layer the batch's per-message transport headers onto a part payload, if any.
41
+ const withMessageTransport = (payload: MessagePayload, message: BatchMessageHeaders | undefined): MessagePayload =>
42
+ message?.transportHeaders === undefined
43
+ ? payload
44
+ : { ...payload, transportHeaders: { ...payload.transportHeaders, ...message.transportHeaders } };
45
+
46
+ /**
47
+ * Build an input encode driver for an input descriptor set bound to a wire name.
48
+ * @template U - The codec's input union.
49
+ * @param descriptors - The input descriptor set (events + batches).
50
+ * @param wireName - The Ably message name for the input direction (`ai-input`).
51
+ * @returns An {@link InputDescriptorEncoder} routing each input through its descriptor.
52
+ */
53
+ export const createInputDescriptorEncoder = <U extends { kind: string }>(
54
+ descriptors: readonly InputDescriptor<U>[],
55
+ wireName: string,
56
+ ): InputDescriptorEncoder<U> => {
57
+ const byKind = new Map<string, InputDescriptor<U>>();
58
+ for (const descriptor of descriptors) byKind.set(descriptor.kind, descriptor);
59
+
60
+ const encodeEvent = async (
61
+ descriptor: InputEventDescriptor,
62
+ input: U,
63
+ core: InputEncoderCore,
64
+ ctx: InputEncodeContext,
65
+ ): Promise<void> => {
66
+ if (descriptor.wireOnly) {
67
+ // Kind only: no fields, no data — the parent/target ride transport headers.
68
+ await core.publishDiscrete(
69
+ { name: wireName, data: '', codecHeaders: { [KIND_HEADER]: descriptor.kind } },
70
+ ctx.opts,
71
+ );
72
+ return;
73
+ }
74
+ // A non-wireOnly input nests its domain data under `payload` — fields and
75
+ // data are authored against it. Fail fast on a payload-less member instead
76
+ // of silently publishing empty data the decoder can't rebuild from.
77
+ const source = prop(input, 'payload');
78
+ if (typeof source !== 'object' || source === null) {
79
+ throw new Ably.ErrorInfo(
80
+ `unable to encode input; event '${descriptor.kind}' carries no payload object — declare it wireOnly or use an encode escape hatch`,
81
+ ErrorCode.InvalidArgument,
82
+ 400,
83
+ );
84
+ }
85
+ const codecHeaders = writeFields(descriptor.fields, descriptor.kind, source);
86
+ const data = descriptor.data ? descriptor.data.encode(source) : '';
87
+ await core.publishDiscrete({ name: wireName, data, codecHeaders }, ctx.opts);
88
+ };
89
+
90
+ const encodeBatch = async (
91
+ descriptor: BatchDescriptor<U>,
92
+ input: U,
93
+ core: InputEncoderCore,
94
+ ctx: InputEncodeContext,
95
+ ): Promise<void> => {
96
+ // Per-message headers (e.g. message id, role) are stamped on every part so
97
+ // the decode side can reconstruct the shared message envelope from any one.
98
+ const message = descriptor.messageHeaders?.(input);
99
+ const payloads: MessagePayload[] = [];
100
+ for (const part of descriptor.explode(input)) {
101
+ const partType = descriptor.partTypeOf(part);
102
+ const partDesc = partFor(descriptor.parts, partType);
103
+ if (!partDesc) continue;
104
+ // CAST: a part is indexed by its declared fields; the part descriptor only
105
+ // runs against the part its predicate/literal matched, so the source has the
106
+ // field's type at runtime. The wire `partType` is the resolved part type.
107
+ const source = part as object;
108
+ const codecHeaders = {
109
+ ...writeFields(partDesc.fields, descriptor.kind, source),
110
+ ...message?.codecHeaders,
111
+ [PART_TYPE_HEADER]: partType,
112
+ };
113
+ const data = partDesc.data ? partDesc.data.encode(part) : '';
114
+ payloads.push(withMessageTransport({ name: wireName, data, codecHeaders }, message));
115
+ }
116
+
117
+ if (payloads.length === 0) {
118
+ // ≥1-event guarantee: emit one bare part so the per-message headers (e.g.
119
+ // the message id and role) reach the wire even when no exploded part
120
+ // matched a descriptor. This fallback carries no partType, so the batch
121
+ // decode path yields no input for it — a codec that needs an empty
122
+ // message to round-trip must guarantee ≥1 encodable part in `explode`
123
+ // (e.g. by substituting a canonical empty part).
124
+ payloads.push(
125
+ withMessageTransport(
126
+ { name: wireName, data: '', codecHeaders: { [KIND_HEADER]: descriptor.kind, ...message?.codecHeaders } },
127
+ message,
128
+ ),
129
+ );
130
+ }
131
+
132
+ await core.publishDiscreteBatch(payloads, ctx.opts);
133
+ };
134
+
135
+ return {
136
+ encode: async (input, core, ctx) => {
137
+ const descriptor = byKind.get(input.kind);
138
+ if (!descriptor) {
139
+ throw new Ably.ErrorInfo(
140
+ `unable to publish; unsupported input kind '${input.kind}'`,
141
+ ErrorCode.InvalidArgument,
142
+ 400,
143
+ );
144
+ }
145
+ await (descriptor.construct === 'event'
146
+ ? encodeEvent(descriptor, input, core, ctx)
147
+ : encodeBatch(descriptor, input, core, ctx));
148
+ },
149
+ };
150
+ };