{"slug": "introducing-angular-support-for-copilotkit-bring-any-agent-into-your-app", "title": "Introducing Angular support for CopilotKit: bring any Agent into your app", "summary": "CopilotKit has released Angular support, an open-source client that brings any AG-UI agent into Angular apps with pre-built chat components, generative UI, shared state, and human-in-the-loop features. The integration, built by Rainer Hahnekamp and Murat Sari, uses the Agent-User Interaction Protocol to stream an agent's lifecycle as events, keeping the Angular app and agent in sync.", "body_md": "Angular apps can now run any agent, with the streaming, tool calls, and shared state already handled.\n\nToday we're releasing [Angular support for CopilotKit](https://www.copilotkit.ai/angular), an open source client that brings any [AG-UI](https://github.com/ag-ui-protocol/ag-ui) agent into your Angular app. It's built with Angular's own patterns, standalone components, dependency injection and signals.\n\nYou get the building blocks for agent-native apps in Angular: pre-built chat components or a fully headless setup, generative UI, shared state, human-in-the-loop, multimodal attachments, threads and more.\n\nUse the CLI to scaffold a full starter Angular app with a Google ADK agent.\n\n```\nnpx copilotkit@latest init --framework adk-angular\n```\n\nLet's see how to set everything up, then go through each of the pieces and give your agent the context. Quickstart docs are on [docs.copilotkit.ai/angular](https://docs.copilotkit.ai/angular).\n\nRainer Hahnekamp (Angular GDE, NgRx core) and Murat Sari helped build the integration and are now taking on its ongoing maintenance.\n\nEverything runs on [Agent-User Interaction Protocol (AG-UI)](https://github.com/ag-ui-protocol/ag-ui), the open protocol that connects agents to user-facing apps. It streams an agent's entire lifecycle as events, the messages, the tool calls, the state changes, which is what keeps your Angular app and the agent in sync.\n\nThat matters because the agent becomes a choice you can change. The runtime can point at a `BuiltInAgent`\n\n, LangGraph, Google ADK, Mastra, Pydantic AI, Claude Agents SDK or any framework that speaks AG-UI and your Angular code doesn't change.\n\nHere's the architecture.\n\n```\n┌──────────────────────────┐        ┌──────────────────────────┐\n│  ANGULAR APP             │        │  COPILOT RUNTIME (Node)  │\n│                          │        │                          │\n│  provideCopilotKit()     │ ─────► │  holds your model keys   │\n│  <copilot-chat />        │ AG-UI  │  connects to your agent  │\n│  tools · context · state │ ◄───── │  streams events back     │\n└──────────────────────────┘        └──────────────┬───────────┘\n                                                       │\n                                                       ▼\n                                    ┌───────────────────────────┐\n                                    │  YOUR AGENT + MODEL       │\n                                    │  LangGraph · ADK · Mastra │\n                                    │  OpenAI or a local model  │\n                                    └───────────────────────────┘\n```\n\nYour Angular app is the browser half. The runtime is a small Node server that keeps your model credentials off the client. Everything below the AG-UI line is swappable, including running the model locally.\n\nThe CLI scaffolds all three parts for you, including a runtime server with a Google ADK agent, already wired with generative UI and threads.\n\n```\nnpx copilotkit@latest init --framework adk-angular --name my-angular-adk-app\n```\n\nLet's also do it manually, since it's just four steps and it shows you where each piece goes.\n\nInstall the client along with Angular's CDK. Make sure the CDK version matches your Angular major version.\n\n```\nnpm install @copilotkit/angular @angular/cdk\n```\n\nAngular configures services through providers, so that's where CopilotKit goes. Point it at a [Copilot Runtime](https://docs.copilotkit.ai/backend/copilot-runtime) endpoint, which is where your agent runs.\n\n``` js\n// src/app/app.config.ts\nimport { ApplicationConfig, provideZonelessChangeDetection } from \"@angular/core\";\nimport { provideCopilotKit, provideCopilotChatConfiguration } from \"@copilotkit/angular\";\n\nexport const AGENT_ID = \"default\";\n\nexport const appConfig: ApplicationConfig = {\n  providers: [\n    provideZonelessChangeDetection(),\n    provideCopilotKit({\n      runtimeUrl: \"http://localhost:8200/api/copilotkit\",\n    }),\n    // owns the active thread that the threads drawer drives\n    provideCopilotChatConfiguration({ agentId: AGENT_ID }),\n  ],\n};\n```\n\nNow import the stylesheet. It comes with everything the chat needs, so you don't have to write any CSS.\n\n```\n/* src/styles.css */\n@import \"@copilotkit/angular/styles.css\";\n```\n\nYou can drop in the ready-made chat, compose it from smaller pieces, or go fully headless and build your own UI.\n\n``` js\n// src/app/app.ts\nimport { Component } from \"@angular/core\";\nimport {\n  CopilotChat,\n  CopilotThreadsDrawer,\n  CopilotChatMessageView,\n  CopilotChatInput,\n} from \"@copilotkit/angular\";\nimport { AGENT_ID } from \"./app.config\";\n\n@Component({\n  selector: \"app-root\",\n  imports: [CopilotChat, CopilotThreadsDrawer, CopilotChatMessageView, CopilotChatInput],\n  template: `\n    <!-- pre-built chat component -->\n    <copilot-chat [agentId]=\"AGENT_ID\" />\n\n    <!-- or compose the pieces into your own panel -->\n    <aside class=\"copilot-panel\">\n      <copilot-threads-drawer />\n      <copilot-chat-message-view />\n      <copilot-chat-input />\n    </aside>\n  `,\n})\nexport class App {\n  protected readonly AGENT_ID = AGENT_ID;\n}\n```\n\nFor a fully custom UI, `injectAgentStore()`\n\ngives you the agent's messages and run state as signals, and you render everything yourself. I've covered this in-depth in the shared state section.\n\nThe last piece is the runtime.\n\n``` js\n// server.ts\nimport \"dotenv/config\";\nimport { createServer } from \"node:http\";\nimport { BuiltInAgent, CopilotRuntime } from \"@copilotkit/runtime/v2\";\nimport { createCopilotNodeListener } from \"@copilotkit/runtime/v2/node\";\n\nconst runtime = new CopilotRuntime({\n  agents: {\n    default: new BuiltInAgent({\n      model: \"openai:gpt-5.5\",\n      prompt: \"You are a helpful assistant inside an Angular app.\",\n    }),\n  },\n});\n\ncreateServer(\n  createCopilotNodeListener({ runtime, basePath: \"/api/copilotkit\", cors: true }),\n).listen(8200, () => {\n  console.log(\"Copilot Runtime on http://localhost:8200/api/copilotkit\");\n});\n```\n\nThe agent is named `default`\n\non purpose, since that's the `AGENT_ID`\n\nthe chat is pointed at.\n\nThat's the whole setup. It streams responses, renders tool calls, and keeps conversation state. Read more on the [docs.copilotkit.ai/angular](https://docs.copilotkit.ai/angular).\n\nGenerative UI lets the agent build interfaces in real time. That ranges from the agent picking components you already wrote, to assembling them from a catalog you define, to generating a whole surface on its own.\n\nIf you are interested, read more on the [Generative UI Spectrum blog](https://www.copilotkit.ai/blog/generative-ui-explained-how-agents-now-ship-their-own-interfaces) that covers all the approaches with the tradeoffs.\n\nThe simplest version is the first one, where you write the components and the agent decides which one to render and what data to pass. Start with a normal standalone component that reads the tool call as a signal input.\n\n``` js\n// src/app/weather-card.ts\nimport { ChangeDetectionStrategy, Component, computed, input } from \"@angular/core\";\nimport { z } from \"zod\";\nimport type { AngularToolCall, ToolRenderer } from \"@copilotkit/angular\";\n\nexport const weatherArgs = z.object({ location: z.string() });\ntype WeatherArgs = z.infer<typeof weatherArgs>;\n\n@Component({\n  selector: \"app-weather-card\",\n  standalone: true,\n  changeDetection: ChangeDetectionStrategy.OnPush,\n  template: `\n    <div class=\"wc\">\n      <h3>{{ location() }}</h3>\n      <div class=\"temp\">70°</div>\n      <p>Clear skies</p>\n    </div>\n  `,\n  styles: [\n    `\n      .wc { border-radius: 12px; padding: 16px; color: #fff; background: #6366f1; width: fit-content; }\n      .wc h3 { margin: 0; text-transform: capitalize; }\n      .temp { font-size: 30px; font-weight: 700; }\n    `,\n  ],\n})\nexport class WeatherCard implements ToolRenderer<WeatherArgs> {\n  readonly toolCall = input.required<AngularToolCall<WeatherArgs>>();\n  protected readonly location = computed(\n    () => this.toolCall().args?.location ?? \"Weather\",\n  );\n}\n```\n\nNow register it as the renderer for a tool the agent calls. The cleanest way is declaratively, in the provider config.\n\n``` js\n// src/app/app.config.ts\nimport { provideCopilotKit } from \"@copilotkit/angular\";\nimport { WeatherCard, weatherArgs } from \"./weather-card\";\n\nprovideCopilotKit({\n  runtimeUrl: \"http://localhost:8200/api/copilotkit\",\n  // when the agent calls get_weather, render WeatherCard with its args\n  renderToolCalls: [\n    { name: \"get_weather\", args: weatherArgs, component: WeatherCard },\n  ],\n});\n```\n\nYou can also register it from inside a component with `registerRenderToolCall({ name, args, component })`\n\n, which is handy when the renderer only makes sense on one page.\n\nFor example: ask about the weather in Tokyo and the card appears in the chat. The agent decides when to render it, and your app decides how it looks.\n\nYou can also use [A2UI (Agent-to-UI)](https://docs.copilotkit.ai/generative-ui/a2ui), Google's schema-driven spec for agent-authored interfaces. The agent describes a surface with a schema, and Angular renders it as real components, so it can go beyond the components you registered ahead of time.\n\nMurat Sari published a tutorial on [building an A2UI app in Angular on a local 12B model](https://soverius.ai/blog/inside-macroquest-agent-generated-ui-in-angular-on-a-local-12b-model), which is worth a read if you want to see it end to end.\n\nReal users don't just type. Let them send images, audio, video, and documents to the agent alongside their message.\n\nIt's one input on `<copilot-chat>`\n\n. Pass an `AttachmentsConfig`\n\nand the composer gets an attach button, drag and drop, previews, and validation.\n\n``` js\n// src/app/app.ts\nimport { Component } from \"@angular/core\";\nimport { CopilotChat, type AttachmentsConfig } from \"@copilotkit/angular\";\nimport { AGENT_ID } from \"./app.config\";\n\n@Component({\n  selector: \"app-root\",\n  imports: [CopilotChat],\n  template: `<copilot-chat [agentId]=\"AGENT_ID\" [attachments]=\"attachments\" />`,\n})\nexport class App {\n  protected readonly AGENT_ID = AGENT_ID;\n\n  protected readonly attachments: AttachmentsConfig = {\n    enabled: true,\n    accept: \"image/*\",          // MIME filter, defaults to all files\n    maxSize: 10 * 1024 * 1024,  // bytes, defaults to 20MB\n  };\n}\n```\n\nWith just `{ enabled: true }`\n\n, files are encoded inline and sent along with the message, which is fine while you're building. In production, you don't want a 10MB base64 blob in every request, so hand large files to your own storage with `onUpload`\n\nand give the agent a URL instead.\n\n```\nprotected readonly attachments: AttachmentsConfig = {\n  enabled: true,\n  accept: \"image/*,application/pdf\",\n  maxSize: 20 * 1024 * 1024,\n  // upload to your bucket, hand the agent a URL instead of base64\n  onUpload: async (file) => {\n    const url = await uploadToStorage(file); // your S3, GCS, whatever\n    return { type: \"url\", value: url, mimeType: file.type };\n  },\n  onUploadFailed: (error) => console.error(error.reason, error.message),\n};\n```\n\nThe attachment travels with the message content, so a vision-capable model reads the image the user sent and can act on it through your tools.\n\nSome critical actions shouldn't run on their own like deleting a record or sending an email, so it's important to configure human-in-the-loop. It pauses the agent mid-run and waits for your approval before it continues.\n\nThe component reads the paused tool call, shows the controls, and calls `respond()`\n\nto unblock the agent. Whatever you pass to `respond()`\n\nbecomes the tool result, so the approve and cancel paths send different messages.\n\n``` js\n// src/app/delete-confirm.ts\nimport { Component, inject, input } from \"@angular/core\";\nimport type { HumanInTheLoopToolCall } from \"@copilotkit/angular\";\nimport { TaskStore } from \"./task-store\";\n\n@Component({\n  selector: \"app-delete-confirm\",\n  standalone: true,\n  template: `\n    @if (toolCall().status === \"complete\") {\n      <div class=\"resolved\">{{ toolCall().result }}</div>\n    } @else {\n      <div class=\"confirm\">\n        <p>Delete \"{{ toolCall().args.title }}\"?</p>\n        <button class=\"danger\" (click)=\"approve()\">Delete</button>\n        <button (click)=\"cancel()\">Keep it</button>\n      </div>\n    }\n  `,\n})\nexport class DeleteConfirm {\n  private store = inject(TaskStore);\n  readonly toolCall =\n    input.required<HumanInTheLoopToolCall<{ id: string; title: string }>>();\n\n  approve(): void {\n    const { id, title } = this.toolCall().args as { id: string; title: string };\n    this.store.remove(id);\n    this.toolCall().respond?.(`Deleted \"${title}\".`);\n  }\n\n  cancel(): void {\n    this.toolCall().respond?.(\"User kept the task.\");\n  }\n}\n```\n\nRegister it as a human-in-the-loop tool. There's no handler here, unlike a frontend tool, because the component is the handler. The outcome depends on what the person clicks.\n\n``` js\n// inside a component's constructor (injection context)\nimport { registerHumanInTheLoop } from \"@copilotkit/angular\";\nimport { z } from \"zod\";\nimport { DeleteConfirm } from \"./delete-confirm\";\n\nregisterHumanInTheLoop({\n  name: \"delete_task\",\n  description: \"Delete a task. Requires user confirmation.\",\n  parameters: z.object({ id: z.string(), title: z.string() }),\n  component: DeleteConfirm,\n});\n```\n\nNow, if you ask the agent to delete something, the agent will stop the run and ask for your confirmation before proceeding.\n\nState flows between your app and the agent in two ways, and the difference is who owns it. Either the agent owns the state and syncs it both directions, or your app owns it and exposes it for the agent to read.\n\nThe snippets use [zod](https://zod.dev) for schemas, so install it alongside the client with `npm install zod`\n\n.\n\nThe agent owns a piece of state, your UI reads it, and your UI can write back to it.\n\n`injectAgentStore`\n\ngives you that state as a signal, so components re-render when the agent changes something, and `setState`\n\nsends your changes the other way.\n\n`setState`\n\nreplaces the whole state object instead of merging, so spread what's already there and change only the key you need.\n\n``` js\n// src/app/proverbs.ts\nimport { Component, computed } from \"@angular/core\";\nimport { injectAgentStore } from \"@copilotkit/angular\";\nimport { AGENT_ID } from \"./app.config\";\n\ninterface AgentState {\n  proverbs?: string[];\n}\n\n@Component({\n  selector: \"app-proverbs\",\n  standalone: true,\n  template: `\n    @for (proverb of proverbs(); track $index) {\n      <div class=\"proverb\">\n        <span>{{ proverb }}</span>\n        <button (click)=\"remove($index)\">✕</button>\n      </div>\n    }\n  `,\n})\nexport class Proverbs {\n  readonly #store = injectAgentStore(AGENT_ID);\n\n  // READ: the agent's state as a signal\n  protected readonly proverbs = computed<string[]>(\n    () => (this.#store().state() as AgentState | undefined)?.proverbs ?? [],\n  );\n\n  // WRITE: spread the existing state, setState replaces the whole object\n  protected remove(index: number): void {\n    const state = (this.#store().state() as AgentState | undefined) ?? {};\n    this.#store().agent.setState({\n      ...state,\n      proverbs: this.proverbs().filter((_, i) => i !== index),\n    });\n  }\n}\n```\n\nIf you only need the agent to see what's on screen, use `connectAgentContext`\n\n.\n\nYou pass a function that reads your state, so it works with whatever you already use, plain signals, a service, or a store like NgRx.\n\n``` js\n// src/app/tasks.ts\nimport { Component, computed, inject } from \"@angular/core\";\nimport { connectAgentContext } from \"@copilotkit/angular\";\nimport { TaskStore } from \"./task-store\";\n\n@Component({ selector: \"app-tasks\", standalone: true, template: `...` })\nexport class Tasks {\n  private store = inject(TaskStore);\n\n  constructor() {\n    connectAgentContext(\n      computed(() => ({\n        description: \"The user's current task list.\",\n        value: JSON.stringify(this.store.tasks()),\n      })),\n    );\n  }\n}\n```\n\nNow the agent can see the tasks and reference one by its ID. But it can only read, so let's add a frontend tool that lets it act on the context.\n\nTo let the agent take action based on that context, we need to add a frontend tool. You give it a Zod schema so the arguments are typed, and a handler that runs in the browser.\n\n``` js\n// inside a component's constructor\nimport { registerFrontendTool } from \"@copilotkit/angular\";\nimport { z } from \"zod\";\n\nregisterFrontendTool({\n  name: \"add_task\",\n  description: \"Add a task to the list.\",\n  parameters: z.object({ title: z.string() }),\n  handler: async ({ title }) => this.store.add(title),\n});\n```\n\nNow the user can say \"add a task to record the demo video\" and the agent calls `add_task`\n\n, your handler adds it to the store, and the dashboard updates.\n\nThat's the whole loop. The agent sees your state, changes it through tools, renders UI for the result, and pauses for a person when it matters.\n\n`@copilotkit/angular`\n\nis open source in the [CopilotKit GitHub Repo](https://github.com/CopilotKit/copilotkit), running on Angular 19, 20, and 21.\n\nRainer Hahnekamp and Murat Sari helped build the integration, with [Soverius AI](https://soverius.ai/blog/soverius-ai-is-now-maintaining-copilotkit-for-angular) now taking on its ongoing maintenance in collaboration with CopilotKit.\n\nAngular 22 is next, along with more docs and examples as the SDK closes the remaining gaps with React. If something's missing, open an issue on the CopilotKit repo and we'll pick it up.\n\nConnect with me on [GitHub](https://github.com/Anmol-Baranwal), [Twitter](https://x.com/Anmol_Codes), [LinkedIn](https://www.linkedin.com/in/Anmol-Baranwal/). thanks for reading!\n\nFollow CopilotKit on [Twitter](https://go.copilotkit.ai/socials-twitter). If you get stuck, reach out to the team on the [CopilotKit](https://go.copilotkit.ai/discord) and [AG-UI](https://go.copilotkit.ai/AG-UI-Discord) communities.", "url": "https://wpnews.pro/news/introducing-angular-support-for-copilotkit-bring-any-agent-into-your-app", "canonical_source": "https://dev.to/copilotkit/introducing-angular-support-for-copilotkit-bring-any-agent-into-your-app-2f3e", "published_at": "2026-07-23 15:43:57+00:00", "updated_at": "2026-07-23 16:05:11.918411+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents", "artificial-intelligence"], "entities": ["CopilotKit", "Rainer Hahnekamp", "Murat Sari", "AG-UI", "Google ADK", "LangGraph", "Mastra", "Pydantic AI"], "alternates": {"html": "https://wpnews.pro/news/introducing-angular-support-for-copilotkit-bring-any-agent-into-your-app", "markdown": "https://wpnews.pro/news/introducing-angular-support-for-copilotkit-bring-any-agent-into-your-app.md", "text": "https://wpnews.pro/news/introducing-angular-support-for-copilotkit-bring-any-agent-into-your-app.txt", "jsonld": "https://wpnews.pro/news/introducing-angular-support-for-copilotkit-bring-any-agent-into-your-app.jsonld"}}