Compare Boundaries, Not Brand Names
Agent UI discussions often put A2UI, AG-UI, and an AI framework runtime in one feature table and choose a winner. That framing hides the engineering decision. A declarative UI contract, an event protocol, and a framework-owned renderer can operate at different boundaries:
| Boundary | Question it answers | It does not provide |
|---|---|---|
| UI contract | What constrained UI description can be rendered? | Identity, authorization, business truth |
| Event protocol | How do events, state, and lifecycle updates move? | Safe rendering or effect permission |
| Framework runtime | How does this product render and coordinate UI code? | Interoperability with every client or service |
| Action service | May this subject perform this effect on this object now? | A model-selected policy |
The names, message types, transports, SDKs, and support matrices associated with these ecosystems change. Pin a specification or package revision and test it in your own deployment before treating a capability as available.
{
"ui_contract": "pinned-spec-or-schema-commit",
"event_profile": "pinned-transport-profile",
"framework_runtime": "package-and-revision",
"renderer_catalog": "product-catalog-revision",
"supported_clients": ["tested-client-and-version"],
"action_api": "server-contract-revision",
"checked_at": "recorded-time"
}
This record is more useful than a market ranking: it tells an operator what must be retested when a dependency changes.
Start with the User Task
An Agent surface is justified when it makes a user task easier to understand or complete. It is not justified simply because a model can describe components.
| User task | Useful interface | Unsafe shortcut |
|---|---|---|
| Compare returned options | Read-only cards with source and freshness | Treating generated rank or price as authoritative |
| Refine a search | Fixed filters with allowed values | Letting the model choose arbitrary query fields |
| Review a draft | Product-owned editor and explicit save | Writing to a record from a chat response |
| Request an external effect | Summary, disclosure, and confirmation | Making a button execute a model-selected action |
Use plain text for explanation. Use a fixed product flow for stable, high-impact workflows. Use a generated contract only when its constrained degrees of freedom improve the experience.
Three Implementation Styles
Declarative UI Contract
An A2UI-style approach sends a description of components and data rather than executable browser or native code. Its value comes from a product-owned renderer that maps a small, reviewed catalog to local components.
It can support an interchange boundary between independently owned Agent services and clients. It does not make a remote Agent trusted: unknown components, rich text, URLs, data references, and actions still need strict admission controls.
Event and State Protocol
An AG-UI-style approach focuses on delivery: progress events, text deltas, lifecycle transitions, user input, state updates, reconnects, and cancellation. It can carry a UI contract, text, or application-defined payload. It does not define which UI is safe to render or which tool call a receiver may execute.
Treat order, duplication, reconnect behavior, backpressure, replay, and event retention as explicit parts of the deployment contract. A streaming transport is not durable workflow state.
Framework-Owned Runtime
A React or other framework runtime can keep rendering code, server data access, and client interaction in one product codebase. That may be the simplest choice when clients and deployment are under one ownership boundary.
It also creates framework and runtime coupling. Do not expose arbitrary component generation or server functions merely because the model can select a tool. Keep the model's output constrained to product-defined intents, and keep credentials, object lookup, and execution behind application services.
Use a Decision Matrix
Choose based on evidence about your system, not a permanent “best” label:
| Question | Prefer a UI contract when | Prefer an event protocol when | Prefer a product runtime when |
|---|---|---|---|
| Independent clients/services | You need a shared, versioned payload | You need lifecycle interoperability | One product owns all clients |
| Delivery behavior | Payload can be fetched or updated simply | Streaming, resume, and interruption matter | Framework transport meets measured needs |
| Rendering | Multiple renderers must consume one catalog | Rendering remains application-defined | One reviewed component system is sufficient |
| Upgrade ownership | Schema compatibility is governed explicitly | Event compatibility is governed explicitly | One team can upgrade runtime and clients together |
| Security boundary | Payload is untrusted and strictly validated | Events are untrusted and session-bound | Server actions remain product-owned and authorized |
It is valid to use a product runtime plus a small declarative catalog. It is also valid to use an event stream without a declarative UI. Avoid adding every layer unless it reduces a real ownership or compatibility problem.
The Shared Security Model
No approach turns model output into authority. A generated component tree, event name, JSON Schema validation result, prompt instruction, or tool annotation cannot grant permission.
The receiver must:
- authenticate the caller and bind the surface to the current session;
- validate contract revision, byte size, component count, nesting, update rate, and allowed properties;
- escape text and constrain images, rich content, URLs, callbacks, and data references;
- resolve data server-side using tenant and object checks;
- map an action only to a reviewed application intent;
- reauthorize the exact action and current object state at execution;
- bind confirmation and idempotency to subject, tenant, action, and normalized parameters;
- record redacted policy and lifecycle evidence.
from dataclasses import dataclass
@dataclass(frozen=True)
class UiIntent:
name: str
action_token: str
confirmation_id: str | None
def accept_intent(session, intent, tokens, inventory):
if intent.name not in {"select_offer", "request_confirmation"}:
return {"status": "rejected", "reason": "unknown_intent"}
offer = tokens.resolve(intent.action_token, subject_id=session.subject_id)
if offer is None or not inventory.is_available(offer.id):
return {"status": "rejected", "reason": "stale_or_unavailable"}
return {"status": "accepted", "offer_id": offer.public_id}
This is an application policy fragment, not an implementation of any protocol or SDK. A consequential write needs a separate, idempotent endpoint that rechecks the user, object, parameters, confirmation, and business invariants immediately before the effect.
Transport and Rendering Need Failure Semantics
An attractive demo often omits the cases users notice most: a duplicate click, a stale screen, a reconnection after an effect, or a renderer upgrade halfway through a session.
Define the following before rollout:
| Concern | Contract to document |
|---|---|
| Event delivery | Ordering, deduplication identity, reconnect cursor, timeout, backpressure |
| Surface lifecycle | Create, replace, expiry, cleanup, unsupported-client fallback |
| Data freshness | Source, observation time, refresh behavior, stale display policy |
| Actions | Confirmation, cancellation, idempotency, outcome-unknown response |
| Compatibility | Schema/event version negotiation, migration fixtures, rollback |
| Privacy | Redaction, retention, deletion propagation, tenant isolation |
A completed render is not proof of a completed side effect. A completed event stream is not proof that all clients observed the same state.
Accessibility and Trustworthy Presentation
Generated UI is still product UI. The catalog must specify semantic roles, labels, keyboard navigation, focus after incremental updates, contrast, zoom, reduced motion, localization, pluralization, and right-to-left layout.
Protect users from UI-level prompt injection and phishing:
- distinguish Agent-generated content from product-owned navigation and security controls;
- show provenance and freshness for externally sourced claims;
- reserve credential, payment, and permission controls for fixed components;
- open external destinations only through a reviewed policy;
- require explicit, comprehensible confirmation for destructive or externally visible actions.
These controls matter regardless of whether the payload arrived through a declarative contract, an event protocol, or a framework runtime.
Test the Choice
Adoption should end with a test plan, not an architecture diagram:
| Test | Failure to demonstrate |
|---|---|
| Contract fuzzing | Unknown property, cyclic tree, oversized payload, unsafe URL |
| Event replay | Duplicate event, gap after reconnect, out-of-order update |
| Authorization | Cross-tenant token, stale object, model-supplied identifier |
| Effect recovery | Timeout after execution, duplicate confirmation, cancellation race |
| Accessibility | Keyboard-only path, screen-reader update, long localized text |
| Upgrade | Older client, incompatible catalog, renderer rollback |
| Privacy | Sensitive payload in telemetry, deletion from cached surfaces |
Measure user completion, clarification requests, rejection rate, recovery time, and accessibility failures on representative tasks. Do not use a generic vendor comparison as proof that an architecture fits your users.
A2UI, AG-UI, and Framework Runtimes Can Coexist
A UI contract can travel through an event protocol. A product runtime can render a strict catalog. A remote Agent may propose a surface while a host application owns the renderer and action service. These are composition choices, not a prediction that one ecosystem will replace the others.
For the underlying UI contract boundary, see A2UI: Building Safe Agent-Driven User Interface Contracts. For service-to-service delegation, see A2A Protocol: Inter-Agent Trust and Task Boundaries. For tool and resource boundaries, see MCP Protocol: Production Boundaries for Tools and Resources.
FAQ
Can an event stream carry a declarative UI payload?
Yes, if the deployment defines the message envelope, schema revision, size limits, session binding, validation behavior, and client compatibility. The receiver must still treat the payload as untrusted and render only its approved catalog.
Is a declarative contract safer than a framework runtime?
They address different risks. A strict declarative catalog can reduce arbitrary-code execution in a payload boundary. A product-owned runtime can safely render reviewed components. Both need server-side authorization, data filtering, URL controls, observability, and recovery behavior.
Should a team standardize on one approach now?
Standardize the contracts your own product can govern: identity propagation, action authorization, audit events, accessibility, payload limits, and upgrade tests. Adopt an external protocol or runtime only after pinning its revision and proving compatibility with the clients you support.
What should be logged?
Log a redacted contract revision, surface and action identifiers, policy decision, lifecycle class, latency, and correlation ID. Do not default to storing raw prompts, UI payloads, private data, or hidden model reasoning.