Foundation
A2UI Architecture
Agent-to-UI is a declarative protocol for rendering agent output as structured UI without coupling agent logic to rendering concerns. Agents emit typed JSON. Renderers consume it.
The Core Idea
Agents should not know about React.
An agent reasoning about a clinical trial enrollment decision should produce a reasoning tree — not JSX. A2UI is the contract between what an agent knows and what a user sees. The agent declares intent. The renderer handles presentation.
01
Declarative
Specs describe what to show, not how to render it. Agent logic stays pure — no UI imports, no React lifecycle, no renderer coupling.
02
Typed Contract
Every spec type has a TypeScript interface. Discriminated unions let renderers narrow types at compile time — zero runtime casting.
03
Multi-Renderer
The same spec renders in React (design system), Lit (web components), mobile, and terminal. Agents emit once, every surface consumes.
Protocol Flow
Agent Logic
Python / TypeScript / any runtime
JSON Spec
nodus:reasoning-tree { … }
A2UISurface
renderSpec() dispatch
Renderer
React / Lit / Mobile / CLI
User
Actionable interface
Dispatch Architecture
renderSpec() is a single switch statement over the discriminated union. TypeScript narrows each branch to the exact concrete type — enabling IDE autocomplete, exhaustiveness checks, and zero-cast renderer implementations.
renderSpec() — the full dispatch layer
// Every renderer starts with a discriminated union switch.
// TypeScript narrows the type at each case — zero casting needed.
function renderSpec(spec: AnySpec): React.ReactNode {
switch (spec.type) {
case "nodus:mit-card": return <RenderMitCard spec={spec} />;
case "nodus:reasoning-tree": return <RenderReasoningTree spec={spec} />;
case "nodus:learning-ledger":return <RenderLearningLedger spec={spec} />;
// … 16 more cases
default:
console.warn(`[A2UI] Unknown type: ${spec.type}`);
return null;
}
}Authoring From an Agent
Agents emit plain JSON objects matching the spec interface. There are no React imports, no renderer awareness, no framework coupling. A Python agent and a TypeScript agent use identical output shapes.
Agent output — pure JSON, no UI concern
// Agents emit JSON — no UI code, no React, no imports.
// The renderer picks up on the other side.
const spec = {
type: "nodus:reasoning-tree",
id: "patient-eligibility-check",
question: "Should patient MR-8823 enroll in CRYO-3?",
conclusion: "Borderline — requires specialist review.",
confidence: 71,
steps: [
{ label: "Age within inclusion band", weight: "supporting", confidence: 99 },
{ label: "eGFR below minimum (48 < 60)", weight: "opposing", confidence: 100 },
{ label: "PI waiver clause may apply", weight: "neutral", confidence: 62 },
],
};
// Pass to any renderer — React, Lit, mobile, terminal
await agent.emit(spec);Spec Type Registry — 19 Types
Types are grouped by agent concern domain. Each type maps to one renderer. The union is open — new types extend the switch without touching existing renderers.
nodus:mit-cardMost Important Task — focus anchor with progress, time block, and mental model.
nodus:approval-cardConfidence-gated human approval gate with reasoning chain and consequence disclosure.
nodus:quick-winsOpportunistic task list scoped to available time window.
nodus:action-itemSingle atomic task with leverage signal, draft status, and context.
nodus:progress-barGeneric progress indicator with semantic color mapping.
nodus:morning-briefingComposite briefing composing MIT + quick-wins + next meeting into one surface.
nodus:meeting-prepPre-meeting intelligence: stakeholder profiles, agenda predictions, open loops, risks.
nodus:meeting-summaryPost-meeting record: action items, decisions, effectiveness score, sentiment.
nodus:text-fieldControlled input field with validation, prefix/suffix, and helper text.
nodus:selectOption selector with groups, placeholder, and multi-select.
nodus:checkboxBoolean field, switch variant, indeterminate state.
nodus:formComposite form grouping fields with layout and submit actions.
nodus:agent-timelineLive step-by-step agent execution trace with tool calls and durations.
nodus:context-windowToken budget visualization broken down by context source type.
nodus:cost-dashboardToken and cost accounting by model with trend signals and budget limits.
nodus:reasoning-treeStructured argument tree — supporting, opposing, and neutral evidence steps with confidence scores.
nodus:provenance-chainSource attribution for a claim — documents, databases, APIs, agents, humans.
nodus:escalation-bannerSLA breach or policy violation surface requiring human attention.
nodus:learning-ledgerAgent learning log — rules derived from corrections, feedback, guardrails, and observations.
Extending the Protocol
Adding a new spec type is a three-step operation: define the interface, add the type guard, wire the renderer. The pattern is identical every time — new types never require modifying existing renderers.
Extension pattern — same 3 steps every time
// Adding a new spec type takes 3 steps:
// 1. Interface in types.ts
interface StatusPanelSpec extends A2UIComponent {
type: "nodus:status-panel";
services: Array<{ name: string; status: "up" | "degraded" | "down" }>;
}
// 2. Type guard
export const isStatusPanel = (c: A2UIComponent): c is StatusPanelSpec =>
c.type === "nodus:status-panel";
// 3. Renderer + dispatch case
function RenderStatusPanel({ spec }: { spec: StatusPanelSpec }) { ... }
case "nodus:status-panel": return <RenderStatusPanel spec={spec} />;Step 1
Interface
packages/nodus-design-system/src/a2ui/types.tsDefine the TypeScript interface extending A2UIComponent. Add to the AllNodusComponents union. Add the type guard predicate.
Step 2
Renderer
packages/nodus-design-system/src/a2ui/components.tsxImplement RenderMySpec({ spec }) using CSS custom properties only. No Tailwind, no external component dependencies unless already in the bundle.
Step 3
Dispatch
packages/nodus-design-system/src/a2ui/A2UISurface.tsxAdd case "nodus:my-spec": return <RenderMySpec key={spec.id} spec={spec} />; to the renderOne() switch. TypeScript will enforce exhaustiveness.
Design Decisions
Why not just use tool_use / function calling output directly?
Tool call results are implementation artifacts — raw, untyped, and tightly coupled to the agent's internal structure. A2UI specs are presentation contracts: typed, versioned, and designed to be consumed by multiple renderers across time. The agent emits intent, not implementation.
Why discriminated unions instead of a registry or plugin system?
Discriminated unions are statically verifiable. TypeScript's exhaustiveness checking catches missing dispatch cases at compile time. A runtime registry adds indirection without adding safety. The switch is the registry — it's visible, traceable, and auditable.
Why CSS custom properties instead of Tailwind in renderers?
Renderers must work in both the Next.js showcase app and the distributable @nodus/design-system package. Tailwind class names are purged at build time per app. CSS custom properties from the token layer survive bundling, tree-shaking, and cross-app distribution unchanged.
Why duplicate types in the showcase file instead of importing from the package?
The showcase file (apps/nodus-design-system/components/patterns/A2UISurface.tsx) predates the distributable package. Local type stubs let the showcase evolve and prototype new types without a package rebuild loop. Production types live in packages/nodus-design-system/src/a2ui/types.ts.
Why 'nodus:' namespace prefix on all type strings?
The nodus: prefix reserves the A2UI type namespace and prevents collisions with third-party spec systems. It also makes type strings self-documenting in logs, network traces, and agent outputs — you know immediately which renderer handles them.
Why no server-side rendering concerns in the spec types?
Specs are pure data — serializable JSON with no DOM, no event handlers, no lifecycle hooks. SSR is entirely the renderer's concern. An agent running in a Python backend emits the same spec shape as one running in a browser extension.
File Map