a2ui

A2UI Protocol Implementation Guide

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "a2ui" with this command: npx skills add ldmrepo/michael/ldmrepo-michael-a2ui

A2UI Protocol Implementation Guide

This skill provides comprehensive knowledge for building AI agents that generate rich, adaptive user interfaces using the A2UI Protocol v0.8.

Reference: https://a2ui.org/specification/v0.8-a2ui/

Protocol Overview

A2UI is a JSONL-based streaming UI protocol enabling AI agents to generate declarative, interactive interfaces that render natively across platforms.

Key Design Principles

Principle Description

Security Declarative JSON, not executable code. Agents use pre-approved component catalogs.

LLM Compatibility Flat streaming JSON structure for incremental generation.

Framework Agnostic Same response renders across React, Flutter, Angular, native mobile.

Progressive Rendering UI streams incrementally for real-time updates.

Architecture Flow

User Input → Agent → A2UI Messages (JSONL Stream) → Client Renderer → Native UI ↓ User Interaction ↓ userAction → Agent

Core Concepts

  1. Surface

A Surface is a distinct, controllable region of the client's UI. Each surface has:

  • Unique surfaceId

  • Component hierarchy (adjacency list)

  • Data model (state container)

  • Root component reference

{ "surfaceId": "main", "components": { ... }, "dataModel": { ... }, "rootId": "root-component" }

  1. Adjacency List Model

Components are stored as a flat map where parent-child relationships use ID references, not nesting:

{ "components": [ { "id": "card-1", "component": { "Card": { "children": { "explicitList": ["card-1-content"] } } } }, { "id": "card-1-content", "component": { "Column": { "children": { "explicitList": ["card-1-title", "card-1-body"] } } } }, { "id": "card-1-title", "component": { "Text": { "text": {"literalString": "Hello World"}, "usageHint": "h2" } } } ] }

  1. BoundValue System

Properties that can be data-bound accept a BoundValue object:

Property Type Description

literalString

string Static string value

literalNumber

number Static number value

literalBoolean

boolean Static boolean value

path

string JSON Pointer to data model (e.g., /user/name )

Examples:

// Literal value only {"literalString": "Hello"}

// Data binding only {"path": "/user/name"}

// Combined (initialize and bind) { "literalString": "Default Name", "path": "/user/name" }

  1. Data Model

The data model stores dynamic state accessible via JSON Pointer paths:

{ "dataModel": { "contents": [ {"key": "user", "valueMap": [ {"key": "name", "valueString": "John"}, {"key": "age", "valueNumber": 30} ]}, {"key": "items", "valueList": [ {"valueString": "Item 1"}, {"valueString": "Item 2"} ]} ] } }

Value Types:

  • valueString : String values

  • valueNumber : Numeric values

  • valueBoolean : Boolean values

  • valueMap : Nested objects (array of key-value pairs)

  • valueList : Arrays

Message Types (Server → Client)

  1. surfaceUpdate

Delivers component definitions to a surface:

{ "surfaceUpdate": { "surfaceId": "main", "components": [ { "id": "title", "component": { "Text": { "text": {"literalString": "Welcome"}, "usageHint": "h1" } } } ] } }

  1. dataModelUpdate

Updates the surface's data model:

{ "dataModelUpdate": { "surfaceId": "main", "contents": [ {"key": "username", "valueString": "Alice"}, {"key": "count", "valueNumber": 42} ] } }

  1. beginRendering

Signals the client to render the surface:

{ "beginRendering": { "surfaceId": "main", "root": "root-component-id", "catalogId": "https://a2ui.org/specification/v0_8/standard_catalog_definition.json" } }

  1. deleteSurface

Removes a surface and its contents:

{ "deleteSurface": { "surfaceId": "secondary-panel" } }

Message Ordering

Required sequence:

  • surfaceUpdate (components)

  • dataModelUpdate (data)

  • beginRendering (trigger render)

Standard Component Catalog (v0.8)

Catalog ID: https://a2ui.org/specification/v0_8/standard_catalog_definition.json

Text

Displays text content with semantic hints:

{ "Text": { "text": {"literalString": "Hello World"}, "usageHint": "h1" } }

usageHint Values: h1 , h2 , h3 , body , caption

Image

Renders images from URLs:

{ "Image": { "url": {"literalString": "https://example.com/image.png"}, "alt": {"literalString": "Description"} } }

Button

Interactive button with action handling:

{ "Button": { "label": {"literalString": "Submit"}, "action": { "name": "submit_form", "context": [ {"key": "formId", "value": {"literalString": "contact-form"}} ] } } }

Action Properties:

  • name : Action identifier sent to server

  • context : Key-value pairs resolved against data model

Card

Container with single child:

{ "Card": { "title": {"literalString": "Card Title"}, "children": { "explicitList": ["card-content-id"] } } }

Row

Horizontal layout container:

{ "Row": { "children": { "explicitList": ["child-1", "child-2", "child-3"] }, "alignment": "center" } }

Alignment: start , center , end , spaceBetween , spaceAround

Column

Vertical layout container:

{ "Column": { "children": { "explicitList": ["child-1", "child-2"] }, "alignment": "start" } }

List

Dynamic list with template rendering:

{ "List": { "children": { "template": { "dataPath": "/items", "componentId": "item-template" } } } }

ListItem

Template for list items:

{ "ListItem": { "title": {"path": "/title"}, "subtitle": {"path": "/subtitle"}, "trailing": {"path": "/price"} } }

User Actions (Client → Server)

When users interact with components having action definitions:

{ "userAction": { "name": "submit_form", "surfaceId": "main", "sourceComponentId": "submit-button", "timestamp": "2024-01-01T12:00:00Z", "context": { "formId": "contact-form", "userName": "Alice" } } }

Fields:

  • name : Action identifier from component's action.name

  • surfaceId : Originating surface

  • sourceComponentId : Component that triggered the action

  • timestamp : ISO 8601 timestamp

  • context : Resolved action.context with all BoundValues evaluated

A2A Integration

A2UI integrates with A2A protocol via the extension URI: https://a2ui.org/a2a-extension/a2ui/v0.8

Agent Card Declaration

{ "capabilities": { "extensions": ["https://a2ui.org/a2a-extension/a2ui/v0.8"] }, "a2uiParams": { "supportedCatalogIds": [ "https://a2ui.org/specification/v0_8/standard_catalog_definition.json" ], "acceptsInlineCatalogs": false } }

A2A DataPart Format

A2UI messages travel as A2A DataPart objects:

{ "type": "data", "mimeType": "application/json+a2ui", "data": { "surfaceUpdate": { ... } } }

Implementation Guide

Python A2UI Builder

from typing import Any, Optional from dataclasses import dataclass, field import json

@dataclass class BoundValue: literal_string: Optional[str] = None literal_number: Optional[float] = None literal_boolean: Optional[bool] = None path: Optional[str] = None

def to_dict(self) -> dict:
    result = {}
    if self.literal_string is not None:
        result["literalString"] = self.literal_string
    if self.literal_number is not None:
        result["literalNumber"] = self.literal_number
    if self.literal_boolean is not None:
        result["literalBoolean"] = self.literal_boolean
    if self.path is not None:
        result["path"] = self.path
    return result

class A2UIBuilder: def init(self, surface_id: str = "main"): self.surface_id = surface_id self.components: list[dict] = [] self.data_model: list[dict] = []

# --- Component Methods ---

def text(
    self,
    id: str,
    text: str | BoundValue,
    usage_hint: str = "body"
) -> "A2UIBuilder":
    text_val = (
        text.to_dict() if isinstance(text, BoundValue)
        else {"literalString": text}
    )
    self.components.append({
        "id": id,
        "component": {
            "Text": {
                "text": text_val,
                "usageHint": usage_hint
            }
        }
    })
    return self

def image(
    self,
    id: str,
    url: str | BoundValue,
    alt: Optional[str] = None
) -> "A2UIBuilder":
    url_val = (
        url.to_dict() if isinstance(url, BoundValue)
        else {"literalString": url}
    )
    img = {"url": url_val}
    if alt:
        img["alt"] = {"literalString": alt}
    self.components.append({
        "id": id,
        "component": {"Image": img}
    })
    return self

def button(
    self,
    id: str,
    label: str,
    action_name: str,
    context: Optional[dict] = None
) -> "A2UIBuilder":
    action = {"name": action_name}
    if context:
        action["context"] = [
            {"key": k, "value": {"literalString": str(v)}}
            for k, v in context.items()
        ]
    self.components.append({
        "id": id,
        "component": {
            "Button": {
                "label": {"literalString": label},
                "action": action
            }
        }
    })
    return self

def card(
    self,
    id: str,
    title: Optional[str] = None,
    children: Optional[list[str]] = None
) -> "A2UIBuilder":
    card = {}
    if title:
        card["title"] = {"literalString": title}
    if children:
        card["children"] = {"explicitList": children}
    self.components.append({
        "id": id,
        "component": {"Card": card}
    })
    return self

def row(
    self,
    id: str,
    children: list[str],
    alignment: str = "start"
) -> "A2UIBuilder":
    self.components.append({
        "id": id,
        "component": {
            "Row": {
                "children": {"explicitList": children},
                "alignment": alignment
            }
        }
    })
    return self

def column(
    self,
    id: str,
    children: list[str],
    alignment: str = "start"
) -> "A2UIBuilder":
    self.components.append({
        "id": id,
        "component": {
            "Column": {
                "children": {"explicitList": children},
                "alignment": alignment
            }
        }
    })
    return self

def list_item(
    self,
    id: str,
    title: str | BoundValue,
    subtitle: Optional[str | BoundValue] = None,
    trailing: Optional[str | BoundValue] = None
) -> "A2UIBuilder":
    def to_bound(val):
        if isinstance(val, BoundValue):
            return val.to_dict()
        return {"literalString": val}

    item = {"title": to_bound(title)}
    if subtitle:
        item["subtitle"] = to_bound(subtitle)
    if trailing:
        item["trailing"] = to_bound(trailing)

    self.components.append({
        "id": id,
        "component": {"ListItem": item}
    })
    return self

# --- Data Model Methods ---

def set_data(self, key: str, value: Any) -> "A2UIBuilder":
    if isinstance(value, str):
        self.data_model.append({"key": key, "valueString": value})
    elif isinstance(value, bool):
        self.data_model.append({"key": key, "valueBoolean": value})
    elif isinstance(value, (int, float)):
        self.data_model.append({"key": key, "valueNumber": value})
    elif isinstance(value, list):
        self.data_model.append({
            "key": key,
            "valueList": [self._convert_value(v) for v in value]
        })
    elif isinstance(value, dict):
        self.data_model.append({
            "key": key,
            "valueMap": [
                {"key": k, **self._convert_value(v)}
                for k, v in value.items()
            ]
        })
    return self

def _convert_value(self, value: Any) -> dict:
    if isinstance(value, str):
        return {"valueString": value}
    elif isinstance(value, bool):
        return {"valueBoolean": value}
    elif isinstance(value, (int, float)):
        return {"valueNumber": value}
    return {"valueString": str(value)}

# --- Build Methods ---

def build_surface_update(self) -> dict:
    return {
        "surfaceUpdate": {
            "surfaceId": self.surface_id,
            "components": self.components
        }
    }

def build_data_model_update(self) -> dict:
    return {
        "dataModelUpdate": {
            "surfaceId": self.surface_id,
            "contents": self.data_model
        }
    }

def build_begin_rendering(self, root_id: str) -> dict:
    return {
        "beginRendering": {
            "surfaceId": self.surface_id,
            "root": root_id,
            "catalogId": "https://a2ui.org/specification/v0_8/standard_catalog_definition.json"
        }
    }

def build_all(self, root_id: str) -> list[dict]:
    """Returns all messages in correct order."""
    messages = [self.build_surface_update()]
    if self.data_model:
        messages.append(self.build_data_model_update())
    messages.append(self.build_begin_rendering(root_id))
    return messages

Example: Travel Card Generator

def generate_travel_card(destination: dict) -> list[dict]: builder = A2UIBuilder(surface_id="main")

# Build component hierarchy
builder.card("card-root", title=destination["name"], children=["card-content"])
builder.column("card-content", children=["card-image", "card-info", "card-actions"])
builder.image("card-image", url=destination["image_url"], alt=destination["name"])
builder.column("card-info", children=["card-desc", "card-price"])
builder.text("card-desc", text=destination["description"], usage_hint="body")
builder.text("card-price", text=f"${destination['price']}", usage_hint="h3")
builder.row("card-actions", children=["btn-details", "btn-book"])
builder.button(
    "btn-details",
    label="View Details",
    action_name="view_details",
    context={"destinationId": destination["id"]}
)
builder.button(
    "btn-book",
    label="Book Now",
    action_name="book_destination",
    context={"destinationId": destination["id"]}
)

# Set data model
builder.set_data("destination", destination)

return builder.build_all(root_id="card-root")

SSE Streaming Example (FastAPI)

from fastapi import FastAPI from fastapi.responses import StreamingResponse from sse_starlette.sse import EventSourceResponse import json

app = FastAPI()

async def generate_a2ui_stream(user_message: str): builder = A2UIBuilder()

# Stream surfaceUpdate
builder.text("loading", "Processing your request...", "body")
yield {
    "event": "message",
    "data": json.dumps(builder.build_surface_update())
}

# Simulate processing...
result = await process_request(user_message)

# Stream updated components
builder = A2UIBuilder()
builder.text("result-title", "Results", "h2")
builder.text("result-content", result, "body")
yield {
    "event": "message",
    "data": json.dumps(builder.build_surface_update())
}

# Stream beginRendering
yield {
    "event": "message",
    "data": json.dumps(builder.build_begin_rendering("result-title"))
}

yield {"event": "done", "data": json.dumps({"status": "complete"})}

@app.post("/chat/stream") async def chat_stream(request: ChatRequest): return EventSourceResponse(generate_a2ui_stream(request.message))

TypeScript Renderer Types

// BoundValue resolution interface BoundValue { literalString?: string; literalNumber?: number; literalBoolean?: boolean; path?: string; }

// Component wrapper interface A2UIComponent { id: string; component: { Text?: TextComponent; Image?: ImageComponent; Button?: ButtonComponent; Card?: CardComponent; Row?: RowComponent; Column?: ColumnComponent; List?: ListComponent; ListItem?: ListItemComponent; }; }

// Surface state interface Surface { surfaceId: string; components: Map<string, A2UIComponent>; dataModel: Map<string, any>; rootId: string | null; isReady: boolean; }

// Message types interface SurfaceUpdate { surfaceUpdate: { surfaceId: string; components: A2UIComponent[]; }; }

interface DataModelUpdate { dataModelUpdate: { surfaceId: string; contents: DataModelContent[]; }; }

interface BeginRendering { beginRendering: { surfaceId: string; root: string; catalogId?: string; }; }

// User action interface UserAction { userAction: { name: string; surfaceId: string; sourceComponentId: string; timestamp: string; context: Record<string, any>; }; }

// Utility: Resolve BoundValue function resolveBoundValue( boundValue: BoundValue, dataModel: Map<string, any> ): any { if (boundValue.path) { return getValueAtPath(dataModel, boundValue.path); } return boundValue.literalString ?? boundValue.literalNumber ?? boundValue.literalBoolean; }

Best Practices

  1. Component ID Strategy

Use consistent, meaningful IDs with prefixes:

card-{entityId} card-{entityId}-title card-{entityId}-content btn-{action}-{entityId}

  1. Data Separation

Keep large data in the data model, reference via paths:

// Good: Data in model, bound in component {"text": {"path": "/article/content"}}

// Avoid: Large data inline {"text": {"literalString": "Very long text..."}}

  1. Streaming Order

Always send messages in order:

  • surfaceUpdate (structure)

  • dataModelUpdate (state)

  • beginRendering (render trigger)

  1. Progressive Updates

For long operations, send intermediate updates:

Initial loading state

yield surface_update(loading_component) yield begin_rendering("loading")

Final result

yield surface_update(result_components) yield data_model_update(result_data) yield begin_rendering("result-root")

  1. Action Context

Include all necessary data in action context:

{ "action": { "name": "add_to_cart", "context": [ {"key": "productId", "value": {"path": "/product/id"}}, {"key": "quantity", "value": {"literalNumber": 1}} ] } }

References

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

a2a-protocol

No summary provided by upstream source.

Repository SourceNeeds Review
General

a2ui

No summary provided by upstream source.

Repository SourceNeeds Review
General

a2ui

No summary provided by upstream source.

Repository SourceNeeds Review
General

音乐生成

Generate custom music tracks (vocal or instrumental) via OhYesAI.

Registry SourceRecently Updated