You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

12 KiB

title
Chapter 10: A2UI Protocol (Streaming UI Components)

The goal of this chapter is to implement the A2UI protocol, rendering Agent output as streaming UI components.

Important Note: The Scope of A2UI

A2UI is not part of the Eino framework itself — it is a business-layer UI protocol/rendering solution. This chapter integrates A2UI into the Agent built step by step in previous chapters, in order to provide a complete end-to-end, production-ready example: from model calls, tool calls, and workflow orchestration, all the way to presenting results in a more user-friendly UI format.

In real business scenarios, you are free to choose different UI forms based on your product needs, such as:

  • 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". As for "how to present it to users", that's a business-layer concern that can be freely extended.

Code Location

Prerequisites

Same as Chapter 1: you need to configure an available ChatModel (OpenAI or Ark).

Running

In the quickstart/chatwitheino directory, run:

go run .

Output example:

starting server on http://localhost:8080

(Optional) Enable Chapter 9 Skills

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, enabling the model to call the skill tool on demand to load eino-guide / eino-component / eino-compose / eino-agent.

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 .

From Text to UI: Why We Need A2UI

In the first eight chapters, the Agent only output 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.)

The role of A2UI:

  • A2UI is the Agent-to-UI protocol: Defines how Agent output maps to UI components
  • A2UI supports streaming rendering: Components can update in real time without waiting for the 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 a subset of A2UI v0.8: the goal is to push Agent event streams 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 be run directly. For the complete code, please refer to a2ui/types.go):

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 ID
  • SurfaceUpdate: Adds/updates a batch of components (components form a tree, referencing each other by id)
  • 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 component types (see a2ui/types.go):

  • Text: Text rendering (supports usageHint to distinguish caption/body/title); when dataKey is present, text comes from DataModelUpdate
  • Column / Row: Layout (children are lists of component IDs)
  • Card: Card container (children are lists of component IDs)

A2UI Implementation: Converting AgentEvent to A2UI SSE

The core pipeline of the final Web version is:

  • The backend runs the Agent, producing *adk.AsyncIterator[*adk.AgentEvent]
  • The event stream is converted to A2UI JSONL/SSE stream output for the browser (see a2ui/streamer.go)
  • The frontend parses data: lines from SSE and renders the component tree (see static/index.html)

Server Routes (High Level)

Key endpoints related to A2UI (see server/server.go):

  • GET /: Returns the frontend page static/index.html
  • POST /sessions/:id/chat: Returns an SSE stream (A2UI messages), rendering Agent results to the UI as they're generated
  • GET /sessions/:id/render: Returns JSONL (A2UI messages), used 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(...), which handles:

  • Splitting user/assistant/tool output
  • Rendering tool calls / tool results as "chip cards"
  • Converting the assistant's streaming tokens into DataModelUpdate, enabling "render as you generate"
  • Sending InterruptRequest when an interrupt is encountered, 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 from res.body, splits by line, and parses the JSON from data: {...} lines (see static/index.html).

Key code snippet (Note: this is a simplified code snippet that cannot be run directly. For the complete code, please refer to static/index.html):

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           |
        |  processing             |
        |  A2UI: AddText          |
        |  "Analyzing..."         |
        +------------------------+
                   |
        +------------------------+
        |  Call Tool              |
        |  A2UI: AddProgress      |
        |  Progress: 0%           |
        +------------------------+
                   |
        +------------------------+
        |  Tool executing         |
        |  A2UI: UpdateProgress   |
        |  Progress: 50%          |
        +------------------------+
                   |
        +------------------------+
        |  Tool complete          |
        |  A2UI: tool result      |
        +------------------------+
                   |
        +------------------------+
        |  Display result         |
        |  A2UI: DataModelUpdate  |
        |  (streaming assistant   |
        |   update)               |
        +------------------------+

Chapter Summary

  • A2UI: The Agent-to-UI protocol, 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, and the frontend incrementally renders the component tree
  • Events to UI: Converts AgentEvent into visualized output for tool calls / tool results / assistant streams

Series Conclusion: The Complete Vision for This Quickstart Agent

By this chapter, we've used a fully runnable Agent to tie together Eino's core capabilities. You can think of it as an extensible "end-to-end Agent application skeleton":

  • Runtime: Runner drives execution, with support for streaming output and event models
  • Tool layer: Filesystem / Shell and other Tool capabilities, with safe tool error handling
  • Middleware: Pluggable middleware/handlers for cross-cutting concerns like error handling, retries, approvals, etc.
  • Observability: Callbacks/trace capabilities connect key call chains, facilitating debugging and production monitoring
  • Human-agent collaboration: Interrupt/resume + checkpoint support for approvals, parameter completion, branch selection, and other interactive flows
  • Deterministic orchestration: compose (graph/chain/workflow) organizes complex business flows into maintainable, reusable execution graphs
  • Business delivery: UI integrations like A2UI are a business-layer choice, used to present Agent capabilities in the appropriate product form to users

You can gradually replace/extend any part of this skeleton — model, tools, storage, workflows, frontend rendering protocol — without starting from scratch.

Further Thinking

Other component types:

  • Chart components (line charts, bar charts, pie charts)
  • Map components
  • Timeline components
  • Tree components
  • Tab components

Advanced features:

  • Component interaction (click, drag, input)
  • Conditional rendering
  • Component animations
  • Responsive layout