Decision records
ADR 0069 — Agent-surface rename + A2UI egress adapter
- Status: Accepted (2026-07-22)
- Date: 2026-07-22
- Builds on: plugin packaging & 1P/3P separation (ADR 0043),
K10-R6 egress redaction (
@loom/classifymaskPaths). - Part of: the kernel/1P/3P triage discipline (P10 — bias outward). KERNEL_ABI unchanged (1.1.0); EVENT_SCHEMA unchanged — the adapter is a read-side projection + wire binding; it introduces no new kernel event type.
Context
The @loom/a2ui package was named for the Google A2UI (Agent-to-UI) protocol but did not implement
it. It was a homegrown, event-sourced data-model patch protocol (A2UI_PATCH events carrying
set/merge/append/remove/increment deltas addressed by dot-path). That is a fine, Loom-native
mechanism — but it diverges from A2UI on every structural axis: A2UI is a component-tree + data-model
protocol with a component catalog (JSON Schema), JSON-Pointer bindings, a message envelope
(createSurface/updateComponents/updateDataModel/deleteSurface), and a bidirectional RPC
surface (callFunction/actionResponse, two-way input binding). A2UI explicitly has no patch/delta
framework — updateDataModel replaces the value at a path.
Keeping the a2ui name on the homegrown protocol was a naming claim we didn’t honor, and it blocked
building a genuine adapter under that name.
Decision
Two changes:
-
Rename
@loom/a2ui→@loom/agent-surface(package, public symbols, and theSURFACE_PATCHevent type;defineConfigflaga2ui→agentSurface). This is Loom’s native surface mechanism and keeps its event-sourced, replayable design unchanged. -
Add
@loom/plugin-a2ui— a real, spec-aligned A2UI egress adapter, as a 1P plugin (composition-time contributor, kernel untouched). It projects a Loom agent surface onto the A2UI wire protocol v1.0 so any A2UI-compliant client can render a Loom-driven UI.
Scope: egress + inbound actions only
The adapter’s A2UISurfaceBridge (one instance per execution’s surface):
- emits
createSurface(component tree + catalog + initial data model),updateDataModel, anddeleteSurface; - translates Loom’s delta ops into A2UI’s absolute value-replacement — it folds the surface locally
(reusing
@loom/agent-surface’sapplySurfacePatches) and emits the concrete post-patch value at each touched JSON Pointer.remove→contents: null(A2UI upsert-to-null). This is the one real impedance point, and it is resolved here, in the adapter, not in the kernel. - applies K10-R6 egress redaction on the way out (internal fold keeps true values so
increment/append/mergestay correct); - ingests the client
actionmessage, normalized for the caller to turn into a Loom inbound event.
Ships one curated default catalog (Text/Column/Row/List/Badge/Button) plus defineCatalog
for BYO. A catalog is mandatory in A2UI (createSurface names a catalogId), so this is not optional.
Deferred (rejected for the MVP)
The A2UI bidirectional RPC surface — callFunction-with-response, actionResponse,
two-way-binding sync — and the rich catalog half (client functions, validation checks,
callableFrom). These require a synchronous request/response correlation layer that fights Loom’s
append-only, replayable event grain. They are edge-of-system concerns; promote only when a concrete
consumer needs them (the “second consumer” evidence gate).
Update:
callFunction/functionResponseand the catalogfunctionsvocabulary were promoted in ADR 0070 once that gate was met — reconciled with the replay grain by the client’s result re-entering as a recorded fact (not a sync RPC in the log), keyed by a deterministiccallId. Continuous two-way binding sync remains out of scope.
Consequences
- The adapter is transport-agnostic: it produces/consumes A2UI messages, so any binding (the app’s SSE broadcaster, WebSocket, A2A/MCP) can carry them; testable fully offline.
- A2UI is young (v1.0, Dec 2025). Housing it in a swappable, versioned plugin — not the kernel — keeps the churn blast radius at the edge.
- Loom keeps one internal UI protocol (
agent-surface); A2UI is purely an optional egress projection.
Rollout (addendum, 2026-07-22)
The decision above shipped in full and was rolled out across the ecosystem:
- Playground — the
apps/playgroundevent stream (“SSE broadcaster”) is now wired through anA2UISurfaceBridge; a live A2UI tab shows the emitted wire messages beside the native surface patches, and the Approve button routes throughingestAction(the inbound leg), demonstrating the full round-trip end-to-end. - Documentation — the living guides (
api-reference,user-guide,architecture,usage,data-classification, …) were updated: native-protocol references renamed toagent-surface(SURFACE_PATCH,SurfaceEmitter,agentSurfaceProjection, config flagagentSurface), and a new@loom/plugin-a2uiAPI section plus a “how to use” walkthrough were added. Historical records (ADRs, requirements, presentation decks) were intentionally left unchanged as point-in-time artifacts. - Downstream repos —
@loom/a2ui→@loom/agent-surfacewas migrated in the sibling repos that consumed it (genUI,aetherOS,loom-examples), each as its own PR (typecheck/tests green). Their own local naming (e.g. genUI’ssurface/a2ui.ts, thea2ui-dashboardexample folder) was kept — only the broken package/symbol references were migrated.
Source: docs/adr/0069-a2ui-egress-adapter.md in the loom repository.