A2UI

Activity, over time

Daily observations from the public event archive.

Attention

Daily stars & forks

Loading attention chart

Star accumulation

Cumulative stars

Loading star accumulation chart

Pull requests

Daily opens & merges

Loading pull requests chart

Conversations

Daily issues & comments

Loading conversations chart

Community

Daily accounts & reviews

Loading community chart

Releases

Daily releases

Loading releases chart

Mentions

3posts · 30d
Nov 30 posts
  1. AG-UI & A2UI: Building Agent-Powered Interfaces Beyond Chat How event-driven agent interactions and generative UI are changing the way we build AI-powered frontend applications. 1. Why Are Chat Interfaces No Longer Enough? Most AI applications start with a familiar interface: a text input, a message history, and a streaming response. This works well for question answering, summarization, and conversational experiences. However, agent-based applications introduce a different set of interaction requirements. Consider an AI assistant that analyzes a user's investment portfolio, investigates market movements, or prepares a technical report. A conventional chat interface might return a response containing: - A summary of the analysis - A list of recommendations - Several data points - A suggested next action But what if the user needs more than a textual response? What if the application needs to display: - Interactive charts - Dynamic forms - Data tables - Approval workflows - Progress indicators - Context-aware actions - Multi-step interfaces that evolve as the agent works We could build every possible interface in advance and ask the model to select the appropriate components. This approach works for predictable workflows, but it becomes restrictive when the interaction itself is dynamic. Another option is to allow the model to generate HTML, React components, or JavaScript directly. However, executing arbitrary model-generated code introduces significant security, maintainability, and governance challenges. This is where two interesting technologies enter the picture: AG-UI (Agent-User Interaction Protocol) and A2UI (Agent-to-User Interface). Although their names are similar, they address different problems. AG-UI standardizes how agents communicate with user-facing applications. A2UI provides a declarative format for describing interfaces that client applications can safely render using their own component libraries. Together, they provide an interesting foundation for building agent-powered applications beyond traditional chat interfaces. References: AG-UI Documentation · Google A2UI 2. What Is AG-UI? AG-UI stands for Agent-User Interaction Protocol. It is an open, lightweight, event-based protocol designed to standardize communication between AI agents and frontend applications. The underlying problem is straightforward. An agent does not always produce one response and finish. It may execute multiple tools, delegate work, update its state, request additional input, and continue processing. A traditional request-response model does not necessarily represent these interactions well. For example, imagine an agent performing a database analysis: 1. The user requests an analysis. 2. The agent starts executing the task. 3. It calls a database tool. 4. The query completes. 5. The agent processes the returned data. 6. It generates a chart. 7. It asks the user to confirm an additional filter. 8. It completes the analysis. If the frontend only receives the final response, the user has no visibility into what happened during execution. We could implement custom WebSocket messages, HTTP streaming events, and application-specific state synchronization. But every application would need to define and maintain its own communication contract. AG-UI provides a standardized event model for this interaction. It allows agent backends to communicate execution events, messages, tool calls, state changes, and activity updates to the frontend. The protocol is intentionally not tied to one agent framework or frontend technology. You can build the backend using LangGraph, Google ADK, CrewAI, or a custom runtime, while the frontend uses React or another supported client implementation. The important point is that AG-UI does not define how an agent reasons or executes its internal logic. It defines how that execution is communicated to the user-facing application. Reference: AG-UI — Introduction 3. How Does AG-UI Work? AG-UI follows an event-driven architecture. Instead of treating an agent interaction as a single response, the backend emits a sequence of typed events that the frontend consumes. A simplified architecture looks like this: User | v React Application | v AG-UI Client | Event Stream / SSE | v Agent Runtime | +--------+--------+ | | | v v v LLM Tools Subagents | | | +--------+--------+ | v Agent Events | v AG-UI Client | v Update UI The frontend and backend have separate responsibilities. Backend responsibilities: - Execute agent logic. - Handle model interactions. - Execute tools. - Maintain execution state. - Emit standardized events. - Handle additional user input and resume interactions. Frontend responsibilities: - Initiate agent runs. - Consume event streams. - Render messages and activity updates. - Synchronize application state. - Present tool activity. - Handle user interactions and approvals. AG-UI is transport-agnostic. Common integration patterns include Server-Sent Events (SSE), WebSockets, and webhooks. The official HttpAgent client supports HTTP SSE and an HTTP binary transport. This separation means we do not have to make frontend components understand the implementation details of the backend agent framework. Reference: AG-UI — Core Architecture 4. Understanding AG-UI Events Events are the foundation of AG-UI. Rather than sending an unstructured text response, an agent can communicate different types of events depending on what is happening during execution. The main event categories include: 4.1. Lifecycle Events These events describe the execution lifecycle. | Event | Purpose | |---|---| | RUN_STARTED | An agent run has started | | RUN_FINISHED | An agent run has finished | | RUN_ERROR | An execution error occurred | | STEP_STARTED | A new execution step started | | STEP_FINISHED | An execution step finished | This allows the frontend to display meaningful execution states instead of relying on a generic loading indicator. 4.2. Text Message Events These events represent streaming messages. | Event | Purpose | |---|---| | TEXT_MESSAGE_START | Starts a message | | TEXT_MESSAGE_CONTENT | Sends a text chunk | | TEXT_MESSAGE_END | Completes a message | The frontend can append each text chunk as it arrives. This provides a familiar streaming experience while keeping the underlying event structure explicit. 4.3. Tool Call Events Tool execution is an important part of agent workflows. AG-UI defines events such as: - TOOL_CALL_START - TOOL_CALL_ARGS - TOOL_CALL_END - TOOL_CALL_RESULT These events allow the frontend to display which tool is being used and follow its execution lifecycle. For example, an interface might display: Analyzing repository... | v GitLab: Fetching merge request | v Code Review: Analyzing changes | v Security Check: Completed | v Generating report One distinction is important: receiving a tool-call event does not automatically mean the frontend should execute that tool. Tool execution and authorization remain responsibilities of the appropriate runtime or application layer. 4.4. State Management Events AG-UI also defines events for synchronizing application and agent state. | Event | Purpose | |---|---| | STATE_SNAPSHOT | Sends a complete state snapshot | | STATE_DELTA | Sends incremental state changes | | MESSAGES_SNAPSHOT | Synchronizes conversation history | For example, an agent might update the current analysis status: { "type": "STATE_SNAPSHOT", "snapshot": { "status": "analyzing", "completedSteps": 3, "totalSteps": 5, "currentTask": "Evaluating transaction data" } } The frontend can consume this state and update progress indicators, dashboards, or other components. 4.5. Activity and Subagent Events Modern agent systems often involve multiple agents working together. AG-UI includes activity and subagent-related events, allowing a frontend to represent more complex execution patterns. For example: - A research agent starts working. - A data analysis agent receives a delegated task. - The security agent completes its review. - The coordinator produces the final result. This makes it possible to build interfaces that expose useful execution activity without requiring users to understand the internal implementation of each agent. The current AG-UI v1.0 schema documents event families for lifecycle, text, tools, state, activity, subagents, reasoning, and custom events. A critical implementation consideration: reasoning-related events should not be treated as permission to expose raw model chain-of-thought. User-facing applications should expose approved progress summaries and operational status instead. Reference: AG-UI — Event Types and Specification 5. How to Integrate AG-UI with a React Application AG-UI provides a TypeScript SDK that can be used to connect frontend applications to compatible agent endpoints. The official package is @ag-ui/client. Step 1: Install the package pnpm add @ag-ui/client @ag-ui/core Step 2: Create an HTTP agent The HttpAgent class provides a client for communicating with an AG-UI-compatible backend. import { HttpAgent } from '@ag-ui/client'; const agent = new HttpAgent({ url: '/api/agent', agentId: 'portfolio-assistant', threadId: crypto.randomUUID(), }); The configuration defines the backend endpoint and identifies the agent and conversation thread. Step 3: Send a message and consume events We can use the agent's addMessage and runAgent methods to start an interaction. import { HttpAgent } from '@ag-ui/client'; import { EventType } from '@ag-ui/core'; const agent = new HttpAgent({ url: '/api/agent', agentId: 'portfolio-assistant', threadId: crypto.randomUUID(), }); agent.addMessage({ id: crypto.randomUUID(), role: 'user', content: 'Analyze my portfolio and summarize its performance.', }); await agent.runAgent( { tools: [], context: [], }, { onEvent: ({ event }) => { switch (event.type) { case EventType.RUN_STARTED: console.log('Agent execution started'); break; case EventType.TEXT_MESSAGE_CONTENT: console.log(event.delta); break; case EventType.TOOL_CALL_START: console.log('Tool started:', event.toolCallName); break; case EventType.RUN_FINISHED: console.log('Agent execution completed'); break; case EventType.RUN_ERROR: console.error('Agent execution failed'); break; } }, }, ); The example demonstrates the client-side event handling pattern. In a complete application, event handlers would update React state, render tool activity, and maintain conversation state. The agent SDK also supports shared state, event subscriptions, middleware, and cancellation through abortRun(). Step 4: Synchronize agent state with React One of the most useful capabilities is the ability to keep frontend and agent state synchronized. For example, an agent can update task progress while the frontend renders the current status. With a framework integration such as CopilotKit, this can be implemented through hooks such as useAgent. Conceptually: import { useAgent } from '@copilotkit/react-core/v2'; function TaskProgress() { const { agent } = useAgent(); const progress = agent.state.progress ?? { completed: 0, total: 0, }; return ( <section> <h3>Agent Progress</h3> <p> {progress.completed} / {progress.total} completed </p> </section> ); } The component reacts to changes in the agent's shared state. The important architectural detail is that AG-UI defines the communication contract, while libraries such as CopilotKit provide additional frontend integration and application-level abstractions. We are not required to use CopilotKit to implement AG-UI. A custom frontend can consume the protocol directly. References: AG-UI TypeScript SDK · CopilotKit Shared State --- 6. What Is A2UI? AG-UI solves the communication problem, but there is another question: What if an agent needs to generate an actual user interface instead of returning text? A2UI stands for Agent-to-User Interface. It is an open-source project initiated by Google that provides a declarative protocol for generating and updating user interfaces from structured JSON messages. The central idea is simple. Instead of asking an LLM to generate HTML, JavaScript, or React code, we let it describe what the interface should contain. For example, an agent may want to display: - A title - A summary card - A table - A date picker - A confirmation button It can describe these elements using A2UI messages. The frontend then maps those descriptions to its own approved components. This creates a separation between UI generation and UI execution. The agent describes the interface. The application decides how that interface is rendered. Why is this important? Imagine allowing an LLM to generate the following: function Dashboard() { // Arbitrary model-generated code } Executing model-generated React code directly would create substantial security and maintenance problems. Instead, we can let the model return structured data: { "component": "Card", "children": [ { "component": "Text", "text": "Portfolio Summary" } ] } The frontend maps Card and Text to components that already exist in its design system. The model never needs to execute arbitrary JavaScript to create the interface. This is the main architectural idea behind A2UI. Reference: Google A2UI — Official Documentation 7. How Does A2UI Work? A2UI uses a JSON-based streaming protocol. The agent sends messages to the frontend, and the frontend processes those messages to create or update UI surfaces. A2UI v0.9.1 defines four primary message types. 7.1. createSurface Creates a new UI surface. A surface represents a UI region that contains components and associated data. { "version": "v0.9.1", "createSurface": { "surfaceId": "portfolio-dashboard", "catalogId": "https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json" } } 7.2. updateComponents Defines or updates the components within a surface. The components are represented as a flat list with references that define their relationships. For example: { "version": "v0.9.1", "updateComponents": { "surfaceId": "portfolio-dashboard", "components": [ { "id": "root", "component": "Column", "children": [ "title", "summary" ] }, { "id": "title", "component": "Text", "text": "Portfolio Overview", "variant": "h1" }, { "id": "summary", "component": "Text", "text": "Your portfolio analysis is ready." } ] } } The renderer understands the component definitions and constructs the corresponding UI tree. 7.3. updateDataModel Updates the data displayed by the components. A2UI separates component structure from application data. This is particularly useful because changing data does not necessarily require sending the entire component tree again. For example: { "version": "v0.9.1", "updateDataModel": { "surfaceId": "portfolio-dashboard", "path": "/portfolio", "value": { "totalValue": 125000, "currency": "USD", "change": 4.2 } } } Components can reference values in the data model using JSON Pointer paths. When the data model changes, the renderer can update the relevant parts of the interface. 7.4. deleteSurface Removes a previously created surface and its associated components and data. { "version": "v0.9.1", "deleteSurface": { "surfaceId": "portfolio-dashboard" } } This is useful for managing dynamic UI regions as an interaction evolves. The complete flow User Request | v Agent Runtime | v LLM Generates A2UI Messages | v JSON Stream | v A2UI Message Processor | v Component Catalog | v React / Flutter / Angular Renderer | v Interactive User Interface The process can happen incrementally. The agent can create a surface first, add components later, and update the data model as more information becomes available. This is different from generating an entire interface in one response. References: A2UI Protocol v0.9.1 · A2UI Quickstart 8. Integrating A2UI with React A2UI provides an official React renderer and a shared web core library. The relevant packages are: - @a2ui/react - @a2ui/web_core Step 1: Install the packages pnpm add @a2ui/react @a2ui/web_core Step 2: Create a MessageProcessor The MessageProcessor handles protocol messages, surface lifecycle, component definitions, and data models. For the v0.9 implementation: import { useState } from 'react'; import { MessageProcessor, } from '@a2ui/web_core/v0_9'; import { A2uiSurface, basicCatalog, } from '@a2ui/react/v0_9'; The processor is initialized with the component catalog that the application allows the agent to use. const processor = new MessageProcessor([ basicCatalog, ]); Step 3: Process agent-generated messages For example, we can create a surface and populate it with components: processor.processMessages([ { version: 'v0.9', createSurface: { surfaceId: 'main', catalogId: basicCatalog.id, }, }, { version: 'v0.9', updateComponents: { surfaceId: 'main', components: [ { id: 'root', component: 'Column', children: ['heading', 'description'], }, { id: 'heading', component: 'Text', text: 'Hello from A2UI', variant: 'h1', }, { id: 'description', component: 'Text', text: 'This interface was described by an agent.', }, ], }, }, ]); The MessageProcessor maintains the surface and its component model. Step 4: Render the surface Once the surface has been created, the React renderer can display it. function DynamicUI() { const [processor] = useState(() => { const instance = new MessageProcessor([ basicCatalog, ]); instance.processMessages([ { version: 'v0.9', createSurface: { surfaceId: 'main', catalogId: basicCatalog.id, }, }, { version: 'v0.9', updateComponents: { surfaceId: 'main', components: [ { id: 'root', component: 'Column', children: ['title'], }, { id: 'title', component: 'Text', text: 'Generated Interface', variant: 'h1', }, ], }, }, ]); return instance; }); const surface = processor.model.surfacesMap.get('main'); if (!surface) { return <p>Preparing interface...</p>; } return <A2uiSurface surface={surface} />; } This is a minimal rendering example. A production implementation should create and manage the processor at the appropriate application or session scope, rather than reconstructing it for each update. As new A2UI messages arrive, the application can pass them to the same processor. The surface model drives the React renderer's updates. Step 5: Handle user interactions An interactive UI needs a return path. For example, the agent may generate a form containing a confirmation button. When the user clicks that button, the frontend captures the associated action and sends it back to the agent through the selected transport. The flow becomes: Agent Generates UI | v A2UI Renderer | v User Interacts | v Frontend Captures Action | v Application / Agent Endpoint | v Agent Processes Input | v New A2UI Messages | v Updated Interface A2UI v0.9.1 uses an external communication channel for these return interactions. Its v1.0 candidate specification introduces additional structured function-call and response mechanisms. This distinction matters when designing the frontend-backend contract. References: A2UI React Renderer · A2UI Client Setup 9. What Is the Difference Between AG-UI and A2UI? This is probably the most important distinction in the article. AG-UI and A2UI are complementary, but they are not interchangeable. | Area | AG-UI | A2UI | |---|---|---| | Primary purpose | Agent-frontend communication | Declarative UI generation | | Main abstraction | Typed events | UI surfaces and components | | Data flow | Run events and state updates | UI structure and data updates | | Main responsibility | Interaction lifecycle | Interface representation | | Frontend role | Consume and handle events | Render approved components | | User interaction | Agent runs, tools, state and interrupts | UI actions and data context | Consider an agent that needs to create a dynamic approval dashboard. AG-UI can communicate: - The agent has started processing. - The data retrieval step is complete. - A tool has returned results. - The agent has updated its state. - A user approval is required. A2UI can describe: - An approval summary card. - A table of requested changes. - A confirmation button. - A detailed information panel. The frontend can render both the interaction lifecycle and the generated interface. Can we use them together? Yes. A possible architecture looks like this: User | v React Frontend | v AG-UI Event Stream | v Agent Runtime | v LLM | +-----+-----+ | | v v AG-UI Events A2UI JSON | | +-----+-----+ | v Frontend Host | v A2UI Renderer | v Native React UI AG-UI manages the interaction boundary. A2UI describes the interface that should be rendered. One protocol does not replace the other. A2UI can also be transported over A2A, MCP-related integrations, SSE, or WebSockets. It is designed to remain independent of a particular transport. References: AG-UI Architecture · A2UI Transport Decoupling 10. How Do They Fit Into the Existing AI Ecosystem? We have already discussed MCP, ACP, and A2A in previous articles. Adding AG-UI and A2UI gives us another important architectural layer. | Technology | Responsibility | |---|---| | MCP | Agent ↔ Tools and external data | | ACP | Coding Agent ↔ Client Application | | A2A | Agent ↔ Agent | | AG-UI | Agent ↔ Frontend interaction | | A2UI | Agent → Declarative UI | A typical enterprise AI platform could use several of these technologies simultaneously. For example: 1. An application communicates with an agent using AG-UI. 2. The agent accesses internal services through MCP. 3. The agent delegates a specialized task to another agent using A2A. 4. The agent returns dynamic interface definitions using A2UI. 5. The application renders those definitions through its own component system. ACP can be used independently when we need to integrate coding agents with a client application. This is not about adopting every protocol. It is about keeping architectural boundaries clear. 11. How Do These Approaches Compare With Traditional Generative UI? AG-UI and A2UI are not the only ways to build agent-powered interfaces. There are several approaches worth understanding. AI SDK Tool-Based UI Vercel AI SDK already allows applications to render custom UI based on tool calls and structured message parts. For example, a model can call a weather tool, and the frontend can render a weather card instead of displaying only raw text. This is a good approach when: - The application has a known set of components. - UI behavior is closely tied to model tool calls. - The application owns the entire interaction contract. However, it does not automatically provide the same cross-framework UI specification and agent interaction model as AG-UI and A2UI. MCP Apps MCP Apps extend MCP tools with interactive UI experiences. A tool can expose a ui:// resource containing its interface, which an MCP host can render in a sandboxed iframe. This is particularly useful when a tool needs to provide its own interactive experience, such as a dashboard, form, or specialized workflow. It solves a different problem from A2UI: the UI is associated with a tool rather than being a general declarative UI surface generated by an agent. A2UI A2UI focuses on agent-generated interface descriptions and framework-specific rendering. The same declarative description can be consumed by compatible clients with different native component implementations. AG-UI AG-UI focuses on the interaction protocol rather than the visual representation. It can be used with ordinary chat interfaces, custom React components, or declarative generative UI approaches. | Approach | Best suited for | |---|---| | AI SDK Tool UI | Application-defined component rendering | | MCP Apps | Interactive UIs provided by individual tools | | A2UI | Agent-generated, declarative interfaces | | AG-UI | Standardized agent-frontend communication | These approaches are not mutually exclusive. References: AI SDK — useChat · Vercel — MCP Apps Integration · Google A2UI 12. Advantages and Trade-offs Introducing these technologies can improve architecture and developer experience, but they also introduce new responsibilities. AG-UI Advantages Standardized event communication Teams can consume a consistent event model instead of maintaining separate custom streaming contracts. Frontend-backend separation The frontend does not need to understand the internal implementation of each agent framework. Better visibility into execution Tool calls, progress updates, and agent state can be represented in the UI. Framework interoperability Different backend agent implementations can expose a common interaction contract. Support for human-in-the-loop workflows Applications can build approval, clarification, and user-steering experiences around agent execution. AG-UI Trade-offs - Existing agent frameworks may require adapters or event translation. - Teams must manage protocol versions and event compatibility. - Application state and agent state can become inconsistent if ownership is unclear. - Authentication, authorization, and event validation remain application responsibilities. - For simple chat applications, a custom endpoint or ordinary AI SDK integration may be sufficient. A2UI Advantages Declarative and safer UI generation Agents send structured data rather than executable React or JavaScript code. Framework independence The same UI description can be mapped to different client-side component systems. Reuse of existing design systems Applications can expose their own approved component catalog. Incremental rendering The UI structure and data can be updated without resending everything. Separation of structure and data Components can remain stable while the agent updates the underlying data model. A2UI Trade-offs - A component catalog must be designed and maintained. - The renderer must support the required component types. - Complex application-specific interactions require additional integration. - Schema and protocol version compatibility require attention. - Debugging streamed component updates can be more difficult than debugging ordinary React components. - Cross-platform rendering does not guarantee identical visual behavior on every platform. An important point: declarative JSON is safer than arbitrary executable code, but it is not automatically trustworthy. The application must still validate messages, restrict available components, and authorize actions. 13. Security and Production Considerations This is where the difference between a successful demo and a maintainable production implementation becomes important. 1. Never execute arbitrary generated code An agent should not be able to send arbitrary JavaScript, HTML, or React code and have the application execute it. A2UI's declarative component catalog provides a safer approach. The application should only render components it explicitly supports. 2. Validate incoming messages AG-UI events and A2UI messages must be treated as untrusted input. Validate event structures, protocol versions, component definitions, data paths, and payload sizes before processing them. 3. Control available components Not every component should be available to an agent. For example, an internal banking application might allow: - Summary cards - Data tables - Charts - Read-only forms - Approval request components However, a sensitive transaction operation should not become available merely because the agent can generate a button. The backend must authorize the actual business operation. 4. Keep application state under application control There should be a clear distinction between UI state, agent state, and authoritative business state. An agent-generated UI should not be considered a source of truth for financial transactions, permissions, or other business-critical information. 5. Require human approval for sensitive actions A dynamically generated interface can make it easier for an agent to request user confirmation. However, the approval must be verified on the backend. Hiding or disabling a frontend button is not an authorization mechanism. 6. Handle interrupted streams and reconnection Streaming applications need predictable recovery behavior. Consider: - Reconnecting after network failures - Handling duplicate events - Recovering from incomplete UI updates - Preserving task and surface identifiers - Handling cancellation - Expiring stale sessions AG-UI and A2UI define communication structures, but the application still needs an operational strategy for stream recovery and state consistency. 7. Audit agent-generated interactions For enterprise applications, it should be possible to understand: - Which agent generated a UI. - Which components were requested. - Which actions the user performed. - Which backend operations were executed. - Whether an approval was obtained. - What the final result was. This is particularly important when AI interfaces are used to interact with internal business systems. 14. What Can an AI Chapter Lead Contribute? I believe this is where these technologies become particularly valuable. The opportunity is not simply introducing generative UI into an application. It is establishing a reusable frontend-agent architecture that product teams can build on. 1. Establish a Shared Agent-Frontend Integration Layer Instead of every team implementing its own event handling and agent-state synchronization, we can define a shared integration layer based on AG-UI. It can provide: - Event subscription and processing - Streaming message handling - Tool execution indicators - State synchronization - Cancellation and error handling - Human approval workflows This reduces duplicated infrastructure across applications. 2. Create a Governed Component Catalog A2UI introduces an interesting opportunity to connect agent capabilities with an organization's existing design system. For example, we can define an approved component catalog: | Component | Purpose | |---|---| | SummaryCard | Display structured summaries | | DataTable | Render tabular information | | LineChart | Display time-series data | | ApprovalPanel | Present approval requests | | DateRangePicker | Collect date filters | | StatusIndicator | Display execution status | The agent can request these components, while the frontend maps them to existing React components. This means teams can introduce dynamic interfaces without giving the model unrestricted access to the frontend codebase. For organizations with established design systems, this could be particularly valuable. 3. Define Reusable Frontend Patterns We can establish standard patterns for: - Agent progress indicators - Interactive forms - Approval interfaces - Dynamic dashboards - Multi-step workflows - Tool execution views - Subagent activity panels These patterns can become shared components and examples that product teams reuse. 4. Standardize the Security Model A shared approach should define which components agents can request, which actions require approval, and how the backend validates every operation. This is especially important for applications interacting with sensitive business systems. 5. Build a Reference Implementation A small reference application could demonstrate the complete interaction: 1. A user asks an agent to analyze a dataset. 2. The agent starts execution and sends AG-UI events. 3. The frontend displays progress and activity. 4. The agent generates an A2UI dashboard. 5. The React renderer maps the description to approved components. 6. The user modifies a filter or submits an action. 7. The frontend sends the interaction back to the agent. 8. The agent updates the interface with new results. This would help developers understand how the protocols work together in a real application. 6. Measure the Developer and User Experience We should not evaluate generative UI only by how quickly an agent can produce a visual result. Useful metrics include: - Time to interactive UI - Agent-to-frontend event latency - UI schema validation failure rate - Unsupported component requests - Rendering errors - User interaction completion rate - Human approval rate - Frontend integration effort These metrics can help determine whether generative UI actually improves the application experience. 15. What Is the Current State of A2UI and AG-UI? Both projects are evolving, so protocol versioning matters. As of November 1, 2026: AG-UI AG-UI publishes a versioned v1.0 specification and provides TypeScript client libraries, event schemas, and integrations with different agent frameworks. Its ecosystem includes integrations with projects such as LangGraph, CrewAI, Google ADK, and other agent runtimes. A2UI A2UI currently identifies v0.9.1 as its production protocol release. The v1.0 specification is a candidate release, not the production version. The project also provides an official React renderer, shared web core, Lit renderer, Angular renderer, and Flutter-based support. There is an important migration consideration: the v1.0 candidate introduces changes to the protocol model and should not be assumed to be wire-compatible with v0.9.1. For production applications, I would start with the stable v0.9.1 protocol and its matching renderer, and evaluate v1.0 separately before planning a migration. References: A2UI Repository and Status · A2UI Roadmap · AG-UI Specification Conclusion AG-UI and A2UI address two important challenges in building modern agent-powered applications. AG-UI standardizes the interaction between agents and frontend applications through a structured, event-driven communication model. A2UI allows agents to describe dynamic interfaces using declarative JSON messages, while applications remain in control of the actual components and rendering process. Together, they make it possible to move beyond traditional chat interfaces and build applications where AI agents can communicate their progress, update application state, and generate context-aware user interfaces. For engineering teams, the opportunity is to establish reusable interaction patterns, govern agent-accessible components, maintain security boundaries, and integrate generative UI into existing design systems. My recommendation: Start with a small React application that uses AG-UI for streaming and state synchronization, then introduce A2UI for one clearly defined dynamic interface. Evaluate rendering reliability, security, and user experience before expanding the approach. The goal should not be to make every screen dynamic. It should be to make the interface adapt when a conversation or workflow genuinely requires it. --- References and Further Reading AG-UI 1. AG-UI Official Documentation — Introduction, architecture, events, and integration. 2. AG-UI GitHub Repository — Protocol implementation and SDKs. 3. AG-UI Architecture — Event-driven architecture and client-server communication. 4. AG-UI Event Reference — Lifecycle, messages, tools, and state events. 5. AG-UI TypeScript SDK — Client implementation and event handling. A2UI 6. Google A2UI Official Documentation — Project overview and developer resources. 7. A2UI Protocol v0.9.1 — JSON protocol, surfaces, components, and data models. 8. A2UI Quickstart — Full-stack example and generated UI workflow. 9. A2UI React Renderer — React rendering implementation. 10. A2UI Client Setup — Framework integration and component catalogs. 11. A2UI Renderer Development — Building custom renderers and integrating design systems. 12. A2UI v1.0 Candidate Specification — Upcoming protocol capabilities and changes. Related Technologies 13. Vercel AI SDK — useChat — Conversational streaming and frontend state. 14. Vercel — MCP Apps Integration — Interactive tool UIs in AI SDK applications. 15. CopilotKit — Shared State — Bidirectional state synchronization between React and agents.

  2. Why bloc.add(...) Isn't What You Think: Synchronous Event Fast-Paths and 0ms Form Bindings --- series: "BlocSignal Architecture & Practice" title: "Why bloc.add(...) Isn't What You Think: Synchronous Event Fast-Paths and 0ms Form Bindings" description: "Why classic BLoC's StreamController forces an asynchronous microtask tax, how BlocSignal executes synchronous event handlers inline on the exact same call stack, and why this is the only way to build 120 FPS Generative UI form bindings." tags: flutter, dart, architecture, statemanagement --- The Muscle-Memory Illusion > "An asynchronous network stream belongs at the perimeter of your architecture; once an AI agent's UI lands on the client, every keystroke, validation rule, and leaf rebuild must execute in 0ms on a synchronous signal graph." > — Randal L. Schwartz When Flutter developers inspected the code samples from our bloc_signals_genui 1.0 announcement, their classic package:bloc muscle memory sparked an immediate question: // Mutate bound form fields synchronously in 0ms surfaceBloc.add( const UpdateFormField( path: '/user/name', value: 'Randal L. Schwartz', ), ); The question went something like this: Wait. It is called a Bloc and you are calling .add(event). Doesn't that push the event into an asynchronous StreamController sink, schedule a microtask on the Dart event loop, sever the call stack, and update the UI in a later frame? In BlocSignal, .add() dispatches events synchronously on the active call stack. When an event handler and its transformer execute synchronously, the entire dispatch-to-emission pipeline runs inline on the exact same call stack in 0ms. By the time the line immediately following surfaceBloc.add(...) executes: 1. The underlying a2ui_core DataModel signal cell is updated. 2. Every GenericBinder signal binding and CheckRule validation expression on the surface has recomputed. 3. surfaceBloc.value holds the new SurfaceReady immutable state snapshot. Understanding this difference requires examining the mechanics of event propagation in Dart and the reasons client applications need a synchronous core. - The Asynchronous Microtask Tax in Classic BLoC: Classic package:bloc routes events through Dart StreamController pipes. Even when an event handler does zero asynchronous work, Dart's stream architecture defers execution across an asynchronous microtask boundary, delaying state propagation by at least one event loop turn, severing call stacks, and inducing input lag during rapid keystrokes. - The FutureOr Fast-Path in BlocSignal: By inspecting handler return types dynamically (final result = onEvent(event); if (result is Future) ...), BlocSignal executes synchronous event handlers immediately inline on the caller's call stack with zero microtask allocation while maintaining full async support for network tasks. - Why Generative UI Demands a Synchronous Core: Google's official package:a2ui_core uses preact_signals for its DataModel and two-way form bindings. Pairing an asynchronous network perimeter (restartable() SSE streaming) with a synchronous event core (UpdateFormField) enables 120 FPS form interactions without frame tearing or flaky test waits. --- Anatomy of the Microtask Tax: Why Classic bloc.add() Was Always Asynchronous To see why developers assume .add() must be asynchronous, look at how classic package:bloc routes events under the hood: flowchart TD UI["Widget Tap / Keystroke\nbloc.add(Event)"] Sink["_eventController.sink.add(event)\n(StreamController.broadcast)"] Loop["Dart Event Loop\n(Scheduled Microtask Hop)"] Transformer["EventTransformer / StreamSubscription\n(stream.asyncExpand)"] Handler["on<Event>((event, emit) {...})\n(Handler Invocation)"] State["_stateController.add(newState)\n(Second Microtask Hop)"] UIRebuild["StreamBuilder / BlocBuilder Rebuild\n(Later Frame)"] UI --> Sink Sink -->|"Microtask Hop #1"| Loop Loop --> Transformer Transformer --> Handler Handler --> State State -->|"Microtask Hop #2"| UIRebuild In package:bloc, every Bloc manages an internal StreamController<Event>. Calling add(event) pushes the event into this stream sink. In Dart's dart:async library, adding an event to a StreamController defers listener execution by scheduling a microtask on the Dart event loop. The Dart runtime continues executing whatever synchronous code remains in the current call stack, and processes stream events only after that synchronous block yields control. Even if your event handler contains zero await calls and returns immediately: // Classic package:bloc on<UpdateFormField>((event, emit) { emit(state.copyWith(name: event.value)); }); The execution flow still incurs two asynchronous microtask hops: one to deliver the event to the handler, and a second to deliver the newly emitted state through the state stream to widget builders. This architecture introduces three concrete penalties in production applications: 1. Stale State on the Next Line Because the microtask has not fired yet, reading bloc.state on the line immediately following bloc.add(...) returns the old state: // Classic package:bloc bloc.add(const UpdateFormField(path: '/user/name', value: 'Randal')); // Stale! The microtask is still waiting in the event loop queue: print(bloc.state.name); // Prints previous value ('Alice'), retaining the stale state Developers working around this behavior must introduce asynchronous delays (await Future<void>.delayed(Duration.zero)) or await pump() in tests just to observe the effect of a synchronous assignment. 2. Severed Call Stacks in Debugging When an exception occurs inside a classic BLoC event handler, or when a developer pauses on a breakpoint, the debug stack trace reveals only the stream infrastructure: #0 MyBloc._onUpdateFormField (my_bloc.dart:42) #1 _MapStream._handleData (dart:async/stream_pipe.dart:214) #2 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:341) #3 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:271) #4 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:774) #5 _StreamController._add (dart:async/stream_controller.dart:648) #6 _StreamController.add (dart:async/stream_controller.dart:596) #7 _microtaskLoop (dart:async/schedule_microtask.dart:40) #8 _startMicrotaskLoop (dart:async/schedule_microtask.dart:49) The original call site—the TextField.onChanged callback, the button tap, or the form submit handler—has been pruned from memory by the microtask queue. The causal link between user gesture and state transition is lost. 3. Frame Tearing and Input Lag in Interactive Forms Modern mobile and desktop displays refresh at 60 Hz (16.6ms per frame) or 120 Hz (8.3ms per frame). When a user types rapidly on a software keyboard, the operating system dispatches input events in rapid succession. If every keystroke schedules a microtask hop through a stream controller, the framework delays processing until after the current frame's layout phase has begun. In complex generative forms with cross-field validation rules, this microtask latency creates a race condition where the native text input field and the bloc's emitted state fall out of phase across frames. The result is visual cursor jitter, character transposition, and dropped inputs. --- Inside the Synchronous Event Fast-Path BlocSignal retains the classic BLoC public API (add, on<E>, emit, state) while eliminating StreamController and microtask queues from event routing. Here is the exact dispatch mechanism in BlocSignalMixin: void add(Event event) { if (isClosed) return; final currentObserver = BlocSignalObserver.observer; if (currentObserver != null) { try { currentObserver.onEvent(this, event); } on Exception catch (e, stackTrace) { onError(e, stackTrace); } } runZoned( () { try { final result = onEvent(event); if (result is Future) { unawaited(_handleAsyncResult(result, event)); } else { _notifyEventCompleted(event); } } on Object catch (e, stackTrace) { onError(e, stackTrace); _notifyEventCompleted(event); if (e is Error) rethrow; } }, zoneValues: { zoneEventKey: event, zoneBlocKey: this, }, ); } Notice the decisive branching check on line 17: final result = onEvent(event); if (result is Future) { unawaited(_handleAsyncResult(result, event)); } else { _notifyEventCompleted(event); } In BlocSignal, on<E> registers handlers that return Dart's FutureOr<void>. When a handler contains synchronous logic, it returns plain void (null). The result is Future check evaluates to false. The engine executes the handler inline on the caller's stack frame, notifies completion, and finishes synchronously. Zero microtask scheduling occurs. Here is the corresponding path through onEvent and the registered handlers: @mustCallSuper FutureOr<void> onEvent(Event event) { final matched = _handlers.where((h) => h.isType(event)); if (_handlers.isNotEmpty && matched.isEmpty) { throw StateError( 'add(${event.runtimeType}) was called without a registered event handler.', ); } List<Future<dynamic>>? futures; for (final registry in matched) { final result = registry.handler(event, emit) as dynamic; if (result is Future) { (futures ??= []).add(result); } } if (futures != null) { return Future.wait(futures).then<void>((_) {}); } } If none of the matched handlers return a Future, futures remains null. The method returns null synchronously. sequenceDiagram autonumber participant UI as Flutter Widget / Caller participant Bloc as BlocSignal.add(event) participant Mixin as onEvent / Handler participant Signal as Signal<State> (_state.value) participant Observer as GenericBinder / Subscriber UI->>Bloc: add(UpdateFormField(...)) activate Bloc Bloc->>Mixin: onEvent(event) activate Mixin Mixin->>Mixin: handler(event, emit) Mixin->>Signal: _state.value = newState activate Signal Signal->>Observer: Synchronous Push/Pull Invalidation deactivate Signal Mixin-->>Bloc: return null (Synchronous) deactivate Mixin Bloc-->>UI: return void (Exact Same Call Stack) deactivate Bloc Note over UI: Next line executes immediately in 0ms! The Async Wrapper Trap (Repository Scar) Maintaining this synchronous fast-path requires strict discipline in concurrency transformers. In an early development phase of BlocSignal, higher-order event transformer wrappers were written using async syntax for convenience: // THE PATHOGEN: Marking transformer closures async unconditionally EventTransformer<E, S> faultyTransformer<E, S>() { return (event, handler, emit) async { // ... await handler(event, emit); // ... }; } In Dart, marking a function async forces the runtime to allocate a Future and schedule execution across a microtask turn upon the first await, even if the wrapped handler is completely synchronous. In concurrency policies like droppable() or restartable(), this microtask gap delayed clearing reentrancy flags (isProcessing = false), causing back-to-back synchronous events emitted in the exact same frame to be dropped. The permanent repository invariant codified in our architecture skill specifies: > The Synchronous Fast-Path Rule: In event transformers, never mark outer or inner forwarding closures async. Inspect the handler's return type dynamically: final result = handler(event, emit); if (result is Future) { return result.whenComplete(...); }. Execute synchronous completions immediately inline so reentrancy flags clear in the exact same call stack. Intact Call Stacks Because synchronous handlers execute inline, setting a breakpoint inside an event handler preserves the entire execution context: #0 A2uiSurfaceBloc._onUpdateFormField (a2ui_surface_bloc.dart:630) #1 BlocSignalMixin.onEvent (bloc_signal_mixin.dart:257) #2 BlocSignalMixin.add.<anonymous closure> (bloc_signal_mixin.dart:99) #3 _rootRun (dart:async/zone.dart:1399) #4 _CustomZone.run (dart:async/zone.dart:1301) #5 runZoned (dart:async/zone.dart:1805) #6 BlocSignalMixin.add (bloc_signal_mixin.dart:96) #7 A2uiTextField.onChanged (a2ui_text_field.dart:118) #8 EditableTextState._formatAndSetValue (editable_text.dart:2890) The stack trace connects the widget's onChanged event directly to the state machine's internal handler. --- Why Generative UI Demands a Synchronous Core This synchronous fast-path is essential when building Generative UI with Google's official A2UI (Agent-to-User Interface) protocol. In A2UI, an LLM streams a declarative UI description over the network using Server-Sent Events (SSE). The JSON payload contains surface definitions, component hierarchies, dynamic data models, and client-side validation rules (CheckRule). Under the hood, Google's official package:a2ui_core uses preact_signals for its reactive primitives: - DataModel stores state in reactive signal nodes addressed by JSON Pointer paths (such as /user/name or /order/total). - GenericBinder provides two-way WritableBinding connections between UI inputs and the data model. - CheckRule evaluates client-side validation logic (for example regular expressions, required field checks, or numeric ranges) synchronously as signal expressions. If you wrap a2ui_core in a traditional stream-based state manager, you sever this reactive signal graph. Every form field change must leave the signal graph, enter a stream sink, pass through a microtask queue, re-emerge into a stream handler, mutate the signal, and schedule another microtask to notify widget builders. In bloc_signals_genui, A2uiSurfaceBloc handles form updates with zero microtask latency: void _onUpdateFormField( UpdateFormField event, void Function(A2uiSurfaceState) emit, ) { final surfaceId = event.surfaceId ?? _activeSurfaceId; if (surfaceId == null) return; final surface = _processor.model.getSurface(surfaceId); if (surface == null) return; // Synchronous data model mutation with signal batching batch(() { surface.dataModel.set(event.path, event.value); }); _touchSurface(surfaceId); _bumpSurfaceVersion(surfaceId); _emitSurfaceSnapshot(emit); } When a user edits an input field in an A2UI surface: 1. The widget dispatches surfaceBloc.add(UpdateFormField(path: '/email', value: input)). 2. _onUpdateFormField executes inline on the exact same thread. 3. batch(() { surface.dataModel.set(...); }) updates the underlying a2ui_core signal graph atomically. 4. CheckRule expressions bound to /email re-evaluate synchronously. If the input is valid, dependent signal properties update in the exact same frame. 5. _emitSurfaceSnapshot(emit) calls emit(SurfaceReady(...)), which writes directly to _state.value. 6. Any submit button whose enabled state depends on CheckRule.isValid reflects the new state on the very next render frame with zero microtask delays. --- Proving It in Code: The Zero-Await Assertion and 1-Pump Widget Test The most direct way to observe the synchronous event fast-path is through unit and widget tests. In classic BLoC, testing an event dispatch requires asynchronous coordination: // Classic package:bloc test: requires async stream waiting test('classic bloc requires stream awaiting', () async { final bloc = MyClassicBloc(); bloc.add(const Increment()); // Fails synchronously! bloc.state is still initial: // expect(bloc.state, equals(1)); // Must await the stream or pump microtasks: await expectLater(bloc.stream, emits(1)); }); In BlocSignal, because the event executes inline, testing is strictly synchronous: // BlocSignal test: 100% synchronous assertions test('supports on<E> registration and handles events synchronously', () { final bloc = RegistryBloc(); expect(bloc.value, equals(0)); bloc.add(Increment()); // Immediately true on the very next line of execution: expect(bloc.value, equals(1)); bloc.add(Decrement()); expect(bloc.value, equals(0)); }); Single-Pump Widget Tests This predictability extends directly to Flutter widget testing. When testing complex A2UI generative surfaces with dynamic validation rules, testing with bloc_signals_genui_flutter requires exactly one pump: testWidgets( 'enables submit button synchronously when form fields pass CheckRule validation', (tester) async { final bloc = A2uiSurfaceBloc()..add(ProcessJsonMessage(signupFormSurfaceJson)); await tester.pumpWidget(MaterialApp(home: A2uiSurfaceView(bloc: bloc))); // Initially, the submit button is disabled by its CheckRule: final initialBtn = tester.widget<ElevatedButton>(find.byType(ElevatedButton)); expect(initialBtn.onPressed, isNull); // Dispatch synchronous form field update: bloc.add( const UpdateFormField( path: '/email', value: 'randal@example.com', ), ); // Exactly one pump to allow Flutter to paint the new frame: await tester.pump(); // The submit button is enabled immediately: final enabledBtn = tester.widget<ElevatedButton>(find.byType(ElevatedButton)); expect(enabledBtn.onPressed, isNotNull); }, ); You never need tester.pumpAndSettle(). You never need arbitrary timeouts. You pump one frame, and the UI reflects the updated state. --- The Architectural Division: Async at the Perimeter, Sync in the Core BlocSignal embraces asynchronous operations where system physics dictates them: network I/O, database access, background isolates, and large language model streams are inherently asynchronous operations. The core architectural principle is the division of responsibilities: ┌────────────────────────────────────────────────────────┐ │ ASYNCHRONOUS PERIMETER │ │ • LLM Streaming SSE / WebSocket Firehose │ │ • REST API Requests & Platform Channels │ │ • Isolated with restartable() / droppable() Mutexes │ └──────────────────────────┬─────────────────────────────┘ │ ▼ (IngestStream / ProcessMessages) ┌────────────────────────────────────────────────────────┐ │ SYNCHRONOUS STATE CORE │ │ • BlocSignal & A2uiSurfaceBloc State Machine │ │ • 0ms Inline on<E> Event Dispatch │ │ • Atomic Two-Way Form Bindings (batch) │ │ • Topological Signal Graph (a2ui_core DataModel) │ └──────────────────────────┬─────────────────────────────┘ │ ▼ (UI = ƒ(State)) ┌────────────────────────────────────────────────────────┐ │ SYNCHRONOUS PRESENTATION │ │ • Flutter 120 FPS Rendering Pipeline (build / paint) │ │ • ANSI Terminal TUI (examples/genui_tui_agent) │ │ • Zero Microtask Lag, Zero Frame Tearing │ └────────────────────────────────────────────────────────┘ 1. Quarantine Asynchrony at the Perimeter: Use BlocSignal with concurrency transformers (restartable(), droppable()) to absorb incoming network streams, handle user cancellation, and sequence external requests. 2. Execute State Transitions Synchronously in the Core: Once data crosses the perimeter into client memory, execute state transitions inline on the caller's call stack. Form inputs, validation rules, navigation state, and derived calculations belong in a synchronous signal graph. 3. Project UI Synchronously Downstream: Flutter's rendering pipeline (build $\rightarrow$ layout $\rightarrow$ paint) is 100% synchronous. Feeding it synchronous state snapshots ensures zero-frame loading flickers, intact debugging call stacks, and 120 FPS interface responsiveness. The next time you call bloc.add(...) in BlocSignal, remember: it is a direct, synchronous state machine executing on your call stack in 0ms. --- Explore the code and packages: - bloc_signals on pub.dev - bloc_signals_genui on pub.dev - bloc_signals_genui_flutter on pub.dev - GitHub Repository: RandalSchwartz/BlocSignal

    9 reactions · 4 comments
  3. Generative UI Without the Async Tax: Introducing bloc_signals_genui 1.0 --- series: "BlocSignal Architecture & Practice" title: "Generative UI Without the Async Tax: Introducing bloc_signals_genui 1.0" description: "Why we built a pure-Dart, 0ms synchronous Generative UI state machine on top of Google's official A2UI protocol—supporting two-way reactive forms, Riverpod/BLoC interop, Flutter Material 3, and ANSI terminal TUIs." tags: flutter, dart, ai, architecture --- Wait—Another Generative UI Package? > "An asynchronous network stream belongs at the perimeter of your architecture; once an AI agent's UI lands on the client, every keystroke, validation rule, and leaf rebuild should execute in 0ms on a synchronous signal graph." > — Randal L. Schwartz When you see a new Generative UI announcement for Dart and Flutter, your first reaction is probably healthy skepticism: Do we really need another GenUI wrapper? Most Generative UI prototypes stop at the initial parlor trick: an LLM streams a JSON payload over Server-Sent Events (SSE), a widget tree appears on screen, and everyone claps. The architectural cracks appear five seconds later when a user tries to interact with that generated interface: - The Everything-Is-Async Tax: Because the LLM response arrives over an asynchronous network stream, most GenUI libraries wire the entire client lifecycle through StreamBuilder, async* generators, or asynchronous controller notifications. Every keystroke in a generated text field, every cross-field validation check, and every widget test pays a microtask and frame-delay tax. - Framework Lock-In: Most GenUI state engines are coupled directly to package:flutter/widgets.dart (BuildContext, Widget, ChangeNotifier), making it impossible to run or test the agent state machine in pure Dart, CLI tools, or terminal interfaces—and forcing you to adopt a bespoke state management paradigm that clashes with your existing Riverpod or flutter_bloc app. Today, we are publishing v1.0.0 of bloc_signals_genui and bloc_signals_genui_flutter on pub.dev. Here is what sets them apart: - 100% Compliant with Google's Emerging A2UI Protocol: Built directly on Google's official package:a2ui_core (^0.3.0) library from the a2ui-project/a2ui specification (v0.9.1 and v1.0). - 0ms Synchronous Core & Live Two-Way Form Bindings: The async SSE stream is quarantined at the edge. Once on the client, two-way WritableBinding form inputs, synchronous CheckRule validation, and NodeResolver list templates execute in 0ms in the exact same frame. - True Pure-Dart Decoupling (From Flutter to Terminal TUIs): bloc_signals_genui has zero Flutter dependencies. The exact same A2uiSurfaceBloc state machine drives both a 120 FPS Material 3 Flutter app and an interactive ANSI Terminal User Interface (TUI). - Universal Interop (Keep Your Riverpod or Classic BLoC Core): Through bloc_signals_riverpod and bloc_signals_bloc, you can drop A2UI surfaces straight into an existing Riverpod or classic BLoC codebase without rewriting your architecture. --- Foundation: Official A2UI Compliance and Shared Reactive DNA We avoided inventing a custom JSON schema or maintaining a divergent protocol parser. Both packages build directly on Google's official package:a2ui_core Dart SDK for the A2UI (Agent-to-User Interface) protocol. (We even contributed upstream in a2ui-project/a2ui#2819 to lower a2ui_core's minimum SDK constraint to Dart ^3.5.0, opening the official A2UI engine to the entire Flutter 3.24+ ecosystem.) When inspecting a2ui_core under the hood, one architectural detail stands out immediately: Google's a2ui_core already standardizes on preact_signals for its internal DataModel, GenericBinder, and reactive path bindings. Wrapping a signal-powered protocol engine inside asynchronous StreamController pipes adds unnecessary translation layers. Because BlocSignal is built on the exact same reactive signal primitives, bloc_signals_genui connects to a2ui_core with zero impedance mismatch: flowchart TD Agent["AI Agent / LLM Backend\n(Streams A2UI v0.9 / v1.0 JSON via SSE)"] Bloc["Edge Container: A2uiSurfaceBloc (Pure Dart)\n• Quarantines async stream with restartable() mutex\n• Sealed lifecycle: Initial → Streaming → Ready → Submitting\n• Bounded multi-surface LRU cache (maxSurfaces)"] Core["Synchronous Reactive Core (a2ui_core + BlocSignal)\n• 0ms Two-Way WritableBinding DataModel sync\n• Synchronous CheckRule validation (isValid / errorText)\n• Reactive NodeResolver ChildList template expansion"] Flutter["Flutter Renderer (bloc_signals_genui_flutter)\n• 16 Material 3 BasicCatalog widgets\n• Surgical leaf rebuilds via GenericBinder signals"] TUI["ANSI Terminal Renderer (examples/genui_tui_agent)\n• UTF-8 box drawing & bracketed hotkeys\n• Interactive stdin prompts & CheckRule badges"] Interop["Grand Central Interop\n• Riverpod 2/3 (.toProvider())\n• Classic BLoC & OpenTelemetry spans"] Agent -->|"1. Async JSON Chunks (Network Perimeter)"| Bloc Bloc -->|"2. 0ms Same-Frame Signal Graph"| Core Core --> Flutter Core --> TUI Core --> Interop --- Pillar 1: A Synchronous Core with Live Two-Way Form Bindings In a real application, an AI-generated surface functions as a live interactive form, not a static poster. When a flight-booking agent streams a checkout card, the user still needs to type passenger names, toggle baggage checkboxes, pick seat classes, and see instant validation feedback before submitting their selection back to the agent. bloc_signals_genui and bloc_signals_genui_flutter support the full 16-component A2UI BasicCatalog (Text, Row, Column, List, Tabs, Modal, Card, Divider, Icon, Image, Button, TextField, CheckBox, ChoicePicker, Slider, and DateTimeInput) with four synchronous capabilities: 1. Two-Way WritableBinding Synchronization in 0ms When an A2UI component binds a property to a JSON Pointer path in the surface's DataModel (for example, {"path": "/user/name"} in v0.9 or {"@path": "/user/name"} in v1.0), A2uiComponentContext exposes resolveBoundPath and updateBoundValue. Typing into an A2uiTextField, sliding an A2uiSlider, or picking a chip in A2uiChoicePicker mutates the underlying DataModel signal synchronously in 0ms. Only the leaf widget observing that path rebuilds—leaving the parent surface scaffold untouched. 2. Same-Frame CheckRule Validation UI A2UI allows components to declare client-side checks rules right in the wire JSON. As the user edits a bound field, GenericBinder evaluates those CheckRule conditions synchronously on every keystroke. A2uiComponentContext exposes isValid and validationErrors, displaying inline errorText on invalid inputs and automatically disabling A2uiButton submissions (onPressed: null) in the exact same frame when required checks fail—with zero LLM round-trips. 3. Data-Driven NodeResolver List Templates Instead of burning LLM output tokens streaming 20 repetitive component subtrees for a list of flights, the agent streams a single template component (flight_card) and a ChildList binding ({"componentId": "flight_card", "path": "/flights"}). A2uiSurfaceView mounts a2ui_core's NodeResolver to dynamically expand and scope each item's data context (/flights/0, /flights/1, ...) reactively as array elements stream in. 4. Bidirectional RPC (RpcHandler) Beyond standard action submissions (SubmitAction), A2uiSurfaceBloc wires MessageProcessor.rpc so the agent can invoke client-side renderer functions (CallRendererFunctionMessage) and your Dart code can invoke agent-side functions via bloc.callAgentFunction(...) with built-in timeout and lifecycle cancellation safety. import 'package:bloc_signals_genui/bloc_signals_genui.dart'; // 1. Create the pure-Dart A2UI state machine final surfaceBloc = A2uiSurfaceBloc( maxSurfaces: 10, maxResponseHistory: 100, ); // 2. Ingest an incoming A2UI v0.9 stream (quarantined by restartable()) surfaceBloc.add(IngestStream(llmChunkStream)); // 3. Mutate bound form fields synchronously in 0ms surfaceBloc.add( const UpdateFormField( path: '/user/name', value: 'Randal L. Schwartz', ), ); And because state transitions and form bindings are synchronous, your Flutter widget tests verify updates in a single tester.pump() without waiting on pumpAndSettle() microtask queues. --- Pillar 2: Streamless Concurrency and Production Scars Built In Streaming UI from an LLM introduces failure modes that traditional CRUD screens never encounter: 1. Impatient Users & Mid-Stream Interruptions (restartable()): What happens when an agent is halfway through streaming a complex itinerary surface and the user changes their mind and sends a new prompt? In naive stream setups, both streams interleave and corrupt the component tree. A2uiSurfaceBloc guards IngestStream with BlocSignal's streamless restartable() mutex transformer—subscribing to the replacement stream, preempting the stale stream immediately, and cleaning up resources without race conditions. 2. Zero-Chunk Stream Failures & Turn-Alternation Protection: Conversational APIs (such as Google Gemini and Firebase Genkit) enforce strict user → model turn alternation. If your app optimistically appends a user prompt to chat history and the network drops before the first SSE chunk arrives (chunkCount == 0), leaving that unfulfilled user turn in history causes the user's next retry to crash with INVALID_ARGUMENT: Consecutive user turns are not allowed. Our architecture and reference apps bake in transactional zero-chunk turn pruning out of the box. 3. Multi-Surface Navigation & Bounded LRU Eviction: Agents frequently spawn multiple surfaces across a conversation (main workspace, comparison cards, detail modals). A2uiSurfaceBloc separates navigation coordinates (activeSurfaceId, a 0ms ReadonlySignal<String?>) from per-surface content revisions (_surfaceVersions), so switching tabs via SelectSurface never triggers redundant binder rebuilds in split-pane views—while maxSurfaces automatically evicts the least-recently-used non-active surfaces so long sessions never leak memory. --- Pillar 3: Pure-Dart Decoupling—Running the Exact Same Agent in a Terminal TUI Because bloc_signals_genui has zero dependency on Flutter, your entire Generative UI state machine can run in CLI tools, backend Dart servers, automated test harnesses, or terminal applications. To prove this architecture in the monorepo, we built two complete reference applications powered by the exact same A2uiSurfaceBloc and BasicCatalog payloads: 1. examples/genui_flight_booking: A Material 3 Flutter application rendering ChoicePicker seat chips, CheckBox preferences, DateTimeInput pickers, NodeResolver flight card templates (/flights), CheckRule passenger validation, and assignPreferredSeat RPC calls. 2. examples/genui_tui_agent: An interactive ANSI Terminal User Interface (TUI) inspired by agy-cli that renders those exact same 16 BasicCatalog components, template paths, and CheckRule validation badges using UTF-8 box-drawing characters (┌─┐, │, └─┘), interactive stdin prompts, and bracketed action hotkeys ([1] Search Flights). # Run the interactive ANSI Terminal TUI agent in pure Dart: dart run examples/genui_tui_agent/bin/main.dart --mock Decoupling the A2UI state machine from the renderer means you can verify and fuzz-test complex multi-turn agent flows in pure dart test in milliseconds, without spinning up a simulator or headless browser. --- Pillar 4: "Grand Central Station" Interop—Keep Your Riverpod or Classic BLoC Core Adopting a Generative UI library should not require rewriting your application's existing state management architecture. Because A2uiSurfaceBloc extends BlocSignal, it plugs directly into the rest of the 13-package BlocSignal ecosystem: - Using Riverpod? Keep Your Riverpod Core: Add bloc_signals_riverpod and call .toProvider(autoClose: true) on A2uiSurfaceBloc. Your Riverpod ConsumerWidgets and providers can watch the A2UI surface state or dispatch events through ref while A2uiSurfaceView handles the granular signal rendering: import 'package:bloc_signals_genui/bloc_signals_genui.dart'; import 'package:bloc_signals_riverpod/bloc_signals_riverpod.dart'; import 'package:riverpod/riverpod.dart'; // Expose A2uiSurfaceBloc as a native Riverpod NotifierProvider final surfaceBloc = A2uiSurfaceBloc(maxSurfaces: 5); final a2uiSurfaceProvider = surfaceBloc.toProvider(autoClose: true); // Inside any Riverpod consumer: // final surfaceState = ref.watch(a2uiSurfaceProvider); // ref.read(a2uiSurfaceProvider.notifier).bloc.add(IngestStream(stream)); - Using Classic flutter_bloc? Bridge It cleanly: Use bloc_signals_bloc to bridge events and states bidirectionally with classic Bloc and Cubit instances. - Want Production Telemetry? Attach OpenTelemetry & DevTools: Drop in bloc_signals_otel and bloc_signals_devtools to trace every A2UI stream ingestion, chunk transition, and restartable() preemption span in OpenTelemetry and the Flutter DevTools extension with zero changes to your UI code. --- Get Started Today Both bloc_signals_genui (1.0.0) and bloc_signals_genui_flutter (1.0.0) are live on pub.dev with 100% test coverage and full support for Dart 3.5+ through Dart 3.13+: # For pure-Dart, CLI, or TUI applications: dart pub add bloc_signals_genui bloc_signals # For Flutter applications: flutter pub add bloc_signals_genui_flutter bloc_signals_genui bloc_signals_flutter In follow-up articles in this series, we will dive deeper into each of these four pillars—including a hands-on walkthrough of building two-way validated A2UI forms with WritableBinding and CheckRule, wiring NodeResolver templates, and building headless/TUI agent runners in pure Dart. Links & Resources - pub.dev: bloc_signals_genui | bloc_signals_genui_flutter | a2ui_core - Documentation Hub: blocsignal.dev/docs/pkg-genui & blocsignal.dev/docs/pkg-genui-flutter - GitHub Monorepo & Examples: RandalSchwartz/BlocSignal (genui_flight_booking & genui_tui_agent)

    11 reactions · 18 comments

Latest stargazers

The last 10 distinct accounts we observed this year.

Most active participants

PR, issue, comment & review events in the past year.

Packages

21
PackageVersionDownloads · 30dTrend · 30d
a2a-chat-canvasv0.0.1-security
v0.0.1-security
≥7
———
———
———
v0.9.0
≥353.4K
———
———
———
———
a2ui-corev0.4.0
v0.4.0
≥295.4K
———
———
———
———
———
———
———
———
v1.0
≥560
———
———

README badges

Embed a badge in your README or website.

  • google/A2UI reached #1 in daily stars on gitwal · Dec 17, 2025
  • google/A2UI reached #1 in weekly stars on gitwal · Week of Dec 15, 2025
Live mentions · last 30 days
google/A2UI mentions in the last 30 days