Skip to content

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.

Planning
nodus:mit-card

Most Important Task — focus anchor with progress, time block, and mental model.

Decision
nodus:approval-card

Confidence-gated human approval gate with reasoning chain and consequence disclosure.

Planning
nodus:quick-wins

Opportunistic task list scoped to available time window.

Planning
nodus:action-item

Single atomic task with leverage signal, draft status, and context.

Feedback
nodus:progress-bar

Generic progress indicator with semantic color mapping.

Daily
nodus:morning-briefing

Composite briefing composing MIT + quick-wins + next meeting into one surface.

Meeting
nodus:meeting-prep

Pre-meeting intelligence: stakeholder profiles, agenda predictions, open loops, risks.

Meeting
nodus:meeting-summary

Post-meeting record: action items, decisions, effectiveness score, sentiment.

Form
nodus:text-field

Controlled input field with validation, prefix/suffix, and helper text.

Form
nodus:select

Option selector with groups, placeholder, and multi-select.

Form
nodus:checkbox

Boolean field, switch variant, indeterminate state.

Form
nodus:form

Composite form grouping fields with layout and submit actions.

Observability
nodus:agent-timeline

Live step-by-step agent execution trace with tool calls and durations.

Observability
nodus:context-window

Token budget visualization broken down by context source type.

Observability
nodus:cost-dashboard

Token and cost accounting by model with trend signals and budget limits.

Epistemics
nodus:reasoning-tree

Structured argument tree — supporting, opposing, and neutral evidence steps with confidence scores.

Epistemics
nodus:provenance-chain

Source attribution for a claim — documents, databases, APIs, agents, humans.

Governance
nodus:escalation-banner

SLA breach or policy violation surface requiring human attention.

Governance
nodus:learning-ledger

Agent learning log — rules derived from corrections, feedback, guardrails, and observations.

Sovereignty
nodus:authority-grant

What an agent is permitted to do — scope, permitted and denied actions, caps with usage, expiry, and revocation.

Sovereignty
nodus:reversal-window

The undo horizon after an agent acts — reversible-until countdown, audit linkage, and the reverse control.

Sovereignty
nodus:action-ledger

Interruptible 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.ts

Define 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.tsx

Implement 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.tsx

Add 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

packages/nodus-design-system/src/a2ui/types.ts

Canonical type interfaces + type guards. Source of truth for the distributable package.

Types
packages/nodus-design-system/src/a2ui/components.tsx

Production React renderers. One function per spec type.

Renderers
packages/nodus-design-system/src/a2ui/A2UISurface.tsx

Public API. Accepts spec | spec[], dispatches to renderers via renderOne().

Surface
packages/nodus-design-system/src/a2ui.ts

Barrel export for the ./a2ui sub-package entry point.

Export
apps/nodus-design-system/components/patterns/A2UISurface.tsx

Showcase component with local type stubs, all renderers, and SCENARIOS demo data.

Showcase
apps/nodus-a2ui-lit/src/

Lit web component renderer implementing the same spec contract.

Alt Renderer