Skip to content

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.

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.

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