v0.2.0· Apache 2.0

Search docs...

corail/agui

3 min read

AG-UI (Agent Generated UI)

Corail agents produce rich structured content during streaming, not just text tokens. The AG-UI protocol defines a set of StreamEvent types that strategies emit alongside plain text. The channel layer serializes these events into the appropriate wire format (SSE, WebSocket).

Stream protocol

Strategies yield a union type during streaming:

StreamToken = str | StreamEvent

Plain str tokens are text fragments. StreamEvent subclasses carry structured data that the dashboard renders as interactive UI components.

Event types

ToolStartEvent

Emitted when a tool execution begins.

@dataclass
class ToolStartEvent(StreamEvent):
    tool: str                          # Tool name
    args: dict[str, Any] = {}          # Arguments passed
    call_id: str = ""                  # Unique call ID (for native tool_use)

SSE payload:

{"type": "tool_start", "tool": "kubectl", "args": {"command": "get pods"}, "call_id": "toolu_01..."}

ToolEndEvent

Emitted when a tool execution completes.

@dataclass
class ToolEndEvent(StreamEvent):
    tool: str                          # Tool name
    output: str = ""                   # Tool output text
    success: bool = True               # Whether execution succeeded
    call_id: str = ""                  # Matching call ID

ConfirmEvent

Emitted when a tool has risk_level: confirm. The dashboard shows a confirmation dialog before proceeding.

@dataclass
class ConfirmEvent(StreamEvent):
    call_id: str                       # Unique call ID
    tool: str                          # Tool requesting confirmation
    args: dict[str, Any] = {}          # Arguments to review
    message: str = ""                  # Human-readable prompt

SSE payload:

{"type": "confirm", "confirm": {"id": "toolu_01...", "tool": "kubectl", "args": {"command": "delete pod x"}, "message": "Execute kubectl?"}}

ComponentEvent

Emitted when a tool returns structured data that should render as a UI component instead of plain text.

@dataclass
class ComponentEvent(StreamEvent):
    component: str                     # Component type or React component name
    props: dict[str, Any] = {}         # Props passed to the component

SSE payload:

{"type": "component", "component": "table", "props": {"columns": [...], "rows": [...]}}

Risk levels

Each tool declares a risk_level in its ToolDefinition:

LevelBehavior
safeExecute immediately, no confirmation needed
confirmEmit ConfirmEvent, wait for human approval
blockedNever execute this tool
ToolDefinition(
    name="kubectl_delete",
    description="Delete a Kubernetes resource",
    risk_level="confirm",
    parameters=[...],
)

Render hints

Tools return a ToolResult with optional render hints that control how the dashboard displays the output:

@dataclass
class ToolResult:
    success: bool
    output: str
    render: str = "text"      # text, table, chart, json, code, react
    component: str = ""       # React component name (when render="react")
    props: dict = {}          # Props for the component
RenderDashboard behavior
textDisplay as plain text (default)
tableRender as a data table
chartRender as a chart
jsonRender as formatted JSON
codeRender as a code block
reactRender a custom React component by name

When a tool returns render != "text", the strategy emits a ComponentEvent with the component name and props. The dashboard's component registry maps the component name to a React component for rendering.

SSE wire format

The REST channel serializes StreamEvent objects as SSE events:

event: message
data: {"type": "tool_start", "tool": "calculator", "args": {"expression": "15*37"}}
 
event: message
data: {"type": "tool_end", "tool": "calculator", "output": "555", "success": true}
 
event: message
data: {"type": "component", "component": "json", "props": {"data": 555}}

Plain text tokens are sent as:

event: message
data: {"type": "token", "content": "The result is "}

This separation keeps strategies transport-agnostic while enabling rich structured content on the frontend.