showcase/shell-docs/src/content/docs/cookbook/angular-adk-agentic-app.mdx
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.
The app has three processes:
Calls to injectAgentStore("default") resolve the same underlying agent. Components that use the same agent id observe the same messages and run state.
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");
}
Use connectAgentContext to send current, non-secret application state with each run:
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:
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.
ADK uses Google's Gemini by default and also supports other models. See the ADK quickstart for configuration.
Two more agent-side habits:
pieChart versus PieChart) in both the inbound and outbound checks. A casing mismatch can bypass a policy check.CopilotKit Intelligence adds persistent threads and durable cross-session memory. Handle a missing or unreachable platform without failing every request:
Memory tools call the platform's streaming JSON-RPC /mcp endpoint, scoped per user:
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.
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.messageViewComponent, and iterate the full message list when you need to interleave generated UI. See the Angular guide.Paste this into your coding agent (Cursor, Claude Code, etc.) once you have the Angular and ADK quickstarts running:
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.