Thread & History Lifecycle
How a threadId is created, how conversation history is restored, how to switch or start threads, and how CopilotKit threads relate to your agent framework's own persistence.
What is this?#
Every CopilotKit conversation is scoped to a thread, identified by a threadId. This page explains the full lifecycle of that thread on the client: how the id is created, how prior history is restored when a conversation loads, how to switch between threads or start a new one, and where CopilotKit's threads end and your agent framework's own persistence (e.g. a LangGraph checkpointer) begins.
This page describes the v2 hooks and components imported from @copilotkit/react-core/v2. The v1 API (useCopilotChat, initialMessages, the v1 useThreads) behaves differently; see the v1 vs v2 notes at the end.
The lifecycle at a glance#
- Mint. When a chat mounts without an explicit
threadId, the client generates one (a UUID v4). - Run. Messages and tool calls stream under that
threadId. If a server-side store is configured (the Enterprise Intelligence Platform, or a persistingAgentRunner), they are persisted as they happen so the thread can be replayed later. A runtime with no persistence layer keeps nothing server-side. See Threads & Persistence Architecture for the full server-side model. - Hydrate. When a chat mounts with a known
threadId, the client connects and replays the persisted history into the UI. - Switch / start. You change the active thread (restoring its history) or start a fresh one (clearing the view).
How the threadId is created#
When you don't supply one, CopilotKit mints a threadId on the client, at mount, using a UUID v4. The value is resolved by this precedence (highest first):
- An explicit
threadIdprop on<CopilotChat>/<CopilotChatConfigurationProvider>(this is authoritative: it also drives history replay and disables the welcome screen). - An active-thread override set via
setActiveThreadId(...)/ a picked thread row /startNewThread(). - A
threadIdinherited from a parent configuration provider. - A non-authoritative
threadIdseed. - Otherwise, a freshly minted
randomUUID().
import { CopilotChat } from "@copilotkit/react-core/v2";
// You control the id: history replays for it, and it's stable across remounts.
<CopilotChat agentId="my-agent" threadId={myThreadId} />Auto-minted ids are stable across re-renders, but re-mint on remount. The fallback id is computed with useMemo, so a component remount (a changed React key, a parent unmount/remount, or React StrictMode's double-mount in dev) produces a new id and silently starts a new conversation. If you need an id that survives remounts, mint it yourself and pass it as the threadId prop (or persist it and restore it via setActiveThreadId). Don't rely on the auto-mint for continuity.
How history is restored (hydration)#
CopilotKit v2 does not use an initialMessages prop to seed a conversation. History is restored one of two ways:
1. Server-side replay. When a chat mounts with an explicit threadId, the client sets agent.threadId and calls connectAgent(), which replays that thread's persisted event history back into the UI, with no manual message wiring:
<CopilotChat agentId="my-agent" threadId={someExistingThreadId} />Replay requires a server-side store to replay from: the Enterprise Intelligence Platform, or a persisting AgentRunner (e.g. the SQLite runner). A self-hosted runtime with no persistence layer has nothing to replay, so connectAgent() returns an empty stream and the conversation starts blank. If history isn't restoring, check that a store is configured, not the client code. The Persistence Architecture page covers how replay works server-side.
For a fresh (non-explicit) thread, e.g. after startNewThread(), there's nothing to replay, so connect is skipped and the message view is cleared.
2. Manual hydration. If you're managing messages yourself (for non-platform or custom cases), read them from the agent and set them directly:
import { useAgent } from "@copilotkit/react-core/v2";
function MyComponent() {
const { agent } = useAgent({ agentId: "my-agent" });
// Read the current conversation
const messages = agent.messages;
// Replace it (e.g. hydrate from your own store)
// agent.setMessages(myPersistedMessages);
return null;
}There is no v2 useCopilotChat hook and no v2 initialMessages prop. Read messages from useAgent().agent.messages and mutate with agent.setMessages(...) / agent.addMessage(...). (initialMessages still exists on the v1 useCopilotChat hook; see below.)
Scope Rich Threads to the signed-in user#
Your application owns end-user authentication. For an Intelligence-enabled
Runtime, implement identifyUser(request) with the server-verified session or
token from your auth provider and return a stable user id and name:
const runtime = new CopilotRuntime({
agents: { default: agent },
intelligence,
identifyUser: async (request) => {
const session = await verifyAppSession(request); // Your server-side auth.
if (!session?.user) throw new Error("Unauthorized");
return {
id: session.user.id,
name: session.user.name,
};
},
});Enterprise Intelligence combines that application user identity with the selected project when it scopes thread lists, subscriptions, history, rename, archive, unarchive, and delete operations. The project Runtime API key is a separate credential: keep it on the server and do not use the developer who created that key as the application user.
A static identifyUser value is suitable only for a single-user demo. In a
multi-user application, every request must resolve the authenticated
application user or those users will share the same thread scope.
See Authentication for forwarding and verifying your provider's auth
context. identifyUser is the additional Runtime contract that assigns the
verified application user to Rich Threads.
Switching threads and starting new ones#
Set the active thread to restore its history, or start a fresh conversation:
import { useCopilotChatConfiguration } from "@copilotkit/react-core/v2";
function ThreadControls() {
const config = useCopilotChatConfiguration();
return (
<>
{/* Restore a known conversation (explicit → replays history) */}
<button onClick={() => config?.setActiveThreadId(existingId, { explicit: true })}>
Open conversation
</button>
{/* Start a fresh, empty conversation (mints a new non-explicit id) */}
<button onClick={() => config?.startNewThread()}>New chat</button>
</>
);
}setActiveThreadId(threadId, { explicit }):explicit: true(the default) treats it as a known thread and replays its history, whileexplicit: falseshows the welcome screen.startNewThread(): resets to a freshly minted, non-explicit id. Also available fromuseThreads()for thread-sidebar UIs.
Both setters no-op and log a warning when the threadId is prop-controlled (i.e. you passed an authoritative threadId prop). If you drive threads imperatively with these setters, don't also pass a threadId prop. Pick one source of truth.
Creating a thread with your own API on the first message#
A common ask: intercept the user's first message to mint a thread via your own backend (e.g. create a DB row and get its id) before the conversation starts.
On the built-in <CopilotChat> this cannot be done through onSubmitMessage: the component sets its own submit handler internally and discards any you pass. Instead, use one of these approaches.
Recommended: mint up front and drive the id. Create your thread when the user starts a new conversation (e.g. on a "New chat" action, or lazily just before you render the chat), then hand the id to CopilotKit:
async function startConversation() {
const { id } = await myApi.createThread(); // your backend
config?.setActiveThreadId(id, { explicit: true });
// or render <CopilotChat threadId={id} />
}At submit-time (headless only). If you must intercept exactly when the user hits send, compose the UI yourself with CopilotChatInput / CopilotChatView and pass your own onSubmitMessage (the built-in <CopilotChat> ignores it). Because the handler is yours, you set the thread on the agent and drive the send:
import { CopilotChatInput, useAgent } from "@copilotkit/react-core/v2";
function MyInput() {
const { agent } = useAgent({ agentId: "my-agent" });
return (
<CopilotChatInput
onSubmitMessage={async (text) => {
const { id } = await myApi.createThread(); // your backend
agent.threadId = id; // set it on the agent directly
agent.addMessage({ role: "user", content: text });
// ...then trigger the run with your send logic
}}
/>
);
}For a send you fire in the same handler, set the id on the agent (agent.threadId = id); don't rely on setActiveThreadId(id). setActiveThreadId updates React state and only reaches the agent on the next render (and only when a <CopilotChat> is mounted to sync it), so a run kicked off immediately would still use the previous thread. Use setActiveThreadId / startNewThread for switching threads in the UI, and set agent.threadId directly when it must take effect before an imperative send.
onSubmitMessage is only a real interception seam in the headless path. On the built-in <CopilotChat> it's overridden, so reach for the "mint up front" approach there.
CopilotKit threads vs. your framework's checkpointer#
These are two different layers, correlated only by the shared threadId:
| Layer | What it stores | Managed by |
|---|---|---|
| CopilotKit threads | Conversation list + full AG-UI event history (messages, tool calls, state), with realtime sync | The Enterprise Intelligence Platform, via useThreads |
| Framework-native persistence | Framework-internal state/checkpoints | Your agent, e.g. a LangGraph AsyncPostgresSaver |
A LangGraph checkpointer (an AsyncPostgresSaver + compile(checkpointer=...)) creates LangGraph's own checkpoint tables. It does not create a CopilotKit "threads" table, and you won't see a threads table appear just from configuring a checkpointer. Conversely, useThreads rename/archive/delete operate on platform threads and do not reach into LangGraph (or ADK, etc.) stores unless your backend explicitly bridges them.
The bridge between the two is the threadId: when you pass an explicit CopilotKit threadId, it's forwarded to the backend as the AG-UI threadId, which your framework can use directly as (or map to) its own checkpoint/thread id.
Future runs can remain durable in both places#
After historical import, future conversations that run through CopilotKit are written to Enterprise Intelligence as Rich Threads. If your LangGraph agent continues using a durable checkpointer or LangGraph Platform, those same runs also continue through LangGraph's native persistence. The same applies to an ADK agent that remains connected to a durable session service with retained sessions. This lets you keep native framework storage and analytics while users get replay, generative UI, multimodal history, and realtime continuity through CopilotKit.
This is coordinated persistence during future CopilotKit-mediated runs, not general database replication. CopilotKit lifecycle actions such as rename, archive, and delete apply to the Enterprise Intelligence thread; they do not rename, archive, or delete records in the native framework store.
See LangGraph persistence for the framework-native (checkpointer) side, and Importing and synchronizing thread history for bringing existing framework conversations into the platform store. (LangGraph Platform requires thread ids to be UUIDs, which CopilotKit's auto-minted ids already are.)
MCP Apps activity and history#
MCP Apps render through activity messages (activityType: "mcp-apps"), and that rendering is a client-side concern: the registered renderer resolves the referenced UI resource and runs any follow-up in the browser. So the visible MCP App UI is re-derived on the client from the conversation when it loads. It isn't a separately-stored artifact you persist on its own.
Practically, MCP App UI restores the same way the rest of the conversation does: if your server-side store replays the thread's history (see server-side replay above), the tool-call/activity messages come back and the renderers run again on hydration. There's no separate MCP-Apps store to configure.
v1 vs v2#
- v2 (
@copilotkit/react-core/v2):useThreadsreturns the platform thread list;threadIdlives per-chat (on<CopilotChat>/<CopilotChatConfigurationProvider>), not on the provider; read messages viauseAgent().agent.messagesand mutate viaagent.setMessages(...). NoinitialMessages. - v1 (
@copilotkit/react-core):useThreadsreturns{ threadId, setThreadId, isThreadIdExplicit }(a single-conversation id manager, not the platform list). ItssetThreadIdsilently no-ops when athreadIdprop is present, because the prop shadows internal state.<CopilotKit threadId>sets the id at the provider, anduseCopilotChat()exposessetMessages(...)and aninitialMessagesoption. (Separately, thesetThreadIdon the main<CopilotKit>hooks throws if you call it whilethreadIdis prop-supplied.)
See also#
- Headless Threads — the full
useThreadsAPI - Threads & Persistence Architecture — server-side replay, resume, realtime sync
- Multi-conversation chat — build a chat-history sidebar
- Importing and synchronizing thread history — bring existing framework conversations into the platform and keep future runs continuous