@uiresponse/agui-client
v0.2.0
Published
The validating AG-UI client: consumes any AG-UI backend; renders UIR payloads with validation before paint; degrades honestly on foreign backends.
Maintainers
Readme
@uiresponse/agui-client
The validating AG-UI client. Connect it to any AG-UI agent and it renders the run: streamed text as prose, shared state as data — and when the backend is a UIR server, the full generated interface, schema-validated block by block before anything paints.
It is an ordinary AG-UI client in every way that matters: it consumes the standard event
vocabulary (RUN_*, TEXT_MESSAGE_*, TOOL_CALL_*, STATE_SNAPSHOT/STATE_DELTA with RFC 6902
patches, MESSAGES_SNAPSHOT, STEP_*) over the reference HTTP + SSE transport, and it tolerates
vocabulary it does not know. What no other client in the ecosystem does: when a backend carries
UIR's validated-UI payloads — namespaced uir.* custom events riding beside the standard state —
this client draws them as live, data-bound interface, with the validation verdicts the backend
streamed per block. No verdicts, no tiles. That is the whole contract.
Package name and status are pre-rename working names; not yet published. Wire shapes are coded against
@ag-ui/core 0.0.57, the same pin as the UIR server's translation module.
Install
npm install @uiresponse/agui-client # after first publish; in this repo it resolves workspace-locallyReact ≥18 is an optional peer — skip it entirely if you only use the headless core.
Quick start (React)
Point <AguiRun> at an AG-UI endpoint and give it an ask:
import { AguiRun, makeUirResolve } from "@uiresponse/agui-client";
import "@uiresponse/agui-client/styles.css";
import "@uiresponse/renderer-react/styles.css"; // dresses the UIR rendering, when a run earns it
export default function Answer() {
return (
<AguiRun
endpoint="/v1/agui"
ask="how is the next release tracking?"
resolve={makeUirResolve()} // UIR backends: datasets resolve by page id against /v1/resolve
/>
);
}Against an UIR backend (any uir server exposes /v1/agui) you get the product behavior over
the standard wire: prose first, then the interface assembling skeleton-first on a ~480ms reveal
beat, per-block validation ticks, narration valued from real rows, and a closing line that waits
for the content it closes. Rows never ride the AG-UI stream — blocks carry queries and bindings,
and resolve fetches rows out of band (that is makeUirResolve, or your own data plane).
Against a foreign backend — one that has never heard of UIR — the same component renders the
run honestly: streamed messages as prose, tool calls and steps as activity lines, the shared state
object as data. It never crashes on unknown vocabulary and never dresses unvalidated state up as
interface. If your agent's state happens to contain a page key — even a perfect imitation of an
UIR document, even alongside non-verdict uir.* events — you still get the plain state view: the
bar for drawing a tile is that block's own streamed verdict, nothing less.
<AguiRun> props
| Prop | Type | What it does |
| --- | --- | --- |
| endpoint | string | The AG-UI run endpoint (POST + SSE). Required. |
| ask | string | The user's ask. A new value starts a new run; the previous run aborts. |
| messages | array | Optional RunAgentInput-style history, sent in the request body. |
| threadId | string | Optional thread continuity, sent in the request body. |
| body, headers, fetchImpl | | Extra body fields, extra headers, injectable fetch. |
| resolve | ({page, dataset, params, signal}) => Promise<{rows, columns?}> | The one data surface for UIR runs. Without it, tiles state the omission — they never fake rows. |
| theme, params | | Passed through to the UIR renderer (brand tokens; url/session params). |
| onFinished | (state) => void | After the stream closed and every reveal actually landed. |
| onError | (error) => void | Transport/HTTP failures. Protocol RUN_ERRORs land in state instead. |
Quick start (headless — no React)
import { consumeAguiRun, uirView } from "@uiresponse/agui-client/core";
const state = await consumeAguiRun({
endpoint: "http://localhost:8787/v1/agui",
ask: "show me every artist and their pipeline stage as a table",
onEvent: (event, state) => console.log(event.type),
});
const view = uirView(state);
if (view.mode === "uir") {
// a validated UIR document: view.page, view.narration, view.validated
} else {
// prose in state.messages/state.timeline, tools in state.toolCalls, data in state.state
}consumeAguiRun resolves with the final run state; onEvent/onState fire per event, in order.
A stream that closes without RUN_FINISHED settles as a truncation error rather than hanging
running: true. Everything the wire said is kept: unknown custom events are counted, tool calls
and steps collected, and a JSON Patch this client cannot apply degrades to "last good state +
patchError" — the UIR adapter's convergence snapshots repair exactly this.
What "validating" means here
A UIR backend schema-checks every block at generation time, as it streams — and publishes the
verdicts on the wire as uir.block_verdict custom events, with repair rounds (uir.round,
uir.block_repaired) on the record. This client is verdict-aware, not a second validator: it
does not re-run schema checks in the browser; it renders exactly the claims the backend streamed,
and no claim it did not. Concretely:
- it assembles the interface from the standard AG-UI state events (snapshot + RFC 6902 deltas), so any generic client sees the same document;
- a tile draws only when its own block's verdict was streamed — per block, no defaults, no inheritance from the run looking UIR-ish;
- the page-level
validatedflag (the renderer's green tick) is stricter still: a UIR document, every page block covered by a verdict, every verdict ok, the run finished clean; - it reflects the backend's honesty about failure: a failed validation round arrives as a reset snapshot (AG-UI has no un-paint), and this client withdraws the blocks — and their verdicts — rather than leaving a stale answer or stale coverage on screen.
The missing-verdict cases, precisely
| Case | What happened | What this client does | Why |
| --- | --- | --- | --- |
| No verdicts at all | The state carries a UIR-shaped document but no uir.block_verdict ever arrived — a foreign imitation, or a middlebox stripped the Customs. Non-verdict uir.* events (ack, status) don't change this. | Plain data view. No tiles. | The verdict vocabulary is the opt-in; the verdict is the claim being rendered. Shape is not a claim. |
| Partial coverage | Some blocks have verdicts, later ones don't (truncated stream, translator gap). | Draws only the covered blocks; reports the rest as withheld (the React surface says so on screen); validated is false. | The covered blocks' claim is true of them regardless of the tail, and withdrawing a true answer punishes the user for a transport fault. But a partially-covered answer must not wear the whole-answer tick. |
| Ordering skew | A verdict arrives before its block's state delta (proxied wires may interleave). | Buffered by block id; covers the block when it lands. A verdict for a block that never arrives covers nothing. | Which frame landed first is the transport's business; "this block was validated" doesn't depend on it. |
| Repaint after a failed round | The un-paint snapshot withdraws the blocks; round 2 re-streams them. | The verdict ledger clears with the un-paint — a repainted block must earn a fresh verdict. | Round 1's paperwork must not cover round 2's content. |
Every row is a test in tests/ (cases labeled (a)/(b)/(c)), with the UIR fixtures produced
by the shipping server-side translator.
API
| Export | From | What it is |
| --- | --- | --- |
| AguiRun, makeUirResolve | . | The React surface. |
| consumeAguiRun | . or ./core | POST + SSE → reduced run state. |
| emptyAguiRun, reduceAguiEvent | . or ./core | The pure reducer, one AG-UI event at a time. |
| uirView | . or ./core | (state) => { mode: "uir"\|"plain", page, narration, validated } — what the run has earned. |
| revealTypeFor | . or ./core | Beat classification for the reveal pacer (presentation policy, not wire semantics). |
| applyJsonPatch | . or ./core | RFC 6902, all six ops, atomic. |
Testing
npm testThe suite runs two backends through the reducer: real UIR wire fixtures produced by the shipping
server-side translator (so a translator change that would break this client breaks the suite
first), and a synthetic foreign agent speaking the documented shapes this repo never emits —
token-delta text, streamed tool-call args, move/copy/test patches, MESSAGES_SNAPSHOT. The
transport tests deliver SSE in 3-byte chunks, because a TCP boundary owes the client nothing.
