A2UI Express and Swappable Inference Formats

A2UI Express and Swappable Inference Formats
A2UI Express and Swappable Inference Formats

In my Agent Protocols - MCP, A2A, A2UI, AG-UI and Agent to UI Protocol (A2UI) with Agent Development Kit (ADK) posts, I introduced A2UI—Google’s declarative generative UI protocol that lets AI agents return rich, interactive user interfaces across web, mobile, and desktop using JSON messages (createSurface, updateComponents, updateDataModel, and deleteSurface).

While standard JSON is an ideal wire protocol for client renderers (Lit, Angular, React, Flutter), having an LLM generate deeply nested wire JSON token-by-token is verbose, token-heavy, and adds latency. Recently, the A2UI team introduced Swappable Inference Formats and made A2UI Express the new default format in the A2UI Agent SDKs—cutting output tokens by ~3x–4x, shrinking system prompts by more than half, and speeding up UI generation without requiring any changes to client renderers.

In this post, I’ll briefly introduce A2UI Express and walk through how to use it, based on the interactive explorer sample in my genai-beyond-basics repository.

What is A2UI Express?

The key insight behind Swappable Inference Formats is decoupling the inference format (what the LLM writes) from the wire format (the standard A2UI JSON that client renderers consume):

A2UI Swappable Inference Pipeline
A2UI Swappable Inference Pipeline

Using the official A2UI Python Agent SDK (a2ui-agent-sdk, along with @a2ui/agent in TypeScript and a2ui_agent in Dart), you can swap the inference format factory on the server while keeping the wire protocol 100% identical:

  • Direct JSON (DirectJsonFormatFactory): The original <a2ui-json> format where the LLM generates full A2UI Wire JSON directly.
  • A2UI Express (ExpressFormatFactory, Default): A compact, Python-like positional DSL enclosed in <a2ui>...</a2ui> tags (root = Card(main_col), _ for omitted optional arguments, $/path = value for data-model bindings, and ?required(...) for validation checks). It cuts system prompt size by ~52–55%, reduces output tokens by ~3x–5.5x, and generates valid UIs reliably even with model thinking turned off (thinking_budget=0).
  • Atom (AtomFormatFactory, Experimental): A Lisp S-expression DSL ((Card (Column :id "main_col" ...))).
  • Elemental (ElementalFormatFactory, Experimental): An HTML Custom Elements DSL (<ui-card id="root"><ui-column id="main_col">...).

A2UI Express & Swappable Inference Formats Explorer

To see how this works in practice and compare the formats side-by-side, I built an interactive A2UI Express & Swappable Inference Formats Explorer using a2ui-agent-sdk, google-genai, FastAPI, and the official @a2ui/lit v0.9 Web Component renderer.

You can run the explorer locally with a single uv command:

uv run --with a2ui-agent-sdk --with google-genai --with fastapi --with uvicorn python app.py

Open http://127.0.0.1:8080 in your browser. At the top of the page, you can switch between 3 built-in UI examples (Restaurant Reservation Form, Login Form with Validation, and Notification Permission Card), toggle which format to compare against Direct JSON (A2UI Express, Atom, or Elemental), or enter a custom prompt and click ⚡ Generate Live with gemini-3.8-flash (thinking_budget=0):

Explorer Controls and Summary Metrics
Explorer Controls and Summary Metrics

Notice the summary metrics for the Restaurant Reservation Form example: A2UI Express uses 4.0x fewer output tokens (262 vs. 1,054 tokens), runs 4.0x faster, and shrinks the system prompt footprint by 55% (6,059 vs. 13,424 input tokens) by replacing full JSON Schemas with compact function signatures.

Let’s look at each part of the explorer and the code under the hood.

Python Agent SDK Code (a2ui-agent-sdk)

On the server, we configure A2uiGenerator with our component catalog (BasicCatalog(ProtocolVersion.V0_9)), optional few-shot examples, and our chosen inference_format_factory:

from a2ui.core.basic_catalog import BasicCatalog
from a2ui.core.schema import ProtocolVersion
from a2ui.inference_formats import ExpressFormatFactory  # or DirectJsonFormatFactory
from a2ui.processor import A2uiGenerator, CatalogConfig

catalog = BasicCatalog(ProtocolVersion.V0_9)
generator = A2uiGenerator(
    [CatalogConfig(catalog)],
    examples=[example_models],
    inference_format_factory=ExpressFormatFactory(),  # Default (or AtomFormatFactory / ElementalFormatFactory)
)

processor = generator.create_processor(
    {"v0.9": {"supportedCatalogIds": [catalog.catalog_id]}}
)

As shown in the explorer, the Python SDK setup is virtually identical between Direct JSON on the left and A2UI Express on the right—the only difference is passing DirectJsonFormatFactory() vs. ExpressFormatFactory():

Python Agent SDK Code
Python Agent SDK Code

What the LLM Generates (<a2ui-json> vs. <a2ui>)

Next, we inject processor.prompt_snippet into Gemini’s system_instruction and ask the model to generate the UI with thinking_budget=0:

from google import genai
from google.genai import types

client = genai.Client()

response = client.models.generate_content(
    model="gemini-3.8-flash",
    contents="Create a reservation form for Xi'an Famous Foods with Name, Guests, Time, and Confirm button.",
    config=types.GenerateContentConfig(
        system_instruction=f"You are an A2UI assistant.\n\n{processor.prompt_snippet}",
        thinking_config=types.ThinkingConfig(thinking_budget=0),
    ),
)

In the next section of the explorer, we can compare what the LLM actually has to generate token-by-token:

What the LLM Generates
What the LLM Generates

On the left, Direct JSON requires 168 lines (3,796 characters / 1,054 tokens) of deeply nested JSON keys ("id", "component", "path"). On the right, A2UI Express expresses the exact same reservation form—including surface initialization, data-model bindings ($/reservation/...), layout hierarchy, a required validation check (?required(...)), and button event actions—in just 20 lines (945 characters / 262 tokens):

<a2ui>
surface("booking", sendDataModel=true)
$/reservation/name = "Alice"
$/reservation/guests = "3"
$/reservation/time = "7:30 PM"
$/reservation/notes = "No peanuts"
root = Card(main_col)
main_col = Column([title, subtitle, divider, name_field, row_fields, notes_field, actions])
title = Text("Confirm Reservation", "h2")
subtitle = Text("Xi'an Famous Foods • Friday Evening", "caption")
divider = Divider()
name_field = TextField("Guest Name", $/reservation/name, [?required("Guest name is required")])
row_fields = Row([guests_field, time_field])
guests_field = TextField("Party Size", $/reservation/guests)
time_field = TextField("Time", $/reservation/time)
notes_field = TextField("Dietary Notes", $/reservation/notes)
actions = Row([confirm_btn, cancel_btn], "end")
confirm_btn = Button(Text("Confirm Booking"), "primary", Event("confirm", {reservation: $/reservation}))
cancel_btn = Button(Text("Cancel"), _, Event("cancel", {}))
</a2ui>

What the Renderer Gets (Validated A2UI v0.9 Wire JSON)

Once the model responds, the server calls processor.parse_response(response.text) and converts the parsed A2uiPart models into standard wire message dictionaries via to_message_dicts():

from a2ui.inference_formats import to_message_dicts
from a2ui.parser import A2uiPart

parts = processor.parse_response(response.text)
wire_messages = [
    msg
    for part in parts
    if isinstance(part, A2uiPart)
    for msg in to_message_dicts(part.a2ui)
]

Comparing the output of processor.parse_response() across both columns in the explorer:

Validated A2UI v0.9 Wire JSON
Validated A2UI v0.9 Wire JSON

While the left column validates the <a2ui-json> payload and the right column compiles the <a2ui> Express DSL, both produce the exact same 166-line (3,766-character) A2UI v0.9 Wire JSON (createSurface, updateComponents, and updateDataModel).

Rendered UI (@a2ui/lit v0.9)

Finally, the server sends wire_messages to the browser, where @a2ui/web_core’s MessageProcessor and @a2ui/lit’s <a2ui-surface> Web Component render the interactive UI:

import '@a2ui/lit/v0_9'; // Registers <a2ui-surface>
import { basicCatalog } from '@a2ui/lit/v0_9';
import { MessageProcessor } from '@a2ui/web_core/v0_9';

const surfaceEl = document.querySelector('a2ui-surface');

const processor = new MessageProcessor([basicCatalog], (action) => {
  console.log('User triggered A2UI action:', action);
});

processor.onSurfaceCreated((surface) => {
  surfaceEl.surface = surface;
});

processor.processMessages(wireMessages);

Client renderers never see <a2ui> Express, Atom, or Elemental syntax—they only consume standard A2UI v0.9 Wire JSON. When you edit the form fields and click Confirm Booking in the explorer, the <a2ui-surface> component automatically dispatches the confirm action event along with the bound $/reservation data model context:

Rendered UI and Dispatched Client Action
Rendered UI and Dispatched Client Action

Wrapping up

Decoupling the LLM inference format from the wire protocol is a great example of optimizing for both sides of an agentic system: A2UI Express gives the LLM a compact, token-efficient DSL to generate UIs up to 4x faster, while compiling down to the exact same validated A2UI JSON that client renderers already speak.

For more details, check out the following resources:


See also