Levain LabsLevain Labs
Embedded Agents

Streaming

Render a live run in your own interface over AG-UI events.

Webhooks tell your backend a run finished; streaming is for the surface your customer is looking at while it runs. Every run exposes a server-sent-events stream that speaks AG-UI, the open protocol front-end agent toolkits already understand — so your interface renders a Levain run the same way it would render any other agent.

Open the stream

Start a run, then connect to its stream with the same org key and workspace header as the rest of the run surface:

RUN_ID=$(curl -s -X POST https://api.levainlabs.com/api/v1/agents/support-triage/runs \
  -H "Authorization: Bearer $LEVAIN_ORG_KEY" \
  -H "X-Workspace-Id: $CUSTOMER_WORKSPACE_ID" \
  -H "Content-Type: application/json" \
  -d '{"prompt": "Triage the tickets that arrived overnight."}' | jq -r .id)

curl -N "https://api.levainlabs.com/api/v1/runs/$RUN_ID/events/stream" \
  -H "Authorization: Bearer $LEVAIN_ORG_KEY" \
  -H "X-Workspace-Id: $CUSTOMER_WORKSPACE_ID"

Each SSE frame's data is one AG-UI event as JSON. Every connection is a complete AG-UI stream: it opens with RUN_STARTED (held until the run leaves pending), carries the run's timeline, and closes with RUN_FINISHED or RUN_ERROR, after which the stream ends. Connect any time — a run already underway replays its stored timeline first, so a late subscriber renders the same transcript as a live one, and a finished run replays and closes.

Event mapping

Stream eventFires forPayload
RUN_STARTEDRun left pendingthreadId (the session id), runId
TEXT_MESSAGE_STARTTEXT_MESSAGE_CONTENTTEXT_MESSAGE_ENDAn agent messagemessageId, role: "assistant"; the full text in one delta
TOOL_CALL_STARTTOOL_CALL_ARGSTOOL_CALL_ENDA tool invocationtoolCallId, toolCallName; arguments JSON in one delta
TOOL_CALL_RESULTA tool resulttoolCallId, content, role: "tool"
CUSTOMProgress eventsname (status_pulse, status_node, sandbox_event, custom), value with the event's payload
RUN_FINISHEDRun succeededthreadId, runId
RUN_ERRORRun failed or cancelledmessage, code (failed or cancelled)

Every event carries a timestamp (milliseconds since epoch). RUN_STARTED appears exactly once per connection and terminal events exactly once per run, so strict AG-UI clients validate the stream as-is. The input side of the conversation is yours already — user messages are not echoed back.

Runs of fleet agents stream the reduced timeline their session surface exposes: messages, lifecycle, and status text, without execution internals.

Resume after a dropped connection

Timeline frames carry an SSE id. On reconnect, send the standard Last-Event-ID header — the browser EventSource does this automatically — or pass ?since=<id> and the stream resumes after that event instead of replaying the whole run. Connections are also capped at one hour server-side; resume the same way. Comment frames (: keep-alive) tick every 15 seconds while the run is quiet, so idle proxies keep the connection open.

Render it

The stream works with anything that consumes AG-UI events — the @ag-ui/client toolchain and its framework integrations, or a few lines of EventSource:

const stream = new EventSource(
  `/your-backend/levain/runs/${runId}/stream`, // proxy that adds the org key
)

stream.onmessage = ({ data }) => {
  const event = JSON.parse(data)
  switch (event.type) {
    case "TEXT_MESSAGE_CONTENT":
      appendToTranscript(event.messageId, event.delta)
      break
    case "CUSTOM":
      if (event.name === "status_pulse") showStatus(event.value.text)
      break
    case "RUN_FINISHED":
    case "RUN_ERROR":
      finishRun(event)
      stream.close()
      break
  }
}

Keep the org key on your backend: terminate the customer's connection in your product and proxy the stream, exactly as you would any other org-authenticated call. Reconnection and Last-Event-ID pass through such a proxy unchanged.

Streaming covers the run in progress; for durable completion signals that survive nobody watching, pair it with webhooks.

On this page