@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
@@ -0,0 +1,240 @@
1
+ import { generateMiddleware, z } from "genkit";
2
+ import {
3
+ DEFAULT_CATALOG_ID,
4
+ renderCatalogInstructions
5
+ } from "./catalog.mjs";
6
+ import { resolveCatalog } from "./loader.mjs";
7
+ import { A2uiStreamParser } from "./parser.mjs";
8
+ import { a2uiPart, isA2uiPart } from "./part.mjs";
9
+ import {
10
+ A2UI_VERSION,
11
+ SUPPORTED_VERSIONS
12
+ } from "./types.mjs";
13
+ const A2uiOptionsSchema = z.object({
14
+ /**
15
+ * The id of the catalog describing what the agent may render. Defaults to
16
+ * `'basic'` (the bundled basic catalog). Register additional catalogs with
17
+ * `loadCatalog(ai, { id, catalog | file })` and reference them by id here.
18
+ */
19
+ catalog: z.string().optional(),
20
+ /**
21
+ * Where to inject the catalog's capabilities. `'system'` (default) appends
22
+ * A2UI instructions to the system prompt; `'none'` injects nothing (useful if
23
+ * you supply your own instructions).
24
+ */
25
+ instructions: z.enum(["system", "none"]).optional(),
26
+ /**
27
+ * Validate emitted envelopes against the catalog. `'warn'` (default) logs a
28
+ * warning and drops the offending block/envelope, keeping the rest of the
29
+ * turn alive; `'strict'` throws on malformed JSON or unknown components (best
30
+ * for development); `'off'` passes them through unchecked.
31
+ *
32
+ * IMPORTANT: this validates envelope structure and component *type names*
33
+ * against the catalog only. It does NOT validate component props or
34
+ * data-model values. Model-controlled values such as `Image.url` and `Text`
35
+ * (inline Markdown, which the renderer may turn into HTML) pass through
36
+ * untouched even under `'strict'`. `'strict'` is a well-formedness check, not
37
+ * a security boundary — see the "Security / trust boundary" section of the
38
+ * README. Prop sanitization is the renderer/catalog's responsibility, and
39
+ * hosts should CSP-restrict image and other remote sources.
40
+ */
41
+ validate: z.enum(["strict", "warn", "off"]).optional(),
42
+ /**
43
+ * Surface id policy. Provide a fixed id to reuse for every surface. Defaults
44
+ * to a fresh UUID per surface.
45
+ */
46
+ surfaceId: z.string().optional(),
47
+ /**
48
+ * Protocol version stamped on emitted envelopes. Defaults to `'v0.9'`.
49
+ * Constrained to the versions the renderer understands so a typo can't emit
50
+ * envelopes that fail at runtime.
51
+ */
52
+ version: z.enum(SUPPORTED_VERSIONS).optional()
53
+ });
54
+ function isTextPart(part) {
55
+ return !!part && typeof part === "object" && typeof part.text === "string";
56
+ }
57
+ function surfaceIdFactory(policy) {
58
+ if (typeof policy === "string") return () => policy;
59
+ return () => globalThis.crypto.randomUUID();
60
+ }
61
+ function replayableSurfaceIds(base) {
62
+ const generated = [];
63
+ let cursor = 0;
64
+ const next = () => {
65
+ const id = base();
66
+ generated.push(id);
67
+ return id;
68
+ };
69
+ const replayNext = () => cursor < generated.length ? generated[cursor++] : next();
70
+ const reset = () => {
71
+ cursor = 0;
72
+ };
73
+ return { next, replayNext, reset };
74
+ }
75
+ function partsFromParse(result) {
76
+ const out = [];
77
+ for (const seg of result.segments) {
78
+ if ("prose" in seg) {
79
+ if (seg.prose) out.push({ text: seg.prose });
80
+ } else {
81
+ out.push(a2uiPart(seg.envelopes));
82
+ }
83
+ }
84
+ return out;
85
+ }
86
+ const a2ui = generateMiddleware(
87
+ {
88
+ name: "a2ui",
89
+ description: "Adds A2UI (Agent-to-UI) streaming UI support: injects catalog capabilities into the prompt and rewrites emitted UI blocks into a2ui data parts.",
90
+ configSchema: A2uiOptionsSchema
91
+ },
92
+ (options) => {
93
+ const { ai, config } = options;
94
+ const {
95
+ catalog: catalogId = DEFAULT_CATALOG_ID,
96
+ instructions = "system",
97
+ validate = "warn",
98
+ version = A2UI_VERSION
99
+ } = config ?? {};
100
+ const nextSurfaceId = surfaceIdFactory(config?.surfaceId);
101
+ return {
102
+ model: async (req, ctx, next) => {
103
+ const catalog = await resolveCatalog(ai, catalogId);
104
+ const surfaceIds = replayableSurfaceIds(nextSurfaceId);
105
+ const sanitized = sanitizeInboundA2ui(req);
106
+ const request = instructions === "none" ? sanitized : injectInstructions(sanitized, catalog);
107
+ const streamParser = new A2uiStreamParser({
108
+ catalog,
109
+ validate,
110
+ version,
111
+ surfaceId: surfaceIds.next
112
+ });
113
+ const originalOnChunk = ctx?.onChunk;
114
+ const wrappedCtx = originalOnChunk ? {
115
+ ...ctx,
116
+ onChunk: (chunk) => {
117
+ const transformed = transformChunk(chunk, streamParser);
118
+ if (transformed) originalOnChunk(transformed);
119
+ }
120
+ } : ctx;
121
+ const response = await next(request, wrappedCtx);
122
+ if (originalOnChunk) {
123
+ const tail = partsFromParse(streamParser.flush());
124
+ if (tail.length > 0) originalOnChunk({ content: tail });
125
+ }
126
+ surfaceIds.reset();
127
+ return transformResponse(response, {
128
+ catalog,
129
+ validate,
130
+ version,
131
+ surfaceId: surfaceIds.replayNext
132
+ });
133
+ }
134
+ };
135
+ }
136
+ );
137
+ function injectInstructions(req, catalog) {
138
+ const text = renderCatalogInstructions(catalog);
139
+ const messages = [...req.messages];
140
+ const sysIdx = messages.findIndex((m) => m.role === "system");
141
+ if (sysIdx >= 0) {
142
+ const sys = messages[sysIdx];
143
+ messages[sysIdx] = {
144
+ ...sys,
145
+ content: [...sys.content, { text: "\n\n" + text }]
146
+ };
147
+ } else {
148
+ messages.unshift({ role: "system", content: [{ text }] });
149
+ }
150
+ return { ...req, messages };
151
+ }
152
+ function transformChunk(chunk, parser) {
153
+ if (!chunk?.content || chunk.content.length === 0) return chunk;
154
+ const newContent = [];
155
+ for (const part of chunk.content) {
156
+ if (isTextPart(part) && part.text !== "") {
157
+ newContent.push(...partsFromParse(parser.push(part.text)));
158
+ } else {
159
+ newContent.push(part);
160
+ }
161
+ }
162
+ if (newContent.length === 0) return null;
163
+ return { ...chunk, content: newContent };
164
+ }
165
+ function transformResponse(response, opts) {
166
+ const message = response.message;
167
+ if (!message?.content) return response;
168
+ const parser = new A2uiStreamParser(opts);
169
+ const newContent = [];
170
+ for (const part of message.content) {
171
+ if (isTextPart(part)) {
172
+ const pushed = parser.push(part.text);
173
+ const flushed = parser.flush();
174
+ const segments = [...pushed.segments, ...flushed.segments];
175
+ newContent.push(...partsFromParse({ segments }));
176
+ } else {
177
+ newContent.push(part);
178
+ }
179
+ }
180
+ return {
181
+ ...response,
182
+ message: { ...message, content: newContent }
183
+ };
184
+ }
185
+ function sanitizeInboundA2ui(req) {
186
+ let changed = false;
187
+ const messages = [];
188
+ for (const message of req.messages) {
189
+ if (!Array.isArray(message.content)) {
190
+ messages.push(message);
191
+ continue;
192
+ }
193
+ let msgChanged = false;
194
+ const content = [];
195
+ for (const part of message.content) {
196
+ if (isA2uiPart(part)) {
197
+ msgChanged = true;
198
+ const text = summarizeA2uiPart(part.data.envelopes);
199
+ if (text) content.push({ text });
200
+ } else {
201
+ content.push(part);
202
+ }
203
+ }
204
+ if (!msgChanged) {
205
+ messages.push(message);
206
+ continue;
207
+ }
208
+ changed = true;
209
+ if (content.length === 0) continue;
210
+ messages.push({ ...message, content });
211
+ }
212
+ return changed ? { ...req, messages } : req;
213
+ }
214
+ function summarizeA2uiPart(envelopes) {
215
+ const out = [];
216
+ let pendingSurface = [];
217
+ const flushSurface = () => {
218
+ if (pendingSurface.length === 0) return;
219
+ out.push("```a2ui\n" + JSON.stringify(pendingSurface) + "\n```");
220
+ pendingSurface = [];
221
+ };
222
+ for (const env of envelopes) {
223
+ if (!env || typeof env !== "object") continue;
224
+ if ("action" in env && env.action) {
225
+ flushSurface();
226
+ const a = env.action;
227
+ const ctx = a.context && Object.keys(a.context).length ? ` context=${JSON.stringify(a.context)}` : "";
228
+ out.push(`[UI action "${a.name}" on surface ${a.surfaceId}${ctx}]`);
229
+ } else if ("createSurface" in env && env.createSurface || "updateComponents" in env && env.updateComponents || "updateDataModel" in env && env.updateDataModel || "deleteSurface" in env && env.deleteSurface) {
230
+ pendingSurface.push(env);
231
+ }
232
+ }
233
+ flushSurface();
234
+ return out.join("\n");
235
+ }
236
+ export {
237
+ A2uiOptionsSchema,
238
+ a2ui
239
+ };
240
+ //# sourceMappingURL=middleware.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/middleware.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 * The `a2ui()` generate middleware — the whole server-side integration.\n *\n * Add it to `defineAgent({ use: [...] })` or `ai.generate({ use: [...] })` and\n * the agent gains the ability to render A2UI surfaces. The middleware:\n *\n * 1. Injects the catalog's capabilities into the system prompt so the model\n * knows what UI it may render.\n * 2. Intercepts the model's output (both the streamed chunks and the final\n * message), extracts any `a2ui` fenced blocks, validates them against the\n * catalog, and rewrites them into the canonical a2ui data part.\n *\n * Downstream (client transport, `@a2ui/web_core`) only ever sees a2ui parts —\n * \"pure vs mixed\" turns are a prompting choice, not a separate code path.\n *\n * Implemented with `generateMiddleware` and the `model` hook, so it wraps each\n * raw model call in the agent's tool loop.\n *\n * @module\n */\n\nimport { generateMiddleware, z, type GenerateMiddleware } from 'genkit';\nimport type {\n GenerateRequest,\n GenerateResponseChunkData,\n GenerateResponseData,\n MessageData,\n Part,\n TextPart,\n} from 'genkit/model';\nimport {\n DEFAULT_CATALOG_ID,\n renderCatalogInstructions,\n type A2uiCatalog,\n} from './catalog.mjs';\nimport { resolveCatalog } from './loader.mjs';\nimport { A2uiStreamParser, type ParseResult } from './parser.mjs';\nimport { a2uiPart, isA2uiPart } from './part.mjs';\nimport {\n A2UI_VERSION,\n SUPPORTED_VERSIONS,\n type A2uiEnvelope,\n} from './types.mjs';\n\n/** Zod schema for the {@link a2ui} middleware configuration. */\nexport const A2uiOptionsSchema = z.object({\n /**\n * The id of the catalog describing what the agent may render. Defaults to\n * `'basic'` (the bundled basic catalog). Register additional catalogs with\n * `loadCatalog(ai, { id, catalog | file })` and reference them by id here.\n */\n catalog: z.string().optional(),\n\n /**\n * Where to inject the catalog's capabilities. `'system'` (default) appends\n * A2UI instructions to the system prompt; `'none'` injects nothing (useful if\n * you supply your own instructions).\n */\n instructions: z.enum(['system', 'none']).optional(),\n\n /**\n * Validate emitted envelopes against the catalog. `'warn'` (default) logs a\n * warning and drops the offending block/envelope, keeping the rest of the\n * turn alive; `'strict'` throws on malformed JSON or unknown components (best\n * for development); `'off'` passes them through unchecked.\n *\n * IMPORTANT: this validates envelope structure and component *type names*\n * against the catalog only. It does NOT validate component props or\n * data-model values. Model-controlled values such as `Image.url` and `Text`\n * (inline Markdown, which the renderer may turn into HTML) pass through\n * untouched even under `'strict'`. `'strict'` is a well-formedness check, not\n * a security boundary — see the \"Security / trust boundary\" section of the\n * README. Prop sanitization is the renderer/catalog's responsibility, and\n * hosts should CSP-restrict image and other remote sources.\n */\n validate: z.enum(['strict', 'warn', 'off']).optional(),\n\n /**\n * Surface id policy. Provide a fixed id to reuse for every surface. Defaults\n * to a fresh UUID per surface.\n */\n surfaceId: z.string().optional(),\n\n /**\n * Protocol version stamped on emitted envelopes. Defaults to `'v0.9'`.\n * Constrained to the versions the renderer understands so a typo can't emit\n * envelopes that fail at runtime.\n */\n version: z\n .enum(SUPPORTED_VERSIONS as unknown as [string, ...string[]])\n .optional(),\n});\n\n/** Configuration for the {@link a2ui} middleware. */\nexport type A2uiOptions = z.infer<typeof A2uiOptionsSchema>;\n\n/** Type guard: is this part a text part? */\nfunction isTextPart(part: Part): part is TextPart {\n return (\n !!part &&\n typeof part === 'object' &&\n typeof (part as { text?: unknown }).text === 'string'\n );\n}\n\n/** Resolves the configured surface-id policy into a factory. */\nfunction surfaceIdFactory(policy: A2uiOptions['surfaceId']): () => string {\n if (typeof policy === 'string') return () => policy;\n // Use the Web Crypto API (available on Node >=16 and all supported \"exotic\"\n // runtimes, e.g. Cloudflare Workers) rather than a hard `node:crypto` import,\n // keeping the middleware portable.\n return () => globalThis.crypto.randomUUID();\n}\n\n/**\n * Wraps a surface-id factory so a single model turn's streamed parse and its\n * final-message parse mint the *same* surface ids.\n *\n * A turn is parsed twice: once incrementally over the streamed chunks, and once\n * over the aggregated final message (so consumers that read `response.message`\n * see a2ui parts too). Each parse pulls surface ids from the factory. With the\n * default `randomUUID` policy those two parses would otherwise produce different\n * ids for the same surface. So while streaming we generate and record ids in\n * order (`next`); before re-parsing the final message we `reset`, then\n * `replayNext` hands back the recorded ids in the same order (only generating a\n * fresh id if the final parse yields more blocks than the stream did).\n */\nfunction replayableSurfaceIds(base: () => string): {\n next: () => string;\n replayNext: () => string;\n reset: () => void;\n} {\n const generated: string[] = [];\n let cursor = 0;\n const next = () => {\n const id = base();\n generated.push(id);\n return id;\n };\n const replayNext = () =>\n cursor < generated.length ? generated[cursor++] : next();\n const reset = () => {\n cursor = 0;\n };\n return { next, replayNext, reset };\n}\n\n/**\n * Turns a parse result into ordered prose + a2ui parts, preserving the exact\n * source order (so prose after a block stays after it). Returns the new parts\n * to substitute for the original text part.\n */\nfunction partsFromParse(result: ParseResult): Part[] {\n const out: Part[] = [];\n for (const seg of result.segments) {\n if ('prose' in seg) {\n if (seg.prose) out.push({ text: seg.prose });\n } else {\n out.push(a2uiPart(seg.envelopes));\n }\n }\n return out;\n}\n\n/**\n * The A2UI generate middleware.\n *\n * @example\n * ```ts\n * import { a2ui } from '@genkit-ai/a2ui';\n *\n * export const uiAgent = ai.defineAgent({\n * name: 'uiAgent',\n * model: 'googleai/gemini-flash-latest',\n * system: 'You help users. Render UI when it is clearer than prose.',\n * use: [a2ui()], // defaults to the bundled 'basic' catalog\n * });\n * ```\n */\nexport const a2ui: GenerateMiddleware<typeof A2uiOptionsSchema> =\n generateMiddleware(\n {\n name: 'a2ui',\n description:\n 'Adds A2UI (Agent-to-UI) streaming UI support: injects catalog ' +\n 'capabilities into the prompt and rewrites emitted UI blocks into a2ui ' +\n 'data parts.',\n configSchema: A2uiOptionsSchema,\n },\n (options) => {\n const { ai, config } = options;\n const {\n catalog: catalogId = DEFAULT_CATALOG_ID,\n instructions = 'system',\n validate = 'warn',\n version = A2UI_VERSION,\n } = config ?? {};\n const nextSurfaceId = surfaceIdFactory(config?.surfaceId);\n\n return {\n model: async (req, ctx, next) => {\n // Resolve the catalog by id from the registry (falls back to the\n // bundled basic catalog for the default id).\n const catalog = await resolveCatalog(ai, catalogId);\n\n // Share surface ids between the streamed parse and the final-message\n // parse of this single turn, so the same surface gets the same id in\n // both (see replayableSurfaceIds).\n const surfaceIds = replayableSurfaceIds(nextSurfaceId);\n\n // 0) Sanitize any inbound a2ui data parts (e.g. a surface action sent\n // back as the next turn, or replayed history) into model-readable\n // text, so the underlying model's converter never sees the a2ui\n // mime type.\n const sanitized = sanitizeInboundA2ui(req);\n\n // 1) Inject catalog instructions into the system prompt.\n const request =\n instructions === 'none'\n ? sanitized\n : injectInstructions(sanitized, catalog);\n\n // 2) Wrap the streaming callback so streamed text is split into prose\n // deltas + whole a2ui parts as blocks complete.\n const streamParser = new A2uiStreamParser({\n catalog,\n validate,\n version,\n surfaceId: surfaceIds.next,\n });\n\n const originalOnChunk = ctx?.onChunk;\n const wrappedCtx = originalOnChunk\n ? {\n ...ctx,\n onChunk: (chunk: GenerateResponseChunkData) => {\n const transformed = transformChunk(chunk, streamParser);\n if (transformed) originalOnChunk(transformed);\n },\n }\n : ctx;\n\n // 3) Run downstream model, then flush the stream parser so the last\n // withheld prose tail (the parser holds back up to a partial\n // opening fence) and any unterminated trailing block still reach\n // the streaming consumer. Without this, clients that render purely\n // from stream deltas would show truncated prose / miss a final\n // block (the aggregated message recovers it, but the stream would\n // not).\n const response = await next(request, wrappedCtx);\n if (originalOnChunk) {\n const tail = partsFromParse(streamParser.flush());\n if (tail.length > 0) originalOnChunk({ content: tail });\n }\n\n // 4) Transform the final message. The final parse replays the same\n // surface ids the stream minted.\n surfaceIds.reset();\n return transformResponse(response, {\n catalog,\n validate,\n version,\n surfaceId: surfaceIds.replayNext,\n });\n },\n };\n }\n );\n\n/** Appends A2UI instructions to (or creates) the system message. */\nfunction injectInstructions(\n req: GenerateRequest,\n catalog: A2uiCatalog\n): GenerateRequest {\n const text = renderCatalogInstructions(catalog);\n const messages: MessageData[] = [...req.messages];\n const sysIdx = messages.findIndex((m) => m.role === 'system');\n if (sysIdx >= 0) {\n const sys = messages[sysIdx];\n messages[sysIdx] = {\n ...sys,\n content: [...sys.content, { text: '\\n\\n' + text }],\n };\n } else {\n messages.unshift({ role: 'system', content: [{ text }] });\n }\n return { ...req, messages };\n}\n\n/** Transforms a single streamed chunk; returns null if nothing to emit. */\nfunction transformChunk(\n chunk: GenerateResponseChunkData,\n parser: A2uiStreamParser\n): GenerateResponseChunkData | null {\n if (!chunk?.content || chunk.content.length === 0) return chunk;\n const newContent: Part[] = [];\n for (const part of chunk.content) {\n if (isTextPart(part) && part.text !== '') {\n newContent.push(...partsFromParse(parser.push(part.text)));\n } else {\n newContent.push(part);\n }\n }\n if (newContent.length === 0) return null;\n return { ...chunk, content: newContent };\n}\n\n/** Transforms the final response message: prose text + a2ui parts. */\nfunction transformResponse(\n response: GenerateResponseData,\n opts: {\n catalog: A2uiCatalog;\n validate: 'strict' | 'warn' | 'off';\n version: string;\n surfaceId: () => string;\n }\n): GenerateResponseData {\n const message = response.message;\n if (!message?.content) return response;\n\n const parser = new A2uiStreamParser(opts);\n const newContent: Part[] = [];\n for (const part of message.content) {\n if (isTextPart(part)) {\n // Combine the streamed-push and final-flush segments so ordering (prose\n // before/after a block) is preserved in the aggregated message too.\n const pushed = parser.push(part.text);\n const flushed = parser.flush();\n const segments = [...pushed.segments, ...flushed.segments];\n newContent.push(...partsFromParse({ segments } as ParseResult));\n } else {\n newContent.push(part);\n }\n }\n return {\n ...response,\n message: { ...message, content: newContent },\n };\n}\n\n/**\n * Converts inbound a2ui data parts in the request into model-readable text.\n *\n * The a2ui data part (mime `application/a2ui+json`) is meaningful to the client\n * renderer, but the underlying model's message converter (e.g. Gemini) does not\n * understand it. When a rendered surface's action is sent back as the next\n * turn's input — or when prior assistant turns containing surfaces are replayed\n * as history — we replace those parts with a compact text summary so the model\n * can reason about them.\n */\nfunction sanitizeInboundA2ui(req: GenerateRequest): GenerateRequest {\n let changed = false;\n const messages: MessageData[] = [];\n for (const message of req.messages) {\n if (!Array.isArray(message.content)) {\n messages.push(message);\n continue;\n }\n let msgChanged = false;\n const content: Part[] = [];\n for (const part of message.content) {\n if (isA2uiPart(part)) {\n msgChanged = true;\n const text = summarizeA2uiPart(part.data.envelopes);\n if (text) content.push({ text });\n } else {\n content.push(part as Part);\n }\n }\n if (!msgChanged) {\n messages.push(message);\n continue;\n }\n changed = true;\n // Drop a message that sanitizing emptied out. This happens when its only\n // content was an a2ui part whose envelopes all summarized to nothing (e.g.\n // an empty or all-unrecognized envelope array). Sending `content: []`\n // downstream would make providers like Gemini and Vertex reject the\n // request, so skip the message entirely instead.\n if (content.length === 0) continue;\n messages.push({ ...message, content });\n }\n return changed ? { ...req, messages } : req;\n}\n\n/**\n * Converts an array of a2ui envelopes from an inbound message part back into\n * model-readable text — the inverse of the outbound block-to-part transform.\n *\n * The two envelope kinds are handled differently on purpose:\n *\n * - Assistant-authored surface envelopes (`createSurface`, `updateComponents`,\n * `updateDataModel`, `deleteSurface`) are reconstructed as the canonical\n * `a2ui` fenced block the model originally emitted. Replaying a prior turn's\n * surface as history therefore shows the model its own UI output in the exact\n * format it is asked to produce, reinforcing correct behavior. (Summarizing it\n * to a sentinel like `[rendered UI surface]` instead taught the model to emit\n * that literal string in place of a real block.)\n * - Client-synthesized `action` envelopes never had a block form, so they\n * become a short text summary the model can reason about.\n *\n * Consecutive surface envelopes are grouped into a single block (one surface is\n * usually several envelopes: create + update(s)). Unknown envelope shapes are\n * dropped, so an all-unrecognized (or empty) envelope array summarizes to an\n * empty string; {@link sanitizeInboundA2ui} then drops the emptied message\n * rather than sending empty content downstream.\n */\nfunction summarizeA2uiPart(envelopes: A2uiEnvelope[]): string {\n const out: string[] = [];\n let pendingSurface: A2uiEnvelope[] = [];\n\n const flushSurface = () => {\n if (pendingSurface.length === 0) return;\n // Keep the real surface ids verbatim. The model may not reuse them for a\n // NEW surface: the parser forces a fresh id onto every `createSurface`\n // block (see `finalizeBlock`), so a copied id can't overwrite a prior\n // surface. Keeping the real ids lets the model correlate a replayed action\n // (`[UI action ... on surface <id>]`) with the surface it targeted — which\n // matters when several surfaces are on screen at once.\n //\n // Encode compactly (not pretty-printed): fewer tokens, and it collapses the\n // payload to a single line so the block is exactly three lines (open fence,\n // JSON, close fence). Because JSON escapes any newline inside a string as\n // `\\n`, an A2UI `Text` value containing a fenced code sample can't put a\n // literal ``` at the start of a line, so it can never prematurely close this\n // block (the parser's close fence is line-anchored). The block text can\n // still contain ``` characters mid-line; a fully robust emitter would use a\n // variable-length fence, but that also requires the parser's fixed\n // three-backtick open fence to become count-aware, so it is deferred.\n out.push('```a2ui\\n' + JSON.stringify(pendingSurface) + '\\n```');\n pendingSurface = [];\n };\n\n for (const env of envelopes) {\n if (!env || typeof env !== 'object') continue;\n if ('action' in env && env.action) {\n // Emit any buffered surface block before the action, preserving order.\n flushSurface();\n const a = env.action;\n const ctx =\n a.context && Object.keys(a.context).length\n ? ` context=${JSON.stringify(a.context)}`\n : '';\n out.push(`[UI action \"${a.name}\" on surface ${a.surfaceId}${ctx}]`);\n } else if (\n ('createSurface' in env && env.createSurface) ||\n ('updateComponents' in env && env.updateComponents) ||\n ('updateDataModel' in env && env.updateDataModel) ||\n ('deleteSurface' in env && env.deleteSurface)\n ) {\n pendingSurface.push(env);\n }\n }\n flushSurface();\n return out.join('\\n');\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,132 @@
1
+ import { A2uiCatalog } from './catalog.mjs';
2
+ import { A2uiEnvelope } from './types.mjs';
3
+
4
+ /**
5
+ * Copyright 2026 Google LLC
6
+ *
7
+ * Licensed under the Apache License, Version 2.0 (the "License");
8
+ * you may not use this file except in compliance with the License.
9
+ * You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software
14
+ * distributed under the License is distributed on an "AS IS" BASIS,
15
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ * See the License for the specific language governing permissions and
17
+ * limitations under the License.
18
+ */
19
+ /**
20
+ * Streaming parser that extracts A2UI envelopes from model output.
21
+ *
22
+ * The model emits A2UI as a fenced code block tagged `a2ui` containing a JSON
23
+ * array of envelopes (see {@link renderCatalogInstructions}). This parser scans
24
+ * a text stream incrementally, separating ordinary prose (which streams through
25
+ * as deltas) from complete A2UI blocks (which are buffered until the closing
26
+ * fence, then parsed into whole, validated envelopes — the protocol requires
27
+ * ordered, complete messages, so we never emit half-parsed JSON).
28
+ *
29
+ * This module is Node-free so it can be unit-tested and reused anywhere.
30
+ *
31
+ * @module
32
+ */
33
+
34
+ /**
35
+ * A single ordered piece of parsed output: either a run of prose or one
36
+ * completed A2UI envelope batch. Segments preserve the exact source order, so
37
+ * prose that appears *after* a block is not reordered ahead of it.
38
+ */
39
+ type ParseSegment = {
40
+ prose: string;
41
+ } | {
42
+ envelopes: A2uiEnvelope[];
43
+ };
44
+ /** Result of feeding text to {@link A2uiStreamParser.push}. */
45
+ interface ParseResult {
46
+ /**
47
+ * Ordered prose/envelope segments exactly as they appear in the source text.
48
+ * Prefer this over {@link ParseResult.prose}/{@link ParseResult.envelopeBatches}
49
+ * when order between prose and blocks matters.
50
+ */
51
+ segments: ParseSegment[];
52
+ /**
53
+ * Convenience: all prose runs concatenated (never contains A2UI blocks). Loses
54
+ * the relative order of prose vs. blocks — use {@link ParseResult.segments}
55
+ * when that matters.
56
+ */
57
+ prose: string;
58
+ /** Convenience: the fully-parsed A2UI envelope batches, in order. */
59
+ envelopeBatches: A2uiEnvelope[][];
60
+ }
61
+ /** Options controlling how the parser finalizes envelopes. */
62
+ interface A2uiParserOptions {
63
+ /** Catalog used to validate component references. */
64
+ catalog?: A2uiCatalog;
65
+ /**
66
+ * `'strict'` throws on unknown components / bad JSON; `'warn'` logs a warning
67
+ * and drops the offending block/envelope; `'off'` skips validation entirely.
68
+ */
69
+ validate?: 'strict' | 'warn' | 'off';
70
+ /** Protocol version stamped onto envelopes lacking one. */
71
+ version?: string;
72
+ /** Produces the surface id substituted for the model's placeholder. */
73
+ surfaceId: () => string;
74
+ }
75
+ /**
76
+ * Incremental A2UI extractor. Create one per model turn, `push()` text deltas as
77
+ * they arrive, and `flush()` at the end to drain any trailing block.
78
+ */
79
+ declare class A2uiStreamParser {
80
+ private readonly options;
81
+ private buffer;
82
+ private inBlock;
83
+ /** Stable surface id for the current block (placeholders map to this). */
84
+ private currentSurfaceId;
85
+ constructor(options: A2uiParserOptions);
86
+ /** Feeds a chunk of model text, returning prose + any completed blocks. */
87
+ push(text: string): ParseResult;
88
+ /** Drains any remaining buffered content at end of stream. */
89
+ flush(): ParseResult;
90
+ private drain;
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
+ private reject;
98
+ /** Parses, validates, and normalizes one block's JSON into envelopes. */
99
+ private finalizeBlock;
100
+ /**
101
+ * Validates a single envelope, substitutes the real surface id for the
102
+ * placeholder, and stamps the protocol version.
103
+ */
104
+ private normalizeEnvelope;
105
+ /**
106
+ * Ensures every component references a known catalog component. Returns an
107
+ * error message describing the first problem found, or `null` if valid.
108
+ *
109
+ * This checks component *type names* against the catalog only. It intentionally
110
+ * does NOT enforce the "must contain a root" protocol rule — that applies only
111
+ * to full-surface renders and is enforced at the batch level in
112
+ * {@link finalizeBlock} (an incremental update to an existing surface may
113
+ * legitimately patch a subtree without re-declaring `root`).
114
+ */
115
+ private validateComponents;
116
+ /**
117
+ * Enforces the "RE-RENDER THE WHOLE SURFACE" protocol rule for full-surface
118
+ * renders. The v0.9 spec requires that "one of the components in one of the
119
+ * component lists MUST have an id of `root`" — a batch-level rule, not a
120
+ * per-message one. A split render is therefore legal: the root (and layout)
121
+ * may live in one `updateComponents` while its leaf components arrive in a
122
+ * later one within the same batch. So this checks that *at least one*
123
+ * `updateComponents` in the batch declares a `root`, not that every one does.
124
+ *
125
+ * Returns an error message, or `null` if valid. Unlike
126
+ * {@link validateComponents} this is a protocol check, not a catalog check, so
127
+ * it runs regardless of whether a catalog is configured.
128
+ */
129
+ private validateRoot;
130
+ }
131
+
132
+ export { type A2uiParserOptions, A2uiStreamParser, type ParseResult, type ParseSegment };
@@ -0,0 +1,132 @@
1
+ import { A2uiCatalog } from './catalog.js';
2
+ import { A2uiEnvelope } from './types.js';
3
+
4
+ /**
5
+ * Copyright 2026 Google LLC
6
+ *
7
+ * Licensed under the Apache License, Version 2.0 (the "License");
8
+ * you may not use this file except in compliance with the License.
9
+ * You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software
14
+ * distributed under the License is distributed on an "AS IS" BASIS,
15
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ * See the License for the specific language governing permissions and
17
+ * limitations under the License.
18
+ */
19
+ /**
20
+ * Streaming parser that extracts A2UI envelopes from model output.
21
+ *
22
+ * The model emits A2UI as a fenced code block tagged `a2ui` containing a JSON
23
+ * array of envelopes (see {@link renderCatalogInstructions}). This parser scans
24
+ * a text stream incrementally, separating ordinary prose (which streams through
25
+ * as deltas) from complete A2UI blocks (which are buffered until the closing
26
+ * fence, then parsed into whole, validated envelopes — the protocol requires
27
+ * ordered, complete messages, so we never emit half-parsed JSON).
28
+ *
29
+ * This module is Node-free so it can be unit-tested and reused anywhere.
30
+ *
31
+ * @module
32
+ */
33
+
34
+ /**
35
+ * A single ordered piece of parsed output: either a run of prose or one
36
+ * completed A2UI envelope batch. Segments preserve the exact source order, so
37
+ * prose that appears *after* a block is not reordered ahead of it.
38
+ */
39
+ type ParseSegment = {
40
+ prose: string;
41
+ } | {
42
+ envelopes: A2uiEnvelope[];
43
+ };
44
+ /** Result of feeding text to {@link A2uiStreamParser.push}. */
45
+ interface ParseResult {
46
+ /**
47
+ * Ordered prose/envelope segments exactly as they appear in the source text.
48
+ * Prefer this over {@link ParseResult.prose}/{@link ParseResult.envelopeBatches}
49
+ * when order between prose and blocks matters.
50
+ */
51
+ segments: ParseSegment[];
52
+ /**
53
+ * Convenience: all prose runs concatenated (never contains A2UI blocks). Loses
54
+ * the relative order of prose vs. blocks — use {@link ParseResult.segments}
55
+ * when that matters.
56
+ */
57
+ prose: string;
58
+ /** Convenience: the fully-parsed A2UI envelope batches, in order. */
59
+ envelopeBatches: A2uiEnvelope[][];
60
+ }
61
+ /** Options controlling how the parser finalizes envelopes. */
62
+ interface A2uiParserOptions {
63
+ /** Catalog used to validate component references. */
64
+ catalog?: A2uiCatalog;
65
+ /**
66
+ * `'strict'` throws on unknown components / bad JSON; `'warn'` logs a warning
67
+ * and drops the offending block/envelope; `'off'` skips validation entirely.
68
+ */
69
+ validate?: 'strict' | 'warn' | 'off';
70
+ /** Protocol version stamped onto envelopes lacking one. */
71
+ version?: string;
72
+ /** Produces the surface id substituted for the model's placeholder. */
73
+ surfaceId: () => string;
74
+ }
75
+ /**
76
+ * Incremental A2UI extractor. Create one per model turn, `push()` text deltas as
77
+ * they arrive, and `flush()` at the end to drain any trailing block.
78
+ */
79
+ declare class A2uiStreamParser {
80
+ private readonly options;
81
+ private buffer;
82
+ private inBlock;
83
+ /** Stable surface id for the current block (placeholders map to this). */
84
+ private currentSurfaceId;
85
+ constructor(options: A2uiParserOptions);
86
+ /** Feeds a chunk of model text, returning prose + any completed blocks. */
87
+ push(text: string): ParseResult;
88
+ /** Drains any remaining buffered content at end of stream. */
89
+ flush(): ParseResult;
90
+ private drain;
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
+ private reject;
98
+ /** Parses, validates, and normalizes one block's JSON into envelopes. */
99
+ private finalizeBlock;
100
+ /**
101
+ * Validates a single envelope, substitutes the real surface id for the
102
+ * placeholder, and stamps the protocol version.
103
+ */
104
+ private normalizeEnvelope;
105
+ /**
106
+ * Ensures every component references a known catalog component. Returns an
107
+ * error message describing the first problem found, or `null` if valid.
108
+ *
109
+ * This checks component *type names* against the catalog only. It intentionally
110
+ * does NOT enforce the "must contain a root" protocol rule — that applies only
111
+ * to full-surface renders and is enforced at the batch level in
112
+ * {@link finalizeBlock} (an incremental update to an existing surface may
113
+ * legitimately patch a subtree without re-declaring `root`).
114
+ */
115
+ private validateComponents;
116
+ /**
117
+ * Enforces the "RE-RENDER THE WHOLE SURFACE" protocol rule for full-surface
118
+ * renders. The v0.9 spec requires that "one of the components in one of the
119
+ * component lists MUST have an id of `root`" — a batch-level rule, not a
120
+ * per-message one. A split render is therefore legal: the root (and layout)
121
+ * may live in one `updateComponents` while its leaf components arrive in a
122
+ * later one within the same batch. So this checks that *at least one*
123
+ * `updateComponents` in the batch declares a `root`, not that every one does.
124
+ *
125
+ * Returns an error message, or `null` if valid. Unlike
126
+ * {@link validateComponents} this is a protocol check, not a catalog check, so
127
+ * it runs regardless of whether a catalog is configured.
128
+ */
129
+ private validateRoot;
130
+ }
131
+
132
+ export { type A2uiParserOptions, A2uiStreamParser, type ParseResult, type ParseSegment };