Back to Copilotkit

Angular + Google ADK

showcase/shell-docs/src/content/docs/cookbook/angular-adk-agentic-app.mdx

1.63.210.6 KB
Original Source

This recipe assembles a production agentic app from three pieces: an Angular frontend built with @copilotkit/angular, a Google ADK agent served over the open AG-UI protocol, and an optional CopilotKit Intelligence layer for persistent threads and cross-session memory.

Start with these two quickstarts:

This recipe covers production concerns that arise after the two quickstarts are connected: shared chat state, request identity, model choice, server-side policy, threads, and memory.

How the pieces fit

The app has three processes:

  1. The Angular client renders chat and generative UI and runs the agent.
  2. A runtime / backend-for-frontend (BFF) wires the Copilot Runtime, resolves the per-request user, enforces tool policy, proxies memory, and forwards each run to your agent. This layer handles identity, policy, and memory access.
  3. The Google ADK agent does the reasoning and emits generative UI. An optional deterministic specialist agent handles fixed payloads.
<Callout type="info" title="Validate identity and policy in the BFF"> Treat data from the browser as untrusted. Resolve the authenticated user, enforce tool policy, and scope memory requests on the server. </Callout>

Share agent state across the chat surface

Calls to injectAgentStore("default") resolve the same underlying agent. Components that use the same agent id observe the same messages and run state.

ts
import { Component } from "@angular/core";
import { CopilotChat, injectAgentStore } from "@copilotkit/angular";

@Component({
  selector: "app-chat",
  imports: [CopilotChat],
  template: `<copilot-chat agentId="default" />`,
  // Give the surface a bounded height to keep the composer visible on long threads.
  styles: [`:host { display: block; height: 100%; min-height: 0; }`],
})
export class ChatComponent {
  readonly agentStore = injectAgentStore("default");
}
<Callout type="info" title="Use one owner for custom submission logic"> A second composer may inject the same agent id and will observe the same agent. For consistent validation, attachments, and message submission, pass one submit function or service to each custom composer. </Callout> <Callout type="warn" title="Do not fetch history for a brand-new thread"> **Symptom:** starting a new conversation throws before the first message. **Cause:** a fresh thread id has no messages, so the history request errors. **Fix:** only fetch history for an existing, user-selected thread. Run new conversations on a fresh id, and reload stored messages only when the bound thread id changes. </Callout> <Callout type="warn" title="Change runtime configuration between runs"> The runtime URL and request headers take part in agent resolution. Apply configuration changes before a run starts. Put non-secret values that change on each request in the run body instead. </Callout>

Send changing context with each run

<Callout type="warn" title="Do not use browser context as authorization"> A user id sent by the browser is untrusted. Resolve the authenticated user on the server. Use run context only to give the agent current, non-secret application state, and validate any identity value against the server session before using it to scope data. </Callout>

Use connectAgentContext to send current, non-secret application state with each run:

ts
import { Component, signal } from "@angular/core";
import { connectAgentContext } from "@copilotkit/angular";

@Component({ /* ... */ })
export class ChatComponent {
  readonly userId = signal(currentUserId);

  constructor() {
    // Re-registers reactively when the signal changes (e.g. an in-session user switch).
    connectAgentContext(() => ({ description: "userId", value: this.userId() }));
  }
}

Do not use this browser value as proof of identity. The BFF must compare it with the authenticated session or replace it with the server identity. If an ADK tool needs the validated user id, map that value into ADK session state in the BFF or agent adapter. The tool can then read the application-defined state key:

python
def _user_id(tool_context) -> str | None:
    return tool_context.state.get("user_id")

def recall_for_user(tool_context) -> dict:
    user_id = _user_id(tool_context)
    # ...scope every read and write to user_id
    return {"ok": True}

The exact mapping belongs to your server bridge. Keep it explicit and test it with the version of the ADK adapter you deploy.

Choose a capable model

ADK uses Google's Gemini by default and also supports other models. See the ADK quickstart for configuration.

  • Prefer a current model with reliable instruction following and tool use.
  • Test older or smaller models against your full tool and policy flow. They may skip steps in multi-part instructions.
  • Use a reasoning model when the task needs it, and account for its added latency and cost.
<Callout type="tip" title="Check model access with your provider"> Confirm a model id actually exists for your account before wiring it in. On Gemini, for example: ```bash curl "https://generativelanguage.googleapis.com/v1beta/models?key=$GOOGLE_API_KEY" | grep '"name"' ``` </Callout>

Two more agent-side habits:

  • Keep MCP and tool timeouts short. A long timeout lets a slow external tool server delay the first reply. A shorter timeout reports the failure sooner.
  • Emit fixed generated-UI payloads from code. A model may change JSON even when asked to return it unchanged.

Enforce governance on the server

<Callout type="warn" title="A client-side tool guard is presentation only"> **Symptom:** a tool you "hid" in the UI still gets called by the agent. **Cause:** the browser is untrusted, and hiding a tool in the UI does nothing to the agent's tool list. **Fix:** on the BFF, strip disallowed tools from the inbound run body so the model cannot call them, and filter the outbound generative-UI stream. Emit a single governance receipt rather than silently dropping output. </Callout>
  • Include the per-request allow-list in the run body. A capability change then takes effect on the next run.
  • Normalize capability-name casing (for example pieChart versus PieChart) in both the inbound and outbound checks. A casing mismatch can bypass a policy check.
  • Let generative-UI cards reflow, do not truncate. Long dynamic labels overflow fixed-size cards. Let the layout grow rather than clipping content.

Add persistent threads and memory (optional)

CopilotKit Intelligence adds persistent threads and durable cross-session memory. Handle a missing or unreachable platform without failing every request:

  • Auto-detect: at startup, probe the platform health endpoint with a short timeout. If it is unreachable or unlicensed, fall back to an in-memory runner.
  • Configurable endpoint: use one base-URL environment variable for the runtime, the agent's memory client, and the health probe. Check the endpoint again after a restart.

Memory tools call the platform's streaming JSON-RPC /mcp endpoint, scoped per user:

http
POST {INTELLIGENCE_API_URL}/mcp
Authorization: Bearer <key>
X-Cpki-User-Id: <user_id>
Accept: application/json, text/event-stream

{"jsonrpc":"2.0","id":1,"method":"tools/call",
 "params":{"name":"recall_memory","arguments":{}}}

The response is a server-sent event stream. Read the JSON-RPC body from the first data: line and join the text content. Writes may include an embedding step and can take several seconds, so run them asynchronously and set a suitable timeout.

<Callout type="warn" title="Query each supported memory scope"> **Symptom:** the agent clearly remembers things a custom memory panel never shows. **Cause:** recall often defaults to *user* scope and caps at a small top-N, so a panel that hard-codes user scope hides project- or team-scoped memories. **Fix:** if you build a memory browser, query both user and project scopes, de-duplicate, and merge. </Callout> <Callout type="info" title="Self-hosting the platform"> Running CopilotKit Intelligence yourself has its own setup notes (license verification and the embedding service). See [Self-hosting](/premium/self-hosting). </Callout>

Production checklist

  1. Use one agent id across the chat surface. Share one submit function or service across custom composers.
  2. Resolve identity on the server. Send changing, non-secret application context with each run.
  3. Apply runtime URL and header changes between runs.
  4. Enforce tool policy on the server. Strip disallowed tools from inbound runs and filter outbound generated UI.
  5. Test a current agentic model against your full tool and policy flow, and confirm that your provider offers it.

Going further

  • Local dev: only your frontend dev server hot-reloads. A tsx/node BFF and a Python agent do not, so restart them after edits. Give each service a stable, non-colliding port and write them down.
  • Custom chat UI: Build welcome and empty states in your application, customize message rendering through messageViewComponent, and iterate the full message list when you need to interleave generated UI. See the Angular guide.
  • Threads in depth: see Threads explained.

Get started with a coding agent

Paste this into your coding agent (Cursor, Claude Code, etc.) once you have the Angular and ADK quickstarts running:

text
Update my Angular + Google ADK + CopilotKit app for multi-user production:

1. Use one agent id across the chat surface. Calls to `injectAgentStore("default")` resolve the
   same agent. Route custom composers through one shared submit function or service.
2. Resolve the authenticated user on the BFF. If ADK tools need that id, map the server-validated
   value into an application-defined ADK session-state key. Do not trust browser context alone.
3. Apply runtime config changes (runtimeUrl/headers) between runs. Carry changing, non-secret
   application context in the run body.
4. Enforce tool policy on the server: strip disallowed tools from the inbound run body and filter the
   outbound generative-UI stream. Include the allow-list in the run body, and normalize tool-name casing.
5. Give the chat surface a bounded height, and only fetch thread history for an existing thread id.

Keep my existing Angular and ADK setup otherwise unchanged.