Skip to content

ARP — Agent Rendering Protocol

ARP is a transport-agnostic, bidirectional protocol for communication between agent backends and frontend renderers. It solves the "generative UI" problem — how does a streaming agent tell a frontend "render a table/chart/form here" alongside plain text?

Every Haira server speaks ARP natively. No configuration needed.

Overview

┌──────────────────────┐         ARP Messages         ┌──────────────────────┐
│   Haira Agent        │ ─────────────────────────────▸│   Frontend           │
│   (Go backend)       │◂──────────────────────────────│   (React/Vue/Web)    │
│                      │     WebSocket or SSE          │                      │
│  tool returns        │                               │  ArpRenderer maps    │
│  ui.table(...)  ─────┼──▸ { type: "render",          │  "table" → <Table>   │
│                      │      component: "table",      │                      │
│                      │      props: {...} }           │                      │
└──────────────────────┘                               └──────────────────────┘

Protocol (v1)

ARP v1 operates in Minimal Mode — a flat, session-addressed message format. Every message is JSON with at minimum { v: 1, type: "<type>" }.

Server → Client Messages

TypePurposeKey Fields
helloCapability handshake on connectcapabilities.components[], capabilities.features[]
deltaIncremental text chunkpayload.delta
tool_startTool execution startedpayload.tool, payload.args
tool_endTool execution finishedpayload.tool, payload.ok
renderGenerative UI componentcomponents[].type, components[].props, tool_name
patchIncremental component updateops[] with op, target, path, value
errorError eventpayload.error
commitStream completefinal: true

Client → Server Messages

TypeInput TypeData Fields
inputtext{ text }
inputaction{ action, payload }
inputform_submit{ action, fields }

Example Message Flow

Server → Client:  { v: 1, type: "hello", capabilities: { components: [...], features: ["streaming", "input"] } }
Client → Server:  { v: 1, type: "input", input_type: "text", data: { text: "Show me the sales data" } }
Server → Client:  { v: 1, type: "delta", payload: { delta: "Here are " } }
Server → Client:  { v: 1, type: "delta", payload: { delta: "the results:" } }
Server → Client:  { v: 1, type: "tool_start", payload: { tool: "query_database", args: "..." } }
Server → Client:  { v: 1, type: "render", components: [{ id: "c_1", type: "table", props: {...} }], tool_name: "query_database" }
Server → Client:  { v: 1, type: "tool_end", payload: { tool: "query_database", ok: true } }
Server → Client:  { v: 1, type: "commit", final: true }

Transports

ARP supports two transports, both available from the same Haira server:

WebSocket (/_arp/v1)

The primary transport for chat workflows. Persistent bidirectional connection.

1. Client connects to ws://localhost:8080/_arp/v1
2. Server sends "hello" message with capabilities
3. Client sends "input" messages (text, actions, form submits)
4. Server streams back delta/render/tool_start/tool_end/commit messages

Features:

  • Auto-reconnect with exponential backoff (1s → 16s cap, max 5 attempts)
  • Session-addressed messages for multi-session support
  • Full bidirectional communication (text input, button clicks, form submissions)

SSE (Server-Sent Events)

Used as fallback when WebSocket is unavailable, and required for file uploads (multipart/form-data). POST to any streaming workflow endpoint with Accept: text/event-stream.

POST /chat HTTP/1.1
Accept: text/event-stream
Content-Type: application/json

{"message": "Show me the sales data", "session_id": "abc"}

SSE event types:

SSE EventARP TypeFormat
(default)deltadata: {"delta":"..."}
tool_starttool_startevent: tool_start\ndata: {"tool":"...","args":"..."}
tool_renderrenderevent: tool_render\ndata: {"tool":"...","component":"...","props":{}}
tool_endtool_endevent: tool_end\ndata: {"tool":"...","ok":true}
errorerrorevent: error\ndata: {"error":"..."}
(default)commitdata: [DONE]

Built-in Components

ARP ships with 14 built-in component types. Every Haira server advertises these in the hello message:

ComponentDescriptionKey Props
textPlain texttext
markdownRendered markdowncontent
status-cardStatus indicator cardstatus, title, message
tableData table with optional tabstitle, headers, rows, tabs
code-blockSyntax-highlighted codetitle, language, code, tabs
diffSide-by-side diff viewtitle, before, after, language
key-valueKey-value pairs displaytitle, items[].key, items[].value
progressMulti-step progress trackertitle, steps[].name, steps[].status
chartData visualizationtype, title, labels, datasets
formInteractive formtitle, fields[], submit_label
confirmYes/no confirmation dialogtitle, confirm_label, deny_label
choicesOption pickertitle, options[]
product-cardsImage card gridtitle, cards[].name, cards[].price
imageImage displaysrc, alt

Capability Discovery

Query GET /_api/arp to discover what the server supports:

json
{
  "v": 1,
  "type": "hello",
  "capabilities": {
    "components": [
      "text", "status-card", "table", "code-block", "diff",
      "key-value", "progress", "chart", "form", "confirm",
      "choices", "product-cards", "markdown", "image"
    ],
    "features": ["streaming", "input"]
  }
}

Using ARP in Haira

Rendering UI Components from Tools

Tools return UI components using the ui module. The component is streamed to the frontend via ARP while the LLM receives a compact text summary:

haira
import "ui"

tool query_database(query: string) -> any {
    """Executes a SQL query and displays results."""

    rows, err = postgres.query(db, query)
    if err != nil {
        return ui.status_card("error", "Query Failed", conv.to_string(err))
    }

    return ui.table("Query Results", headers, rows)
}

Available UI Functions

haira
import "ui"

// Status cards
ui.status_card("success", "Title", "Optional message")
ui.status_card("error", "Title", "Error details")
ui.status_card("warning", "Title")
ui.status_card("info", "Title")

// Tables
ui.table("Title", ["Col A", "Col B"], [["row1a", "row1b"], ["row2a", "row2b"]])

// Key-value displays
ui.key_value("User Details", {"Name": "Alice", "Role": "Admin"})

// Charts
ui.chart("bar", "Revenue", ["Q1", "Q2", "Q3"], [dataset])

// Confirmation dialogs
ui.confirm("Delete this?", "Yes, delete", "Cancel")

// Product card grids
ui.product_cards("Search Results", cards)

// Compose multiple components
ui.group(
    ui.status_card("success", "Query Complete", "42 rows"),
    ui.table("Results", headers, rows)
)

Enabling UI on Agents

Add ui: ui to your agent declaration to register all built-in UI tools:

haira
import "ui"

agent DataExplorer {
    provider: OpenAI
    model: "gpt-4o"
    tools: [query_database, search_data]
    ui: ui
    memory: conversation(max_turns: 30)
}

The agent can then use UI components in any of its tools. The frontend receives render events and displays the appropriate component.

Client SDKs

ARP client libraries let you build custom frontends that connect to any Haira server (or any ARP-compatible backend).

@haira/arp — Core (Zero Dependencies)

bash
npm install @haira/arp

WebSocket client:

typescript
import { ArpClient } from '@haira/arp'

const client = new ArpClient('ws://localhost:8080/_arp/v1', {
  onConnect: (caps) => console.log('Components:', caps.components),
  onDelta: (text) => appendToChat(text),
  onRender: (event) => renderComponent(event.component, event.props),
  onToolStart: (tool) => showToolRunning(tool),
  onToolEnd: (tool, ok) => showToolDone(tool, ok),
  onDone: () => markStreamComplete(),
  onError: (err) => showError(err),
})

client.connect()
client.sendText('Show me the sales data')

SSE client (for file uploads or WebSocket fallback):

typescript
import { streamSSE } from '@haira/arp'

streamSSE('http://localhost:8080/chat', {
  body: formData, // supports FormData for file uploads
  onDelta: (text) => appendToChat(text),
  onToolRender: (event) => renderComponent(event),
  onDone: () => markComplete(),
})

Session management:

typescript
import { createSessionAPI } from '@haira/arp'

const api = createSessionAPI('http://localhost:8080')
const sessions = await api.listSessions()
const session = await api.getSession('session-id')
await api.deleteSession('session-id')

@haira/arp-react — React Hooks & Components

bash
npm install @haira/arp-react

Drop-in chat UI — zero-config full chat interface:

tsx
import { ArpChat } from '@haira/arp-react'

function App() {
  return (
    <ArpChat
      url="ws://localhost:8080/_arp/v1"
      theme="dark"
      accentColor="#E8A317"
      title="Data Explorer"
      suggestions={['Show database schema', 'Query recent orders']}
    />
  )
}

Custom UI with hooks — full control over rendering:

tsx
import { useArpChat } from '@haira/arp-react'
import { DEFAULT_COMPONENTS } from '@haira/arp-react/ui'

function Chat() {
  const {
    messages,
    isStreaming,
    isConnected,
    toolCards,
    sendMessage,
    sessions,
    startNewChat,
  } = useArpChat({
    url: 'ws://localhost:8080/_arp/v1',
    components: DEFAULT_COMPONENTS,
  })

  return (
    <div>
      {messages.map(msg => <Message key={msg.id} {...msg} />)}
      <input onSubmit={(text) => sendMessage(text)} />
    </div>
  )
}

Built-in UI components (import from @haira/arp-react/ui): StatusCard, Table, CodeBlock, Diff, KeyValue, Progress, Chart, Form, Confirm, Choices, ProductCards

@haira/arp-vue — Vue Composables & Components

bash
npm install @haira/arp-vue

Drop-in chat UI:

vue
<script setup>
import { ArpChat } from '@haira/arp-vue'
</script>

<template>
  <ArpChat
    url="ws://localhost:8080/_arp/v1"
    theme="dark"
    accentColor="#E8A317"
    title="Data Explorer"
    :suggestions="['Show database schema', 'Query recent orders']"
  />
</template>

Custom UI with composables:

vue
<script setup>
import { useArpChat } from '@haira/arp-vue'

const { messages, isStreaming, sendMessage, startNewChat } = useArpChat({
  url: 'ws://localhost:8080/_arp/v1',
})
</script>

Built-in UI components available at @haira/arp-vue/ui.

Go SDK

The Go SDK is available as a standalone package for building ARP-compatible backends outside Haira:

bash
go get github.com/haira-lang/arp-go
go
import "github.com/haira-lang/arp-go"

// Create a server with default capabilities
server := arp.NewServer(arp.ServerConfig{
    InputHandler: func(sessionID, inputType, text string, raw json.RawMessage) (<-chan arp.StreamChunk, error) {
        chunks := make(chan arp.StreamChunk)
        go func() {
            defer close(chunks)
            chunks <- arp.StreamChunk{Delta: "Hello from ARP!"}
            chunks <- arp.StreamChunk{Done: true}
        }()
        return chunks, nil
    },
})

server.ListenAndServe(":8080")

API Routes

Every Haira server automatically exposes these ARP-related routes:

RouteMethodDescription
/_arp/v1WebSocketARP WebSocket endpoint
/_api/arpGETCapability discovery
/_api/workflowsGETList available workflows
/_api/chatsGETList chat sessions
/_api/chats/{id}GETGet session with message history
/_api/chats/{id}DELETEDelete a session
/_api/runsGETRun history
/_api/runs/{id}GETRun detail
/_api/runs/stream/{id}GET (SSE)Reconnect to in-progress run

Patch Operations

For incremental component updates without re-rendering the entire component:

OperationDescription
updateUpdate a property at path on target component
insertInsert a new component after a target
removeRemove the target component
replaceReplace target with a new component
reorderMove target to after another component
json
{
  "v": 1,
  "type": "patch",
  "ops": [
    { "op": "update", "target": "c_1", "path": "props.rows", "value": [[...]] }
  ]
}

Released under the Apache-2.0 License.