skills/copilotkit-upgrade/references/v1-to-v2-migration.md
The @copilotkit/* package names are unchanged in v2 -- the v2 APIs ship from the /v2 subpath of the same packages. There is no @copilotkit/react or @copilotkit/agent package; do not install them. You only need to remove the packages that no longer have a v2 surface and upgrade the rest to their latest versions:
# Remove packages with no v2 surface (uninstall @copilotkit/react-textarea only
# AFTER you have migrated off CopilotTextarea -- the v1 package still exists and
# stays installable for backward compatibility)
npm uninstall @copilotkit/runtime-client-gql @copilotkit/sdk-js
# Upgrade the packages you keep to their latest (v2) versions.
# `hono` is only needed for the Hono endpoint (createCopilotHonoHandler); `zod` is
# the peer dep used for tool parameter schemas.
npm install @copilotkit/react-core@latest @copilotkit/runtime@latest @copilotkit/shared@latest zod
Package mapping:
| v1 Package | v2 Package | Notes |
|---|---|---|
@copilotkit/react-core | @copilotkit/react-core/v2 | Same package; v2 hooks, provider, types, and chat components live under the /v2 subpath |
@copilotkit/react-ui | @copilotkit/react-core/v2 | Chat components moved into react-core/v2; react-ui contributes only styles in v2 |
@copilotkit/react-textarea | -- | No v2 equivalent; the v1 @copilotkit/[email protected] package stays installable -- drop it only after migrating off CopilotTextarea |
@copilotkit/runtime | @copilotkit/runtime/v2 | Same package; v2 runtime/agents live under the /v2 subpath |
@copilotkit/runtime-client-gql | @ag-ui/client | Re-exported by @copilotkit/react-core/v2 |
@copilotkit/shared | @copilotkit/shared | Utility types and constants |
@copilotkit/sdk-js | @copilotkit/runtime/v2 | BuiltInAgent and agent definitions now ship from runtime/v2 |
Find-and-replace import paths across your codebase:
@copilotkit/react-core -> @copilotkit/react-core/v2
@copilotkit/react-ui -> @copilotkit/react-core/v2 (plus import "@copilotkit/react-core/v2/styles.css")
@copilotkit/runtime -> @copilotkit/runtime/v2 (Express helpers: @copilotkit/runtime/v2/express)
@copilotkit/shared -> @copilotkit/shared
The provider component keeps the name CopilotKit -- only the import path changes. The package root (@copilotkit/react-core) is the legacy v1 provider; the /v2 subpath (@copilotkit/react-core/v2) is the migration target.
CopilotKit from the package rootimport { CopilotKit } from "@copilotkit/react-core";
function App() {
return (
<CopilotKit runtimeUrl="/api/copilotkit">
<MyApp />
</CopilotKit>
);
}
CopilotKit from @copilotkit/react-core/v2import { CopilotKit } from "@copilotkit/react-core/v2";
function App() {
return (
<CopilotKit runtimeUrl="/api/copilotkit">
<MyApp />
</CopilotKit>
);
}
Key differences:
/v2 subpathCopilotKitProps (also exported from /v2); it extends Omit<CopilotKitProviderProps, "children"> (with a narrowed children type), so every non-children CopilotKitProvider prop also works on itcredentials, selfManagedAgents, renderToolCalls, renderActivityMessages propsagents prop (use selfManagedAgents or agents__unsafe_dev_only for local dev)Note:
@copilotkit/react-core/v2also exports aCopilotKitProvidercomponent. Do not migrate to it -- it is a functionality subset ofCopilotKit, which is the compatibility bridge across v1 and v2.
v1:
import { useCopilotAction } from "@copilotkit/react-core";
useCopilotAction({
name: "addTodo",
description: "Add a new todo item",
parameters: [
{
name: "title",
type: "string",
description: "Todo title",
required: true,
},
{ name: "priority", type: "number", description: "Priority 1-5" },
],
handler: ({ title, priority }) => {
addTodo({ title, priority: priority ?? 3 });
},
render: ({ status, args }) => (
<div>
Adding: {args.title} (status: {status})
</div>
),
});
v2:
import { useFrontendTool } from "@copilotkit/react-core/v2";
import { z } from "zod";
useFrontendTool({
name: "addTodo",
description: "Add a new todo item",
parameters: z.object({
title: z.string().describe("Todo title"),
priority: z.number().optional().describe("Priority 1-5"),
}),
handler: async (args) => {
addTodo({ title: args.title, priority: args.priority ?? 3 });
},
render: ({ status, args }) => (
<div>
Adding: {args.title} (status: {status})
</div>
),
});
Key differences:
useCopilotAction to useFrontendTool{ name, type, required })handler receives the full args object directly (not destructured from { arg1, arg2 })render prop works similarly but status is now a ToolCallStatus enum member (ToolCallStatus.InProgress / .Executing / .Complete, whose values are "inProgress" / "executing" / "complete")disabled boolean with available -- also a boolean (defaults to true; set false to hide the tool)agentId prop to scope a tool to a specific agentv1:
import { useCopilotReadable } from "@copilotkit/react-core";
function EmployeeList({ employees }) {
useCopilotReadable({
description: "The list of employees",
value: employees,
});
return <div>...</div>;
}
v2:
import { useAgentContext } from "@copilotkit/react-core/v2";
function EmployeeList({ employees }) {
useAgentContext({
description: "The list of employees",
value: employees,
});
return <div>...</div>;
}
Key differences:
useCopilotReadable to useAgentContextparentId hierarchical context feature from v1 is not available in v2; flatten your context insteadvalue prop accepts JsonSerializable (string, number, boolean, null, arrays, objects) -- objects are auto-serialized to JSON stringsv1:
import { useMakeCopilotDocumentReadable } from "@copilotkit/react-core";
useMakeCopilotDocumentReadable(documentPointer, ["category1"]);
v2:
import { useAgentContext } from "@copilotkit/react-core/v2";
useAgentContext({
description: "Document content for category1",
value: documentContent,
});
Key differences:
DocumentPointer equivalent in v2; pass document content directly via useAgentContextdescription field to provide contextid (used for parentId chaining); useAgentContext returns nothing, and hierarchical context via parentId is gone -- flatten insteadv1:
import { useCoAgent } from "@copilotkit/react-core";
type AgentState = { count: number };
const { name, state, setState, running, start, stop, run } =
useCoAgent<AgentState>({
name: "my-agent",
initialState: { count: 0 },
});
v2:
import { useAgent } from "@copilotkit/react-core/v2";
const agent = useAgent({ agentId: "my-agent" });
// Access agent state, messages, run status through the AbstractAgent interface
// agent.run(), agent.stop(), etc.
Key differences:
useCoAgent to useAgentname prop renamed to agentIdinitialState / setState -- agent state is managed through AG-UI protocol events (StateSnapshotEvent, StateDeltaEvent)AbstractAgent instance instead of a destructured state objectrun / start / stop API surface differs -- v2 uses AG-UI protocol methodsv1:
import { useCoAgentStateRender } from "@copilotkit/react-core";
useCoAgentStateRender<YourAgentState>({
name: "basic_agent",
nodeName: "search_node",
render: ({ status, state, nodeName }) => (
<SearchProgress state={state} status={status} />
),
});
v2:
import { useRenderTool } from "@copilotkit/react-core/v2";
// Register a render-only renderer for a tool BY NAME (the declarative successor):
useRenderTool({
name: "basic_agent",
render: ({ name, args, status, result }) => (
<SearchProgress args={args} status={status} />
),
});
Key differences:
useRenderTool({ name, render }) -- a declarative, render-only registration keyed by tool name (no handler). If you also need execution behavior, use useFrontendTool, which accepts both a handler and a render.useRenderActivityMessage() -- a zero-arg hook returning { renderActivityMessage, findRenderer }.useRenderToolCall() is the lower-level imperative hook (zero-arg, returns a renderToolCall({ toolCall, toolMessage }) function) used internally by the chat views; prefer useRenderTool for migration.nodeName filtering; renderers match by tool name (with an agentId tie-break and a "*" wildcard fallback).ToolCall / ToolMessage types instead of the v1 agent state shape.v1:
import { useLangGraphInterrupt } from "@copilotkit/react-core";
useLangGraphInterrupt({
name: "confirm-action",
nodeName: "confirmation_node",
agentName: "my-agent",
render: ({ event, resolve }) => (
<ConfirmDialog
message={event.value}
onConfirm={() => resolve("confirmed")}
onCancel={() => resolve("cancelled")}
/>
),
});
v2:
import { useInterrupt } from "@copilotkit/react-core/v2";
const interruptElement = useInterrupt({
renderInChat: false, // false = you render it yourself; true = renders in CopilotChat
render: ({ event, resolve }) => (
<ConfirmDialog
message={event.value}
onConfirm={() => resolve("confirmed")}
onCancel={() => resolve("cancelled")}
/>
),
handler: async ({ event }) => {
// Optional: handle programmatically before render
},
enabled: (event) => event.value?.type === "confirm", // Optional filter
agentId: "my-agent",
});
// If renderInChat is false, render interruptElement in your UI:
return <div>{interruptElement}</div>;
Key differences:
useLangGraphInterrupt to useInterruptagentName renamed to agentIdnodeName filtering removed; use the enabled predicate insteadrenderInChat prop (default true) -- when true, interrupt UI renders inside <CopilotChat> automaticallyhandler for programmatic interrupt handling before renderingReact.ReactElement | null when renderInChat: falsev1:
import { useCopilotChat } from "@copilotkit/react-core";
import { TextMessage, MessageRole } from "@copilotkit/runtime-client-gql";
const { appendMessage, visibleMessages, isLoading, stopGeneration, reset } =
useCopilotChat();
await appendMessage(
new TextMessage({ role: MessageRole.User, content: "Hello" }),
);
v2:
import { useAgent } from "@copilotkit/react-core/v2";
const agent = useAgent({ agentId: "my-agent" });
// Messages, run status, etc. are available through the agent's AG-UI event stream
// Chat UI components (CopilotChat, CopilotPopup, CopilotSidebar) handle this automatically
Key differences:
useCopilotChat is replaced by useAgent for agent interactionTextMessage/MessageRole (GraphQL) to AG-UI event typesAbstractAgent API directlyv1:
import { useCopilotChatSuggestions } from "@copilotkit/react-core";
useCopilotChatSuggestions({
instructions: "Suggest helpful actions based on the current page",
maxSuggestions: 3,
});
v2:
import {
useConfigureSuggestions,
useSuggestions,
} from "@copilotkit/react-core/v2";
// Configure suggestion generation:
useConfigureSuggestions({
instructions: "Suggest helpful actions based on the current page",
maxSuggestions: 3,
});
// Read suggestions:
const { suggestions, reloadSuggestions, clearSuggestions, isLoading } =
useSuggestions({
agentId: "my-agent",
});
Key differences:
useConfigureSuggestions (write config) and useSuggestions (read state)useSuggestions returns { suggestions, reloadSuggestions, clearSuggestions, isLoading }v1:
import { useCopilotAdditionalInstructions } from "@copilotkit/react-core";
useCopilotAdditionalInstructions({
instructions: "Do not answer questions about the weather.",
});
v2:
import { useAgentContext } from "@copilotkit/react-core/v2";
useAgentContext({
description: "Additional instructions for the agent",
value: "Do not answer questions about the weather.",
});
v1:
import { useHumanInTheLoop } from "@copilotkit/react-core";
v2:
import { useHumanInTheLoop } from "@copilotkit/react-core/v2";
The API is similar -- registers a tool that pauses for user input via a render function with a respond callback.
import {
CopilotRuntime,
OpenAIAdapter,
GoogleGenerativeAIAdapter,
} from "@copilotkit/runtime";
import { copilotRuntimeNextJSAppRouterEndpoint } from "@copilotkit/runtime"; // Next.js App Router
const serviceAdapter = new OpenAIAdapter({ model: "gpt-4o" });
const runtime = new CopilotRuntime({
actions: [
{
name: "lookupWeather",
description: "Look up the weather",
parameters: [{ name: "city", type: "string" }],
handler: async ({ city }) => fetchWeather(city),
},
],
remoteEndpoints: [
{ url: "http://localhost:8000/copilotkit", type: "langgraph" },
],
});
// Next.js App Router
export const POST = copilotRuntimeNextJSAppRouterEndpoint({
runtime,
serviceAdapter,
endpoint: "/api/copilotkit",
});
import {
CopilotRuntime,
BuiltInAgent,
createCopilotHonoHandler,
} from "@copilotkit/runtime/v2";
import { LangGraphAgent } from "@copilotkit/runtime/langgraph";
const runtime = new CopilotRuntime({
agents: {
default: new BuiltInAgent({
model: "openai/gpt-4o",
// Frontend tools are registered client-side via useFrontendTool
}),
myLangGraphAgent: new LangGraphAgent({
deploymentUrl: "http://localhost:8000",
graphId: "my-graph",
}),
},
// Optional middleware
a2ui: {}, // A2UI middleware config
mcpApps: {
// MCP Apps middleware -- each server config is flat (type + url at the top level)
servers: [{ type: "sse", url: "http://localhost:3001/sse" }],
},
});
// Hono-based endpoint. `createCopilotEndpoint` is a deprecated alias for
// `createCopilotHonoHandler`. For Express, use `createCopilotExpressHandler`
// from "@copilotkit/runtime/v2/express".
const app = createCopilotHonoHandler({
runtime,
basePath: "/api/copilotkit",
});
// Standalone Hono: `export default app`.
// Next.js App Router (app/api/copilotkit/route.ts): export the fetch handler instead:
// export const POST = app.fetch;
// export const GET = app.fetch;
export default app;
Key differences:
OpenAIAdapter, LangChainAdapter, etc.) -- model selection is done inside agentsactions array on the runtime -- frontend tools are registered via useFrontendTool, backend tools via agent configurationremoteEndpoints -- agents are passed directly as AbstractAgent instancescreateCopilotHonoHandler (Hono, from @copilotkit/runtime/v2; also exported as the deprecated alias createCopilotEndpoint) or createCopilotExpressHandler (Express, from @copilotkit/runtime/v2/express) instead of framework-specific integrations// SSE Mode (default -- stateless, server-sent events)
const runtime = new CopilotRuntime({
agents: { default: myAgent },
});
// Intelligence Mode (durable threads, realtime, requires CopilotKitIntelligence)
import { CopilotKitIntelligence } from "@copilotkit/runtime/v2";
const runtime = new CopilotRuntime({
agents: { default: myAgent },
intelligence: new CopilotKitIntelligence({
/* config */
}),
identifyUser: (request) => ({ id: "user-123" }),
generateThreadNames: true,
});
Chat components have the same names but move to @copilotkit/react-core/v2:
v1:
import {
CopilotChat,
CopilotPopup,
CopilotSidebar,
} from "@copilotkit/react-ui";
v2:
import {
CopilotChat,
CopilotPopup,
CopilotSidebar,
} from "@copilotkit/react-core/v2";
v2 adds new chat sub-components for granular customization:
CopilotChatView -- the main chat viewCopilotChatInput -- input areaCopilotChatAssistantMessage / CopilotChatUserMessage -- message componentsCopilotChatReasoningMessage -- reasoning/thinking displayCopilotChatToolCallsView -- tool call renderingCopilotChatSuggestionView / CopilotChatSuggestionPill -- suggestion UICopilotChatToggleButton -- toggle button for popup/sidebarCopilotSidebarView / CopilotPopupView -- layout containersCopilotModalHeader -- header for modal layoutsCopilotTextarea from @copilotkit/react-textarea has no v2 equivalent. If you were using it for AI-assisted text input, replace it with:
<textarea> or rich text editoruseFrontendTool hook to provide AI writing assistanceCopilotChat in a sidebar/popup for inline AI helpv1 used GraphQL-based message types from @copilotkit/runtime-client-gql:
import { TextMessage, MessageRole } from "@copilotkit/runtime-client-gql";
v2 uses AG-UI protocol types from @ag-ui/client (re-exported by @copilotkit/react-core/v2):
import {
Message, // the message union
AssistantMessage, // per-role message types (UserMessage, SystemMessage, ToolMessage, ...)
ToolCall,
ToolMessage,
EventType,
} from "@copilotkit/react-core/v2"; // re-exports from @ag-ui/client
v2 has no standalone
TextMessagetype (that was the v1 GraphQL name). Use theMessageunion or the per-role types (AssistantMessage/UserMessage/ ...); streamed text arrives via events such asTextMessageChunkEvent.