Agora
Generative UI

Generative UI telemetry

How often the model's UI calls are right the first time, what it gets wrong, what the UI costs and how long the user waits — recorded per UI call as metadata, shown on the dashboard's Generative UI page.

Every call of a UI tool — ui__render, ui__sandbox, ui__show_*, ui__show — is recorded by the agent loop as one row of telemetry: which tool, on which channel and in which mode, whether the model got it right the first time, what went wrong when it did not, what the call cost, and how long the user waited for the first and for the complete frame. The dashboard's Generative UI page reads it.

It is on by default with genui({ … }) and a store that records it (the Lucid store and the in-memory store both do). Nothing to migrate: the table is the library's own, created by the store's schema heal at boot like the others.

What is recorded — metadata only

Privacy default

A row never holds the generated HTML/JSX or the props. Only names, counts, timings, an error's category and its message with every quoted value replaced by "…" (a schema's received 'Jane' is stored as received '…'). Health and finance apps can leave it on.

FieldWhat it is
toolName, mode, channelui__sandbox, sandbox (or tree, per-component, show), web
modelId, runId, threadId, actorRef, agentName, stepwho and where
attempt1 + the UI calls refused since the turn's last accepted one — a refused ui__render followed by a ui__sandbox is a retry
outcome, validityok/error; first_try (accepted on attempt 1), retried (accepted after a refusal), failed (refused)
errorCategory, errorMessageshape, syntax, missing-app, bridge, unknown-component, unavailable, other; the scrubbed message (≤ 300 chars)
inputTokens, outputTokens, costUsd, costSourcethe call's share of the model step that wrote it — the step's usage and cost (from the agent_token_usage ledger's figures: provider-reported, or estimated from the price list) split by the characters each tool call's arguments and the step's text took
firstFrameMs, completeFrameMsturn start → the call's first UI frame on the stream (a streaming preview counts), and → its final frame (accepted calls only)
payloadBytesthe size of the call's arguments
payloadonly with samplePayloads (below)

The Lucid table is agent_genui_call (id = the tool call's id; indexes on created_at and run_id; its rows go with their thread, ON DELETE CASCADE). A call recorded again — a durable step replayed — replaces its row.

Options

// config/agent.ts
genui: genui({
  catalog,
  sandbox: { kit: true },
  componentTools: ['Sandbox'],
  // Default: metadata only. For debugging a model's UI, also keep each call's input:
  telemetry: { samplePayloads: true },
  // Or record nothing:
  // telemetry: false,
}),

samplePayloads stores what the model wrote

With samplePayloads: true the generated code and props — whatever data the view shows — are stored in payload, and error messages are kept verbatim. Turn it on for a debugging session, never where a view may show personal data.

The dashboard page

GET <governance>/genui?from=YYYY-MM-DD&to=YYYY-MM-DD (gated like every governance route; 501 when the configured governance queries do not implement genuiStats) returns:

  • totals — UI calls, turns, first attempts, first-try successes and rate, retried, failed, tokens, cost;
  • byToolModel — first-try success per tool and model;
  • errors — the most common (category, tool, message), with counts and when last seen;
  • byDay — UI calls, turns, tokens, cost and first-try rate per UTC day;
  • costPerTurn — average, p50 and p95 UI cost of a turn;
  • latency — p50/p95 per turn from its start to its first UI frame, and to the first complete frame of an accepted call.

The page shows them as KPI tiles, the tool × model table, the errors table and the daily cost chart.

On the OpenCode engine

OpenCode runs its own loop and calls the tools through the MCP endpoint (its code mode included), so the endpoint records the UI calls it serves, with attempts counted per run (GenuiRunRecorder, exported for an engine of your own). Two fields stay empty there: the model's step is not split per call (inputTokens/outputTokens are 0, no cost — the step's usage is in the token ledger as usual), and the frames are not timed (no latency).

Your own store

A store records telemetry by implementing the optional recordGenuiCalls(rows), and its governance queries serve the page with the optional genuiStats(range). Both are small: summarizeGenuiCalls(rows) (exported) turns the rows of a range into the stats, so a store only selects them.

See also

The same telemetry, table and page ship in @dudousxd/nestjs-agent (AgentGenuiModule.forRoot({ telemetry })).

On this page

Partner

Your product as the official integration for Agent, right here.

Become a partner →