You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/content/docs/learn/threads.mdx
+64-78Lines changed: 64 additions & 78 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,6 +1,6 @@
1
1
---
2
2
title: "How Threads & Persistence Work"
3
-
description: "Architecture and mental model behind CopilotKit threads — how the Intelligence Platform, IntelligenceAgent, and AgentRunner enable durable, resumable conversations with realtime sync via Phoenix WebSocket."
3
+
description: "Architecture and mental model behind CopilotKit threads — how persistent conversations work, how reconnection replays history, and what to expect from thread lifecycle operations."
4
4
icon: "lucide/MessageSquareMore"
5
5
doc_type: explanation
6
6
---
@@ -17,130 +17,116 @@ Threads are a platform-level concept, not tied to any specific agent framework.
17
17
18
18
A **thread** is the durable container. A **run** is a single agent execution within that thread. One thread can have many runs — each time the user sends a message and the agent responds, that's a new run. The thread accumulates events across all its runs.
19
19
20
-
### The three layers
20
+
### How the pieces fit together
21
21
22
-
Thread functionality spans all three layers of the CopilotKit architecture:
22
+
From a developer's perspective, threads involve three things:
23
23
24
-
| Layer | Component | Role |
25
-
|-------|-----------|------|
26
-
|**Frontend**|`useThreads` hook | Lists, renames, archives, and deletes threads. Subscribes to realtime metadata updates. |
27
-
|**Frontend**|`IntelligenceAgent`| Client-side AG-UI agent that connects to threads via WebSocket. Handles starting new runs and reconnecting to existing ones. |
28
-
|**Runtime**|`IntelligenceAgentRunner`| Server-side runner that executes agents and relays events through Phoenix channels. Stamps each event with sequence metadata. |
29
-
|**Platform**| Intelligence Platform | Stores threads, runs, and events durably. Provides WebSocket infrastructure and REST APIs. |
24
+
| What you use | What it does |
25
+
|-------------|-------------|
26
+
|`useThreads` hook | Lists, renames, archives, and deletes threads. Stays in sync across tabs and devices via WebSocket. |
27
+
|`CopilotChat` with `threadId`| Connects to a specific thread, loads its history, and streams new events in realtime. |
28
+
|`CopilotRuntime`| Server-side layer that executes agents, stores thread data on the Intelligence Platform, and relays events to connected clients. |
29
+
30
+
You interact with the first two. The runtime and platform handle persistence and sync behind the scenes.
30
31
31
32
### Auto-naming
32
33
33
-
When a new thread is created and the first run completes, the runtime automatically generates a short name (2–5 words) using the LLM. This runs asynchronously — it doesn't block thread creation. The generated name is pushed to all clients via the `renamed` WebSocket event.
34
+
When a new thread is created and the first run completes, the runtime automatically generates a short name (2–5 words) using the LLM. This runs asynchronously — it doesn't block thread creation or the agent's response. The generated name appears in `useThreads`via the realtime sync.
34
35
35
36
Auto-naming is enabled by default. Disable it with `generateThreadNames: false` on the runtime. Users can always override the generated name via `renameThread()`.
36
37
37
38
### Archive vs. delete
38
39
39
40
Threads support two removal operations with different semantics:
40
41
41
-
-**Archive** — a soft delete. The thread remains in the database with `archived: true`. It disappears from the default list but can be shown again with `includeArchived: true`. The platform also supports **unarchive** — the `unarchived` metadata event restores the thread to the active list.
42
-
-**Delete** — permanent and irreversible. The thread record is removed from the database entirely.
42
+
-**Archive** — a soft delete. The thread remains stored but disappears from the default list. Show archived threads by passing `includeArchived: true` to `useThreads`. Threads can also be unarchived, which restores them to the active list.
43
+
-**Delete** — permanent and irreversible. The thread and its history are removed entirely.
44
+
45
+
Neither operation has a built-in confirmation dialog — your application should implement its own if needed.
43
46
44
-
Neither operation has a built-in confirmation flow — your application should implement its own if needed.
47
+
## How it works
45
48
46
-
### Event sequencing
49
+
### Starting a new conversation
47
50
48
-
Every event in a thread is stamped with two pieces of metadata:
49
-
-**`cpki_event_id`** — a unique identifier for the event
50
-
-**`cpki_event_seq`** — a monotonically increasing sequence number within the thread
51
+
When a user sends their first message on a new thread:
51
52
52
-
These enable reliable replay. When a client reconnects to a thread, it sends its `lastSeenEventId`, and the platform responds with only the events that occurred after that point.
53
+
1. Your app renders `CopilotChat` (with or without a `threadId` — if omitted, a new thread is created automatically).
54
+
2. The runtime creates the thread on the Intelligence Platform and begins executing the agent.
55
+
3. Events stream back to the client via WebSocket in realtime — messages, tool calls, and state updates appear as they happen.
56
+
4. Once the first run completes, the runtime auto-generates a thread name (if enabled).
1. The client calls `run()` on the `IntelligenceAgent` with a thread ID and messages.
78
-
2. The agent sends a REST request to the runtime, which creates or retrieves the thread from the Intelligence Platform.
79
-
3. The platform returns a `joinToken` for WebSocket authentication.
80
-
4. Both the client agent and server runner join the same Phoenix channel (`thread:{threadId}`).
81
-
5. The runner executes the agent and pushes events to the channel.
82
-
6. The client receives events in realtime and fires them as AG-UI events.
83
-
84
-
### Resuming a conversation (connect flow)
85
-
86
-
When a user returns to an existing thread, the client needs to catch up on any events it missed:
87
-
88
-
1. The client calls `connect()` with the thread ID and its `lastSeenEventId`.
60
+
When a user returns to an existing thread (e.g., by clicking a thread in the sidebar), the client needs to catch up on any events it missed:
61
+
62
+
1.`CopilotChat` receives the new `threadId` and requests the thread's history from the platform.
89
63
2. The platform checks whether the thread has a run in progress:
90
-
-**Bootstrap mode** — no active run. The platform returns historical events only. The client replays them to reconstruct the conversation.
91
-
-**Live mode** — a run is active. The platform returns historical events *plus*a join token. The client replays the history, then joins the WebSocket channel to receive live events.
92
-
3. In either case, the client seamlessly transitions from replayed history to live updates.
64
+
-**No active run** — the platform returns historical events only. The client replays them to reconstruct the conversation.
65
+
-**Active run** — the platform returns historical events *plus*opens a WebSocket connection. The client replays the history, then receives live events as they stream in.
66
+
3. In either case, the transition from replayed history to live updates is seamless.
93
67
94
68
### Switching threads
95
69
96
-
When the `threadId` prop on `CopilotChat` changes, the component:
70
+
When the `threadId` prop on `CopilotChat` changes:
71
+
72
+
1. Any active run on the current thread is detached.
73
+
2. All messages and agent state are cleared.
74
+
3. The new thread's history is fetched and replayed.
75
+
4. A WebSocket connection is established for live updates on the new thread.
76
+
77
+
The UI briefly shows an empty chat before the history loads. This is by design — it prevents stale messages from the previous thread appearing in the new one.
97
78
98
-
1.**Detaches** any active run on the current thread
99
-
2.**Clears** all messages and agent state
100
-
3.**Calls `connect()`** on the new thread, which fetches historical events and replays them
101
-
4.**Re-establishes** the WebSocket channel for live updates
79
+
**Safe during tool execution:** If a tool call from the old thread completes during a switch, its result is discarded rather than inserted into the new thread's messages.
102
80
103
-
The clear-then-replay approach means the UI briefly shows an empty chat before the history loads. This is by design — it prevents stale messages from the previous thread flashing in the new one.
81
+
### Realtime sync
104
82
105
-
**Race condition safety:** If a tool call completes during a thread switch, the result is discarded rather than inserted into the new thread's messages. The implementation tracks message identity across switches to prevent cross-thread contamination.
83
+
The `useThreads` hook maintains a WebSocket subscription for thread metadata changes. When any client creates, renames, archives, or deletes a thread, the update is pushed to all connected clients automatically. This is how a thread created on one tab appears in the sidebar on another tab without polling.
106
84
107
85
### Pessimistic updates
108
86
109
-
Thread mutations (`rename`, `archive`, `delete`) use a pessimistic update model. The client sends the HTTP request and waits for the server to confirm via a WebSocket event before updating the UI. This means:
87
+
Thread mutations (`rename`, `archive`, `delete`) use a pessimistic update model — the client waits for the server to confirm via WebSocket before updating the thread list. This means:
110
88
111
89
- The thread list doesn't change until the server confirms the operation
112
90
- If the server rejects the mutation, the UI never shows an incorrect state
113
-
- The returned promise resolves only after server confirmation (or rejects on failure, with a 15-second timeout)
91
+
- The returned promise resolves only after server confirmation, or rejects on failure
114
92
115
-
This is different from optimistic updates where the UI changes immediately and rolls back on failure. The pessimistic approach was chosen because thread operations are infrequent and correctness matters more than perceived speed.
93
+
## Error handling
116
94
117
-
### Realtime thread metadata
95
+
### Mutation failures
118
96
119
-
The `useThreads` hook maintains a separate WebSocket subscription for thread metadata changes. When any client creates, renames, archives, or deletes a thread, the update is broadcast to all connected clients via a Phoenix channel (`user_meta:{joinCode}`). This is how a thread created on one tab appears in the sidebar on another tab without polling.
97
+
All mutation methods (`renameThread`, `archiveThread`, `deleteThread`) return promises that reject with an `Error` if the server cannot complete the operation. Common causes:
-**Network failure** — the client can't reach the runtime
100
+
-**Thread not found** — another client deleted the thread before your mutation arrived
101
+
-**Authorization failure** — the user doesn't have permission to modify the thread
102
+
-**Timeout** — the server didn't respond within 15 seconds
122
103
123
-
## Design decisions
104
+
The `error` field on `useThreads` always reflects the most recent error. It resets to `null` on the next successful operation.
105
+
106
+
### WebSocket disconnection
107
+
108
+
If the WebSocket connection drops (network change, server restart, laptop sleep):
124
109
125
-
### Why Phoenix WebSocket?
110
+
-**Thread list** — `useThreads` stops receiving realtime updates. The list becomes stale until the connection is re-established. Reconnection is automatic with exponential backoff.
111
+
-**Active conversation** — if `CopilotChat` loses its WebSocket mid-run, the agent's output may be interrupted. Reloading the page or switching away and back to the thread triggers the reconnection flow, which replays any missed events.
126
112
127
-
CopilotKit uses [Phoenix channels](https://hexdocs.pm/phoenix/channels.html) rather than raw WebSocket or SSE for thread communication. Phoenix provides:
128
-
-**Multiplexed channels** — one socket connection carries both the thread event stream and the metadata subscription
129
-
-**Built-in reconnection** with exponential backoff and jitter
130
-
-**Server-side channel state** — the platform can track which clients are connected to which threads
113
+
### Thread locked
114
+
115
+
If a thread already has an active run and another client tries to start a new run on the same thread, the request is rejected with a **409 Conflict**. This prevents two agent runs from interleaving events on the same thread. The existing run must complete or be stopped before a new one can begin.
116
+
117
+
## Design decisions
131
118
132
119
### Why event replay instead of message snapshots?
133
120
134
121
Threads store the raw event stream rather than a snapshot of the final message list. This enables:
135
-
-**Time travel** — reconstruct the conversation state at any point
136
-
-**Partial replay** — when reconnecting, only fetch events since `lastSeenEventId` rather than the entire history
122
+
-**Partial replay** — when reconnecting, the client only fetches events it missed rather than reloading the entire history
137
123
-**Faithful reproduction** — streaming tokens, tool calls, and state changes replay exactly as they originally occurred
138
124
139
-
The trade-off is that replay is more complex than loading a message array, and the event log grows over time. The Intelligence Platform handles this by supporting cursor-based pagination for the event history.
125
+
The trade-off is that replay is more complex than loading a message array. The platform handles this complexity so your application doesn't have to.
140
126
141
127
### When threads are the wrong tool
142
128
143
-
-**Ephemeral interactions** — if your users don't need conversation history (e.g., a one-shot Q&A widget), threads add unnecessary complexity. Use a standard `CopilotChat` without a `threadId`.
129
+
-**Ephemeral interactions** — if your users don't need conversation history (e.g., a one-shot Q&A widget), threads add unnecessary complexity. Use `CopilotChat` without a `threadId`.
144
130
-**Client-only state** — if you need local-only chat history without server persistence, manage messages in React state or localStorage instead.
Copy file name to clipboardExpand all lines: docs/content/docs/learn/tutorials/multi-conversation-chat.mdx
+3-3Lines changed: 3 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -209,7 +209,7 @@ A chat application with a thread sidebar — similar to ChatGPT or Claude's conv
209
209
210
210
return (
211
211
<CopilotChat
212
-
threadId={activeThreadId}// [!code highlight]
212
+
threadId={activeThreadId}{/* [!code highlight]*/}
213
213
className="h-full"
214
214
/>
215
215
);
@@ -254,12 +254,12 @@ A chat application with a thread sidebar — similar to ChatGPT or Claude's conv
254
254
255
255
## What's next
256
256
257
-
You now have a working multi-conversation chat app with persistent threads. Here are some ideas for extending it:
257
+
You now have a working multi-conversation chat app with persistent threads. Thread names are auto-generated by the LLM after the first message — you'll see them appear in the sidebar automatically. Here are some ideas for extending further:
258
258
259
259
-**Search** — add a search input that filters threads by name
260
-
-**Thread auto-naming** — use your agent to generate a thread name from the first message
261
260
-**Unread indicators** — track which threads have new messages since the user last viewed them
262
261
-**Drag to reorder** — let users pin important threads to the top
262
+
-**Archive view** — add a toggle to show archived threads using `includeArchived: true`
0 commit comments