Chapter 10: A2UI Protocol (Streaming UI Components)
Goal of this chapter: Implement the A2UI protocol to render Agent output as streaming UI components.
Important Note: The Scope of A2UI
A2UI does not belong to the Eino framework itself — it is a business-layer UI protocol/rendering solution. This chapter integrates A2UI into the Agent built progressively in previous chapters to provide an end-to-end, production-ready complete example: from model calls, tool calls, workflow orchestration, to finally presenting results in a more user-friendly UI form.
In real business scenarios, you can choose different UI forms depending on the product:
- Web / App: Custom components, tables, cards, charts, etc.
- IM/Office suites: Message cards, interactive forms
- Command line: Plain text or TUI (Terminal UI)
Eino focuses on “composable intelligent execution and orchestration capabilities.” How to present results to users is a business-layer concern that can be freely extended.
Code Locations
- Entry code (Runner version): cmd/ch10/main.go
- A2UI subset implementation: a2ui/types.go
- A2UI event stream conversion: a2ui/streamer.go
- Frontend page: static/index.html
Prerequisites
Same as Chapter 1: You need to configure an available ChatModel (OpenAI or Ark)
Running
In the quickstart/chatwitheino directory, execute:
go run ./cmd/ch10/
Example output:
starting server on http://localhost:8080
(Optional) Enable ch09 skills capability
The final Web version uses Agent construction logic aligned with Chapter 9: when EINO_EXT_SKILLS_DIR points to a valid skills directory, the skill middleware is automatically registered, allowing the model to load eino-guide / eino-component / eino-compose / eino-agent via the skill tool on demand.
go run ./scripts/sync_eino_ext_skills.go -src /path/to/eino-ext -dest ./skills/eino-ext -clean
EINO_EXT_SKILLS_DIR="$(pwd)/skills/eino-ext" go run ./cmd/ch10/
Sessions are saved by default in ./data/sessions_agentic.
From Text to UI: Why A2UI is Needed
In the first eight chapters, our Agent only outputs text, but modern AI applications need richer interactions.
Limitations of plain text output:
- Cannot display structured data (tables, lists, cards, etc.)
- Cannot update in real-time (progress bars, status changes, etc.)
- Cannot embed interactive elements (buttons, forms, links, etc.)
- Cannot support multimedia (images, video, audio, etc.)
A2UI’s positioning:
- A2UI is a protocol from Agent to UI: Defines how Agent output maps to UI components
- A2UI supports streaming rendering: Components can update in real-time without waiting for a complete response
- A2UI is declarative: The Agent only needs to declare “what to display,” and the UI handles rendering
Simple analogy:
- Plain text output = “Terminal command line” (can only display text)
- A2UI = “Web application” (can display any UI component)
Key Concepts
A2UI v0.8 Subset (Scope of This Example)
This quickstart does not implement a “complete A2UI standard library.” Instead, it implements an A2UI v0.8 subset: the goal is to push the Agent’s event stream to the browser as a stable, incrementally renderable UI component tree.
The currently implemented A2UI message types and component types are defined in a2ui/types.go.
A2UI Messages: BeginRendering / SurfaceUpdate / DataModelUpdate / InterruptRequest
Each SSE line (data: {...}) carries one A2UI Message. A Message is an “envelope structure” where only one field is present at a time:
Key code snippet (Note: this is a simplified code snippet that cannot run directly. See a2ui/types.go for complete code):
type Message struct {
BeginRendering *BeginRenderingMsg
SurfaceUpdate *SurfaceUpdateMsg
DataModelUpdate *DataModelUpdateMsg
DeleteSurface *DeleteSurfaceMsg
InterruptRequest *InterruptRequestMsg
}
Where:
BeginRendering: Tells the frontend to “start rendering a surface (session)” and specifies the root node IDSurfaceUpdate: Adds/updates a batch of components (components form a tree, referencing each other byid)DataModelUpdate: Updates data bindings (used to incrementally update streaming text to a Text component)InterruptRequest: When the Agent triggers an interrupt (e.g., approval), notifies the frontend to display an approve/reject entry
A2UI Components: Text / Column / Card / Row
This example implements only 4 UI components (see a2ui/types.go):
Text: Text rendering (supportsusageHintto distinguish caption/body/title); whendataKeyis present, text comes fromDataModelUpdateColumn/Row: Layout (children are component ID lists)Card: Card container (children are component ID lists)
A2UI Implementation: Converting AgentEvent to A2UI SSE
The core pipeline of the final Web version is:
- The backend runs the Agent, obtaining
*adk.AsyncIterator[*adk.TypedAgentEvent[M]] - The event stream is converted to A2UI JSONL/SSE output for the browser (see a2ui/streamer.go)
- The frontend parses SSE
data:lines and renders the component tree (see static/index.html)
Server Routes (High Level)
Key interfaces related to A2UI (see cmd/ch10/main.go):
GET /: Returns the frontend pagestatic/index.htmlPOST /sessions/:id/chat: Returns an SSE stream (A2UI messages), rendering Agent results to the UI as they executeGET /sessions/:id/render: Returns JSONL (A2UI messages) for “replaying history when selecting a session”POST /sessions/:id/approve: Handles interrupt approval/rejection and continues returning the SSE stream
Event Stream Conversion (High Level)
The server passes the Runner.Run(...) event stream to a2ui.StreamToWriter[M](...), which is responsible for:
- Splitting user/assistant/tool output
- Rendering tool call / tool result as “chip cards”
- Converting the assistant’s streaming tokens into
DataModelUpdatefor “render while generating” - Sending
InterruptRequestwhen encountering an interrupt, and pausing to wait for human approval
Frontend Integration: fetch + SSE (Not WebSocket)
- The frontend initiates a request via
fetch('/sessions/:id/chat'), then reads streaming bytes fromres.body, splits by line, and parsesdata: {...}JSON (see static/index.html).
Key code snippet (Note: this is a simplified code snippet that cannot run directly. See static/index.html for complete code):
const res = await fetch(`/sessions/${id}/chat`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({message}),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const {done, value} = await reader.read();
if (done) break;
buffer += decoder.decode(value, {stream: true});
const lines = buffer.split('\n');
buffer = lines.pop();
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith('data:')) {
const jsonStr = trimmed.slice(5).trimStart();
processA2UIMessage(JSON.parse(jsonStr));
}
}
}
A2UI Streaming Rendering Flow (Overview)
┌─────────────────────────────────────────┐
│ User: Analyze this file │
└─────────────────────────────────────────┘
↓
┌──────────────────────┐
│ Agent starts │
│ A2UI: AddText │
│ "Analyzing..." │
└──────────────────────┘
↓
┌──────────────────────┐
│ Call Tool │
│ A2UI: AddProgress │
│ Progress: 0% │
└──────────────────────┘
↓
┌──────────────────────┐
│ Tool executing │
│ A2UI: UpdateProgress│
│ Progress: 50% │
└──────────────────────┘
↓
┌──────────────────────┐
│ Tool complete │
│ A2UI: tool result │
└──────────────────────┘
↓
┌──────────────────────┐
│ Display results │
│ A2UI: DataModelUpdate│
│ (streaming assistant)│
└──────────────────────┘
Chapter Summary
- A2UI: A protocol from Agent to UI, defining how Agent output maps to UI components
- Subset implementation: This example only implements Text/Column/Card/Row and data binding
- Streaming output: The backend pushes A2UI JSONL via SSE; the frontend incrementally renders the component tree
- Events to UI: Converts
AgentEventinto visualized output oftool call / tool result / assistant stream
Next Steps
This chapter’s cmd/ch10 uses adk.Runner to implement a complete Web application. However, Runner is a “one-shot” model — if a user sends a new question while the Agent is still answering, Runner has no built-in mechanism to cancel the current execution and switch to the new input.
The next chapter introduces adk.TurnLoop, adding Preempt and Abort capabilities to the Agent.