A2UI + ADK Skill
Build Google ADK agents that generate rich, interactive UIs using the A2UI declarative JSON format, styled with Tailwind CSS and shadcn/ui design principles.
What is A2UI?
A2UI (Agent-to-User Interface) is an open standard (Apache 2.0, github.com/google/A2UI) that lets agents "speak UI." Agents send declarative JSON describing UI intent; client apps render it using native components (Flutter, Angular, Lit, React, etc.).
Core principles:
- Security first: Declarative data format, not executable code. Clients maintain a catalog of trusted, pre-approved UI components.
- LLM-friendly: Flat list of components with ID references, easy for LLMs to generate incrementally.
- Framework-agnostic: Same JSON payload renders on any supported client framework.
- Currently v0.8 (Public Preview), v0.9 also available.
Architecture Overview
Agent (ADK + A2UI SDK) → A2UI JSON Response → Transport (A2A/AG UI) → Client Renderer (Lit/Flutter/etc.)
- Generation: Agent uses LLM to generate A2UI JSON payload
- Transport: Sent via A2A protocol or AG UI
- Resolution: Client's A2UI Renderer parses the JSON
- Rendering: Maps abstract components to native widgets
Key Dependencies
[project]
dependencies = [
"a2a-sdk>=0.3.0",
"google-adk>=1.8.0",
"google-genai>=1.27.0",
"litellm",
"jsonschema>=4.0.0",
"a2ui-agent", # The A2UI agent SDK (from agent_sdks/python in the A2UI repo)
"python-dotenv>=1.1.0",
"click>=8.1.8",
]
The a2ui-agent package is sourced from the A2UI repo at agent_sdks/python (use uv workspace source):
[tool.uv.sources]
a2ui-agent = { path = "../../../agent_sdks/python", editable = true }
Core A2UI SDK Imports
# Schema management
from a2ui.core.schema.constants import VERSION_0_8, VERSION_0_9, A2UI_OPEN_TAG, A2UI_CLOSE_TAG
from a2ui.core.schema.manager import A2uiSchemaManager
from a2ui.core.schema.common_modifiers import remove_strict_validation
# Catalog (built-in component catalog)
from a2ui.basic_catalog.provider import BasicCatalog
# Response parsing
from a2ui.core.parser.parser import parse_response, ResponsePart
# A2A integration helpers
from a2ui.a2a import (
create_a2ui_part, # Wrap A2UI JSON as A2A DataPart
is_a2ui_part, # Check if an A2A Part contains A2UI data
get_a2ui_agent_extension,# Create AgentExtension for agent card
parse_response_to_parts, # Parse LLM response → list of A2A Parts
try_activate_a2ui_extension, # Activate A2UI extension from request context
A2UI_EXTENSION_URI,
)
Pattern 1: Single Agent with A2UI (e.g., Restaurant Finder)
Step 1: Schema Manager Setup
from a2ui.core.schema.manager import A2uiSchemaManager
from a2ui.core.schema.constants import VERSION_0_8
from a2ui.basic_catalog.provider import BasicCatalog
from a2ui.core.schema.common_modifiers import remove_strict_validation
schema_manager = A2uiSchemaManager(
VERSION_0_8,
catalogs=[
BasicCatalog.get_config(version=VERSION_0_8, examples_path="examples")
],
schema_modifiers=[remove_strict_validation],
)
examples_pathpoints to a directory of JSON example files used for few-shot promptingremove_strict_validationstripsadditionalProperties: falseto make LLM generation easier
Step 2: Build the ADK LlmAgent
from google.adk.agents.llm_agent import LlmAgent
from google.adk.models.lite_llm import LiteLlm
ROLE_DESCRIPTION = "You are a helpful assistant. Your final output MUST be an A2UI UI JSON response."
UI_DESCRIPTION = "Describe when to use which template/layout..."
instruction = schema_manager.generate_system_prompt(
role_description=ROLE_DESCRIPTION,
ui_description=UI_DESCRIPTION,
include_schema=True, # Injects the JSON schema into the prompt
include_examples=True, # Injects example A2UI payloads
validate_examples=True, # Validates examples against schema at startup
)
agent = LlmAgent(
model=LiteLlm(model="gemini/gemini-2.5-flash"),
name="my_agent",
description="Agent description",
instruction=instruction,
tools=[my_tool_function],
)
Step 3: Agent Class with Streaming + Validation
from google.adk.runners import Runner
from google.adk.artifacts import InMemoryArtifactService
from google.adk.memory.in_memory_memory_service import InMemoryMemoryService
from google.adk.sessions import InMemorySessionService
from google.genai import types
from a2ui.core.parser.parser import parse_response
from a2ui.a2a import parse_response_to_parts
class MyAgent:
def __init__(self, base_url: str, use_ui: bool = False):
self.use_ui = use_ui
self._schema_manager = A2uiSchemaManager(...) if use_ui else None
self._agent = self._build_agent(use_ui)
self._runner = Runner(
app_name=self._agent.name,
agent=self._agent,
artifact_service=InMemoryArtifactService(),
session_service=InMemorySessionService(),
memory_service=InMemoryMemoryService(),
)
async def stream(self, query, session_id):
# Create/get session
session = await self._runner.session_service.get_session(...)
if session is None:
session = await self._runner.session_service.create_session(...)
# Run agent with retry on validation failure
max_retries = 1
for attempt in range(max_retries + 1):
message = types.Content(role="user", parts=[types.Part.from_text(text=query)])
final_response = None
async for event in self._runner.run_async(
user_id=self._user_id, session_id=session.id, new_message=message
):
if event.is_final_response():
final_response = "\n".join(
[p.text for p in event.content.parts if p.text]
)
break
else:
yield {"is_task_complete": False, "updates": "Processing..."}
# Validate A2UI JSON if using UI
if self.use_ui and final_response:
try:
response_parts = parse_response(final_response)
for part in response_parts:
if part.a2ui_json:
selected_catalog = self._schema_manager.get_selected_catalog()
selected_catalog.validator.validate(part.a2ui_json)
# Valid! Send final response
yield {
"is_task_complete": True,
"parts": parse_response_to_parts(final_response, fallback_text="OK."),
}
return
except Exception as e:
if attempt < max_retries:
query = f"Previous response invalid: {e}. Retry with valid A2UI JSON."
continue
# Exhausted retries, send error
yield {"is_task_complete": True, "parts": [Part(root=TextPart(text="Error generating UI."))]}
return
else:
yield {"is_task_complete": True, "parts": parse_response_to_parts(final_response)}
return
Step 4: Agent Executor (A2A Server)
The executor bridges A2A protocol to your agent. Key pattern: maintain both a UI agent and a text-only agent, selecting based on whether the A2UI extension is active.
from a2a.server.agent_execution import AgentExecutor, RequestContext
from a2a.server.events import EventQueue
from a2a.server.tasks import TaskUpdater
from a2a.types import DataPart, Part, TaskState, TextPart
from a2a.utils import new_agent_parts_message, new_agent_text_message, new_task
from a2ui.a2a import try_activate_a2ui_extension
class MyAgentExecutor(AgentExecutor):
def __init__(s