Integrating A2UI with AG-UI in Angular

  1. Understanding AG-UI: The Standard for Agentic User Interfaces
  2. AG-UI in Practice: The SDK for TypeScript
  3. Implementing AG-UI with Angular
  4. A2UI: How AI Generates Dynamic UIs at Runtime
  5. Integrating A2UI with AG-UI in Angular
  6. Custom Catalogs in A2UI: Your Own Components for AI-Generated UIs

From the hardcoded example to a real agent connection via AG-UI.

In practice, A2UI messages don't come from static code, but from the responses of a real language model – and these first need to be transported from the agent to the client. AG-UI offers the right transport layer for this, but the official specification leaves open how A2UI is to be transmitted over it. This article shows a pragmatic solution and an Angular abstraction that connects both protocols.

This second part of the three-part series picks up where the first part left off, where we still ran A2UI and the Angular renderer with hardcoded messages.

📂 Source Code (see branch a2ui-dynamic)

How Do You Transmit A2UI Messages over AG-UI?

So far we've looked at A2UI in isolation. In practice, however, the client communicates with the agent via HTTP calls. This is exactly where the protocol AG-UI, mentioned in the previous article in this series, comes into play, standardizing the communication between frontend and agent.

We can elegantly embed A2UI into this world by transmitting the A2UI messages inside an AG-UI message of type ACTIVITY_SNAPSHOT. The browser's developer tools show such a snapshot:

Browser devtools trace: A2UI operations createSurface, updateComponents, and updateDataModel embedded in an AG-UI ACTIVITY_SNAPSHOT

In textual form, the snapshot looks like this:

{
  "type": "ACTIVITY_SNAPSHOT",
  "messageId": "call_6mk44Dh0MNnmPDETq7tvjkON",
  "activityType": "a2ui-surface",
  "content": {
    "operations": [
      {
        "version": "v0.9",
        "createSurface": {
          "surfaceId": "srf-france-confirm",
          "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json"
        }
      },
      {
        "version": "v0.9",
        "updateComponents": {
          [...]
        }
      },
      {
        "version": "v0.9",
        "updateDataModel": {
          "surfaceId": "srf-france-confirm",
          "path": "/dummy",
          "value": ""
        }
      }
    ]
  }
}

Unfortunately, there is no official definition for how A2UI messages are to be transmitted over AG-UI. The solution shown here, an ACTIVITY_SNAPSHOT with a corresponding activityType, fits well with the semantics of AG-UI and is also used in this way by the solution CopilotKit. Since the makers of CopilotKit are among the initiators of AG-UI, their interpretation of the standard naturally carries significant weight.

As an alternative to an ACTIVITY_SNAPSHOT, a server-side tool call would also be conceivable, especially since AG-UI informs the client about tool calls and their results. In this case, however, the client and agent would have to agree on the name of such a tool that delivers A2UI as its result.

Angular Abstraction for A2UI and AG-UI

There is an Angular renderer for A2UI as well as a TypeScript SDK for AG-UI, but in practice this is not enough to seamlessly integrate both concepts into an application. For convenient use, an additional abstraction layer is required that encapsulates the protocol details.

In the example repository, this layer is found in the libs folder. For the following considerations, we treat it as a black box and access it through @intern/ag-ui-angular.

This implementation guides the language model via prompting to produce A2UI structures – not directly for the client, however, but as input for a server-side tool. This tool plays a central role: it validates the A2UI messages generated by the LLM and ensures that only consistent and executable structures are processed further.

This is crucial, because LLMs are not guaranteed to deliver valid results. If validation fails, the feedback is sent back to the model so that it can try again. Only when a valid structure is available is it forwarded.

The server-side implementation then transforms the validated A2UI messages into an ACTIVITY_SNAPSHOT. This becomes part of the regular AG-UI stream and can therefore be processed uniformly by the client abstraction.

A small but practical convention helps when dealing with charts: since A2UI itself does not provide a chart component, we explicitly instruct the LLM in the prompt to render charts via an Image component using a QuickChart URL. This is a web service that takes the data to be displayed as URL parameters and returns an image with a chart. This way, the component catalog stays lean while the LLM is given a clear path for visual data.

Alternative Approach

One possible simplification is to have the LLM not generate A2UI directly, but rather a reduced, use-case-specific DSL (Domain-Specific Language). The server-side tool takes care of translating this DSL into valid A2UI operations and thus acts as a central control instance. From the client's perspective, nothing changes. It still receives an ACTIVITY_SNAPSHOT with regular A2UI.

The DSL makes generation considerably more robust for the LLM and is particularly suitable for weaker or more cost-effective models. At the same time, more logic shifts to the server, which increases control and stability. The price for this is an additional transformation layer as well as a lower expressiveness and flexibility compared to direct A2UI.

An implementation of this approach can be found in the example project on the branch a2ui-dsl.

Client-Side Integration with agUiResource

On the client side, the agUiResource takes care of the connection to the agent. It manages the chat history, executes client-side tool calls, and provides the received messages. The consumer registers, among other things, the URL of the agent and the offered client tools with this resource:

import { agUiResource } from '@internal/ag-ui-client';

[...]

@Injectable({ providedIn: 'root' })
export class TicketingChatService {
  private readonly config = inject(ConfigService);
  private readonly chatStore = inject(ChatRegistry);
  private readonly destroyRef = inject(DestroyRef);

  private readonly chat = agUiResource({
    url: this.config.agUiUrl,
    model: this.config.model,
    useServerMemory: true,
    tools: [
      findFlightsTool,
      getLoadedFlightsTool,
      toggleFlightSelectionTool,
      getCurrentBasketTool,
      displayFlightDetailTool,
    ],
  });

  constructor() {
    registerHandlers({
      checkIn: (action) => checkInAction(action),
      submitAnswer: (action) => submitAnswerAction(action, this.chat),
    });

    this.destroyRef.onDestroy(() => this.cleanupChat());
  }

  public init(): void {
    this.chatStore.setChat(this.chat);
  }

  private cleanupChat(): void {
    this.chat.dispose();
    this.chatStore.clearChat();
  }
}

The helper function registerHandlers defines the event handlers and internally delegates to the onAction property discussed in the first part. To send a message to the agent, the agUiResource provides a sendMessage method:

this.chat.sendMessage({
  role: 'user',
  content: 'Did I book my flight to France?',
});

NOTE

New: Agentic UI with Angular

If you don’t just want to integrate A2UI but embed it into a scalable architecture:
In my book Agentic UI with Angular, I cover the underlying patterns and trade-offs in depth.

Cover of the eBook Agentic UI with Angular

Learn more about the eBook →

Displaying the Chat History and A2UI Surfaces in the Template

The entire chat history is found in the value of the AgUiResource. It is a signal containing an array of AgUiChatMessages and can be rendered in the Angular template:

@for (message of chat.value(); track message.id) {

  @if (message.content) {
    <div>{{ message.content }}</div>
  }

  @for (widget of message.widgets; track widget.id) {
    <app-widget-container [widget]="widget" />
  }

  @for (toolCall of message.toolCalls; track toolCall.id) {
    <div>Tool Call: {{ toolCall.name }}</div>
  }

}

The agUiResource evaluates the ACTIVITY_SNAPSHOT messages and attaches the contained A2UI messages to the AgUiChatMessages. The WidgetContainer, also delivered by @intern/ag-ui-angular, delegates these messages to the A2UI renderer and displays the received surface. To do so, it uses the SurfaceComponent of the Angular adapter for A2UI discussed in the first part of this series.

To provide the Basic Catalog, @internal/ag-ui-client offers the function provideA2uiCatalog. In addition, a Markdown implementation has to be wired up via provideMarkdownRenderer here as well:

import { provideMarkdownRenderer } from '@a2ui/angular/v0_9';
import { provideA2uiCatalog } from '@internal/ag-ui-client';

[...]

export const appConfig: ApplicationConfig = {
  providers: [
    provideA2uiCatalog(),
    provideMarkdownRenderer(async (markdown) =>
      marked.parse(String(markdown ?? '')),
    ),
  ],
};

This bridges the gap between agent and A2UI renderer: the agent delivers its A2UI messages via AG-UI ACTIVITY_SNAPSHOTs, the agUiResource receives them, and the WidgetContainer renders the contained surfaces. The individual components of our application don't need to know either the AG-UI or the A2UI protocol in detail.

Summary

In combination with AG-UI, A2UI fits seamlessly into an existing agentic architecture. AG-UI handles the structured communication between client and agent, while A2UI transports the UI-specific content. The Angular abstraction shown, based on the agUiResource, enables simple and idiomatic use in Angular.

A not-to-be-underestimated advantage of this architecture lies in the validation on the server side: it shields the client from inconsistent responses of the language model and ensures that only verified structures end up in the browser. The optional DSL variant additionally shows that the same mechanism can be balanced for differently powerful models.

The Next Step

So far we have relied on the Basic Catalog – the next part shows how to extend it with your own domain-specific components as a Custom Catalog.

Next article →


FAQ

How do you transmit A2UI messages over AG-UI?

There is no official definition. In practice, it has proven effective to transport the A2UI operations inside an AG-UI message of type ACTIVITY_SNAPSHOT with its own activityType (for example, a2ui-surface). This variant fits the semantics of AG-UI and is also used by CopilotKit.

Why is server-side validation part of this?

Language models are not guaranteed to deliver valid A2UI structures. Server-side validation – for example, in a tool – catches faulty responses, returns feedback to the model, and ensures that only consistent and executable structures reach the client.

What does the agUiResource do in Angular?

The agUiResource from the Angular abstraction manages the connection to the agent. It bundles the chat history, executes client-side tool calls, registers event handlers, and provides the received messages as a signal to the template. As a result, individual components don't need to know either AG-UI or A2UI in detail.

When does a custom DSL make sense instead of direct A2UI?

A reduced, application-specific DSL can make generation considerably more robust for weaker or more cost-effective models. The server centrally translates it into A2UI. The price for this is an additional transformation layer and somewhat less expressiveness compared to direct A2UI.

Agentic UI with Angular

Architecting Agentic AI with Open Standards

Integrate AI Agents in Angular with Open Standards.

More About the Book