ワンクリックで
a2ui-developer
Expert guide and patterns for building Agent-Driven User Interfaces (A2UI) using the ADK.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Expert guide and patterns for building Agent-Driven User Interfaces (A2UI) using the ADK.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
Deploys an A2UI ADK agent cleanly to Google Cloud Run in namespaced A2A-compatibility mode.
Deploys ADK agents to Vertex AI Agent Engine using the Python SDK and registers them with Gemini Enterprise.
Comprehensive guide to building, orchestrating, and deploying agents with the Google Agent Development Kit (ADK).
Provisions a PostgreSQL Cloud SQL instance, database, database user, and registers connection details in Secret Manager using Terraform.
Provisions a scheduled Cloud Scheduler job with secure OIDC token authentication to invoke HTTP targets (such as Reasoning Engine endpoints) using Terraform.
Provisions one or more Google Cloud Storage buckets with secure uniform bucket-level access and public access prevention using Terraform.
| name | A2UI Developer |
| description | Expert guide and patterns for building Agent-Driven User Interfaces (A2UI) using the ADK. |
This skill equips you with the knowledge and best practices to design, implement, and debug A2UI-compliant agents and clients.
CRITICAL: This skill relies on local reference documentation that must be kept in sync with the official repository.
When starting a new session or tasks involving A2UI:
python3 scripts/update_skill.py
A2UI decouples UI generation (Agent) from UI rendering (Client).
---a2ui_JSON---)surfaceUpdate and dataModelUpdate.surfaceUpdate (Structure)Defines the component hierarchy using a flat Adjacency List.
surfaceId: Target surface (e.g., "main").components: Array of component definitions.{
"surfaceUpdate": {
"surfaceId": "main",
"components": [
{
"id": "root_col",
"component": {
"Column": {
"children": { "explicitList": ["header_1", "card_1"] }
}
}
}
]
}
}
dataModelUpdate (Data)Populates data for components.
{
"dataModelUpdate": {
"surfaceId": "main",
"contents": [
{ "key": "user_name", "valueString": "Alice" }
]
}
}
beginRendering (Signal)Tells the client to start drawing a specific root component on a surface.
{
"beginRendering": {
"surfaceId": "main",
"root": "root_col"
}
}
[agent_name]_a2ui). NEVER modify the original reference agent code in place.A2UI Local Tester skill before attempting any deployment.agent.py, a2ui_examples.py, a2ui_schema.py, and a2ui_tools.py (if using the tool-based pattern). Do not omit these supporting files unless the user explicitly says they are not needed.CRITICAL IF UNKNOWN: Before writing any integration code, you must ask the user:
"Are you deploying this A2UI agent to Cloud Run (Raw HTTP/A2A Mode) or natively to Vertex AI Agent Engine (Native Tools Mode)?"
The implementation architecture differs significantly between the two (see Section 4).
An A2UI agent MUST inherently be an A2A (Agent-to-Agent) agent first. The A2A protocol serves as the base data transfer and transport layer for A2UI structured payloads. Therefore, before adding A2UI generation or parsing, you must ensure the agent successfully implements the A2A specification.
Agents must be explicitly instructed to generate A2UI JSON.
Critical Rules:
---a2ui_JSON--- to separate text from JSON.---a2ui_JSON---.Sample Instruction:
You are an agent that generates UIs. You MUST separate your conversational response from your A2UI JSON output using the delimiter
---a2ui_JSON---. The JSON must appear EXACTLY once at the end. When calling a tool that returns UI JSON, you MUST copy the returned string exactly without modification or adding markdown code blocks. Echo Constraint: "When a tool returns a string starting with '---a2ui_JSON---', you MUST include that exact string in your response without modification."
Requires a custom AgentExecutor to intercept tool responses or stream outputs and yield a native DataPart with mimeType="application/json+a2ui" directly to the HTTP stream.
import json
from a2a import types
from a2a.server import agent_execution
from a2a.server import events
from google.adk import runners
class AdkAgentToA2AExecutor(agent_execution.AgentExecutor):
def __init__(self):
self._agent = local_agent.root_agent # Your ADK LlmAgent
self._runner = runners.Runner(...)
async def execute(self, context: agent_execution.RequestContext, event_queue: events.EventQueue) -> None:
query = context.get_user_input()
# 1. Run the agent stream
final_response_content = ""
async for event in self._runner.run_async(...):
if event.is_final_response():
final_response_content += event.content.parts[0].text
# 2. Extract A2UI JSON and convert to DataPart
text_part, json_string = final_response_content.split("---a2ui_JSON---", 1)
# ... parse json_string ...
parts = []
parts.append(types.Part(root=types.TextPart(text=text_part)))
parts.append(types.Part(root=types.DataPart(
data=json.loads(json_string),
metadata={"mimeType": "application/json+a2ui"}
)))
# 3. Yield to stream
await updater.add_artifact(parts, name="response")
await updater.complete()
from starlette.applications import Starlette
from a2a.server.apps import A2AStarletteApplication
from a2a.server.request_handlers import DefaultRequestHandler
agent_executor = DefaultRequestHandler() # Or appropriate executor
request_handler = DefaultRequestHandler(agent_executor=agent_executor)
app = Starlette()
a2a_app = A2AStarletteApplication(agent_card=agent_card, http_handler=request_handler)
a2a_app.add_routes_to_app(app, rpc_url="/a2a/my_agent", agent_card_url="/a2a/my_agent/.well-known/agent-card.json")
In this mode, standard tools return text by default. The Gemini Model reads that text and decides how to present it. Native binary application/json+a2ui injection inside standard tools can cause platform-level security blocks (or get stringified for the model).
For Agent Engine, you must use the text-based delimiter fallbacks (---JSON_DATA---) so the model can render them in plain text for readability (often as ASCII-art charts) when interactive cards are not supported natively by the runtime layer.
| File | Why it's Needed | Required for Text-Based Delimiter? | Required for Tool-Based Validation? |
|---|---|---|---|
app.py | Entry point for WSGI/ASGI compute (AdkApp). | YES (Can be manually created or generated dynamically via Terraform). | YES |
a2ui_tools.py | Defines Python function render_ui for LLM validation. | NO | YES |
a2ui_schema.json | Validates LLM tool arguments using jsonschema. | NO | YES |
agent_executor.py | Custom stream interceptor (Translates text blocks context to binary DataParts). | NO (Agent Engine handles native parsing without it). | NO |
[!TIP] Simplicity First: If you use standard text delimiters (
---JSON_DATA---), you do not needa2ui_tools.pyora2ui_schema.jsonin your file archive. The platform intercepts text automatically!
For A2UI agents deployed on Agent Engine using the Python SDK, you MUST include the a2ui-agent-sdk package in your requirements.txt and in the config["requirements"] list of your deployment script. Removing it will break A2A communication and cause 400 Bad Request errors.
a2ui-agent-sdk
[!IMPORTANT] Do NOT remove unpublished dependencies from git repos if they are required by the system architecture.
When implementing a custom AgentExecutor (like AdkAgentToA2AExecutor), ensure it translates the LLM's output stream into A2UI DataParts rather than replacing it with static mock data.
DataPart inside the loop for testing.MOCK_A2UI_RESPONSE=True) to toggle intercepts, and ensure the default flow always accumulates the real run_async stream from the LLM.executor.execute loop. Wrap in try/except.run_async streaming splits the JSON string. Must accumulate before parsing.---a2ui_JSON--- delimiter is missing or if strict markdown blocks were used.npm install @a2ui/lit @a2a-js/sdk<a2ui-surface>.Column, Row, Divider, Modal, TabsText, Image, Video, AudioPlayer, CardButton, TextField, CheckBox, Slider---a2ui_JSON---.adk api_server . --a2a --allow_origins "*".agent.json (not agent-card.json) exists and has "capabilities": { "streaming": true }.A2AClient with JsonRpcTransportFactory and inject A2UI extension headers.For complex UIs or when you need strict validation before sending data to the client, use the Tool-Based Pattern instead of the streaming delimiter.
Mechanism:
send_a2ui_json_to_client) that accepts the A2UI JSON as an argument.DataPart for the client.Benefits:
DataPart event, not a raw text stream it has to parse.Implementation Example (Python ADK):
from a2ui.send_a2ui_to_client_toolset import SendA2uiToClientToolset
# In Agent Initialization
root_agent = LlmAgent(
# ...
tools=[
SendA2uiToClientToolset(
a2ui_enabled=True,
a2ui_schema=A2UI_SCHEMA
)
]
)
For consistent UIs, inject "Template Examples" into the system prompt.
loading.json, error.json, dashboard.json).This reduces hallucination and ensures visual consistency.
The A2UI Composer (https://a2ui-composer.ag-ui.com/) is a visual rapid prototyping tool.
examples in basic tool definitions.CopilotKit provides native support for A2UI as a "Declarative Generative UI".
<CopilotKit /> provider detects the A2UI spec and invokes the built-in renderer.[!WARNING] While the platform documentation may describe "native text interception" using delimiters like
---JSON_DATA---for standard Agent Engine deployments, in practice this has been found to be unreliable and is unsupported for rich A2UI rendering in this repository.
For reliable A2UI rendering (yielding binary DataPart objects), you MUST use the Custom Executor approach deployed via the Python SDK (Pickle-based).
Refer to the Agent Engine Python Deployer skill for instructions on how to package your agent with a custom agent_executor.py.
doctor_card_1, doctor_card_2).When deploying A2UI agents to Gemini Enterprise and using rich UI components (like buttons or forms), be aware of how user inputs are delivered to the backend:
"User action triggered." as the primary user input. Relying solely on context.get_user_input() will cause the agent to receive this generic string and lose context, leading to loops or resets.context defined in the button action) is delivered in a separate DataPart of the message with mimeType application/json+a2ui.agent_executor.py, you MUST iterate through the parts of the incoming message, look for the DataPart containing userAction, and extract the specific message or parameters from it to override or augment the query before passing it to the agent! # Example extraction logic in execute() method
try:
if hasattr(context, 'message') and context.message and hasattr(context.message, 'parts'):
for part in context.message.parts:
if hasattr(part, 'root') and hasattr(part.root, 'data'):
data_part = part.root
if hasattr(data_part, 'metadata') and data_part.metadata and data_part.metadata.get('mimeType') == 'application/json+a2ui':
data = data_part.data
if 'userAction' in data:
user_action = data['userAction']
if 'context' in user_action:
action_context = user_action['context']
if 'message' in action_context:
query = action_context['message'] # Override query
# Save other context to session state
for k, v in action_context.items():
if k != 'message':
session.state[k] = v
except Exception as e:
logger.warning("Failed to extract action context: %s", e)
When building a custom local tester (e.g., using FastAPI and a standalone HTML file) to test A2UI agents before deployment, be aware of these common pitfalls:
index.html) hardcodes the check for action name (e.g., if (action.name === 'submit')), it will ignore buttons with custom action names like submit_route_plan.
index.html to process all actions and send them to the server via DataPart.<input type="date"> blindly for DateTimeInput, it will ignore the enableTime property.
enableDate and enableTime properties in the A2UI component definition and set the input type to time, date, or datetime-local accordingly.DataPart or state, and might re-render the form!
message from the action context and override the query passed to the agent, rather than just appending it. This forces the agent to read the intent (e.g., "Plan my route with these details") as the primary user input!json.dumps and return it, rather than letting the LLM type it out. This guarantees valid JSON delivery.TrustedHTML CSP policy errors in the browser console when A2UI components fail to render. This can easily be mistaken for a component schema or sanitization issue.session.state to preserve user choices (like plan type) across turns can fail. If Turn 2 hits a different replica than Turn 1, the in-memory state is lost, causing the agent to loop back and ask for the information again.message field in the action context of buttons (e.g., "Search for providers for my PPO plan.") to explicitly include the state variables. This forces the client to store the state and send it back to whatever replica handles the next turn, guaranteeing state continuity! # Inject session state into query
state_vars = [f"{k}={v}" for k, v in session.state.items()]
if state_vars:
query = f"{query} [State: {', '.join(state_vars)}]"
logger.warning("[DEBUG] Appended state to query: %s", query)
session_id parameter to the context_id field inside the forwarded A2A Message payload. Because of this, the A2A SDK generated a new, random UUID context ID for every turn, creating a fresh, empty session on each request.context_id property of the A2A message definition:
a2a_message = {
"role": "ROLE_USER",
"content": a2a_parts,
"context_id": params.get("session_id")
}
VertexAiSessionService and VertexAiMemoryBankService.agent_engine_id parameter. At runtime, read this from the environment variable GOOGLE_CLOUD_AGENT_ENGINE_ID which is automatically injected by the platform:
agent_engine_id = os.environ.get("GOOGLE_CLOUD_AGENT_ENGINE_ID")
session_service = VertexAiSessionService(
project=project_id, location=location, agent_engine_id=agent_engine_id
)
VertexAiSessionService to solve the multi-replica state issue by persisting state in Vertex AI.VertexAiSessionService does NOT support user-provided session IDs when creating sessions (raises ValueError: User-provided Session id is not supported).agent_executor.py relies on mapping external task/context IDs to specific ADK session IDs, you cannot use VertexAiSessionService directly. You must either adapt your ID mapping strategy or fallback to InMemorySessionService combined with Transcript Echoing![!IMPORTANT] Reasoning Engine replicas are stateless. Under JSON-RPC,
RequestContext.metadatareturns a transient dictionary if the underlying_params.metadataisNone. To prevent updates from being discarded, you must read/write states usingcontext._params.metadatadirectly.
Implementation Logic:
data as an attribute or key, and support converting Protobuf Struct objects to dictionaries using MessageToDict.userAction.inputs for form elements and userAction.context for custom parameters.context._params.metadata (if _params exists) so that it persists across reasoning engine tasks.query = f"{query} [State: key=value]"), allowing stateless model worker replicas to recover context easily.When deploying to environments like Gemini Enterprise, the frontend strictly enforces the official A2UI v0.8 Schema. If any component is structurally invalid, it actively blocks rendering (via TrustedHTML CSP policies) to protect the UI, resulting in blank responses despite correct network payloads.
Common Schema Pitfalls to Avoid:
MultipleChoice (Nesting Strictness): Use the options array containing objects with label and value.
label MUST be an object with a literalString property (e.g., {"label": {"literalString": "Option A"}}).value MUST be a plain string (e.g., {"value": "option_a"}).choices property. Do NOT over-nest value (making it an object like {"value": {"literalString": "option_a"}}).Button: The text on a button must be provided via the child property (which refers to a separate Text component id). Do NOT use a text property directly on the Button.
Card: A Card only accepts a single child component (typically a Column or Row used to stack multiple elements). Do NOT use title or subtitle properties directly on the Card.
Column and Row (Nesting Strictness): The explicitList of child IDs MUST be wrapped in a children object.
"Column": { "children": { "explicitList": ["id1", "id2"] } }"Column": { "explicitList": ["id1", "id2"] } (Missing children wrapper!)TextField Hallucinations (TextInput): A highly common LLM hallucination is generating an Unknown element TextInput. The official v0.8 A2UI schema component for free-form text entry is strictly named TextField. You MUST instruct your agent to use TextField and explicitly forbid TextInput. The TextField properties text and label must be objects (e.g. {"text": {"path": "my_txt"}, "label": {"literalString": "Reason"}}), NOT plain strings, and the property is text, not content.
CheckBox Hallucinations (selected): LLMs frequently hallucinate selected or checked properties for the CheckBox component. The official schema strictly requires value (e.g. {"value": {"path": "is_checked"}}).
dataModelUpdate Hallucinations (valueBool): When populating booleans in dataModelUpdate, LLMs may hallucinate valueBool. The schema strictly requires valueBoolean.
The Array-vs-Object Wrapper Pitfall (Top-Level): The A2UI payload returned by the agent MUST be a JSON object with a top-level "a2ui_messages" key.
{ "a2ui_messages": [ ... ] }[ ... ] (Returning a raw array fails parsing!).a2ui_examples.py) use this object wrapper instead of raw arrays, so the LLM learns the correct top-level structure.agent_executor.py) to deliver A2UI payloads, you MUST extract the list of messages from the top-level "a2ui_messages" object and send each message as a separate DataPart with mimeType="application/json+a2ui". Sending the entire wrapped object in a single part will cause rendering failures (like raw JSON display or blank responses) in the platform client.Message Order Sensitivity (CRITICAL): When sending multiple A2UI instructions in the same response (e.g., beginRendering and surfaceUpdate), ensure that beginRendering appears FIRST in the list of messages. The platform client renderer may fail to process updates if it doesn't know the root container target first!
The Python f-string Escaping Trap: When using Python f-strings to define prompts that contain inline JSON examples, literal curly braces {} MUST be escaped as {{}}. Failure to do so triggers SyntaxError: Expression nested too deeply.
Deployment Pack Completeness (extra_packages): When deploying via the Python SDK (Pickle-based), if your agent imports local helper modules (e.g., a2ui_examples.py, a2ui_schema.py), you MUST add them to the extra_packages list in your deployment script. Unlisted files will not be uploaded to the server, causing ModuleNotFoundError at runtime.
usageHint Schema Strictness: Gemini Enterprise strictly enforces the usageHint enum values (e.g., "icon", "avatar", "header"). Using unsupported values like "body", "caption", or "h4" (even if seen in some examples) can cause silent rendering failures, displaying raw JSON instead. When in doubt, omit usageHint or stick to the official enum list.
gemini-2.5-flash may occasionally fail to generate perfectly valid JSON payloads for complex UIs, or may truncate output due to token limits when prompt instructions are long.gemini-2.5-pro.flash to pro resolved persistent JSON truncation errors and allowed the workflow to complete successfully.usageHint and fit to guide the client renderer."smallFeature" or "mediumFeature" (e.g., for phone plan logos or device images in a list)."icon" for small status indicators or UI controls."avatar" specifically for user/agent faces (often rendered circular)."header" for large images at the top of a card.fit property (contain, cover, etc.) to ensure the image scales correctly within the area allocated by the client based on the hint.message literals that explicitly append the target IDs inside the user-visible text string part (e.g. I want to book Dr. Smith (id: p1).). This guarantees that even if the server-side RAM cache flushes, the next replica reads the ID directly from the client-passed transcript body, enabling it to invoke continuity tool checks instantly!A2UI_SCHEMA before debugging the renderer. A missing wrapper (e.g., Text.literalString vs Text.text.literalString) breaks rendering.Card or styled Column to group related inputs. A flat list of components is valid but visually confusing.explicitList, ensure every ID appears exactly once to prevent "ghost" duplicates.Text should often inherit styles (e.g., color) from their containers (Buttons, Headers) rather than having hardcoded default styles.To handle LLM JSON errors gracefully:
A2UI_SCHEMA.Symptom: Server crashes on startup with PydanticSchemaGenerationError or RecursionError, especially involving ClientSession or GenericAlias.
Cause: Incompatibility between Pydantic's schema generator and Python 3.13's type handling in the ADK/FastAPI stack.
Fix: Apply strict monkey-patches to pydantic._internal._generate_schema to fallback to any_schema for unknown types.
Symptom: Client fails to connect to ADK server with Network Error or CORS policy block. Fix:
--allow_origins "*".vite.config.ts to proxy requests:
server: { proxy: { '/a2a': 'http://localhost:8000' } }
Lesson: When verifying A2UI in a browser (e.g., using adk web or Playwright):
Lesson: When migrating tools to A2UI agents:
open('data.json') fails if the file isn't copied to the new agent's folder.open( or read_text during migration.Symptom: 400 REMOTE_AGENT_FAILURE (internal 403 PermissionDenied in initializer.py).
Cause: The SDK reads the project number injected by the runtime and tries to resolve it to a name via Cloud Resource Manager API (which fails if the Service Account lacks resourcemanager.projects.get).
Fix: Initialize the Vertex AI SDK explicitly using aiplatform.init(project=...) using a user-provided PROJECT_ID environment variable (e.g., from Terraform) before any other vertexai imports. This forces the SDK to use the correct project ID and bypasses the project number resolution lookup:
import os
import google.cloud.aiplatform as aiplatform
if "PROJECT_ID" in os.environ:
aiplatform.init(project=os.environ["PROJECT_ID"])
os.environ["GOOGLE_CLOUD_PROJECT"] = os.environ["PROJECT_ID"]
Symptom: Server crashes on startup with RecursionError or compilation drops during cloudpickle unpickling.
Cause: Missing template type alignments in the runtime vertexai._genai suite.
Fix: At the very top of your agent_executor.py (before ADK imports), force alignment of Starlette request types:
import sys
from typing import Any
try:
import vertexai.preview.reasoning_engines.templates.a2a as a2a_module
import starlette.requests
import a2a.server.apps.rest.rest_adapter as adapter_module
if "Request" not in a2a_module.__dict__:
a2a_module.__dict__["Request"] = starlette.requests.Request
if "ServerCallContext" not in a2a_module.__dict__:
a2a_module.__dict__["ServerCallContext"] = adapter_module.ServerCallContext
except ImportError:
pass
KeyError: 'serialized' on Python 3.13Symptom: Server crashes on startup in remote Agent Engine environment with KeyError: 'serialized' in google/protobuf/message.py during cloudpickle.loads.
Cause: Protocol Buffer objects rely on C-extensions and do not always pickle/unpickle reliably across different environment versions on Python 3.13.
Fix: Apply a monkey-patch to Message.__setstate__ at the very top of your agent_executor.py (or entry point file) to handle the missing key gracefully:
try:
from google.protobuf.message import Message
original_setstate = Message.__setstate__
def patched_setstate(self, state):
if 'serialized' not in state:
state['serialized'] = b''
return original_setstate(self, state)
Message.__setstate__ = patched_setstate
except Exception as e:
pass
Lesson: When deploying via the Python SDK (Pickle-based), the local environment's package versions are captured in the pickle. If the remote environment installs different versions (due to unpinned requirements), unpickling failures like KeyError: 'serialized' are highly likely.
Action:
deploy_sdk.py (or deploy_ae.py) to match known working versions (e.g., from a reference working file)."a2ui-agent-sdk" in the requirements list of the config dictionary passed to client.agent_engines.create.Lesson: Always reference the package by its official name a2ui-agent-sdk:
a2ui-agent-sdk
The workspace contains a comprehensive library of A2UI samples. Always prefer reading these verified implementations over generating code from scratch.
For additional examples of agents that work with Gemini Enterprise, refer to: https://github.com/GCPartner/partner-reference-agents
Root Path: ./examples (Relative to this SKILL.md)
| Type | Name | Path | Key Patterns |
|---|---|---|---|
| Agent | RizzCharts | examples/agent/rizzcharts | Tool-Based Pattern, Dynamic Charts/Maps, Schema Wrapping |
| Agent | Contact Lookup | examples/agent/contact_lookup | Simple Forms, Card Layouts, Static Resources |
| Agent | Reasoning Engine (A2A) | examples/agent/agent_engine_a2a | A2aAgent, agent_executor.py, a2ui_schema.py |
| Client | Generic Shell | examples/client/generic_shell | Hybrid Chat Loop, A2UI Renderer Integration, SSE Parsing |
Instruction: When asked to build a feature (e.g., "add a chart"), first use list_dir on the relevant local example directory to find precedent, then view_file to copy the proven pattern.
This skill contains the entire A2UI framework source for reference.
| Resource | Path (Relative) | Description |
|---|---|---|
| Online Specification | https://a2ui.org/reference/components | Official web component gallery and behavioral reference. |
| Specification | ./specification | Canonical JSON Schemas (a2ui_schema.json) and Protocol specs. |
| Documentation | ./docs | Comprehensive Markdown guides on Architecture, Component Library, and Best Practices. |
| Renderers | ./renderers | Source code for official client renderers (e.g., ./renderers/lit, ./renderers/angular). Use for deep debugging of client issues. |
| Tools | ./tools | Helper scripts for schema validation and scaffolding. |
Usage:
view_file ./specification/components/<Component>.jsonview_file ./renderers/lit/src/components/<Component>.tsUse this a2ui_schema.py module to validate generated LLM payloads against platform validation strictness before streaming:
A2UI_SCHEMA = r"""{
"title": "A2UI Message Schema",
"description": "Describes a JSON payload for an A2UI (Agent to UI) message...",
"type": "object",
"properties": {
"beginRendering": { ... },
"surfaceUpdate": { ... },
"dataModelUpdate": { ... },
"deleteSurface": { ... }
}
}
"""
[!TIP] Refer to the repository root
/usr/local/google/home/veermuchandi/code/agents/rad-workshop/careconnect_navigator_a2ui/a2ui_schema.pyfor the full 300+ line JSON Schema string block ensuring v0.8 compliance.
Use this template to build a robust local tester for A2UI agents.
server.py (FastAPI)from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
import json
app = FastAPI()
@app.post("/jsonrpc")
async def handle_jsonrpc(request: Request):
body = await request.json()
params = body.get("params", {})
message = params.get("message", {})
query = message.get("text", "")
parts = message.get("parts", [])
# Extract userAction from DataPart
user_action = None
for part in parts:
if part.get("metadata", {}).get("mimeType") == "application/json+a2ui":
data = part.get("data")
if isinstance(data, dict) and 'userAction' in data:
user_action = data['userAction']
break
# Process Action Context
state = {}
if user_action:
action_context = user_action.get('context', {})
for key, value in action_context.items():
state[key] = value
if key == 'message':
query = value # Override query with message from context
# Call your agent here...
# response = agent.run(query, state)
# Return A2UI response...
index.html (Client Snippets)<!-- Handle DateTimeInput -->
if (comp.DateTimeInput) {
const input = document.createElement('input');
const dt = comp.DateTimeInput;
if (dt.enableDate === false && dt.enableTime === true) {
input.type = 'time';
} else if (dt.enableDate === true && dt.enableTime === false) {
input.type = 'date';
} else {
input.type = 'datetime-local';
}
...
}
<!-- Handle Button Actions -->
if (comp.Button.action) { // Generic handler
btn.onclick = () => {
const context = comp.Button.action.context || [];
// Extract data from model and send...
};
}
This template handles session recovery, state injection, and robust A2UI output parsing.
[!NOTE] A standalone version of this template is available at
templates/golden_agent_executor.pywithin the skill directory.
import json, logging, re
from a2a import types, utils
from a2a.server import agent_execution, events, tasks
from google.adk import runners
from google.adk.sessions import in_memory_session_service
from google.genai import types as genai_types
from agent import root_agent
logger = logging.getLogger(__name__)
class AdkAgentToA2AExecutor(agent_execution.AgentExecutor):
def __init__(self):
self._runner = runners.Runner(
app_name="A2UIAgent",
agent=root_agent,
session_service=in_memory_session_service.InMemorySessionService(),
auto_create_session=True,
)
async def execute(self, context: agent_execution.RequestContext, event_queue: events.EventQueue) -> None:
query = context.get_user_input()
task = context.current_task
task_id = context.task_id or (task.id if task else "default_task")
context_id = context.context_id or (task.context_id if task else "default")
session_id = context_id or "default"
# 1. SESSION RECOVERY: Extract state from A2UI payload
try:
if hasattr(context, 'message') and context.message:
for part in context.message.parts:
if hasattr(part, 'root') and hasattr(part.root, 'data'):
data = part.root.data
if isinstance(data, dict) and 'userAction' in data:
action_ctx = data['userAction'].get('context', {})
query = action_ctx.get('message', query)
# Recover Form Inputs
for item in data['userAction'].get('inputs', []):
if item.get('id'): context.metadata[item['id']] = item['value']
except Exception as e: logger.warning(f"Recovery failed: {e}")
# 2. STATE INJECTION: Persist state via prompt (Transcript Echoing)
state_str = "|".join([f"{k}={v}" for k, v in context.metadata.items()])
if state_str: query = f"{query} [State: {state_str}]"
# 3. EXECUTION: Run ADK Runner with correct types
updater = tasks.TaskUpdater(event_queue, task_id, context_id)
await updater.start_work()
full_text = ""
async for event in self._runner.run_async(
user_id="user",
session_id=session_id,
new_message=genai_types.Content(parts=[genai_types.Part(text=query)])
):
if event.is_final_response():
full_text += "".join([p.text for p in event.content.parts if hasattr(p, 'text')])
# 4. OUTPUT PARSING: Regex-based extraction (Required for v0.8)
json_match = re.search(r"(\{.*\"a2ui_messages\".*\})", full_text, re.DOTALL)
parts = [types.Part(root=types.TextPart(text=re.sub(r"---a2ui_JSON---.*", "", full_text, flags=re.DOTALL).strip()))]
if json_match:
try:
for msg in json.loads(json_match.group(1)).get("a2ui_messages", []):
parts.append(types.Part(root=types.DataPart(data=msg, metadata={"mimeType": "application/json+a2ui"})))
except: pass
await updater.add_artifact(parts, name="response")
await updater.complete()
async def cancel(self, context, event_queue): pass
This section summarizes critical fixes for issues encountered when deploying A2UI agents to Vertex AI Reasoning Engine.
| Issue Category & Name | Log/Browser Signature 🔍 | Generic Root Cause 🧠 | The Reusable Solution 🛠️ |
|---|---|---|---|
| Model Access Lockout (404) | 404 Publisher Model ... was not found. | The project/region has either deprecated the model or restricted it. | Implementation: Never assume a model is available. Create a standalone troubleshoot_access.py and run a "probe" across the frontier (e.g., gemini-2.5-flash, gemini-3.1-flash) to find the valid regional standard. |
| Input Form Loop | Browser keeps showing the same input form again after submission. | Missing user action in few-shot examples. | Architectural Patches: 1. Add intent-specific examples to AgentSkill card. 2. Refine System Instructions with a numbered "Turn Sequence". |
| State Persistence Gap | NO STATE RECOVERED. Model asks for data you just provided. | Statelessness of Reasoning Engine. | The "Recovery Loop" Pattern: In your executor's execute() method, recursively search for userAction objects in context.message.parts and inject their inputs back into the prompt as a [State: key=val] block. |
| Protocol UI rejection | Browser says "Missing Card" or blank UI. | Malformed JSON or missing mimeType. | A2UI v0.8 Compliance: Ensure the executor wraps all JSON after ---a2ui_JSON--- into an a2a.types.DataPart with metadata={"mimeType": "application/json+a2ui"}. Use regex to strip conversational text. |
| Cloud Serialization Error | TASK_STATE_FAILED or PickleError. | Protobuf unpickling issues. | The Serialization Patch: Add a monkey-patch to your deployment script that injects a __setstate__ method into google.protobuf.message.Message. |
| Execution Context Flickering | Permissions errors or "Project not found". | Loss of env vars. | Forced Initialization: Use the numeric Project Number. Call aiplatform.init and vertexai.init inside the __init__ AND the execute() methods of your executor. |
| Chain-of-Thought Break | Agent calls Turn 1 tools instead of Turn 2 tools. | Attention on text rather than state. | Delimited Prompts: Append state to query: `Actual User Text [State: param1=val |
| Dependency Build Failure | Deployment fails with "requirements not satisfied". | Version conflicts. | Clean Build Strategy: Avoid pinning google-cloud-aiplatform to a specific sub-version. Use @ git+https://github.com/... syntax for side-loaded SDKs. |
| Echo Behavior Failure | Tool JSON is swallowed or double-wrapped. | Agent tries to be "helpful" by summarizing. | Echo Constraint: Add hard constraint: "When a tool returns a string starting with '---a2ui_JSON---', you MUST include that exact string in your response without modification." |
agent_executor.py.---a2ui_JSON--- to ensure text and data are separated.init calls.