@genkit-ai/a2ui 0.5.0-rc.1 → 0.5.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.
- package/lib/middleware.js +20 -10
- package/lib/middleware.js.map +1 -1
- package/lib/middleware.mjs +20 -10
- package/lib/middleware.mjs.map +1 -1
- package/package.json +3 -3
package/lib/middleware.js
CHANGED
|
@@ -181,24 +181,34 @@ function transformChunk(chunk, parser) {
|
|
|
181
181
|
return { ...chunk, content: newContent };
|
|
182
182
|
}
|
|
183
183
|
function transformResponse(response, opts) {
|
|
184
|
-
const message = response.message;
|
|
184
|
+
const message = response.message ?? response.candidates?.[0]?.message;
|
|
185
185
|
if (!message?.content) return response;
|
|
186
186
|
const parser = new import_parser.A2uiStreamParser(opts);
|
|
187
187
|
const newContent = [];
|
|
188
|
+
const flushHeld = () => {
|
|
189
|
+
const tail = parser.flush();
|
|
190
|
+
newContent.push(...partsFromParse(tail));
|
|
191
|
+
};
|
|
188
192
|
for (const part of message.content) {
|
|
189
|
-
if (isTextPart(part)) {
|
|
190
|
-
|
|
191
|
-
const flushed = parser.flush();
|
|
192
|
-
const segments = [...pushed.segments, ...flushed.segments];
|
|
193
|
-
newContent.push(...partsFromParse({ segments }));
|
|
193
|
+
if (isTextPart(part) && part.text !== "") {
|
|
194
|
+
newContent.push(...partsFromParse(parser.push(part.text)));
|
|
194
195
|
} else {
|
|
196
|
+
flushHeld();
|
|
195
197
|
newContent.push(part);
|
|
196
198
|
}
|
|
197
199
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
200
|
+
flushHeld();
|
|
201
|
+
const newMessage = { ...message, content: newContent };
|
|
202
|
+
const result = { ...response };
|
|
203
|
+
if (result.message) {
|
|
204
|
+
result.message = newMessage;
|
|
205
|
+
}
|
|
206
|
+
if (result.candidates?.[0]) {
|
|
207
|
+
result.candidates = result.candidates.map(
|
|
208
|
+
(c, i) => i === 0 ? { ...c, message: newMessage } : c
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
return result;
|
|
202
212
|
}
|
|
203
213
|
function sanitizeInboundA2ui(req) {
|
|
204
214
|
let changed = false;
|
package/lib/middleware.js.map
CHANGED
|
@@ -1 +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.js';\nimport { resolveCatalog } from './loader.js';\nimport { A2uiStreamParser, type ParseResult } from './parser.js';\nimport { a2uiPart, isA2uiPart } from './part.js';\nimport {\n A2UI_VERSION,\n SUPPORTED_VERSIONS,\n type A2uiEnvelope,\n} from './types.js';\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":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAqCA,oBAA+D;AAS/D,qBAIO;AACP,oBAA+B;AAC/B,oBAAmD;AACnD,kBAAqC;AACrC,mBAIO;AAGA,MAAM,oBAAoB,gBAAE,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMxC,SAAS,gBAAE,OAAO,EAAE,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAO7B,cAAc,gBAAE,KAAK,CAAC,UAAU,MAAM,CAAC,EAAE,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiBlD,UAAU,gBAAE,KAAK,CAAC,UAAU,QAAQ,KAAK,CAAC,EAAE,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA,EAMrD,WAAW,gBAAE,OAAO,EAAE,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAO/B,SAAS,gBACN,KAAK,+BAAsD,EAC3D,SAAS;AACd,CAAC;AAMD,SAAS,WAAW,MAA8B;AAChD,SACE,CAAC,CAAC,QACF,OAAO,SAAS,YAChB,OAAQ,KAA4B,SAAS;AAEjD;AAGA,SAAS,iBAAiB,QAAgD;AACxE,MAAI,OAAO,WAAW,SAAU,QAAO,MAAM;AAI7C,SAAO,MAAM,WAAW,OAAO,WAAW;AAC5C;AAeA,SAAS,qBAAqB,MAI5B;AACA,QAAM,YAAsB,CAAC;AAC7B,MAAI,SAAS;AACb,QAAM,OAAO,MAAM;AACjB,UAAM,KAAK,KAAK;AAChB,cAAU,KAAK,EAAE;AACjB,WAAO;AAAA,EACT;AACA,QAAM,aAAa,MACjB,SAAS,UAAU,SAAS,UAAU,QAAQ,IAAI,KAAK;AACzD,QAAM,QAAQ,MAAM;AAClB,aAAS;AAAA,EACX;AACA,SAAO,EAAE,MAAM,YAAY,MAAM;AACnC;AAOA,SAAS,eAAe,QAA6B;AACnD,QAAM,MAAc,CAAC;AACrB,aAAW,OAAO,OAAO,UAAU;AACjC,QAAI,WAAW,KAAK;AAClB,UAAI,IAAI,MAAO,KAAI,KAAK,EAAE,MAAM,IAAI,MAAM,CAAC;AAAA,IAC7C,OAAO;AACL,UAAI,SAAK,sBAAS,IAAI,SAAS,CAAC;AAAA,IAClC;AAAA,EACF;AACA,SAAO;AACT;AAiBO,MAAM,WACX;AAAA,EACE;AAAA,IACE,MAAM;AAAA,IACN,aACE;AAAA,IAGF,cAAc;AAAA,EAChB;AAAA,EACA,CAAC,YAAY;AACX,UAAM,EAAE,IAAI,OAAO,IAAI;AACvB,UAAM;AAAA,MACJ,SAAS,YAAY;AAAA,MACrB,eAAe;AAAA,MACf,WAAW;AAAA,MACX,UAAU;AAAA,IACZ,IAAI,UAAU,CAAC;AACf,UAAM,gBAAgB,iBAAiB,QAAQ,SAAS;AAExD,WAAO;AAAA,MACL,OAAO,OAAO,KAAK,KAAK,SAAS;AAG/B,cAAM,UAAU,UAAM,8BAAe,IAAI,SAAS;AAKlD,cAAM,aAAa,qBAAqB,aAAa;AAMrD,cAAM,YAAY,oBAAoB,GAAG;AAGzC,cAAM,UACJ,iBAAiB,SACb,YACA,mBAAmB,WAAW,OAAO;AAI3C,cAAM,eAAe,IAAI,+BAAiB;AAAA,UACxC;AAAA,UACA;AAAA,UACA;AAAA,UACA,WAAW,WAAW;AAAA,QACxB,CAAC;AAED,cAAM,kBAAkB,KAAK;AAC7B,cAAM,aAAa,kBACf;AAAA,UACE,GAAG;AAAA,UACH,SAAS,CAAC,UAAqC;AAC7C,kBAAM,cAAc,eAAe,OAAO,YAAY;AACtD,gBAAI,YAAa,iBAAgB,WAAW;AAAA,UAC9C;AAAA,QACF,IACA;AASJ,cAAM,WAAW,MAAM,KAAK,SAAS,UAAU;AAC/C,YAAI,iBAAiB;AACnB,gBAAM,OAAO,eAAe,aAAa,MAAM,CAAC;AAChD,cAAI,KAAK,SAAS,EAAG,iBAAgB,EAAE,SAAS,KAAK,CAAC;AAAA,QACxD;AAIA,mBAAW,MAAM;AACjB,eAAO,kBAAkB,UAAU;AAAA,UACjC;AAAA,UACA;AAAA,UACA;AAAA,UACA,WAAW,WAAW;AAAA,QACxB,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AACF;AAGF,SAAS,mBACP,KACA,SACiB;AACjB,QAAM,WAAO,0CAA0B,OAAO;AAC9C,QAAM,WAA0B,CAAC,GAAG,IAAI,QAAQ;AAChD,QAAM,SAAS,SAAS,UAAU,CAAC,MAAM,EAAE,SAAS,QAAQ;AAC5D,MAAI,UAAU,GAAG;AACf,UAAM,MAAM,SAAS,MAAM;AAC3B,aAAS,MAAM,IAAI;AAAA,MACjB,GAAG;AAAA,MACH,SAAS,CAAC,GAAG,IAAI,SAAS,EAAE,MAAM,SAAS,KAAK,CAAC;AAAA,IACnD;AAAA,EACF,OAAO;AACL,aAAS,QAAQ,EAAE,MAAM,UAAU,SAAS,CAAC,EAAE,KAAK,CAAC,EAAE,CAAC;AAAA,EAC1D;AACA,SAAO,EAAE,GAAG,KAAK,SAAS;AAC5B;AAGA,SAAS,eACP,OACA,QACkC;AAClC,MAAI,CAAC,OAAO,WAAW,MAAM,QAAQ,WAAW,EAAG,QAAO;AAC1D,QAAM,aAAqB,CAAC;AAC5B,aAAW,QAAQ,MAAM,SAAS;AAChC,QAAI,WAAW,IAAI,KAAK,KAAK,SAAS,IAAI;AACxC,iBAAW,KAAK,GAAG,eAAe,OAAO,KAAK,KAAK,IAAI,CAAC,CAAC;AAAA,IAC3D,OAAO;AACL,iBAAW,KAAK,IAAI;AAAA,IACtB;AAAA,EACF;AACA,MAAI,WAAW,WAAW,EAAG,QAAO;AACpC,SAAO,EAAE,GAAG,OAAO,SAAS,WAAW;AACzC;AAGA,SAAS,kBACP,UACA,MAMsB;AACtB,QAAM,UAAU,SAAS;AACzB,MAAI,CAAC,SAAS,QAAS,QAAO;AAE9B,QAAM,SAAS,IAAI,+BAAiB,IAAI;AACxC,QAAM,aAAqB,CAAC;AAC5B,aAAW,QAAQ,QAAQ,SAAS;AAClC,QAAI,WAAW,IAAI,GAAG;AAGpB,YAAM,SAAS,OAAO,KAAK,KAAK,IAAI;AACpC,YAAM,UAAU,OAAO,MAAM;AAC7B,YAAM,WAAW,CAAC,GAAG,OAAO,UAAU,GAAG,QAAQ,QAAQ;AACzD,iBAAW,KAAK,GAAG,eAAe,EAAE,SAAS,CAAgB,CAAC;AAAA,IAChE,OAAO;AACL,iBAAW,KAAK,IAAI;AAAA,IACtB;AAAA,EACF;AACA,SAAO;AAAA,IACL,GAAG;AAAA,IACH,SAAS,EAAE,GAAG,SAAS,SAAS,WAAW;AAAA,EAC7C;AACF;AAYA,SAAS,oBAAoB,KAAuC;AAClE,MAAI,UAAU;AACd,QAAM,WAA0B,CAAC;AACjC,aAAW,WAAW,IAAI,UAAU;AAClC,QAAI,CAAC,MAAM,QAAQ,QAAQ,OAAO,GAAG;AACnC,eAAS,KAAK,OAAO;AACrB;AAAA,IACF;AACA,QAAI,aAAa;AACjB,UAAM,UAAkB,CAAC;AACzB,eAAW,QAAQ,QAAQ,SAAS;AAClC,cAAI,wBAAW,IAAI,GAAG;AACpB,qBAAa;AACb,cAAM,OAAO,kBAAkB,KAAK,KAAK,SAAS;AAClD,YAAI,KAAM,SAAQ,KAAK,EAAE,KAAK,CAAC;AAAA,MACjC,OAAO;AACL,gBAAQ,KAAK,IAAY;AAAA,MAC3B;AAAA,IACF;AACA,QAAI,CAAC,YAAY;AACf,eAAS,KAAK,OAAO;AACrB;AAAA,IACF;AACA,cAAU;AAMV,QAAI,QAAQ,WAAW,EAAG;AAC1B,aAAS,KAAK,EAAE,GAAG,SAAS,QAAQ,CAAC;AAAA,EACvC;AACA,SAAO,UAAU,EAAE,GAAG,KAAK,SAAS,IAAI;AAC1C;AAwBA,SAAS,kBAAkB,WAAmC;AAC5D,QAAM,MAAgB,CAAC;AACvB,MAAI,iBAAiC,CAAC;AAEtC,QAAM,eAAe,MAAM;AACzB,QAAI,eAAe,WAAW,EAAG;AAiBjC,QAAI,KAAK,cAAc,KAAK,UAAU,cAAc,IAAI,OAAO;AAC/D,qBAAiB,CAAC;AAAA,EACpB;AAEA,aAAW,OAAO,WAAW;AAC3B,QAAI,CAAC,OAAO,OAAO,QAAQ,SAAU;AACrC,QAAI,YAAY,OAAO,IAAI,QAAQ;AAEjC,mBAAa;AACb,YAAM,IAAI,IAAI;AACd,YAAM,MACJ,EAAE,WAAW,OAAO,KAAK,EAAE,OAAO,EAAE,SAChC,YAAY,KAAK,UAAU,EAAE,OAAO,CAAC,KACrC;AACN,UAAI,KAAK,eAAe,EAAE,IAAI,gBAAgB,EAAE,SAAS,GAAG,GAAG,GAAG;AAAA,IACpE,WACG,mBAAmB,OAAO,IAAI,iBAC9B,sBAAsB,OAAO,IAAI,oBACjC,qBAAqB,OAAO,IAAI,mBAChC,mBAAmB,OAAO,IAAI,eAC/B;AACA,qBAAe,KAAK,GAAG;AAAA,IACzB;AAAA,EACF;AACA,eAAa;AACb,SAAO,IAAI,KAAK,IAAI;AACtB;","names":[]}
|
|
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.js';\nimport { resolveCatalog } from './loader.js';\nimport { A2uiStreamParser, type ParseResult } from './parser.js';\nimport { a2uiPart, isA2uiPart } from './part.js';\nimport {\n A2UI_VERSION,\n SUPPORTED_VERSIONS,\n type A2uiEnvelope,\n} from './types.js';\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 // Real models (e.g. google-genai) return the candidates shape, not a\n // top-level `message`; only the GenerateResponse constructor later collapses\n // `message ?? candidates[0].message`. Read from whichever the model used so\n // the transform runs on the real path, not just when a caller hands us a\n // pre-collapsed `message`.\n const message = response.message ?? response.candidates?.[0]?.message;\n if (!message?.content) return response;\n\n const parser = new A2uiStreamParser(opts);\n\n const newContent: Part[] = [];\n\n // Drains whatever the parser is still holding (a withheld prose tail, or an\n // unterminated trailing block) and appends it. Called at every non-text\n // boundary and once at the end.\n const flushHeld = () => {\n const tail = parser.flush();\n newContent.push(...partsFromParse(tail));\n };\n\n for (const part of message.content) {\n if (isTextPart(part) && part.text !== '') {\n // Push WITHOUT flushing between consecutive text parts so an a2ui block\n // that spans several adjacent text parts is stitched back together. The\n // model's final message is not guaranteed to coalesce adjacent text: the\n // Gemini plugin, for instance, aggregates a turn into many separate text\n // parts (fence, JSON body split many ways, close fence, then a trailing\n // empty-text part carrying the thought signature), so a per-part flush\n // would reset the parser mid-block and leak the whole surface back out as\n // raw prose. This mirrors the streaming path, which shares one parser\n // across all chunks and flushes only once at the end.\n newContent.push(...partsFromParse(parser.push(part.text)));\n } else {\n // A non-text part (e.g. a toolRequest) or an empty-text part (e.g. the\n // trailing thought-signature carrier) is a boundary: flush any held tail\n // so it lands before this part, preserving order. This is what a\n // tool-calling turn [text(\"Checking the weather.\"), toolRequest] relies\n // on so the prose is not reordered behind the toolRequest. Then carry the\n // part through untouched so its metadata survives.\n flushHeld();\n newContent.push(part);\n }\n }\n flushHeld();\n\n const newMessage = { ...message, content: newContent };\n\n // Write the transformed message back to WHICHEVER shape(s) the response\n // carries, so no consumer sees the raw prose. Real providers (e.g.\n // google-genai) use the candidates shape, but a response could carry a\n // top-level `message` and a `candidates` array at once (e.g. a prior\n // middleware pre-populated `message` while keeping `candidates`); updating\n // only one would leave the other holding the untransformed fence text. Only\n // candidate 0 is transformed: the framework collapses to\n // `candidates[0].message` and candidateCount defaults to 1, and each\n // candidate would otherwise consume the shared surface-id replay state.\n const result = { ...response };\n if (result.message) {\n result.message = newMessage;\n }\n if (result.candidates?.[0]) {\n result.candidates = result.candidates.map((c, i) =>\n i === 0 ? { ...c, message: newMessage } : c\n );\n }\n return result;\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":[]}
|
package/lib/middleware.mjs
CHANGED
|
@@ -163,24 +163,34 @@ function transformChunk(chunk, parser) {
|
|
|
163
163
|
return { ...chunk, content: newContent };
|
|
164
164
|
}
|
|
165
165
|
function transformResponse(response, opts) {
|
|
166
|
-
const message = response.message;
|
|
166
|
+
const message = response.message ?? response.candidates?.[0]?.message;
|
|
167
167
|
if (!message?.content) return response;
|
|
168
168
|
const parser = new A2uiStreamParser(opts);
|
|
169
169
|
const newContent = [];
|
|
170
|
+
const flushHeld = () => {
|
|
171
|
+
const tail = parser.flush();
|
|
172
|
+
newContent.push(...partsFromParse(tail));
|
|
173
|
+
};
|
|
170
174
|
for (const part of message.content) {
|
|
171
|
-
if (isTextPart(part)) {
|
|
172
|
-
|
|
173
|
-
const flushed = parser.flush();
|
|
174
|
-
const segments = [...pushed.segments, ...flushed.segments];
|
|
175
|
-
newContent.push(...partsFromParse({ segments }));
|
|
175
|
+
if (isTextPart(part) && part.text !== "") {
|
|
176
|
+
newContent.push(...partsFromParse(parser.push(part.text)));
|
|
176
177
|
} else {
|
|
178
|
+
flushHeld();
|
|
177
179
|
newContent.push(part);
|
|
178
180
|
}
|
|
179
181
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
flushHeld();
|
|
183
|
+
const newMessage = { ...message, content: newContent };
|
|
184
|
+
const result = { ...response };
|
|
185
|
+
if (result.message) {
|
|
186
|
+
result.message = newMessage;
|
|
187
|
+
}
|
|
188
|
+
if (result.candidates?.[0]) {
|
|
189
|
+
result.candidates = result.candidates.map(
|
|
190
|
+
(c, i) => i === 0 ? { ...c, message: newMessage } : c
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
return result;
|
|
184
194
|
}
|
|
185
195
|
function sanitizeInboundA2ui(req) {
|
|
186
196
|
let changed = false;
|
package/lib/middleware.mjs.map
CHANGED
|
@@ -1 +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":[]}
|
|
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 // Real models (e.g. google-genai) return the candidates shape, not a\n // top-level `message`; only the GenerateResponse constructor later collapses\n // `message ?? candidates[0].message`. Read from whichever the model used so\n // the transform runs on the real path, not just when a caller hands us a\n // pre-collapsed `message`.\n const message = response.message ?? response.candidates?.[0]?.message;\n if (!message?.content) return response;\n\n const parser = new A2uiStreamParser(opts);\n\n const newContent: Part[] = [];\n\n // Drains whatever the parser is still holding (a withheld prose tail, or an\n // unterminated trailing block) and appends it. Called at every non-text\n // boundary and once at the end.\n const flushHeld = () => {\n const tail = parser.flush();\n newContent.push(...partsFromParse(tail));\n };\n\n for (const part of message.content) {\n if (isTextPart(part) && part.text !== '') {\n // Push WITHOUT flushing between consecutive text parts so an a2ui block\n // that spans several adjacent text parts is stitched back together. The\n // model's final message is not guaranteed to coalesce adjacent text: the\n // Gemini plugin, for instance, aggregates a turn into many separate text\n // parts (fence, JSON body split many ways, close fence, then a trailing\n // empty-text part carrying the thought signature), so a per-part flush\n // would reset the parser mid-block and leak the whole surface back out as\n // raw prose. This mirrors the streaming path, which shares one parser\n // across all chunks and flushes only once at the end.\n newContent.push(...partsFromParse(parser.push(part.text)));\n } else {\n // A non-text part (e.g. a toolRequest) or an empty-text part (e.g. the\n // trailing thought-signature carrier) is a boundary: flush any held tail\n // so it lands before this part, preserving order. This is what a\n // tool-calling turn [text(\"Checking the weather.\"), toolRequest] relies\n // on so the prose is not reordered behind the toolRequest. Then carry the\n // part through untouched so its metadata survives.\n flushHeld();\n newContent.push(part);\n }\n }\n flushHeld();\n\n const newMessage = { ...message, content: newContent };\n\n // Write the transformed message back to WHICHEVER shape(s) the response\n // carries, so no consumer sees the raw prose. Real providers (e.g.\n // google-genai) use the candidates shape, but a response could carry a\n // top-level `message` and a `candidates` array at once (e.g. a prior\n // middleware pre-populated `message` while keeping `candidates`); updating\n // only one would leave the other holding the untransformed fence text. Only\n // candidate 0 is transformed: the framework collapses to\n // `candidates[0].message` and candidateCount defaults to 1, and each\n // candidate would otherwise consume the shared surface-id replay state.\n const result = { ...response };\n if (result.message) {\n result.message = newMessage;\n }\n if (result.candidates?.[0]) {\n result.candidates = result.candidates.map((c, i) =>\n i === 0 ? { ...c, message: newMessage } : c\n );\n }\n return result;\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":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genkit-ai/a2ui",
|
|
3
|
-
"version": "0.5.0
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "A Genkit plugin providing A2UI (Agent-to-UI) streaming UI protocol support via model middleware.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"genkit",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"LICENSE"
|
|
56
56
|
],
|
|
57
57
|
"peerDependencies": {
|
|
58
|
-
"genkit": "^1.42.0
|
|
58
|
+
"genkit": "^1.42.0"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@types/node": "^20.11.16",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"tsup": "^8.3.5",
|
|
65
65
|
"tsx": "^4.19.2",
|
|
66
66
|
"typescript": "^5.9.3",
|
|
67
|
-
"genkit": "1.42.0
|
|
67
|
+
"genkit": "1.42.0"
|
|
68
68
|
},
|
|
69
69
|
"engines": {
|
|
70
70
|
"node": ">=20"
|