@copilotkit/react-core
Version:
<img src="https://github.com/user-attachments/assets/0a6b64d9-e193-4940-a3f6-60334ac34084" alt="banner" style="border-radius: 12px; border: 2px solid #d6d4fa;" />
232 lines (177 loc) • 5.63 kB
Markdown
# CopilotKit Switching Agents (React)
This skill builds on `copilotkit/agent-access`, `copilotkit/client-side-tools`,
and `copilotkit/rendering-tool-calls`.
Three main patterns:
1. **Parallel panels** — one `useAgent({ agentId })` per surface.
2. **Slot swap** — `<CopilotChat key={agentId} agentId={agentId} />`.
3. **Discovery** — subscribe to `onAgentsChanged` (no `useAgents()` hook).
## Setup
```tsx
"use client";
import { CopilotChat } from "@copilotkit/react-core/v2";
import { useState } from "react";
export function AgentSwitcherChat() {
const [activeAgent, setActiveAgent] = useState("research");
return (
<div>
<div>
<button onClick={() => setActiveAgent("research")}>Research</button>
<button onClick={() => setActiveAgent("coding")}>Coding</button>
</div>
{/* key={activeAgent} forces remount so thread state doesn't leak */}
<CopilotChat key={activeAgent} agentId={activeAgent} />
</div>
);
}
```
## Core Patterns
### Side-by-side chat panels
```tsx
<div className="grid grid-cols-2 gap-4">
<CopilotChat agentId="research" threadId="research-main" />
<CopilotChat agentId="coding" threadId="coding-main" />
</div>
```
### Agent-scoped tool
```tsx
import { useFrontendTool } from "@copilotkit/react-core/v2";
import { z } from "zod";
useFrontendTool({
name: "saveFindings",
agentId: "research", // ← only the research agent sees this tool
parameters: z.object({ summary: z.string() }),
handler: async ({ summary }) => {
await fetch("/api/findings", { method: "POST", body: summary });
},
});
```
### Agent-scoped renderer
```tsx
import { useRenderTool } from "@copilotkit/react-core/v2";
import { z } from "zod";
useRenderTool({
name: "search",
agentId: "research", // ← only applies to research's "search" tool
parameters: z.object({ q: z.string() }),
render: ({ status, parameters, result }) => {
if (status === "inProgress") return <div>Preparing...</div>;
if (status === "executing") return <div>Searching {parameters.q}</div>;
return <div>{result}</div>;
},
});
```
### Discover available agents (no `useAgents` hook)
```tsx
"use client";
import { useCopilotKit } from "@copilotkit/react-core/v2";
import { useEffect, useState } from "react";
export function useAvailableAgents() {
const { copilotkit } = useCopilotKit();
const [ids, setIds] = useState<string[]>(() =>
Object.keys(copilotkit.agents ?? {}),
);
useEffect(() => {
const subscription = copilotkit.subscribe({
onAgentsChanged: ({ agents }) => {
setIds(Object.keys(agents ?? {}));
},
});
return () => subscription.unsubscribe();
}, [copilotkit]);
return ids;
}
```
## Common Mistakes
### HIGH — Switching `agentId` on a persisted `<CopilotChat>` without `key`
Wrong:
```tsx
<CopilotChat agentId={activeAgent} />
```
Correct:
```tsx
<CopilotChat key={activeAgent} agentId={activeAgent} />
```
Without remount via `key`, prior thread state and in-flight runs leak into
the new agent's view. The remount pattern gives each agent a clean slate.
Source: `examples/v2/react-router/app/routes/_index.tsx:38-39`
### MEDIUM — Omitting `agentId` when multiple agents share a tool name
Wrong:
```tsx
// Both research and coding agents have a "search" tool — unscoped wins globally
useRenderToolCall({
name: "search",
args: z.object({ q: z.string() }),
render,
});
```
Correct:
```tsx
useRenderTool({
name: "search",
agentId: "research",
parameters: z.object({ q: z.string() }),
render: researchSearchRender,
});
useRenderTool({
name: "search",
agentId: "coding",
parameters: z.object({ q: z.string() }),
render: codingSearchRender,
});
```
Unscoped renderers apply to every agent. When two agents have a tool with
the same name and only one has a renderer, the unscoped renderer wins
globally and the other agent never gets its intended renderer.
Source: `packages/react-core/src/v2/hooks/use-render-tool-call.tsx:145-154`
### MEDIUM — Tools registered without `agentId` leak across panels
Wrong:
```tsx
useFrontendTool({
name: "saveFindings",
parameters: z.object({ summary: z.string() }),
handler,
});
// Both research and coding agents now see saveFindings.
```
Correct:
```tsx
useFrontendTool({
name: "saveFindings",
agentId: "research",
parameters: z.object({ summary: z.string() }),
handler,
});
```
Omitting `agentId` attaches the tool to every agent. In a multi-agent UI
this leaks the handler across panels. Scope tools explicitly when they
should only apply to one agent.
Source: `packages/react-core/src/v2/hooks/use-frontend-tool.tsx`
### MEDIUM — Using `useAgents()` (does not exist)
Wrong:
```tsx
import { useAgents } from "@copilotkit/react-core/v2"; // not exported
const agents = useAgents();
```
Correct:
```tsx
import { useCopilotKit } from "@copilotkit/react-core/v2";
import { useEffect, useState } from "react";
function useAvailableAgents() {
const { copilotkit } = useCopilotKit();
const [ids, setIds] = useState<string[]>(() =>
Object.keys(copilotkit.agents ?? {}),
);
useEffect(() => {
const sub = copilotkit.subscribe({
onAgentsChanged: ({ agents }) => setIds(Object.keys(agents ?? {})),
});
return () => sub.unsubscribe();
}, [copilotkit]);
return ids;
}
```
There is no `useAgents` hook in v2. Discover agents by subscribing to
`onAgentsChanged` on the core client.
Source: `packages/react-core/src/v2/hooks/index.ts` (no `useAgents` export)
## References
- [Agent switcher recipes](switching-agents-recipes.md) — dropdown, tabs, keyboard shortcuts