@genkit-ai/a2ui 0.5.0-rc.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 (51) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +314 -0
  3. package/lib/catalog.d.mts +70 -0
  4. package/lib/catalog.d.ts +70 -0
  5. package/lib/catalog.js +285 -0
  6. package/lib/catalog.js.map +1 -0
  7. package/lib/catalog.mjs +256 -0
  8. package/lib/catalog.mjs.map +1 -0
  9. package/lib/client.d.mts +59 -0
  10. package/lib/client.d.ts +59 -0
  11. package/lib/client.js +82 -0
  12. package/lib/client.js.map +1 -0
  13. package/lib/client.mjs +55 -0
  14. package/lib/client.mjs.map +1 -0
  15. package/lib/index.d.mts +8 -0
  16. package/lib/index.d.ts +8 -0
  17. package/lib/index.js +62 -0
  18. package/lib/index.js.map +1 -0
  19. package/lib/index.mjs +36 -0
  20. package/lib/index.mjs.map +1 -0
  21. package/lib/loader.d.mts +70 -0
  22. package/lib/loader.d.ts +70 -0
  23. package/lib/loader.js +92 -0
  24. package/lib/loader.js.map +1 -0
  25. package/lib/loader.mjs +61 -0
  26. package/lib/loader.mjs.map +1 -0
  27. package/lib/middleware.d.mts +112 -0
  28. package/lib/middleware.d.ts +112 -0
  29. package/lib/middleware.js +259 -0
  30. package/lib/middleware.js.map +1 -0
  31. package/lib/middleware.mjs +240 -0
  32. package/lib/middleware.mjs.map +1 -0
  33. package/lib/parser.d.mts +132 -0
  34. package/lib/parser.d.ts +132 -0
  35. package/lib/parser.js +282 -0
  36. package/lib/parser.js.map +1 -0
  37. package/lib/parser.mjs +260 -0
  38. package/lib/parser.mjs.map +1 -0
  39. package/lib/part.d.mts +56 -0
  40. package/lib/part.d.ts +56 -0
  41. package/lib/part.js +52 -0
  42. package/lib/part.js.map +1 -0
  43. package/lib/part.mjs +26 -0
  44. package/lib/part.mjs.map +1 -0
  45. package/lib/types.d.mts +153 -0
  46. package/lib/types.d.ts +153 -0
  47. package/lib/types.js +38 -0
  48. package/lib/types.js.map +1 -0
  49. package/lib/types.mjs +11 -0
  50. package/lib/types.mjs.map +1 -0
  51. package/package.json +80 -0
package/lib/parser.js ADDED
@@ -0,0 +1,282 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var parser_exports = {};
20
+ __export(parser_exports, {
21
+ A2uiStreamParser: () => A2uiStreamParser
22
+ });
23
+ module.exports = __toCommonJS(parser_exports);
24
+ var import_catalog = require("./catalog.js");
25
+ var import_types = require("./types.js");
26
+ const OPEN_FENCE_RE = /```a2ui[ \t]*\r?\n/i;
27
+ const PARTIAL_OPEN_FENCE_RE = /(?:`|``|```(?:a(?:2(?:u(?:i[ \t]*\r?)?)?)?)?)$/i;
28
+ const CLOSE_FENCE_RE = /(^|\n)[ \t]*```/;
29
+ class A2uiStreamParser {
30
+ constructor(options) {
31
+ this.options = options;
32
+ }
33
+ buffer = "";
34
+ inBlock = false;
35
+ /** Stable surface id for the current block (placeholders map to this). */
36
+ currentSurfaceId = null;
37
+ /** Feeds a chunk of model text, returning prose + any completed blocks. */
38
+ push(text) {
39
+ this.buffer += text;
40
+ return this.drain(false);
41
+ }
42
+ /** Drains any remaining buffered content at end of stream. */
43
+ flush() {
44
+ return this.drain(true);
45
+ }
46
+ drain(final) {
47
+ const segments = [];
48
+ let proseBuf = "";
49
+ const flushProse = () => {
50
+ if (proseBuf) {
51
+ segments.push({ prose: proseBuf });
52
+ proseBuf = "";
53
+ }
54
+ };
55
+ for (; ; ) {
56
+ if (!this.inBlock) {
57
+ const open = this.buffer.match(OPEN_FENCE_RE);
58
+ if (!open) {
59
+ if (final) {
60
+ proseBuf += this.buffer;
61
+ this.buffer = "";
62
+ } else {
63
+ const partial = this.buffer.match(PARTIAL_OPEN_FENCE_RE);
64
+ const keep = partial ? partial[0].length : 0;
65
+ const safeLen = this.buffer.length - keep;
66
+ if (safeLen > 0) {
67
+ proseBuf += this.buffer.slice(0, safeLen);
68
+ this.buffer = this.buffer.slice(safeLen);
69
+ }
70
+ }
71
+ break;
72
+ }
73
+ proseBuf += this.buffer.slice(0, open.index);
74
+ this.buffer = this.buffer.slice(open.index + open[0].length);
75
+ this.inBlock = true;
76
+ this.currentSurfaceId = this.options.surfaceId();
77
+ continue;
78
+ }
79
+ const close = this.buffer.match(CLOSE_FENCE_RE);
80
+ if (!close) {
81
+ if (final) {
82
+ const batch2 = this.finalizeBlock(this.buffer);
83
+ if (batch2) {
84
+ flushProse();
85
+ segments.push({ envelopes: batch2 });
86
+ }
87
+ this.buffer = "";
88
+ this.inBlock = false;
89
+ }
90
+ break;
91
+ }
92
+ const blockText = this.buffer.slice(0, close.index);
93
+ this.buffer = this.buffer.slice(close.index + close[0].length);
94
+ this.buffer = this.buffer.replace(/^[ \t]*\r?\n/, "");
95
+ this.inBlock = false;
96
+ const batch = this.finalizeBlock(blockText);
97
+ if (batch) {
98
+ flushProse();
99
+ segments.push({ envelopes: batch });
100
+ }
101
+ continue;
102
+ }
103
+ flushProse();
104
+ let prose = "";
105
+ const envelopeBatches = [];
106
+ for (const seg of segments) {
107
+ if ("prose" in seg) prose += seg.prose;
108
+ else envelopeBatches.push(seg.envelopes);
109
+ }
110
+ return { segments, prose, envelopeBatches };
111
+ }
112
+ /**
113
+ * Handles a validation failure according to the configured `validate` mode:
114
+ * throws in `'strict'`, logs a warning in `'warn'` (the middleware default),
115
+ * and is silent in `'off'`. Always returns `null` so callers can
116
+ * `return this.reject(...)`.
117
+ */
118
+ reject(message) {
119
+ const full = `A2UI: ${message}`;
120
+ if (this.options.validate === "off") return null;
121
+ if (this.options.validate === "warn") {
122
+ console.warn(`${full} (dropping block/envelope)`);
123
+ return null;
124
+ }
125
+ throw new Error(full);
126
+ }
127
+ /** Parses, validates, and normalizes one block's JSON into envelopes. */
128
+ finalizeBlock(raw) {
129
+ const surfaceId = this.currentSurfaceId ?? this.options.surfaceId();
130
+ this.currentSurfaceId = null;
131
+ const text = raw.trim();
132
+ if (!text) return null;
133
+ let parsed;
134
+ try {
135
+ parsed = JSON.parse(text);
136
+ } catch (e) {
137
+ return this.reject(
138
+ `failed to parse envelope block as JSON: ${e.message}`
139
+ );
140
+ }
141
+ const envelopes = Array.isArray(parsed) ? parsed : [parsed];
142
+ const out = [];
143
+ for (const env of envelopes) {
144
+ const normalized = this.normalizeEnvelope(env, surfaceId);
145
+ if (normalized) out.push(normalized);
146
+ }
147
+ if (out.length === 0) return null;
148
+ const hasCreate = out.some(isCreateSurface);
149
+ if (hasCreate) {
150
+ for (const e of out) forceSurfaceId(e, surfaceId);
151
+ const err = this.validateRoot(out);
152
+ if (err) return this.reject(err);
153
+ return out;
154
+ }
155
+ const targetId = out.map(envelopeSurfaceId).find((id) => id !== void 0) ?? surfaceId;
156
+ const isFreshRender = targetId === surfaceId;
157
+ if (isFreshRender) {
158
+ const err = this.validateRoot(out);
159
+ if (err) return this.reject(err);
160
+ out.unshift({
161
+ version: this.options.version ?? import_types.A2UI_VERSION,
162
+ createSurface: {
163
+ surfaceId,
164
+ catalogId: this.options.catalog?.id ?? ""
165
+ }
166
+ });
167
+ }
168
+ return out;
169
+ }
170
+ /**
171
+ * Validates a single envelope, substitutes the real surface id for the
172
+ * placeholder, and stamps the protocol version.
173
+ */
174
+ normalizeEnvelope(env, surfaceId) {
175
+ if (typeof env !== "object" || env === null) {
176
+ return this.reject("envelope must be an object.");
177
+ }
178
+ const e = env;
179
+ const version = e.version ?? this.options.version ?? import_types.A2UI_VERSION;
180
+ const swapSurfaceId = (payload) => {
181
+ if (!payload) return;
182
+ if (payload.surfaceId === void 0 || payload.surfaceId === import_catalog.SURFACE_ID_PLACEHOLDER || payload.surfaceId === "") {
183
+ payload.surfaceId = surfaceId;
184
+ }
185
+ };
186
+ const normalized = () => ({ ...env, version });
187
+ if (e.createSurface) {
188
+ swapSurfaceId(e.createSurface);
189
+ return normalized();
190
+ }
191
+ if (e.updateComponents) {
192
+ swapSurfaceId(e.updateComponents);
193
+ if (this.options.validate !== "off") {
194
+ const err = this.validateComponents(e.updateComponents.components);
195
+ if (err) return this.reject(err);
196
+ }
197
+ return normalized();
198
+ }
199
+ if (e.updateDataModel) {
200
+ swapSurfaceId(e.updateDataModel);
201
+ return normalized();
202
+ }
203
+ if (e.deleteSurface) {
204
+ swapSurfaceId(e.deleteSurface);
205
+ return normalized();
206
+ }
207
+ return this.reject(
208
+ `unknown envelope type (keys: ${Object.keys(e).join(", ")}).`
209
+ );
210
+ }
211
+ /**
212
+ * Ensures every component references a known catalog component. Returns an
213
+ * error message describing the first problem found, or `null` if valid.
214
+ *
215
+ * This checks component *type names* against the catalog only. It intentionally
216
+ * does NOT enforce the "must contain a root" protocol rule — that applies only
217
+ * to full-surface renders and is enforced at the batch level in
218
+ * {@link finalizeBlock} (an incremental update to an existing surface may
219
+ * legitimately patch a subtree without re-declaring `root`).
220
+ */
221
+ validateComponents(components) {
222
+ const catalog = this.options.catalog;
223
+ if (!catalog) return null;
224
+ if (!Array.isArray(components)) {
225
+ return "updateComponents.components must be an array.";
226
+ }
227
+ const known = new Set(catalog.components.map((c) => c.name));
228
+ for (const c of components) {
229
+ if (!c || typeof c.component !== "string") {
230
+ return 'every component needs a "component" type name.';
231
+ }
232
+ if (!known.has(c.component)) {
233
+ return `component "${c.component}" is not in catalog "${catalog.id}".`;
234
+ }
235
+ }
236
+ return null;
237
+ }
238
+ /**
239
+ * Enforces the "RE-RENDER THE WHOLE SURFACE" protocol rule for full-surface
240
+ * renders. The v0.9 spec requires that "one of the components in one of the
241
+ * component lists MUST have an id of `root`" — a batch-level rule, not a
242
+ * per-message one. A split render is therefore legal: the root (and layout)
243
+ * may live in one `updateComponents` while its leaf components arrive in a
244
+ * later one within the same batch. So this checks that *at least one*
245
+ * `updateComponents` in the batch declares a `root`, not that every one does.
246
+ *
247
+ * Returns an error message, or `null` if valid. Unlike
248
+ * {@link validateComponents} this is a protocol check, not a catalog check, so
249
+ * it runs regardless of whether a catalog is configured.
250
+ */
251
+ validateRoot(envelopes) {
252
+ let sawComponentList = false;
253
+ for (const e of envelopes) {
254
+ const uc = e.updateComponents;
255
+ if (uc && Array.isArray(uc.components)) {
256
+ sawComponentList = true;
257
+ if (uc.components.some((c) => c.id === "root")) {
258
+ return null;
259
+ }
260
+ }
261
+ }
262
+ if (!sawComponentList) return null;
263
+ return 'component list must contain a component id "root".';
264
+ }
265
+ }
266
+ function envelopeSurfaceId(e) {
267
+ const anyE = e;
268
+ return anyE.createSurface?.surfaceId ?? anyE.updateComponents?.surfaceId ?? anyE.updateDataModel?.surfaceId ?? anyE.deleteSurface?.surfaceId;
269
+ }
270
+ function forceSurfaceId(e, surfaceId) {
271
+ const anyE = e;
272
+ const payload = anyE.createSurface ?? anyE.updateComponents ?? anyE.updateDataModel ?? anyE.deleteSurface;
273
+ if (payload) payload.surfaceId = surfaceId;
274
+ }
275
+ function isCreateSurface(e) {
276
+ return e.createSurface !== void 0;
277
+ }
278
+ // Annotate the CommonJS export names for ESM import in node:
279
+ 0 && (module.exports = {
280
+ A2uiStreamParser
281
+ });
282
+ //# sourceMappingURL=parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/parser.ts"],"sourcesContent":["/**\n * Copyright 2026 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Streaming parser that extracts A2UI envelopes from model output.\n *\n * The model emits A2UI as a fenced code block tagged `a2ui` containing a JSON\n * array of envelopes (see {@link renderCatalogInstructions}). This parser scans\n * a text stream incrementally, separating ordinary prose (which streams through\n * as deltas) from complete A2UI blocks (which are buffered until the closing\n * fence, then parsed into whole, validated envelopes — the protocol requires\n * ordered, complete messages, so we never emit half-parsed JSON).\n *\n * This module is Node-free so it can be unit-tested and reused anywhere.\n *\n * @module\n */\n\nimport type { A2uiCatalog } from './catalog.js';\nimport { SURFACE_ID_PLACEHOLDER } from './catalog.js';\nimport {\n A2UI_VERSION,\n type A2uiComponent,\n type A2uiEnvelope,\n type CreateSurfaceEnvelope,\n type DeleteSurfaceEnvelope,\n type SupportedVersion,\n type UpdateComponentsEnvelope,\n type UpdateDataModelEnvelope,\n} from './types.js';\n\n/** A parsed-but-not-yet-normalized envelope, as read from the model's JSON. */\ninterface RawEnvelope {\n version?: string;\n createSurface?: CreateSurfaceEnvelope['createSurface'];\n updateComponents?: UpdateComponentsEnvelope['updateComponents'];\n updateDataModel?: UpdateDataModelEnvelope['updateDataModel'];\n deleteSurface?: DeleteSurfaceEnvelope['deleteSurface'];\n}\n\n/**\n * Opening fence, matched case-insensitively (```a2ui, optional trailing spaces,\n * then a newline).\n *\n * The fence is deliberately NOT anchored to line start (unlike\n * {@link CLOSE_FENCE_RE}): a model may legitimately begin the block right after\n * inline prose on the same line, and the streaming parser must still catch it.\n * The required trailing newline is what guards against a false positive from\n * prose that merely *mentions* the fence: A2UI `Text` \"may use inline Markdown\",\n * so the model can write \"I emit an ```a2ui block like this\", but there the\n * `a2ui` tag is followed by more text on the same line, not a newline, so it\n * does not match.\n */\nconst OPEN_FENCE_RE = /```a2ui[ \\t]*\\r?\\n/i;\n/**\n * Matches, at the very end of the buffer, the longest suffix that could still be\n * completing an opening fence on the next chunk: 1-3 backticks, then a partial\n * `a2ui` tag, then optional trailing spaces/tabs and an optional `\\r` awaiting\n * its `\\n`. Held back from prose so a fence split across chunks is never leaked.\n *\n * A fixed-length holdback would be wrong: {@link OPEN_FENCE_RE} allows unbounded\n * `[ \\t]*` padding before the newline, so the incomplete-fence suffix has no\n * fixed maximum length. Anchoring to the end (`$`) lets us hold back exactly it.\n */\nconst PARTIAL_OPEN_FENCE_RE = /(?:`|``|```(?:a(?:2(?:u(?:i[ \\t]*\\r?)?)?)?)?)$/i;\n/**\n * Closing fence: ``` at the start of a line (optionally indented). Anchoring to\n * line start (mirroring {@link OPEN_FENCE_RE}) is important: A2UI `Text` values\n * \"may use inline Markdown\", so the JSON payload can legitimately contain a\n * ```` ``` ```` inside a string. A bare `/```/` would match that and truncate\n * the block mid-JSON, dropping the whole surface.\n */\nconst CLOSE_FENCE_RE = /(^|\\n)[ \\t]*```/;\n\n/**\n * A single ordered piece of parsed output: either a run of prose or one\n * completed A2UI envelope batch. Segments preserve the exact source order, so\n * prose that appears *after* a block is not reordered ahead of it.\n */\nexport type ParseSegment = { prose: string } | { envelopes: A2uiEnvelope[] };\n\n/** Result of feeding text to {@link A2uiStreamParser.push}. */\nexport interface ParseResult {\n /**\n * Ordered prose/envelope segments exactly as they appear in the source text.\n * Prefer this over {@link ParseResult.prose}/{@link ParseResult.envelopeBatches}\n * when order between prose and blocks matters.\n */\n segments: ParseSegment[];\n /**\n * Convenience: all prose runs concatenated (never contains A2UI blocks). Loses\n * the relative order of prose vs. blocks — use {@link ParseResult.segments}\n * when that matters.\n */\n prose: string;\n /** Convenience: the fully-parsed A2UI envelope batches, in order. */\n envelopeBatches: A2uiEnvelope[][];\n}\n\n/** Options controlling how the parser finalizes envelopes. */\nexport interface A2uiParserOptions {\n /** Catalog used to validate component references. */\n catalog?: A2uiCatalog;\n /**\n * `'strict'` throws on unknown components / bad JSON; `'warn'` logs a warning\n * and drops the offending block/envelope; `'off'` skips validation entirely.\n */\n validate?: 'strict' | 'warn' | 'off';\n /** Protocol version stamped onto envelopes lacking one. */\n version?: string;\n /** Produces the surface id substituted for the model's placeholder. */\n surfaceId: () => string;\n}\n\n/**\n * Incremental A2UI extractor. Create one per model turn, `push()` text deltas as\n * they arrive, and `flush()` at the end to drain any trailing block.\n */\nexport class A2uiStreamParser {\n private buffer = '';\n private inBlock = false;\n /** Stable surface id for the current block (placeholders map to this). */\n private currentSurfaceId: string | null = null;\n\n constructor(private readonly options: A2uiParserOptions) {}\n\n /** Feeds a chunk of model text, returning prose + any completed blocks. */\n push(text: string): ParseResult {\n this.buffer += text;\n return this.drain(false);\n }\n\n /** Drains any remaining buffered content at end of stream. */\n flush(): ParseResult {\n return this.drain(true);\n }\n\n private drain(final: boolean): ParseResult {\n const segments: ParseSegment[] = [];\n // Accumulates prose across loop iterations so consecutive prose runs (e.g.\n // when a partial fence is held back) coalesce into a single segment.\n let proseBuf = '';\n const flushProse = () => {\n if (proseBuf) {\n segments.push({ prose: proseBuf });\n proseBuf = '';\n }\n };\n\n // Loop because a single push may contain multiple prose/block transitions.\n // Each iteration makes progress or returns.\n for (;;) {\n if (!this.inBlock) {\n const open = this.buffer.match(OPEN_FENCE_RE);\n if (!open) {\n // No opening fence (yet). Emit prose, but hold back a tail that could\n // be the start of an incomplete opening fence, unless finalizing.\n if (final) {\n proseBuf += this.buffer;\n this.buffer = '';\n } else {\n // Hold back a trailing suffix that could still be completing an\n // opening fence on the next chunk (e.g. \"```a2u\" or \"```a2ui \\r\").\n const partial = this.buffer.match(PARTIAL_OPEN_FENCE_RE);\n const keep = partial ? partial[0].length : 0;\n const safeLen = this.buffer.length - keep;\n if (safeLen > 0) {\n proseBuf += this.buffer.slice(0, safeLen);\n this.buffer = this.buffer.slice(safeLen);\n }\n }\n break;\n }\n // Emit prose before the fence, then enter the block.\n proseBuf += this.buffer.slice(0, open.index);\n this.buffer = this.buffer.slice(open.index! + open[0].length);\n this.inBlock = true;\n this.currentSurfaceId = this.options.surfaceId();\n continue;\n }\n\n // In a block: look for the closing fence.\n const close = this.buffer.match(CLOSE_FENCE_RE);\n if (!close) {\n if (final) {\n // Unterminated block at end of stream — try to parse what we have.\n const batch = this.finalizeBlock(this.buffer);\n if (batch) {\n flushProse();\n segments.push({ envelopes: batch });\n }\n this.buffer = '';\n this.inBlock = false;\n }\n break;\n }\n const blockText = this.buffer.slice(0, close.index);\n this.buffer = this.buffer.slice(close.index! + close[0].length);\n // Consume an optional trailing newline after the closing fence.\n this.buffer = this.buffer.replace(/^[ \\t]*\\r?\\n/, '');\n this.inBlock = false;\n const batch = this.finalizeBlock(blockText);\n if (batch) {\n // Emit any prose seen before this block first, preserving source order.\n flushProse();\n segments.push({ envelopes: batch });\n }\n continue;\n }\n flushProse();\n\n // Derive the convenience fields from the ordered segments.\n let prose = '';\n const envelopeBatches: A2uiEnvelope[][] = [];\n for (const seg of segments) {\n if ('prose' in seg) prose += seg.prose;\n else envelopeBatches.push(seg.envelopes);\n }\n return { segments, prose, envelopeBatches };\n }\n\n /**\n * Handles a validation failure according to the configured `validate` mode:\n * throws in `'strict'`, logs a warning in `'warn'` (the middleware default),\n * and is silent in `'off'`. Always returns `null` so callers can\n * `return this.reject(...)`.\n */\n private reject(message: string): null {\n const full = `A2UI: ${message}`;\n if (this.options.validate === 'off') return null;\n if (this.options.validate === 'warn') {\n // Keep this module Node-free / browser-safe: use console, not the logger.\n console.warn(`${full} (dropping block/envelope)`);\n return null;\n }\n throw new Error(full);\n }\n\n /** Parses, validates, and normalizes one block's JSON into envelopes. */\n private finalizeBlock(raw: string): A2uiEnvelope[] | null {\n const surfaceId = this.currentSurfaceId ?? this.options.surfaceId();\n this.currentSurfaceId = null;\n\n const text = raw.trim();\n if (!text) return null;\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch (e) {\n return this.reject(\n `failed to parse envelope block as JSON: ${(e as Error).message}`\n );\n }\n\n const envelopes: unknown[] = Array.isArray(parsed) ? parsed : [parsed];\n const out: A2uiEnvelope[] = [];\n for (const env of envelopes) {\n const normalized = this.normalizeEnvelope(env, surfaceId);\n if (normalized) out.push(normalized);\n }\n if (out.length === 0) return null;\n\n const hasCreate = out.some(isCreateSurface);\n\n if (hasCreate) {\n // A full-surface render. `createSurface` means \"new surface\" by\n // definition, so the id the model wrote is never authoritative — force\n // every envelope in this block onto the freshly-minted `surfaceId`, even\n // if the model copied a real id from replayed history (which would\n // otherwise reuse and overwrite that prior surface in place). This is the\n // single chokepoint that guarantees a distinct surface per render, so\n // history can keep real ids verbatim (needed to correlate actions with\n // their surface) without risking id reuse.\n for (const e of out) forceSurfaceId(e, surfaceId);\n // Enforce the \"must contain a root\" protocol rule.\n const err = this.validateRoot(out);\n if (err) return this.reject(err);\n return out;\n }\n\n // Update-only batch. Two cases:\n //\n // 1. The model targeted the *placeholder* (or omitted the id), so\n // `normalizeEnvelope` swapped in our freshly-minted `surfaceId`. That's\n // a fresh render the model forgot to `createSurface` for — synthesize\n // one so the client has a surface before the updates land, and enforce\n // the root rule as for any full render.\n //\n // 2. The updates target an *explicit, pre-existing* surface id (one the\n // model learned from a prior turn, e.g. via a summarized action). That's\n // a genuine incremental update: do NOT synthesize a `createSurface`\n // (that would reset the surface to empty and drop the update as\n // \"surface not found\" if ids disagreed), and do NOT require a `root`.\n const targetId =\n out.map(envelopeSurfaceId).find((id) => id !== undefined) ?? surfaceId;\n const isFreshRender = targetId === surfaceId;\n\n if (isFreshRender) {\n const err = this.validateRoot(out);\n if (err) return this.reject(err);\n // Guarantee the block opens with a `createSurface`, so the client always\n // has a surface before any update targets it. Idempotent re-creation is\n // fine — it resets the surface.\n out.unshift({\n version: (this.options.version ?? A2UI_VERSION) as SupportedVersion,\n createSurface: {\n surfaceId,\n catalogId: this.options.catalog?.id ?? '',\n },\n });\n }\n return out;\n }\n\n /**\n * Validates a single envelope, substitutes the real surface id for the\n * placeholder, and stamps the protocol version.\n */\n private normalizeEnvelope(\n env: unknown,\n surfaceId: string\n ): A2uiEnvelope | null {\n if (typeof env !== 'object' || env === null) {\n return this.reject('envelope must be an object.');\n }\n const e = env as RawEnvelope;\n const version = e.version ?? this.options.version ?? A2UI_VERSION;\n\n const swapSurfaceId = (payload: { surfaceId?: string } | undefined) => {\n if (!payload) return;\n if (\n payload.surfaceId === undefined ||\n payload.surfaceId === SURFACE_ID_PLACEHOLDER ||\n payload.surfaceId === ''\n ) {\n payload.surfaceId = surfaceId;\n }\n };\n\n // Copies the incoming envelope and stamps the version, preserving any\n // forward-compatible top-level keys the middleware does not inspect. The\n // A2UI protocol is \"open-ended and versioned\", so rebuilding a fresh\n // { version, <kind> } object would silently strip anything the spec adds\n // later; spreading keeps it intact. The Go parser does the same.\n const normalized = (): A2uiEnvelope =>\n ({ ...(env as object), version }) as A2uiEnvelope;\n\n if (e.createSurface) {\n swapSurfaceId(e.createSurface);\n return normalized();\n }\n if (e.updateComponents) {\n swapSurfaceId(e.updateComponents);\n if (this.options.validate !== 'off') {\n const err = this.validateComponents(e.updateComponents.components);\n if (err) return this.reject(err);\n }\n return normalized();\n }\n if (e.updateDataModel) {\n swapSurfaceId(e.updateDataModel);\n return normalized();\n }\n if (e.deleteSurface) {\n swapSurfaceId(e.deleteSurface);\n return normalized();\n }\n return this.reject(\n `unknown envelope type (keys: ${Object.keys(e).join(', ')}).`\n );\n }\n\n /**\n * Ensures every component references a known catalog component. Returns an\n * error message describing the first problem found, or `null` if valid.\n *\n * This checks component *type names* against the catalog only. It intentionally\n * does NOT enforce the \"must contain a root\" protocol rule — that applies only\n * to full-surface renders and is enforced at the batch level in\n * {@link finalizeBlock} (an incremental update to an existing surface may\n * legitimately patch a subtree without re-declaring `root`).\n */\n private validateComponents(components: unknown): string | null {\n const catalog = this.options.catalog;\n if (!catalog) return null;\n if (!Array.isArray(components)) {\n return 'updateComponents.components must be an array.';\n }\n const known = new Set(catalog.components.map((c) => c.name));\n for (const c of components as A2uiComponent[]) {\n if (!c || typeof c.component !== 'string') {\n return 'every component needs a \"component\" type name.';\n }\n if (!known.has(c.component)) {\n return `component \"${c.component}\" is not in catalog \"${catalog.id}\".`;\n }\n }\n return null;\n }\n\n /**\n * Enforces the \"RE-RENDER THE WHOLE SURFACE\" protocol rule for full-surface\n * renders. The v0.9 spec requires that \"one of the components in one of the\n * component lists MUST have an id of `root`\" — a batch-level rule, not a\n * per-message one. A split render is therefore legal: the root (and layout)\n * may live in one `updateComponents` while its leaf components arrive in a\n * later one within the same batch. So this checks that *at least one*\n * `updateComponents` in the batch declares a `root`, not that every one does.\n *\n * Returns an error message, or `null` if valid. Unlike\n * {@link validateComponents} this is a protocol check, not a catalog check, so\n * it runs regardless of whether a catalog is configured.\n */\n private validateRoot(envelopes: A2uiEnvelope[]): string | null {\n let sawComponentList = false;\n for (const e of envelopes) {\n const uc = (e as UpdateComponentsEnvelope).updateComponents;\n if (uc && Array.isArray(uc.components)) {\n sawComponentList = true;\n if (uc.components.some((c) => c.id === 'root')) {\n return null;\n }\n }\n }\n // Only a batch that actually carries component lists must declare a root.\n if (!sawComponentList) return null;\n return 'component list must contain a component id \"root\".';\n }\n}\n\n/**\n * Reads the surface id an envelope targets, regardless of its variant. Used to\n * decide whether an update-only batch targets the freshly-minted surface (a\n * fresh render the model forgot to `createSurface` for) or an explicit existing\n * one (a genuine incremental update).\n */\nfunction envelopeSurfaceId(e: A2uiEnvelope): string | undefined {\n const anyE = e as {\n createSurface?: { surfaceId?: string };\n updateComponents?: { surfaceId?: string };\n updateDataModel?: { surfaceId?: string };\n deleteSurface?: { surfaceId?: string };\n };\n return (\n anyE.createSurface?.surfaceId ??\n anyE.updateComponents?.surfaceId ??\n anyE.updateDataModel?.surfaceId ??\n anyE.deleteSurface?.surfaceId\n );\n}\n\n/**\n * Overwrites the `surfaceId` of whichever payload `e` carries with `surfaceId`,\n * unconditionally (unlike the placeholder-only swap in `normalizeEnvelope`).\n * Used to force every envelope in a `createSurface`-bearing block onto a single\n * freshly-minted id.\n */\nfunction forceSurfaceId(e: A2uiEnvelope, surfaceId: string): void {\n const anyE = e as {\n createSurface?: { surfaceId?: string };\n updateComponents?: { surfaceId?: string };\n updateDataModel?: { surfaceId?: string };\n deleteSurface?: { surfaceId?: string };\n };\n const payload =\n anyE.createSurface ??\n anyE.updateComponents ??\n anyE.updateDataModel ??\n anyE.deleteSurface;\n if (payload) payload.surfaceId = surfaceId;\n}\n\n/** Type guard: does this envelope create a surface? */\nfunction isCreateSurface(e: A2uiEnvelope): e is CreateSurfaceEnvelope {\n return (e as CreateSurfaceEnvelope).createSurface !== undefined;\n}\n"],"mappings":"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","names":["batch"]}
package/lib/parser.mjs ADDED
@@ -0,0 +1,260 @@
1
+ import { SURFACE_ID_PLACEHOLDER } from "./catalog.mjs";
2
+ import {
3
+ A2UI_VERSION
4
+ } from "./types.mjs";
5
+ const OPEN_FENCE_RE = /```a2ui[ \t]*\r?\n/i;
6
+ const PARTIAL_OPEN_FENCE_RE = /(?:`|``|```(?:a(?:2(?:u(?:i[ \t]*\r?)?)?)?)?)$/i;
7
+ const CLOSE_FENCE_RE = /(^|\n)[ \t]*```/;
8
+ class A2uiStreamParser {
9
+ constructor(options) {
10
+ this.options = options;
11
+ }
12
+ buffer = "";
13
+ inBlock = false;
14
+ /** Stable surface id for the current block (placeholders map to this). */
15
+ currentSurfaceId = null;
16
+ /** Feeds a chunk of model text, returning prose + any completed blocks. */
17
+ push(text) {
18
+ this.buffer += text;
19
+ return this.drain(false);
20
+ }
21
+ /** Drains any remaining buffered content at end of stream. */
22
+ flush() {
23
+ return this.drain(true);
24
+ }
25
+ drain(final) {
26
+ const segments = [];
27
+ let proseBuf = "";
28
+ const flushProse = () => {
29
+ if (proseBuf) {
30
+ segments.push({ prose: proseBuf });
31
+ proseBuf = "";
32
+ }
33
+ };
34
+ for (; ; ) {
35
+ if (!this.inBlock) {
36
+ const open = this.buffer.match(OPEN_FENCE_RE);
37
+ if (!open) {
38
+ if (final) {
39
+ proseBuf += this.buffer;
40
+ this.buffer = "";
41
+ } else {
42
+ const partial = this.buffer.match(PARTIAL_OPEN_FENCE_RE);
43
+ const keep = partial ? partial[0].length : 0;
44
+ const safeLen = this.buffer.length - keep;
45
+ if (safeLen > 0) {
46
+ proseBuf += this.buffer.slice(0, safeLen);
47
+ this.buffer = this.buffer.slice(safeLen);
48
+ }
49
+ }
50
+ break;
51
+ }
52
+ proseBuf += this.buffer.slice(0, open.index);
53
+ this.buffer = this.buffer.slice(open.index + open[0].length);
54
+ this.inBlock = true;
55
+ this.currentSurfaceId = this.options.surfaceId();
56
+ continue;
57
+ }
58
+ const close = this.buffer.match(CLOSE_FENCE_RE);
59
+ if (!close) {
60
+ if (final) {
61
+ const batch2 = this.finalizeBlock(this.buffer);
62
+ if (batch2) {
63
+ flushProse();
64
+ segments.push({ envelopes: batch2 });
65
+ }
66
+ this.buffer = "";
67
+ this.inBlock = false;
68
+ }
69
+ break;
70
+ }
71
+ const blockText = this.buffer.slice(0, close.index);
72
+ this.buffer = this.buffer.slice(close.index + close[0].length);
73
+ this.buffer = this.buffer.replace(/^[ \t]*\r?\n/, "");
74
+ this.inBlock = false;
75
+ const batch = this.finalizeBlock(blockText);
76
+ if (batch) {
77
+ flushProse();
78
+ segments.push({ envelopes: batch });
79
+ }
80
+ continue;
81
+ }
82
+ flushProse();
83
+ let prose = "";
84
+ const envelopeBatches = [];
85
+ for (const seg of segments) {
86
+ if ("prose" in seg) prose += seg.prose;
87
+ else envelopeBatches.push(seg.envelopes);
88
+ }
89
+ return { segments, prose, envelopeBatches };
90
+ }
91
+ /**
92
+ * Handles a validation failure according to the configured `validate` mode:
93
+ * throws in `'strict'`, logs a warning in `'warn'` (the middleware default),
94
+ * and is silent in `'off'`. Always returns `null` so callers can
95
+ * `return this.reject(...)`.
96
+ */
97
+ reject(message) {
98
+ const full = `A2UI: ${message}`;
99
+ if (this.options.validate === "off") return null;
100
+ if (this.options.validate === "warn") {
101
+ console.warn(`${full} (dropping block/envelope)`);
102
+ return null;
103
+ }
104
+ throw new Error(full);
105
+ }
106
+ /** Parses, validates, and normalizes one block's JSON into envelopes. */
107
+ finalizeBlock(raw) {
108
+ const surfaceId = this.currentSurfaceId ?? this.options.surfaceId();
109
+ this.currentSurfaceId = null;
110
+ const text = raw.trim();
111
+ if (!text) return null;
112
+ let parsed;
113
+ try {
114
+ parsed = JSON.parse(text);
115
+ } catch (e) {
116
+ return this.reject(
117
+ `failed to parse envelope block as JSON: ${e.message}`
118
+ );
119
+ }
120
+ const envelopes = Array.isArray(parsed) ? parsed : [parsed];
121
+ const out = [];
122
+ for (const env of envelopes) {
123
+ const normalized = this.normalizeEnvelope(env, surfaceId);
124
+ if (normalized) out.push(normalized);
125
+ }
126
+ if (out.length === 0) return null;
127
+ const hasCreate = out.some(isCreateSurface);
128
+ if (hasCreate) {
129
+ for (const e of out) forceSurfaceId(e, surfaceId);
130
+ const err = this.validateRoot(out);
131
+ if (err) return this.reject(err);
132
+ return out;
133
+ }
134
+ const targetId = out.map(envelopeSurfaceId).find((id) => id !== void 0) ?? surfaceId;
135
+ const isFreshRender = targetId === surfaceId;
136
+ if (isFreshRender) {
137
+ const err = this.validateRoot(out);
138
+ if (err) return this.reject(err);
139
+ out.unshift({
140
+ version: this.options.version ?? A2UI_VERSION,
141
+ createSurface: {
142
+ surfaceId,
143
+ catalogId: this.options.catalog?.id ?? ""
144
+ }
145
+ });
146
+ }
147
+ return out;
148
+ }
149
+ /**
150
+ * Validates a single envelope, substitutes the real surface id for the
151
+ * placeholder, and stamps the protocol version.
152
+ */
153
+ normalizeEnvelope(env, surfaceId) {
154
+ if (typeof env !== "object" || env === null) {
155
+ return this.reject("envelope must be an object.");
156
+ }
157
+ const e = env;
158
+ const version = e.version ?? this.options.version ?? A2UI_VERSION;
159
+ const swapSurfaceId = (payload) => {
160
+ if (!payload) return;
161
+ if (payload.surfaceId === void 0 || payload.surfaceId === SURFACE_ID_PLACEHOLDER || payload.surfaceId === "") {
162
+ payload.surfaceId = surfaceId;
163
+ }
164
+ };
165
+ const normalized = () => ({ ...env, version });
166
+ if (e.createSurface) {
167
+ swapSurfaceId(e.createSurface);
168
+ return normalized();
169
+ }
170
+ if (e.updateComponents) {
171
+ swapSurfaceId(e.updateComponents);
172
+ if (this.options.validate !== "off") {
173
+ const err = this.validateComponents(e.updateComponents.components);
174
+ if (err) return this.reject(err);
175
+ }
176
+ return normalized();
177
+ }
178
+ if (e.updateDataModel) {
179
+ swapSurfaceId(e.updateDataModel);
180
+ return normalized();
181
+ }
182
+ if (e.deleteSurface) {
183
+ swapSurfaceId(e.deleteSurface);
184
+ return normalized();
185
+ }
186
+ return this.reject(
187
+ `unknown envelope type (keys: ${Object.keys(e).join(", ")}).`
188
+ );
189
+ }
190
+ /**
191
+ * Ensures every component references a known catalog component. Returns an
192
+ * error message describing the first problem found, or `null` if valid.
193
+ *
194
+ * This checks component *type names* against the catalog only. It intentionally
195
+ * does NOT enforce the "must contain a root" protocol rule — that applies only
196
+ * to full-surface renders and is enforced at the batch level in
197
+ * {@link finalizeBlock} (an incremental update to an existing surface may
198
+ * legitimately patch a subtree without re-declaring `root`).
199
+ */
200
+ validateComponents(components) {
201
+ const catalog = this.options.catalog;
202
+ if (!catalog) return null;
203
+ if (!Array.isArray(components)) {
204
+ return "updateComponents.components must be an array.";
205
+ }
206
+ const known = new Set(catalog.components.map((c) => c.name));
207
+ for (const c of components) {
208
+ if (!c || typeof c.component !== "string") {
209
+ return 'every component needs a "component" type name.';
210
+ }
211
+ if (!known.has(c.component)) {
212
+ return `component "${c.component}" is not in catalog "${catalog.id}".`;
213
+ }
214
+ }
215
+ return null;
216
+ }
217
+ /**
218
+ * Enforces the "RE-RENDER THE WHOLE SURFACE" protocol rule for full-surface
219
+ * renders. The v0.9 spec requires that "one of the components in one of the
220
+ * component lists MUST have an id of `root`" — a batch-level rule, not a
221
+ * per-message one. A split render is therefore legal: the root (and layout)
222
+ * may live in one `updateComponents` while its leaf components arrive in a
223
+ * later one within the same batch. So this checks that *at least one*
224
+ * `updateComponents` in the batch declares a `root`, not that every one does.
225
+ *
226
+ * Returns an error message, or `null` if valid. Unlike
227
+ * {@link validateComponents} this is a protocol check, not a catalog check, so
228
+ * it runs regardless of whether a catalog is configured.
229
+ */
230
+ validateRoot(envelopes) {
231
+ let sawComponentList = false;
232
+ for (const e of envelopes) {
233
+ const uc = e.updateComponents;
234
+ if (uc && Array.isArray(uc.components)) {
235
+ sawComponentList = true;
236
+ if (uc.components.some((c) => c.id === "root")) {
237
+ return null;
238
+ }
239
+ }
240
+ }
241
+ if (!sawComponentList) return null;
242
+ return 'component list must contain a component id "root".';
243
+ }
244
+ }
245
+ function envelopeSurfaceId(e) {
246
+ const anyE = e;
247
+ return anyE.createSurface?.surfaceId ?? anyE.updateComponents?.surfaceId ?? anyE.updateDataModel?.surfaceId ?? anyE.deleteSurface?.surfaceId;
248
+ }
249
+ function forceSurfaceId(e, surfaceId) {
250
+ const anyE = e;
251
+ const payload = anyE.createSurface ?? anyE.updateComponents ?? anyE.updateDataModel ?? anyE.deleteSurface;
252
+ if (payload) payload.surfaceId = surfaceId;
253
+ }
254
+ function isCreateSurface(e) {
255
+ return e.createSurface !== void 0;
256
+ }
257
+ export {
258
+ A2uiStreamParser
259
+ };
260
+ //# sourceMappingURL=parser.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/parser.ts"],"sourcesContent":["/**\n * Copyright 2026 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Streaming parser that extracts A2UI envelopes from model output.\n *\n * The model emits A2UI as a fenced code block tagged `a2ui` containing a JSON\n * array of envelopes (see {@link renderCatalogInstructions}). This parser scans\n * a text stream incrementally, separating ordinary prose (which streams through\n * as deltas) from complete A2UI blocks (which are buffered until the closing\n * fence, then parsed into whole, validated envelopes — the protocol requires\n * ordered, complete messages, so we never emit half-parsed JSON).\n *\n * This module is Node-free so it can be unit-tested and reused anywhere.\n *\n * @module\n */\n\nimport type { A2uiCatalog } from './catalog.mjs';\nimport { SURFACE_ID_PLACEHOLDER } from './catalog.mjs';\nimport {\n A2UI_VERSION,\n type A2uiComponent,\n type A2uiEnvelope,\n type CreateSurfaceEnvelope,\n type DeleteSurfaceEnvelope,\n type SupportedVersion,\n type UpdateComponentsEnvelope,\n type UpdateDataModelEnvelope,\n} from './types.mjs';\n\n/** A parsed-but-not-yet-normalized envelope, as read from the model's JSON. */\ninterface RawEnvelope {\n version?: string;\n createSurface?: CreateSurfaceEnvelope['createSurface'];\n updateComponents?: UpdateComponentsEnvelope['updateComponents'];\n updateDataModel?: UpdateDataModelEnvelope['updateDataModel'];\n deleteSurface?: DeleteSurfaceEnvelope['deleteSurface'];\n}\n\n/**\n * Opening fence, matched case-insensitively (```a2ui, optional trailing spaces,\n * then a newline).\n *\n * The fence is deliberately NOT anchored to line start (unlike\n * {@link CLOSE_FENCE_RE}): a model may legitimately begin the block right after\n * inline prose on the same line, and the streaming parser must still catch it.\n * The required trailing newline is what guards against a false positive from\n * prose that merely *mentions* the fence: A2UI `Text` \"may use inline Markdown\",\n * so the model can write \"I emit an ```a2ui block like this\", but there the\n * `a2ui` tag is followed by more text on the same line, not a newline, so it\n * does not match.\n */\nconst OPEN_FENCE_RE = /```a2ui[ \\t]*\\r?\\n/i;\n/**\n * Matches, at the very end of the buffer, the longest suffix that could still be\n * completing an opening fence on the next chunk: 1-3 backticks, then a partial\n * `a2ui` tag, then optional trailing spaces/tabs and an optional `\\r` awaiting\n * its `\\n`. Held back from prose so a fence split across chunks is never leaked.\n *\n * A fixed-length holdback would be wrong: {@link OPEN_FENCE_RE} allows unbounded\n * `[ \\t]*` padding before the newline, so the incomplete-fence suffix has no\n * fixed maximum length. Anchoring to the end (`$`) lets us hold back exactly it.\n */\nconst PARTIAL_OPEN_FENCE_RE = /(?:`|``|```(?:a(?:2(?:u(?:i[ \\t]*\\r?)?)?)?)?)$/i;\n/**\n * Closing fence: ``` at the start of a line (optionally indented). Anchoring to\n * line start (mirroring {@link OPEN_FENCE_RE}) is important: A2UI `Text` values\n * \"may use inline Markdown\", so the JSON payload can legitimately contain a\n * ```` ``` ```` inside a string. A bare `/```/` would match that and truncate\n * the block mid-JSON, dropping the whole surface.\n */\nconst CLOSE_FENCE_RE = /(^|\\n)[ \\t]*```/;\n\n/**\n * A single ordered piece of parsed output: either a run of prose or one\n * completed A2UI envelope batch. Segments preserve the exact source order, so\n * prose that appears *after* a block is not reordered ahead of it.\n */\nexport type ParseSegment = { prose: string } | { envelopes: A2uiEnvelope[] };\n\n/** Result of feeding text to {@link A2uiStreamParser.push}. */\nexport interface ParseResult {\n /**\n * Ordered prose/envelope segments exactly as they appear in the source text.\n * Prefer this over {@link ParseResult.prose}/{@link ParseResult.envelopeBatches}\n * when order between prose and blocks matters.\n */\n segments: ParseSegment[];\n /**\n * Convenience: all prose runs concatenated (never contains A2UI blocks). Loses\n * the relative order of prose vs. blocks — use {@link ParseResult.segments}\n * when that matters.\n */\n prose: string;\n /** Convenience: the fully-parsed A2UI envelope batches, in order. */\n envelopeBatches: A2uiEnvelope[][];\n}\n\n/** Options controlling how the parser finalizes envelopes. */\nexport interface A2uiParserOptions {\n /** Catalog used to validate component references. */\n catalog?: A2uiCatalog;\n /**\n * `'strict'` throws on unknown components / bad JSON; `'warn'` logs a warning\n * and drops the offending block/envelope; `'off'` skips validation entirely.\n */\n validate?: 'strict' | 'warn' | 'off';\n /** Protocol version stamped onto envelopes lacking one. */\n version?: string;\n /** Produces the surface id substituted for the model's placeholder. */\n surfaceId: () => string;\n}\n\n/**\n * Incremental A2UI extractor. Create one per model turn, `push()` text deltas as\n * they arrive, and `flush()` at the end to drain any trailing block.\n */\nexport class A2uiStreamParser {\n private buffer = '';\n private inBlock = false;\n /** Stable surface id for the current block (placeholders map to this). */\n private currentSurfaceId: string | null = null;\n\n constructor(private readonly options: A2uiParserOptions) {}\n\n /** Feeds a chunk of model text, returning prose + any completed blocks. */\n push(text: string): ParseResult {\n this.buffer += text;\n return this.drain(false);\n }\n\n /** Drains any remaining buffered content at end of stream. */\n flush(): ParseResult {\n return this.drain(true);\n }\n\n private drain(final: boolean): ParseResult {\n const segments: ParseSegment[] = [];\n // Accumulates prose across loop iterations so consecutive prose runs (e.g.\n // when a partial fence is held back) coalesce into a single segment.\n let proseBuf = '';\n const flushProse = () => {\n if (proseBuf) {\n segments.push({ prose: proseBuf });\n proseBuf = '';\n }\n };\n\n // Loop because a single push may contain multiple prose/block transitions.\n // Each iteration makes progress or returns.\n for (;;) {\n if (!this.inBlock) {\n const open = this.buffer.match(OPEN_FENCE_RE);\n if (!open) {\n // No opening fence (yet). Emit prose, but hold back a tail that could\n // be the start of an incomplete opening fence, unless finalizing.\n if (final) {\n proseBuf += this.buffer;\n this.buffer = '';\n } else {\n // Hold back a trailing suffix that could still be completing an\n // opening fence on the next chunk (e.g. \"```a2u\" or \"```a2ui \\r\").\n const partial = this.buffer.match(PARTIAL_OPEN_FENCE_RE);\n const keep = partial ? partial[0].length : 0;\n const safeLen = this.buffer.length - keep;\n if (safeLen > 0) {\n proseBuf += this.buffer.slice(0, safeLen);\n this.buffer = this.buffer.slice(safeLen);\n }\n }\n break;\n }\n // Emit prose before the fence, then enter the block.\n proseBuf += this.buffer.slice(0, open.index);\n this.buffer = this.buffer.slice(open.index! + open[0].length);\n this.inBlock = true;\n this.currentSurfaceId = this.options.surfaceId();\n continue;\n }\n\n // In a block: look for the closing fence.\n const close = this.buffer.match(CLOSE_FENCE_RE);\n if (!close) {\n if (final) {\n // Unterminated block at end of stream — try to parse what we have.\n const batch = this.finalizeBlock(this.buffer);\n if (batch) {\n flushProse();\n segments.push({ envelopes: batch });\n }\n this.buffer = '';\n this.inBlock = false;\n }\n break;\n }\n const blockText = this.buffer.slice(0, close.index);\n this.buffer = this.buffer.slice(close.index! + close[0].length);\n // Consume an optional trailing newline after the closing fence.\n this.buffer = this.buffer.replace(/^[ \\t]*\\r?\\n/, '');\n this.inBlock = false;\n const batch = this.finalizeBlock(blockText);\n if (batch) {\n // Emit any prose seen before this block first, preserving source order.\n flushProse();\n segments.push({ envelopes: batch });\n }\n continue;\n }\n flushProse();\n\n // Derive the convenience fields from the ordered segments.\n let prose = '';\n const envelopeBatches: A2uiEnvelope[][] = [];\n for (const seg of segments) {\n if ('prose' in seg) prose += seg.prose;\n else envelopeBatches.push(seg.envelopes);\n }\n return { segments, prose, envelopeBatches };\n }\n\n /**\n * Handles a validation failure according to the configured `validate` mode:\n * throws in `'strict'`, logs a warning in `'warn'` (the middleware default),\n * and is silent in `'off'`. Always returns `null` so callers can\n * `return this.reject(...)`.\n */\n private reject(message: string): null {\n const full = `A2UI: ${message}`;\n if (this.options.validate === 'off') return null;\n if (this.options.validate === 'warn') {\n // Keep this module Node-free / browser-safe: use console, not the logger.\n console.warn(`${full} (dropping block/envelope)`);\n return null;\n }\n throw new Error(full);\n }\n\n /** Parses, validates, and normalizes one block's JSON into envelopes. */\n private finalizeBlock(raw: string): A2uiEnvelope[] | null {\n const surfaceId = this.currentSurfaceId ?? this.options.surfaceId();\n this.currentSurfaceId = null;\n\n const text = raw.trim();\n if (!text) return null;\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch (e) {\n return this.reject(\n `failed to parse envelope block as JSON: ${(e as Error).message}`\n );\n }\n\n const envelopes: unknown[] = Array.isArray(parsed) ? parsed : [parsed];\n const out: A2uiEnvelope[] = [];\n for (const env of envelopes) {\n const normalized = this.normalizeEnvelope(env, surfaceId);\n if (normalized) out.push(normalized);\n }\n if (out.length === 0) return null;\n\n const hasCreate = out.some(isCreateSurface);\n\n if (hasCreate) {\n // A full-surface render. `createSurface` means \"new surface\" by\n // definition, so the id the model wrote is never authoritative — force\n // every envelope in this block onto the freshly-minted `surfaceId`, even\n // if the model copied a real id from replayed history (which would\n // otherwise reuse and overwrite that prior surface in place). This is the\n // single chokepoint that guarantees a distinct surface per render, so\n // history can keep real ids verbatim (needed to correlate actions with\n // their surface) without risking id reuse.\n for (const e of out) forceSurfaceId(e, surfaceId);\n // Enforce the \"must contain a root\" protocol rule.\n const err = this.validateRoot(out);\n if (err) return this.reject(err);\n return out;\n }\n\n // Update-only batch. Two cases:\n //\n // 1. The model targeted the *placeholder* (or omitted the id), so\n // `normalizeEnvelope` swapped in our freshly-minted `surfaceId`. That's\n // a fresh render the model forgot to `createSurface` for — synthesize\n // one so the client has a surface before the updates land, and enforce\n // the root rule as for any full render.\n //\n // 2. The updates target an *explicit, pre-existing* surface id (one the\n // model learned from a prior turn, e.g. via a summarized action). That's\n // a genuine incremental update: do NOT synthesize a `createSurface`\n // (that would reset the surface to empty and drop the update as\n // \"surface not found\" if ids disagreed), and do NOT require a `root`.\n const targetId =\n out.map(envelopeSurfaceId).find((id) => id !== undefined) ?? surfaceId;\n const isFreshRender = targetId === surfaceId;\n\n if (isFreshRender) {\n const err = this.validateRoot(out);\n if (err) return this.reject(err);\n // Guarantee the block opens with a `createSurface`, so the client always\n // has a surface before any update targets it. Idempotent re-creation is\n // fine — it resets the surface.\n out.unshift({\n version: (this.options.version ?? A2UI_VERSION) as SupportedVersion,\n createSurface: {\n surfaceId,\n catalogId: this.options.catalog?.id ?? '',\n },\n });\n }\n return out;\n }\n\n /**\n * Validates a single envelope, substitutes the real surface id for the\n * placeholder, and stamps the protocol version.\n */\n private normalizeEnvelope(\n env: unknown,\n surfaceId: string\n ): A2uiEnvelope | null {\n if (typeof env !== 'object' || env === null) {\n return this.reject('envelope must be an object.');\n }\n const e = env as RawEnvelope;\n const version = e.version ?? this.options.version ?? A2UI_VERSION;\n\n const swapSurfaceId = (payload: { surfaceId?: string } | undefined) => {\n if (!payload) return;\n if (\n payload.surfaceId === undefined ||\n payload.surfaceId === SURFACE_ID_PLACEHOLDER ||\n payload.surfaceId === ''\n ) {\n payload.surfaceId = surfaceId;\n }\n };\n\n // Copies the incoming envelope and stamps the version, preserving any\n // forward-compatible top-level keys the middleware does not inspect. The\n // A2UI protocol is \"open-ended and versioned\", so rebuilding a fresh\n // { version, <kind> } object would silently strip anything the spec adds\n // later; spreading keeps it intact. The Go parser does the same.\n const normalized = (): A2uiEnvelope =>\n ({ ...(env as object), version }) as A2uiEnvelope;\n\n if (e.createSurface) {\n swapSurfaceId(e.createSurface);\n return normalized();\n }\n if (e.updateComponents) {\n swapSurfaceId(e.updateComponents);\n if (this.options.validate !== 'off') {\n const err = this.validateComponents(e.updateComponents.components);\n if (err) return this.reject(err);\n }\n return normalized();\n }\n if (e.updateDataModel) {\n swapSurfaceId(e.updateDataModel);\n return normalized();\n }\n if (e.deleteSurface) {\n swapSurfaceId(e.deleteSurface);\n return normalized();\n }\n return this.reject(\n `unknown envelope type (keys: ${Object.keys(e).join(', ')}).`\n );\n }\n\n /**\n * Ensures every component references a known catalog component. Returns an\n * error message describing the first problem found, or `null` if valid.\n *\n * This checks component *type names* against the catalog only. It intentionally\n * does NOT enforce the \"must contain a root\" protocol rule — that applies only\n * to full-surface renders and is enforced at the batch level in\n * {@link finalizeBlock} (an incremental update to an existing surface may\n * legitimately patch a subtree without re-declaring `root`).\n */\n private validateComponents(components: unknown): string | null {\n const catalog = this.options.catalog;\n if (!catalog) return null;\n if (!Array.isArray(components)) {\n return 'updateComponents.components must be an array.';\n }\n const known = new Set(catalog.components.map((c) => c.name));\n for (const c of components as A2uiComponent[]) {\n if (!c || typeof c.component !== 'string') {\n return 'every component needs a \"component\" type name.';\n }\n if (!known.has(c.component)) {\n return `component \"${c.component}\" is not in catalog \"${catalog.id}\".`;\n }\n }\n return null;\n }\n\n /**\n * Enforces the \"RE-RENDER THE WHOLE SURFACE\" protocol rule for full-surface\n * renders. The v0.9 spec requires that \"one of the components in one of the\n * component lists MUST have an id of `root`\" — a batch-level rule, not a\n * per-message one. A split render is therefore legal: the root (and layout)\n * may live in one `updateComponents` while its leaf components arrive in a\n * later one within the same batch. So this checks that *at least one*\n * `updateComponents` in the batch declares a `root`, not that every one does.\n *\n * Returns an error message, or `null` if valid. Unlike\n * {@link validateComponents} this is a protocol check, not a catalog check, so\n * it runs regardless of whether a catalog is configured.\n */\n private validateRoot(envelopes: A2uiEnvelope[]): string | null {\n let sawComponentList = false;\n for (const e of envelopes) {\n const uc = (e as UpdateComponentsEnvelope).updateComponents;\n if (uc && Array.isArray(uc.components)) {\n sawComponentList = true;\n if (uc.components.some((c) => c.id === 'root')) {\n return null;\n }\n }\n }\n // Only a batch that actually carries component lists must declare a root.\n if (!sawComponentList) return null;\n return 'component list must contain a component id \"root\".';\n }\n}\n\n/**\n * Reads the surface id an envelope targets, regardless of its variant. Used to\n * decide whether an update-only batch targets the freshly-minted surface (a\n * fresh render the model forgot to `createSurface` for) or an explicit existing\n * one (a genuine incremental update).\n */\nfunction envelopeSurfaceId(e: A2uiEnvelope): string | undefined {\n const anyE = e as {\n createSurface?: { surfaceId?: string };\n updateComponents?: { surfaceId?: string };\n updateDataModel?: { surfaceId?: string };\n deleteSurface?: { surfaceId?: string };\n };\n return (\n anyE.createSurface?.surfaceId ??\n anyE.updateComponents?.surfaceId ??\n anyE.updateDataModel?.surfaceId ??\n anyE.deleteSurface?.surfaceId\n );\n}\n\n/**\n * Overwrites the `surfaceId` of whichever payload `e` carries with `surfaceId`,\n * unconditionally (unlike the placeholder-only swap in `normalizeEnvelope`).\n * Used to force every envelope in a `createSurface`-bearing block onto a single\n * freshly-minted id.\n */\nfunction forceSurfaceId(e: A2uiEnvelope, surfaceId: string): void {\n const anyE = e as {\n createSurface?: { surfaceId?: string };\n updateComponents?: { surfaceId?: string };\n updateDataModel?: { surfaceId?: string };\n deleteSurface?: { surfaceId?: string };\n };\n const payload =\n anyE.createSurface ??\n anyE.updateComponents ??\n anyE.updateDataModel ??\n anyE.deleteSurface;\n if (payload) payload.surfaceId = surfaceId;\n}\n\n/** Type guard: does this envelope create a surface? */\nfunction isCreateSurface(e: A2uiEnvelope): e is CreateSurfaceEnvelope {\n return (e as CreateSurfaceEnvelope).createSurface !== undefined;\n}\n"],"mappings":"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","names":["batch"]}