| name | castella-a2ui |
| description | Render A2UI JSON as native Castella widgets. Parse A2UI messages, handle actions, progressive rendering, data binding, and connect to A2UI-enabled agents. |
Castella A2UI Integration
A2UI (Agent-to-User Interface) enables AI agents to generate rich, interactive UIs by outputting JSON component descriptions. Castella renders these natively across desktop, web, and terminal platforms.
When to use: "render A2UI JSON", "A2UI component", "A2UIRenderer", "A2UI data binding", "A2UI streaming", "connect to A2UI agent", "updateDataModel", "A2UIClient"
Quick Start
Render A2UI JSON to a Castella widget:
from castella import App
from castella.a2ui import A2UIRenderer, A2UIComponent
from castella.frame import Frame
renderer = A2UIRenderer()
widget = renderer.render_json({
"components": [
{"id": "root", "component": "Column", "children": {"explicitList": ["text1"]}},
{"id": "text1", "component": "Text", "text": {"literalString": "Hello A2UI!"}}
],
"rootId": "root"
})
App(Frame("A2UI Demo", 800, 600), widget).run()
Core Concepts
A2UIRenderer
The main class for converting A2UI messages to Castella widgets:
from castella.a2ui import A2UIRenderer, UserAction
def on_action(action: UserAction):
print(f"Action: {action.name}")
print(f"Source: {action.source_component_id}")
print(f"Context: {action.context}")
renderer = A2UIRenderer(on_action=on_action)
Value Types
A2UI uses typed values for properties:
# Literal values (static)
{"text": {"literalString": "Hello"}}
{"value": {"literalNumber": 42}}
{"checked": {"literalBoolean": True}}
# Data binding (dynamic, JSON Pointer RFC 6901)
{"text": {"path": "/user/name"}}
{"value": {"path": "/counter"}}
Helper functions:
from castella.a2ui import literal, binding
literal("Hello") # {"literalString": "Hello"}
literal(42) # {"literalNumber": 42}
literal(True) # {"literalBoolean": True}
binding("/counter") # {"path": "/counter"}
Supported Components
| A2UI Component | Castella Widget | Notes |
|---|---|---|
| Text | Text | usageHint: h1-h5, body, caption |
| Button | Button | action with context |
| TextField | Input/MultilineInput | usageHint: password, multiline |
| CheckBox | CheckBox | Two-way binding |
| Slider | Slider | min/max range |
| DateTimeInput | DateTimeInput | Date/time picker |
| ChoicePicker | RadioButtons/Column | Single/multiple selection |
| Image | NetImage | URL-based images |
| Icon | Text | Material Icons → emoji |
| Divider | Spacer | Horizontal/vertical |
| Row | Row | Horizontal layout |
| Column | Column | Vertical layout |
| Card | Column | Container with styling |
| List | Column | TemplateChildren support |
| Tabs | Tabs | Tabbed navigation |
| Modal | Modal | Overlay dialog |
| Markdown | Markdown | Rich text (Castella extension) |
See references/components.md for detailed component reference.
Data Binding
Bind widget values to a data model using JSON Pointer paths:
a2ui_json = {
"components": [
{"id": "root", "component": "Column", "children": {"explicitList": ["greeting"]}},
{"id": "greeting", "component": "Text", "text": {"path": "/message"}}
],
"rootId": "root"
}
# Provide initial data
initial_data = {"/message": "Hello, World!"}
widget = renderer.render_json(a2ui_json, initial_data=initial_data)
Actions
Handle user interactions via actions:
{
"id": "submit_btn",
"component": "Button",
"text": {"literalString": "Submit"},
"action": {"name": "submit", "context": ["/formData"]}
}
Action handler receives UserAction:
def on_action(action: UserAction):
print(action.name) # "submit"
print(action.source_component_id) # "submit_btn"
print(action.context) # ["/formData"]
updateDataModel
Update bound values dynamically:
renderer.handle_message({
"updateDataModel": {
"surfaceId": "default",
"data": {"/message": "Updated message!"}
}
})
A2UIComponent (Reactive)
Wrap surface in A2UIComponent for automatic UI updates:
from castella.a2ui import A2UIComponent
renderer.render_json(a2ui_json, initial_data=initial_data)
surface = renderer.get_surface("default")
component = A2UIComponent(surface) # Auto-updates on data changes
App(Frame("A2UI", 800, 600), component).run()
TemplateChildren (Dynamic Lists)
Render lists from data arrays:
a2ui_json = {
"components": [
{"id": "root", "component": "Column", "children": {"explicitList": ["user_list"]}},
{"id": "user_list", "component": "List", "children": {
"path": "/users", # JSON Pointer to array
"componentId": "user_item" # Template component
}},
{"id": "user_item", "component": "Text", "text": {"path": "name"}} # Relative path
],
"rootId": "root"
}
initial_data = {"/users": [{"name": "Alice"}, {"name": "Bob"}, {"name": "Charlie"}]}
widget = renderer.render_json(a2ui_json, initial_data=initial_data)
ChoicePicker
Single or multiple selection:
# Single selection (renders as RadioButtons)
{
"id": "color_picker",
"component": "ChoicePicker",
"choices": [
{"literalString": "Red"},
{"literalString": "Green"},
{"literalString": "Blue"}
],
"selected": {"literalString": "Red"},
"allowMultiple": False
}
# Multiple selection (renders as CheckBox list)
{
"id": "toppings",
"component": "ChoicePicker",
"choices": [
{"literalString": "Cheese"},
{"literalString": "Pepperoni"},
{"literalString": "Mushrooms"}
],
"selected": {"path": "/selectedToppings"},
"allowMultiple": True
}
Progressive Rendering (Streaming)
Handle JSONL streams for incremental UI updates:
# From JSONL string
jsonl_content = """
{"beginRendering": {"surfaceId": "main", "root": "root"}}
{"updateComponents": {"surfaceId": "main", "components": [...]}}
"""
surface = renderer.handle_jsonl(jsonl_content)
# From file
with open("ui.jsonl") as f:
surface = renderer.handle_stream(f, on_update=lambda s: app.redraw())
# From async SSE stream
from castella.a2ui.transports import sse_stream
surface = await renderer.handle_stream_async(await sse_stream(url))
Message sequence:
beginRendering- Start progressive renderupdateComponents- Add/update componentsupdateDataModel- Update data values
See references/streaming.md for transport details.
A2UIClient
Connect to A2A agents with A2UI extension:
from castella.a2ui import A2UIClient, A2UIComponent, UserAction
def on_action(action: UserAction):
print(f"Action: {action.name}")
client = A2UIClient("http://localhost:10002", on_action=on_action)
surface = client.send("Find restaurants in Tokyo")
if surface:
App(Frame("Restaurant Finder", 800, 600), A2UIComponent(surface)).run()
Async usage:
async def main():
client = A2UIClient("http://localhost:10002")
surface = await client.send_async("Hello!")
if surface:
# Send action back to agent
await client.send_action_async(action)
A2UI 0.9 Compatibility
Castella auto-normalizes A2UI 0.9 spec format:
# A2UI 0.9 format (plain values) - accepted
{"text": "Hello", "children": ["a", "b"]}
# Castella internal format (wrapped) - also accepted
{"text": {"literalString": "Hello"}, "children": {"explicitList": ["a", "b"]}}
Key normalizations:
text: "Hello"→text: {literalString: "Hello"}children: ["a", "b"]→children: {explicitList: ["a", "b"]}usageHint: "shortText"→usageHint: "text"usageHint: "obscured"→usageHint: "password"
TextField usageHint
# Password field (masked ●●●●)
{"id": "password", "component": "TextField", "usageHint": "password"}
# Multiline field
{"id": "comments", "component": "TextField", "usageHint": "multiline"}
Best Practices
- Use A2UIComponent wrapper for reactive data binding
- Provide initial_data for TemplateChildren/List components
- Handle actions to update data model dynamically
- Use semantic IDs - A2UI component IDs become MCP semantic IDs
- Test with mock data before connecting to live agents
Installation
uv sync --extra agent # A2UI + A2A support
Reference
references/components.md- Complete A2UI component referencereferences/messages.md- A2UI message typesreferences/streaming.md- Streaming and transportsscripts/- Executable examples (basic_a2ui.py, a2ui_form.py, a2ui_list.py)- A2UI Specification: https://a2ui.org/specification/v0.9-a2ui/