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.