UNPKG

@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;" />

356 lines (268 loc) 9.35 kB
# CopilotKit Agent Access (React) This skill builds on `copilotkit/provider-setup`. `useAgent` reads from the same registry the provider populates from `/info`. Two complementary surfaces: - `useAgent` — imperative access to an agent instance, subscribe to messages/state/run-status changes. - `useAgentContext` — declarative push of app state to every agent run. ## Setup ```tsx "use client"; import { useAgent, useAgentContext, UseAgentUpdate, } from "@copilotkit/react-core/v2"; import { useMemo } from "react"; export function ChatDriver({ route, userId, }: { route: string; userId: string; }) { const { agent } = useAgent({ agentId: "default", threadId: "main", updates: [ UseAgentUpdate.OnMessagesChanged, UseAgentUpdate.OnRunStatusChanged, ], throttleMs: 100, }); const context = useMemo(() => ({ route, userId }), [route, userId]); useAgentContext({ description: "app context", value: context }); return ( <div> {agent.isRunning ? "…thinking" : "idle"} — {agent.messages.length}{" "} messages </div> ); } ``` ## Core Patterns ### Send a message and stream the response ```tsx const { agent } = useAgent({ agentId: "default" }); const { copilotkit } = useCopilotKit(); async function ask(text: string) { agent.addMessage({ id: crypto.randomUUID(), role: "user", content: text }); await copilotkit.runAgent({ agent }); } ``` ### Subscribe only to run-status to reduce re-renders ```tsx const { agent } = useAgent({ agentId: "default", updates: [UseAgentUpdate.OnRunStatusChanged], }); const isRunning = agent.isRunning; ``` `useAgent` returns `{ agent, isReady }`; `isRunning` lives on the agent itself. Subscribing to `OnRunStatusChanged` forces a re-render when the value flips, so reading `agent.isRunning` stays live. ### Share app state with every agent run (global) ```tsx const value = useMemo( () => ({ cartItems: cart.items, currentRoute: router.pathname }), [cart.items, router.pathname], ); useAgentContext({ description: "user cart + route", value }); ``` ### Abort the run ```tsx const { agent } = useAgent({ agentId: "default" }); <button onClick={() => agent.abortRun()}>Stop</button>; ``` ### Wait for the real agent before attaching to it ```tsx const { agent, isReady } = useAgent({ agentId: "default" }); useEffect(() => { if (!isReady) return; // provisional stand-in — don't attach yet const sub = agent.subscribe({ onRunStartedEvent: handleRunStarted }); return () => sub.unsubscribe(); }, [agent, isReady]); ``` Until the runtime `/info` sync resolves, `agent` is a provisional stand-in. It is a fully-constructed `AbstractAgent`, so every call on it is safe — but it is then **replaced**, and `agent` changes reference. Anything keyed to the old instance goes with it. ## Common Mistakes ### CRITICAL — Custom `AbstractAgent.clone()` that returns `this` Wrong: ```tsx class MyAgent extends AbstractAgent { clone() { return this; // wrong — same instance is reused across threads } } ``` Correct: ```tsx class MyAgent extends AbstractAgent { clone() { const next = new MyAgent(this.config); next.state = { ...this.state }; return next; } } ``` `useAgent` calls `source.clone()` to build a per-thread clone and throws `clone() must return a new, independent object` if the clone is the same instance. This guards per-thread isolation. Source: `packages/react-core/src/v2/hooks/use-agent.tsx:58-69` ### HIGHDeriving app state from `agent` without guarding on `isReady` Wrong: ```tsx const { agent } = useAgent({ agentId: "default" }); // Correlation map for matching responses back to the row that asked. const pending = useRef(new Map<string, string>()); useEffect(() => { pending.current = new Map(); // re-runs when `agent` is swapped const sub = agent.subscribe({ onRunFinishedEvent: resolvePending }); return () => sub.unsubscribe(); }, [agent]); ``` Correct: ```tsx const { agent, isReady } = useAgent({ agentId: "default" }); // Owned by the component, not by the agent — survives the swap. const pending = useRef(new Map<string, string>()); useEffect(() => { if (!isReady) return; const sub = agent.subscribe({ onRunFinishedEvent: resolvePending }); return () => sub.unsubscribe(); }, [agent, isReady]); ``` `agent` changes reference exactly once per mount, when `/info` resolves and the provisional stand-in is swapped for the real instance. Any effect with `agent` in its dependency array re-runs at that moment — so app state initialized inside such an effect is silently reset partway through the first interaction. This bites hardest with Intelligence configured, because license verification and thread-endpoint discovery lengthen the provisional window past the first user action. In plain SSE mode the window usually closes before anyone can interact, which is why the bug does not reproduce in OSS-only development. Never put per-component bookkeeping (correlation maps, in-flight request records, refs) behind an `agent` dependency. Initialize it in the ref itself and let the effect only manage the subscription. Source: `packages/react-core/src/v2/hooks/use-agent.tsx:226-290,465-481` ### HIGHMutating `agent.messages` directly Wrong: ```tsx agent.messages.push({ id, role: "user", content: "hi" }); ``` Correct: ```tsx agent.addMessage({ id: crypto.randomUUID(), role: "user", content: "hi" }); // or: agent.setMessages([...agent.messages, newMessage]); ``` AG-UI fires `onMessagesChanged` subscribers via `addMessage` / `setMessages`. Direct array mutation bypasses subscribers and the UI never re-renders. Source: `packages/react-core/src/v2/hooks/use-agent.tsx` (throughout) ### HIGHRegistering non-serializable values via `useAgentContext` Wrong: ```tsx useAgentContext({ description: "user", value: { name: "Alice", lastLogin: new Date(), onLogout: () => logout(), // dropped silently }, }); ``` Correct: ```tsx useAgentContext({ description: "user", value: { name: "Alice", lastLogin: new Date().toISOString() }, }); ``` `useAgentContext` runs the value through `JSON.stringify`. Functions are dropped, `Date` coerces to an ISO string (which the agent has to parse), and circular references throw. Source: `packages/react-core/src/v2/hooks/use-agent-context.tsx:30-35` ### MEDIUMExpecting lifecycle callbacks to be throttled Wrong: ```tsx useAgent({ agentId: "default", throttleMs: 300, // expecting onRunInitialized / onRunFinalized / onRunFailed to also be throttled }); ``` Correct: ```tsx // Only OnMessagesChanged / OnStateChanged / OnRunStatusChanged are throttled. // Lifecycle callbacks always fire immediately — handle them synchronously. useAgent({ agentId: "default", throttleMs: 300 }); ``` `throttleMs` only applies to the three subscribed updates enumerated in `UseAgentUpdate`. Lifecycle callbacks bypass the throttler. Source: `packages/react-core/src/v2/hooks/use-agent.tsx:36-48` ### MEDIUMUnstable context value identity Wrong: ```tsx useAgentContext({ description: "cart", value: { items: cart.items } }); ``` Correct: ```tsx const value = useMemo(() => ({ items: cart.items }), [cart.items]); useAgentContext({ description: "cart", value }); ``` A fresh object literal on every render invalidates the `useMemo` inside `useAgentContext` that serializes the value, causing constant remove/re-add churn in the core context store. Source: `packages/react-core/src/v2/hooks/use-agent-context.tsx:30-35` ### MEDIUMExpecting `useAgentContext` or `copilotkit.addContext` to scope context per agent Wrong: ```tsx useAgentContext({ agentId: "research", description: "paper list", value }); // or the imperative form: copilotkit.addContext({ description: "paper list", value: JSON.stringify(value), agentId: "research", }); ``` Correct: ```tsx // Context is global — every agent run sees every registered entry. useAgentContext({ description: "paper list", value }); // When only one agent should key off a value, branch inside its prompt // or tool logic instead of trying to scope the context entry. ``` Context is intentionally global and there is no per-agent scoping hook. `useAgentContext` has no `agentId` parameter, and `copilotkit.addContext` destructures only `{ description, value }` — any `agentId` passed is silently dropped. Treat context as "state of the world" that every agent sees. Source: `packages/react-core/src/v2/hooks/use-agent-context.tsx` (no `agentId` parameter); `packages/core/src/core/context-store.ts:26-31` ### MEDIUM — Two components using the same `(agentId, threadId)` expecting isolation Wrong: ```tsx function A() { const { agent } = useAgent({ agentId: "default", threadId: "t1" }); } function B() { const { agent } = useAgent({ agentId: "default", threadId: "t1" }); } ``` Correct: ```tsx function A() { useAgent({ agentId: "default", threadId: "a" }); } function B() { useAgent({ agentId: "default", threadId: "b" }); } ``` Per-thread clones are cached in a module-level WeakMap keyed by `(registryAgent, threadId)`. Two consumers of the same `(agentId, threadId)` observe the same state. Give each surface a distinct `threadId` when isolation is intentional. Source: `packages/react-core/src/v2/hooks/use-agent.tsx:78-119`