Back to Copilotkit

Threads Lifecycle

showcase/shell-docs/src/content/snippets/shared/threads/threads-lifecycle.mdx

1.63.214.3 KB
Original Source

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.

<Callout type="info"> 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](#v1-vs-v2) at the end. </Callout>

The lifecycle at a glance

  1. Mint. When a chat mounts without an explicit threadId, the client generates one (a UUID v4).
  2. Run. Messages and tool calls stream under that threadId. If a server-side store is configured (the Enterprise Intelligence Platform, or a persisting AgentRunner), 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.
  3. Hydrate. When a chat mounts with a known threadId, the client connects and replays the persisted history into the UI.
  4. 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):

  1. An explicit threadId prop on <CopilotChat> / <CopilotChatConfigurationProvider> (this is authoritative: it also drives history replay and disables the welcome screen).
  2. An active-thread override set via setActiveThreadId(...) / a picked thread row / startNewThread().
  3. A threadId inherited from a parent configuration provider.
  4. A non-authoritative threadId seed.
  5. Otherwise, a freshly minted randomUUID().
tsx
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} />
<Callout type="warn"> **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. </Callout>

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:

tsx
<CopilotChat agentId="my-agent" threadId={someExistingThreadId} />
<Callout type="warn"> 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](/premium/threads-explained) page covers how replay works server-side. </Callout>

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:

tsx
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;
}
<Callout type="info"> 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](#v1-vs-v2).) </Callout>

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:

ts
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.

<Callout type="warn"> 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. </Callout>

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:

tsx
import { useCopilotChatConfiguration } from "@copilotkit/react-core/v2";

function ThreadControls() {
  const config = useCopilotChatConfiguration();

  return (
    <>
      <button onClick={() => config?.setActiveThreadId(existingId, { explicit: true })}>
        Open conversation
      </button>
      <button onClick={() => config?.startNewThread()}>New chat</button>
    </>
  );
}
  • setActiveThreadId(threadId, { explicit }): explicit: true (the default) treats it as a known thread and replays its history, while explicit: false shows the welcome screen.
  • startNewThread(): resets to a freshly minted, non-explicit id. Also available from useThreads() for thread-sidebar UIs.
<Callout type="warn"> 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. </Callout>

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:

tsx
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:

tsx
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
      }}
    />
  );
}
<Callout type="warn"> 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. </Callout> <Callout type="info"> `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. </Callout>

CopilotKit threads vs. your framework's checkpointer

These are two different layers, correlated only by the shared threadId:

LayerWhat it storesManaged by
CopilotKit threadsConversation list + full AG-UI event history (messages, tool calls, state), with realtime syncThe Enterprise Intelligence Platform, via useThreads
Framework-native persistenceFramework-internal state/checkpointsYour 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.

<Callout type="info"> See [LangGraph persistence](/integrations/langgraph/advanced/persistence/message-persistence) for the framework-native (checkpointer) side, and [Importing and synchronizing thread history](/threads-import) 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.) </Callout>

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): useThreads returns the platform thread list; threadId lives per-chat (on <CopilotChat> / <CopilotChatConfigurationProvider>), not on the provider; read messages via useAgent().agent.messages and mutate via agent.setMessages(...). No initialMessages.
  • v1 (@copilotkit/react-core): useThreads returns { threadId, setThreadId, isThreadIdExplicit } (a single-conversation id manager, not the platform list). Its setThreadId silently no-ops when a threadId prop is present, because the prop shadows internal state. <CopilotKit threadId> sets the id at the provider, and useCopilotChat() exposes setMessages(...) and an initialMessages option. (Separately, the setThreadId on the main <CopilotKit> hooks throws if you call it while threadId is prop-supplied.)

See also