Agentic UI mit Angular, CopilotKit und AG-UI umsetzen

  1. AG-UI verstehen: Der Standard für Agentic User Interfaces
  2. AG-UI in der Praxis: Das SDK für TypeScript
  3. AG-UI End-2-End: Server und Client verbinden
  4. Agentic UI mit Angular, CopilotKit und AG-UI umsetzen
  5. A2UI: Wie KI dynamische UIs zur Laufzeit erzeugt
  6. A2UI mit AG-UI und CopilotKit in Angular integrieren
  7. Custom Catalogs in A2UI: Eigene Komponenten für KI-generierte UIs
  8. A2UI mit DSL: Kontrollierbare Dashboards optimiert für Performance
  9. Agentic UI mit MCP Apps: Tool-Ergebnisse als interaktive Widgets
  10. MCP Apps in Angular mit CopilotKit: Rich Chat Interfaces Instead of Text Responses

Agents via AG-UI und CopilotKit in Angular-Anwendungen nutzen

AG-UI definiert die Kommunikation zwischen Frontend und Agent. Mit @copilotkit/angular steht mittlerweile auch eine Angular-Integration bereit. Sie stammt vom Team hinter CopilotKit, aus dessen Umfeld auch AG-UI selbst hervorgegangen ist.

Genau hier setzt dieser Artikel an. Während der vorige Teil mit dem fertigen Chat-Control rasch zu einem Ergebnis kam, nutzen wir ab hier den flexiblen Headless-Mode von CopilotKit: Er liefert Zustand und Verhalten des Chats, überlässt die Darstellung aber vollständig der Anwendung. Auf dieser Basis zeigt der Artikel, wie sich Agents mit CopilotKit an eine Angular-Anwendung anbinden lassen, wie clientseitige Tools und Widgets definiert werden und wo derzeit noch eine kleine selbstgeschriebene Hilfsfunktion nötig ist.

📂 Source Code (siehe Branch copilotkit)

Die Beispielanwendung

Als durchgängiges Beispiel dient – wie in der ganzen Serie – eine Flight-Booking-Anwendung. Sie lässt sich ganz klassisch bedienen; wer aber feststeckt oder Arbeitsschritte abkürzen möchte, aktiviert ein Sidecar: einen Chat, über den ein serverseitiger Agent aushilft. Er beantwortet Anfragen mit Text, ruft client- und serverseitige Tools auf und blendet interaktive Komponenten wie eine Flugkarte in den Verlauf ein.

Sidecar in einer Flight-Booking-Anwendung mit Chat, Tool Calls und Flugkarte

CopilotKit in Angular integrieren

CopilotKit setzt direkt auf dem besprochenen AG-UI SDK auf: Der Agent wird clientseitig durch einen HttpAgent aus @ag-ui/client repräsentiert, den CopilotKit verwaltet und dessen gestreamte AG-UI-Events es in einen Signal-basierten Chatverlauf übersetzt. Die serverseitige Implementierung bleibt davon unberührt – wer bereits einen AG-UI-Endpunkt hat, kann ihn unverändert weiterverwenden.

Die Einbindung in die Anwendung erfolgt wie üblich über einen Provider in der app.config.ts:

// src/app/app.config.ts
import { provideCopilotKit } from '@copilotkit/angular';

export const appConfig: ApplicationConfig = {
  providers: [
    [...]
    provideCopilotKit({
      defaultToolRendering: true,
    }),
  ],
};

Die Option defaultToolRendering aktiviert CopilotKits eingebauten Standard-Renderer für Tool-Aufrufe ohne eigene Darstellungskomponente. Dadurch werden solche Tool-Aufrufe im Chatverlauf protokolliert und sind so für die Benutzer nachvollziehbar.

Durch den Einsatz von AG-UI ist das Angular-Beispiel unabhängig von serverseitigen Technologien und Modellen. Um eine möglichst einfache Ausführung zu ermöglichen, enthält die Demo-Anwendung einen Agent, der das äußerst komfortable und auf TypeScript basierende Agent-Framework Mastra nutzt. Da das AG-UI SDK für Mastra einen Adapter bereitstellt, lassen sich damit entwickelte Agents einfach über AG-UI anbinden.

Der Client wurde sowohl mit OpenAIs GPT 5 als auch mit Googles Gemini 3 getestet. Details für das Einrichten und Starten der Demo finden sich in der Readme.

Agents über einen Agent Store anbinden

Dreh- und Angelpunkt der Angular-Integration von CopilotKit ist der sogenannte Agent Store. Er repräsentiert einen konkreten Agent samt Chatverlauf und Ausführungszustand. Das Demo-Projekt kapselt die Einrichtung des Ticketing-Agents in einer Funktion injectTicketingAgentStore:

// src/app/domains/ticketing/ai/ticketing-agent-store.ts
import { injectAgentStore } from '@copilotkit/angular';
import { initAgentStore } from '../../shared/util-copilotkit/init-agent-store';

[...]

export const TICKETING_AGENT_ID = 'ticketingAgent';

export function injectTicketingAgentStore() {
  initAgentStore({
    agentId: TICKETING_AGENT_ID,
    url: 'http://localhost:3001/ag-ui/ticketingAgent',
    useServerMemory: true,
    frontendTools: [
      findFlightsTool,
      getLoadedFlightsTool,
      toggleFlightSelectionTool,
      getCurrentBasketTool,
      displayFlightDetailTool,
      flightWidget,
    ],
  });

  return injectAgentStore(TICKETING_AGENT_ID);
}

Die Hilfsfunktion initAgentStore registriert den Agent samt seiner clientseitigen Tools bei CopilotKit; ihr Aufbau wird im nächsten Abschnitt besprochen. Danach liefert das von CopilotKit angebotene injectAgentStore den eigentlichen Agent Store zurück – ein Signal<AgentStore>, das unter anderem den Chatverlauf (messages) und den Ausführungszustand (isRunning) als Signals bereitstellt.

Die Konfiguration verweist auf die URL des Agents. Dahinter steht ein serverseitig implementierter Ticketing-Agent, der Reisende rund um ihre Flugbuchungen unterstützt. Dafür verfügt er über Tools zum Nachschlagen gebuchter Flüge sowie zum Buchen und Stornieren (findBookedFlightsTool, bookFlightTool, cancelFlightTool). Umgesetzt ist er in unserem Fall mit dem TypeScript-Framework Mastra; für die hier gezeigte Anbindung spielt das jedoch keine Rolle – entscheidend ist lediglich, dass der Agent AG-UI unterstützt.

Mit useServerMemory gibt der Aufrufer an, ob der Agent den Chatverlauf speichert. Ist dem nicht so, muss der Client den gesamten Chatverlauf in jeden Aufruf wiederholen.

In der Auflistung frontendTools registriert der Konsument sämtliche clientseitigen Tools, deren Aufruf der Agent anfordern darf. Interessanterweise findet sich darunter auch das Widget flightWidget – die eingangs gezeigte Flugkarte. Das ist kein Zufall: In CopilotKit sind Widgets nichts anderes als Frontend-Tools, denen eine Angular-Komponente für die Darstellung zugeordnet wurde.

NOTE

Agentic UI with Angular

Wenn du AG-UI nicht nur integrieren, sondern sauber in größere Architekturen einbetten willst:
In meinem Buch Agentic UI mit Angular gehe ich genau auf diese Patterns und Trade-offs im Detail ein.

Cover des eBooks Agentic UI with Angular

Mehr zum eBook →

Die Hilfsfunktion initAgentStore

CopilotKit sieht derzeit vor, dass Agents beim Aufruf von provideCopilotKit während des Anwendungsstarts konfiguriert und damit registriert werden. Für lazy geladene Feature-Bereiche ist das unpraktisch: Deren Tools und Widgets sollen erst mit dem jeweiligen Bundle in den Browser wandern und nicht schon im Hauptbundle landen.

Genau diese Lücke schließt initAgentStore. Sie läuft in einem Injection Context und erledigt im Kern zwei Dinge: Sie registriert den Agent zur Laufzeit als sogenannten self-managed Agent bei CopilotKit und meldet anschließend sämtliche Frontend-Tools für dessen agentId an (leicht verkürzt):

// src/app/domains/shared/util-copilotkit/init-agent-store.ts
import { randomUUID } from '@ag-ui/client';
import { inject } from '@angular/core';
import { CopilotKit, registerFrontendTool } from '@copilotkit/angular';

[...]

export function initAgentStore(config: InitAgentStoreConfig): void {
  const copilotKit = inject(CopilotKit);

  const httpAgent = new AppHttpAgent(
    {
      agentId: config.agentId,
      url: config.url,
      threadId: randomUUID(),
    },
    { useServerMemory: config.useServerMemory },
  );

  copilotKit.updateRuntime({
    selfManagedAgents: {
      ...copilotKit.agents(),
      [config.agentId]: httpAgent,
    },
  });

  for (const tool of config.frontendTools ?? []) {
    registerFrontendTool({
      ...tool,
      agentId: config.agentId,
    });
  }

  [...]
}

Der Aufruf von updateRuntime ergänzt die bei CopilotKit hinterlegten Agents um einen neuen Eintrag. Bei AppHttpAgent handelt es sich um eine schlanke Ableitung des HttpAgent aus dem AG-UI SDK. Sie implementiert unter anderem die Option useServerMemory: Merkt sich der Server den Chatverlauf, filtert der AppHttpAgent bereits übertragene Nachrichten aus den Requests heraus.

Die anschließende Schleife registriert die übergebenen Tools mit registerFrontendTool bei CopilotKit. Dabei bindet sie jedes Tool an die agentId – die Tool-Definitionen selbst bleiben dadurch agnostisch und wiederverwendbar.

Ein angenehmer Nebeneffekt: registerFrontendTool merkt sich den aktuellen Injection Context und führt die Tool-Handler später darin aus. Deshalb dürfen die Handler, wie im nächsten Abschnitt zu sehen, Angulars inject verwenden.

Clientseitige Tools definieren

Die einzelnen Tools beschreibt das Demo-Projekt mit der Hilfsfunktion createFrontendTool:

// src/app/domains/ticketing/ai/tools/find-flights.tool.ts
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { z } from 'zod';

import { createFrontendTool } from '../../../shared/util-copilotkit/tool-definition';

[...]

export const findFlightsTool = createFrontendTool({
  name: 'findFlights',
  description: `Searches for flights and redirects the user to the result`,
  parameters: z.object({
    from: z.string().describe('airport of departure'),
    to: z.string().describe('airport of destination'),
  }),
  handler: async ({ from, to }) => {
    const store = inject(FlightStore);
    const router = inject(Router);
    store.updateFilter(from, to);
    await router.navigate(['/ticketing/booking/flight-search']);
    return { ok: true };
  },
});

Neben einem Namen, einer Beschreibung und Parameterdefinitionen, die auf Zod basieren, enthält die Tooldefinition auch einen handler. Fordert der Agent das Tool an, führt CopilotKit diesen Handler aus – dank des zuvor erwähnten Injection Contexts inklusive funktionierendem inject.

Das Parameter-Objekt unterliegt jenem TypeScript-Typ, der aus dem Zod-Schema hervorgeht. In unserem Fall handelt es sich dabei um ein Objekt mit den Eigenschaften from und to. Die gezeigte Implementierung triggert eine Flugsuche, indem sie diese Suchkriterien an den FlightStore übergibt und navigiert den Benutzer danach auf die Ergebnisseite.

Tools, die Daten in Erfahrung bringen, zum Beispiel lokal vorliegende Zustände oder Antworten des Benutzers, können diese über den Rückgabewert ans Modell zurückmelden. Ein Beispiel dafür ist das getLoadedFlightsTool, das den Agent über jene Flüge informiert, die die Anwendung dem Benutzer gerade anzeigt:

// src/app/domains/ticketing/ai/tools/get-loaded-flights.tool.ts
export const getLoadedFlightsTool = createFrontendTool({
  name: 'getLoadedFlights',
  description: `Returns the currently loaded/displayed flights`,
  parameters: z.object({}),
  handler: async () => {
    const store = inject(FlightStore);
    return store.flightsValue().map(toFlightInfo);
  },
});

Das Innenleben von createFrontendTool

Ein Blick hinter die Kulissen zeigt: createFrontendTool ist im Kern eine Identitätsfunktion, die die Tool-Definition lediglich durchreicht – sie registriert nichts und injiziert nichts. Einzig bei followUp: false ergänzt sie die Beschreibung um den weiter unten gezeigten Hinweis (leicht verkürzt):

// src/app/domains/shared/util-copilotkit/tool-definition.ts
import { type FrontendToolConfig } from '@copilotkit/angular';

export function createFrontendTool<Args extends Record<string, unknown>>(
  tool: FrontendToolConfig<Args>,
): FrontendToolConfig<Args> {
  [...]
  return tool;
}

Der Sinn dieses Durchreichens liegt in der Type Inference: TypeScript leitet den generischen Typparameter Args aus dem übergebenen Zod-Schema in parameters ab und wendet ihn auf die gesamte Definition an. Dadurch kennt der handler die Typen seiner Parameter – im Fall von findFlights also from und to als string –, ohne dass die Tool-Definition sie explizit annotieren müsste.

Ohne die Hilfsfunktion gäbe es nur unbequemere Alternativen: die Definition von Hand als FrontendToolConfig<...> zu typisieren und damit die Parametertypen redundant zum Schema zu pflegen, oder auf die Typisierung zu verzichten und Tippfehler im Handler erst zur Laufzeit zu bemerken.

Widgets sind Tools mit Komponente

Für Widgets wie die Flugkarte sieht CopilotKit kein eigenes Konzept vor – und braucht auch keines. Ein Widget ist schlicht ein Frontend-Tool, dem über die Eigenschaft component eine Angular-Komponente zugeordnet wurde. Ruft der Agent das Tool auf, rendert CopilotKit diese Komponente im Chatverlauf und übergibt ihr die vom LLM gelieferten Parameter:

// src/app/domains/ticketing/ui/flight-widget.ts
const flightSchema = z.object({
  id: z.number().describe('The flight id'),
  from: z.string().describe('Departure city'),
  to: z.string().describe('Arrival city'),
  date: z.string().describe('Departure date in ISO format'),
  delay: z.number().describe('Delay in minutes'),
});

const flightWidgetSchema = z.object({
  flight: flightSchema,
  status: z.enum(['booked', 'other', 'none']).describe('Status of the flight'),
});

export const flightWidget = createFrontendTool({
  name: 'flightWidget',
  description: `Displays a concrete flight as an interactive card.
Use it when referring to one or more specific flights.`,
  parameters: flightWidgetSchema,
  component: FlightWidget,
  followUp: false,
  handler: async () => ({ shown: true }),
});

Das Schema beschreibt hier also nicht nur die Parameter eines Funktionsaufrufs, sondern zugleich die Daten, mit denen die Komponente versorgt wird. Die Option followUp: false teilt CopilotKit mit, dass nach dem Anzeigen des Widgets kein weiterer Roundtrip zum Agent nötig ist – das Widget bildet den Abschluss der Antwort.

Wichtig dabei: followUp: false steuert nur den Client – damit auch das Modell solche Aufrufe als Abschluss seines Zugs behandelt, hängt createFrontendTool im Demo-Projekt automatisch einen entsprechenden Hinweis an die Tool-Beschreibung und damit an den Prompt an.

Die zugeordnete Komponente implementiert das Interface ToolRenderer und nimmt den Tool-Aufruf als Input entgegen. Über toolCall().args gelangt sie an die vom LLM übergebenen, vom Schema typisierten Werte:

// src/app/domains/ticketing/ui/flight-widget.ts
import { type AngularToolCall, type ToolRenderer } from '@copilotkit/angular';

@Component({
  selector: 'app-flight-widget',
  imports: [FlightCard, RouterLink],
  template: `
    @let flight = toolCall().args.flight;
    @if (flight) {
      <app-flight-card [item]="flight" [readonly]="true">
        [...]
      </app-flight-card>
    }
  `,
})
export class FlightWidget implements ToolRenderer<FlightWidgetArgs> {
  readonly toolCall = input.required<AngularToolCall<FlightWidgetArgs>>();
  [...]
}

Anfragen an den Agent senden

Die Chat-Komponente der Anwendung – im Demo-Projekt AssistantChat – leitet ihren Zustand aus dem Agent Store ab. Die folgende, vereinfachte Fassung besorgt sich den Store direkt über die eingangs gezeigte Funktion; die reale Komponente erhält ihn über eine Registry vom jeweiligen Chat-Service, der injectTicketingAgentStore aufruft:

// src/app/domains/shared/ui-assistant/assistant-chat/assistant-chat.ts
import { CopilotKit } from '@copilotkit/angular';

[...]

export class AssistantChat {
  private readonly copilotKit = inject(CopilotKit);

  protected readonly store = injectTicketingAgentStore();

  protected readonly messages = computed(() => this.store().messages());
  protected readonly isRunning = computed(() => this.store().isRunning());

  protected submit(): void {
    void sendMessage(this.copilotKit, this.store, this.message());
  }
}

Das Senden übernimmt die Funktion sendMessage. Sie fügt die Benutzernachricht dem Agent hinzu und stößt danach über CopilotKit einen Run an:

// src/app/domains/shared/util-copilotkit/agent-store-helper.ts
export async function sendMessage(
  copilotKit: CopilotKit,
  store: Signal<AgentStore>,
  content: string,
): Promise<void> {
  const agent = store().agent;
  agent.addMessage({ id: randomUUID(), role: 'user', content });
  await copilotKit.core.runAgent({ agent });
}

Der Umweg über copilotKit.core.runAgent ist wichtig: Nur so nehmen die zuvor registrierten Frontend-Tools am Run teil. Nun muss der Client nur noch die vom Agent via AG-UI retournierten Antwortnachrichten im Chatverlauf anzeigen.

Chatverlauf in Angular-Templates präsentieren

Der gesamte Chatverlauf befindet sich im Signal messages des Agent Stores. Das Template iteriert die Nachrichten und gibt neben der textuellen Antwort (content) auch die angeforderten Tool-Aufrufe aus. Letzteres übernimmt die von CopilotKit bereitgestellte Komponente RenderToolCalls:

<!-- src/app/domains/shared/ui-assistant/chat-messages/chat-messages.html -->
@for (message of messages(); track message.id) {
  @if (message.content) {
    <div>{{ message.content }}</div>
  }
  @if (message.role === 'assistant' && message.toolCalls?.length) {
    <copilot-render-tool-calls
      [message]="message"
      [messages]="messages()"
      [agentId]="'ticketingAgent'" />
  }
}

RenderToolCalls schlägt für jeden Tool-Aufruf die registrierte Darstellungskomponente nach – für das flightWidget erscheint so die interaktive Flugkarte. Um die Ausführung clientseitiger Tools müssen wir uns hingegen nicht kümmern, denn diese Aufgabe übernimmt CopilotKit ohne weiteres Zutun.

Die gestreamten AG-UI-Nachrichten lassen sich über die Developer Tools des Browsers nachvollziehen:

Gestreamte AG-UI-Nachrichten in den Developer Tools des Browsers

Diese Informationen machen zum einen das Protokoll greifbar und helfen zum anderen beim Troubleshooting.

Zeit für einen Test

Damit ist alles beisammen, um die Lösung auszuprobieren; Details zum Starten von Client und Agent finden sich in der Readme des Demo-Projekts. Bitten wir den Assistenten etwa um Flüge von Graz nach Hamburg, fordert der Agent das Frontend-Tool findFlights an, und die Anwendung navigiert zur Ergebnisliste. Fragen zu konkreten Flügen beantwortet er mit der als Widget registrierten Flugkarte direkt im Chatverlauf:

Der Sidecar-Chat beantwortet eine Fluganfrage mit Text und der interaktiven Flugkarte

Auch das Nachschlagen gebuchter Flüge sowie Buchungen und Stornierungen beherrscht der Agent bereits – dahinter stehen seine serverseitigen Tools. Deren Aufrufe protokolliert der eingangs aktivierte Standard-Renderer im Chatverlauf, sodass auch diese Aktionen für die Benutzer nachvollziehbar bleiben.

Zusammenfassung

Mit @copilotkit/angular lässt sich AG-UI einfach in Angular integrieren. Clientseitige Tools und Widgets sind einheitlich als Frontend-Tools beschrieben, wobei Widgets lediglich eine zusätzliche Darstellungskomponente mitbringen. Auch serverseitige Tool-Aufrufe erscheinen im Chatverlauf – dargestellt durch CopilotKits eingebauten Standard-Renderer, den die Option defaultToolRendering aktiviert. Der Agent Store stellt den Chatverlauf als Signal bereit, und die im Lieferumfang enthaltene Komponente RenderToolCalls bringt die registrierten Komponenten im Chat zur Anzeige.

Der nächste Schritt

Mit AG-UI ist die Kommunikation zwischen Frontend und Agent geklärt – die nächste Serie zeigt, wie das LLM die UI selbst aus Layouts, Anzeigen und Eingaben zusammenbauen kann, ohne neuen Frontend-Code für jeden Use Case.

Nächster Artikel →


Interesse an produktionsreifen Agentic-UI-Architekturen?

In meinem Workshop beschäftigen wir uns mit AG-UI, A2UI, MCP Apps, HITL-Patterns und modernen Angular-Architekturen für reale agentische Systeme.

Workshop: Agentic AI mit Angular – AG-UI, A2UI, MCP Apps & HITL-Patterns

Alle Details →

FAQ

Wie lässt sich AG-UI in Angular integrieren?

Mit @copilotkit/angular steht eine Angular-Integration bereit, die direkt auf dem AG-UI SDK aufsetzt. Sie verwaltet den Agent, führt clientseitige Tools aus und stellt den Chatverlauf als Signal-basierten Agent Store bereit.

Was ist ein Agent Store?

Der Agent Store repräsentiert einen konkreten Agent samt Zustand. injectAgentStore liefert ihn als Signal<AgentStore>, das unter anderem den Chatverlauf (messages) und den Ausführungszustand (isRunning) als Signals anbietet.

Wozu dient die Hilfsfunktion initAgentStore?

CopilotKit bietet derzeit keine Möglichkeit, Agent Stores für lazy geladene Feature-Bereiche nachträglich zu initialisieren. initAgentStore schließt diese Lücke: Sie registriert den Agent zur Laufzeit als self-managed Agent und meldet die Frontend-Tools für dessen agentId an.

Wie gelangen Widgets in den Chatverlauf?

Widgets sind Frontend-Tools mit einer zugeordneten Angular-Komponente. Fordert der Agent solch ein Tool an, rendert die CopilotKit-Komponente RenderToolCalls die hinterlegte Komponente im Chatverlauf und versorgt sie mit den vom LLM übergebenen, vom Zod-Schema typisierten Parametern.

Wie lassen sich serverseitige Tool-Aufrufe visualisieren?

Serverseitige Tool-Aufrufe erscheinen ebenfalls im Chatverlauf, auch wenn der Client sie nicht ausführt. Die Option defaultToolRendering: true von provideCopilotKit aktiviert CopilotKits eingebauten Standard-Renderer, der solche Aufrufe als aufklappbare Karte samt Parametern darstellt.

Agentic UI with Angular

Architecting Agentic AI with Open Standards

Integriere AI-Agents in Angular mit offenen Standards.

Mehr zum Buch