Help us improve
Share bugs, ideas, or general feedback.
From copilotkit
Scaffolds and wires a CopilotKit v2 app — mounts runtime, provider, chat, and first tool. Loads when bootstrapping a new CopilotKit app or adding runtime to an existing React app.
npx claudepluginhub copilotkit/copilotkit --plugin copilotkitHow this skill is triggered — by the user, by Claude, or both
Slash command
/copilotkit:0-to-working-chatThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
One agent, one tool, one chat. The React Router v7 framework-mode branch is
Guides CopilotKit setup in React projects with Next.js (App/Pages), Vite+React, or Angular: detects framework, installs packages, wires runtime (Hono/Express), configures AI providers, adds chat UI.
Delivers provider-agnostic type-safe LLM chat SDK with streaming, tools, agent loops for OpenAI, Anthropic, Gemini, Ollama in React, Solid, Next.js apps.
Provides Vercel AI SDK v5 React hooks (useChat, useCompletion, useObject) for streaming AI chat UIs in Next.js apps. Fixes parse stream errors, no responses, and streaming issues.
Share bugs, ideas, or general feedback.
One agent, one tool, one chat. The React Router v7 framework-mode branch is the canonical example — pick it first unless you're on a different stack.
npx copilotkit create -f react-router my-app
cd my-app
pnpm install
Create a catch-all resource route app/routes/api.copilotkit.$.tsx. React
Router v7 framework mode runs its own server — mounting the runtime as a
loader+action in a resource route is the canonical pattern. Do NOT spin up
a sidecar Express or Hono server.
import type { Route } from "./+types/api.copilotkit.$";
import {
CopilotRuntime,
createCopilotRuntimeHandler,
InMemoryAgentRunner,
BuiltInAgent,
convertInputToTanStackAI,
} from "@copilotkit/runtime/v2";
import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
const tanstackAgent = new BuiltInAgent({
type: "tanstack",
factory: ({ input, abortController }) => {
const { messages, systemPrompts } = convertInputToTanStackAI(input);
return chat({
adapter: openaiText("gpt-4o"),
messages,
systemPrompts,
abortController,
});
},
});
const runtime = new CopilotRuntime({
agents: { default: tanstackAgent },
runner: new InMemoryAgentRunner(),
});
const handler = createCopilotRuntimeHandler({
runtime,
basePath: "/api/copilotkit",
});
export async function loader({ request }: Route.LoaderArgs) {
return handler(request);
}
export async function action({ request }: Route.ActionArgs) {
return handler(request);
}
app/routes/_index.tsx)import { useState } from "react";
import {
CopilotKitProvider,
CopilotChat,
useFrontendTool,
} from "@copilotkit/react-core/v2";
import "@copilotkit/react-core/v2/styles.css";
import { z } from "zod";
function RegisterTools() {
useFrontendTool({
name: "getCurrentLocation",
description: "Return the user's current location name.",
parameters: z.object({}),
handler: async () => ({ city: "San Francisco", country: "US" }),
});
return null;
}
export default function Index() {
return (
<CopilotKitProvider runtimeUrl="/api/copilotkit" showDevConsole="auto">
<RegisterTools />
<div className="h-screen">
<CopilotChat
agentId="default"
className="h-full"
attachments={{ enabled: true }}
/>
</div>
</CopilotKitProvider>
);
}
That's the quickstart. Run pnpm dev; visit the app; the chat connects to
/api/copilotkit/info, the agent runs, the tool fires.
No dedicated helper — mount createCopilotRuntimeHandler in a Start server
route's Request handler.
// app/routes/api/copilotkit.$.ts
import { createAPIFileRoute } from "@tanstack/react-start/api";
import {
CopilotRuntime,
createCopilotRuntimeHandler,
BuiltInAgent,
convertInputToTanStackAI,
} from "@copilotkit/runtime/v2";
import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
const runtime = new CopilotRuntime({
agents: {
default: new BuiltInAgent({
type: "tanstack",
factory: ({ input, abortController }) => {
const { messages, systemPrompts } = convertInputToTanStackAI(input);
return chat({
adapter: openaiText("gpt-4o"),
messages,
systemPrompts,
abortController,
});
},
}),
},
});
const handler = createCopilotRuntimeHandler({
runtime,
basePath: "/api/copilotkit",
});
export const APIRoute = createAPIFileRoute("/api/copilotkit/$")({
GET: ({ request }) => handler(request),
POST: ({ request }) => handler(request),
});
// app/api/copilotkit/[[...slug]]/route.ts
//
// Optional catch-all ([[...slug]]) so the bare /api/copilotkit basePath
// and every sub-path (/info, /agent/*/run, /threads, etc.) all route to
// this handler. A non-optional [...slug] 404s the bare basePath.
import {
CopilotRuntime,
createCopilotRuntimeHandler,
BuiltInAgent,
convertInputToTanStackAI,
} from "@copilotkit/runtime/v2";
import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
const runtime = new CopilotRuntime({
agents: {
default: new BuiltInAgent({
type: "tanstack",
factory: ({ input, abortController }) => {
const { messages, systemPrompts } = convertInputToTanStackAI(input);
return chat({
adapter: openaiText("gpt-4o"),
messages,
systemPrompts,
abortController,
});
},
}),
},
});
const handler = createCopilotRuntimeHandler({
runtime,
basePath: "/api/copilotkit",
});
export const GET = handler;
export const POST = handler;
Hoist the runtime + handler to module scope and construct them lazily on
first request. Workers isolates reuse module globals across requests, so
a let-cached instance persists in-memory runner state within the isolate
(this does NOT span isolates — for durable cross-isolate state, pair with
SqliteAgentRunner or Intelligence). Constructing new CopilotRuntime(...)
inside fetch(request, env) on every call wastes CPU and throws away the
in-memory thread state.
import {
CopilotRuntime,
createCopilotRuntimeHandler,
BuiltInAgent,
} from "@copilotkit/runtime/v2";
interface Env {
OPENAI_API_KEY: string;
}
// Module-scoped cache. `env` arrives per-request, so we initialize lazily
// the first time we see it. Subsequent requests in the same isolate reuse.
let cachedHandler: ((request: Request) => Response | Promise<Response>) | null =
null;
function getHandler(env: Env) {
if (cachedHandler) return cachedHandler;
const runtime = new CopilotRuntime({
agents: {
// Simple Mode: thread the API key through the `apiKey` option — on
// Workers `process.env` is undefined, so BuiltInAgent's env-var
// fallback never fires. Wire env.OPENAI_API_KEY explicitly.
default: new BuiltInAgent({
model: "openai/gpt-4o",
apiKey: env.OPENAI_API_KEY,
}),
},
});
cachedHandler = createCopilotRuntimeHandler({
runtime,
basePath: "/api/copilotkit",
cors: true,
});
return cachedHandler;
}
export default {
fetch(request: Request, env: Env) {
return getHandler(env)(request);
},
};
Point the provider at CopilotKit Cloud via publicApiKey — no backend,
no runtimeUrl. This is the ONLY production-safe SPA path. See
copilotkit/spa-without-runtime for the full treatment.
import { CopilotKitProvider, CopilotChat } from "@copilotkit/react-core/v2";
import "@copilotkit/react-core/v2/styles.css";
export default function App() {
return (
<CopilotKitProvider publicApiKey={import.meta.env.VITE_CPK_PUBLIC_API_KEY}>
<CopilotChat agentId="default" className="h-full" />
</CopilotKitProvider>
);
}
Wrong:
// server.js — spun up alongside the RR v7 app
import express from "express";
import { createCopilotExpressHandler } from "@copilotkit/runtime/v2/express";
const app = express();
app.use(
"/api/copilotkit",
createCopilotExpressHandler({ runtime, basePath: "/api/copilotkit" }),
);
app.listen(3001);
Correct:
// app/routes/api.copilotkit.$.tsx
export async function loader({ request }: Route.LoaderArgs) {
return handler(request);
}
export async function action({ request }: Route.ActionArgs) {
return handler(request);
}
RR v7 framework mode already runs its own server; a sidecar Express/Hono app
duplicates servers and breaks unified routing/SSR. Same principle applies to
Next.js (use route.ts) and TanStack Start (use an APIRoute). Maintainer
guidance: avoid the Express/Hono adapters.
Source: examples/v2/react-router/app/routes/api.copilotkit.$.tsx
Wrong:
import { CopilotKitProvider } from "@copilotkitnext/react-core";
import { CopilotRuntime } from "@copilotkitnext/runtime";
Correct:
import { CopilotKitProvider } from "@copilotkit/react-core/v2";
import { CopilotRuntime } from "@copilotkit/runtime/v2";
// Only Angular uses the @copilotkitnext/ scope:
// import { ... } from "@copilotkitnext/angular";
Every CopilotKit package except Angular uses @copilotkit/. Agents
over-generalize from the Angular example and hallucinate the scope for
react-core / runtime / etc.
Source: packages/angular/package.json; all other packages/*/package.json
Wrong:
<CopilotKitProvider runtimeUrl="api/copilotkit" />
Correct:
<CopilotKitProvider runtimeUrl="/api/copilotkit" />
Without the leading slash the URL resolves relative to the current page — breaks on any nested route.
Source: docs/snippets/shared/troubleshooting/common-issues.mdx:38-42
Wrong:
import { CopilotChat } from "@copilotkit/react-core/v2";
Correct:
import { CopilotChat } from "@copilotkit/react-core/v2";
import "@copilotkit/react-core/v2/styles.css";
Without the stylesheet, the chat renders unstyled/broken — no layout, no spacing, no theme.
Source: examples/v2/react-router/app/routes/_index.tsx:3
Wrong:
// server
new CopilotRuntime({ agents: { default: agent } });
// client
<CopilotChat agentId="main" />
Correct:
<CopilotChat agentId="default" />
// or rename the server key to "main" so both sides match
Mismatched IDs surface as agent_not_found on first run. Keep the string
identical on both sides.
Source: packages/core/src/core/core.ts:80
Wrong:
// Module-scoped — `process.env` is undefined on Workers:
const agent = new BuiltInAgent({
type: "tanstack",
factory: ({ input, abortController }) =>
chat({
adapter: openaiText("gpt-4o"), // no access to process.env.OPENAI_API_KEY
messages: convertInputToTanStackAI(input).messages,
abortController,
}),
});
Correct: use Simple Mode and let the runtime read OPENAI_API_KEY from
the env binding (see the Cloudflare Workers branch above), or thread
env.OPENAI_API_KEY in through a closure if you genuinely need Factory
Mode.
Workers don't expose process.env. Secrets arrive via the env binding
argument to fetch(request, env).
Source: examples/v2/runtime/cf-workers/src/index.ts:7-17
Wrong:
const h = createCopilotRuntimeHandler({ runtime });
server.on("request", h);
Correct:
import { createCopilotNodeHandler } from "@copilotkit/runtime/v2/node";
const node = createCopilotNodeHandler(
createCopilotRuntimeHandler({
runtime,
basePath: "/api/copilotkit",
cors: true,
}),
);
server.on("request", node);
createCopilotRuntimeHandler takes a Web Request; Node's
IncomingMessage shape is different. createCopilotNodeHandler adapts the
fetch handler for http.Server — for frameworks (RR v7 / Start / Next.js)
use the fetch handler directly.
Source: examples/v2/runtime/node/src/index.ts:1-21