# AG-UI Relay

Bring an AG-UI agent into your Rails app: run its endpoint, fold the event stream into a transcript, relay every change as a versioned Turbo Stream, let the page's components answer the agent's tool calls, and pause on interrupts - the full host recipe.

AG-UI is the event protocol between an agent backend and a user-facing application: a run streams `RUN_STARTED`, text and reasoning deltas, tool calls with their arguments and results, shared state (`STATE_SNAPSHOT` / `STATE_DELTA`, RFC 6902), activities, and `RUN_FINISHED` - which may carry an interrupt the user answers before the next run. poetry-agent is the Rails CLIENT of that protocol (`Poetry::Agent::AGUI`): `Client.new(url:, headers:).run(input) { |event| ... }` runs an agent endpoint and yields its events; `Transcript.new(client_tools: [...])` folds them into chat-shaped messages whose `parts` are `{ kind: :text | :reasoning | :tool | :activity }` (tool parts carry `:loading`, `:awaiting_client`, `:done`, `:error`), plus `state`, `interrupts`, `pending_client_tools`, and `messages_for_input` for the next run; `Relay.new(transcript:, render:, container:, append_render:, morph: true)` turns every change into a Turbo Stream - an append into your MessageScroller on a message's first appearance, a versioned morphing replace (`vreplace`, `data-version`) after, so an out-of-order SSE delivery never paints an older frame over a newer one.

The host recipe: an `ActionController::Live` action sets `Content-Type: text/event-stream`, builds `RunInput.build(thread_id:, messages: [RunInput.user_message(text)], tools:)`, runs the client, and writes `TurboStream.sse(html)` for each stream `relay.apply(event)` returns; the page holds `poetry_message_scroller` plus a `<turbo-stream-source src=...>` pointing at that action; a row partial renders `message.parts`. Frontend tools: a rendered component's declared WebMCP tools become the agent's tools through `Poetry::Agent::AGUI.tool_descriptor("sections", definition)` (names like `poetry.sections.set_value`); when the agent calls one, `relay.client_tool_streams(continue_url:)` appends a bridge element and the `poetry--agent--agui-client-tool` controller executes the call through the registrar (no WebMCP browser needed) and POSTs `{ toolCallId, name, content, error }` to your continue endpoint, which calls `transcript.resolve_client_tool(id, content)` and starts the next run from `transcript.messages_for_input`. Interrupts: `transcript.interrupted?` and `transcript.interrupts` (id, reason, message) render a decision form; the next run carries `resume: [RunInput.resume_entry(id, status: "approved")]`. A2UI surfaces arrive inside the stream as `ACTIVITY_SNAPSHOT` events with `activityType: "a2ui-surface"`: `Poetry::Agent::A2UI::Session.new.apply_activity(part[:content])` folds them and the renderer draws them as forms whose action feeds the next run as `forwarded_props:` (see the A2UI Surfaces guide). Script the agent during development (the AG-UI Relay demo's `AguiReplay`) so every stream replays byte-identical; `relay.mark_seen(ids)` tells the relay which rows the page already rendered. Setup: `gem "poetry-agent"` and `registerPoetryAgent(application)` beside `registerPoetryControllers` - the same runtime installs the versioned replace action and the morph guard.
