Foundation
A2UI Architecture
A2UI — the Nodus A2UI Profile — is a declarative agent-to-UI protocol for rendering agent output as structured UI without coupling agent logic to rendering concerns. Agents emit typed JSON. Renderers consume it.
Naming note: Google's A2UI (a2ui.org) is an open protocol in the same agent-to-UI space, with different payload semantics — a flat component list rendered from a client-side trusted catalog. The Nodus dialect documented on this page — reasoning-tree payloads rendered by A2UISurface via nodus:* spec types — is referred to as the Nodus A2UI Profile to disambiguate the two. This is descriptive naming only; no compatibility or affiliation is implied.
Generative UI
The profile's live counterpart: two frontend render actions — renderNodusComponent (one catalog component) and renderA2UILayout (a composite of nodus:* specs) — let an agent render real design system components inline in chat. Both ship from the package as @nodus/design-system/generative-ui via the useNodusGenerativeUI hook.
01
Render Actions
The model never emits JSX. It calls a named action with a component name (or spec list) — the client resolves it against the lazy render registry and renders the real, versioned component.
02
Data Props Only
The model supplies data props as a JSON string, parsed exactly once and validated against the registry prop schema. Functions and unknown shapes are dropped with visible warnings — validation never throws, a render never crashes the chat.
03
Provenance Chrome
Every generated render is wrapped in ProvenanceFrame: a labeled boundary marking output as ILLUSTRATIVE agent-generated content, streaming state while the call is in flight, and any validation warnings. Generated UI is never visually mistaken for live data.
Living demo: /a2ui/generate — the template generator renders catalog components live from chat, provenance-framed.
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 — 22 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.
nodus:authority-grantWhat an agent is permitted to do — scope, permitted and denied actions, caps with usage, expiry, and revocation.
nodus:reversal-windowThe undo horizon after an agent acts — reversible-until countdown, audit linkage, and the reverse control.
nodus:action-ledgerInterruptible running log of agent actions with pause, stop, and hand-back controls.
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