Skip to content

Commit cc260e3

Browse files
BenTaylorDevclaude
andcommitted
docs(threads): rewrite for developer-facing surface, add error handling, fix JSX highlight
- Rewrite explanation page: remove internal class names (IntelligenceAgent, AgentRunner), Phoenix channel references, and internal metadata fields (cpki_event_id) - Add error handling section: mutation failures, WebSocket disconnection, thread locked (409) - Add auto-naming, archive vs delete, and thread switching details across all pages - Fix [!code highlight] in JSX (use {/* */} syntax) - Fix tutorial What's Next suggesting auto-naming as extension (already built in) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7db5d06 commit cc260e3

4 files changed

Lines changed: 69 additions & 83 deletions

File tree

docs/content/docs/(root)/threads.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ CopilotKit threads enable persistent, resumable multi-turn conversations. The `u
105105
return (
106106
<div className="flex">
107107
<ThreadSidebar onSelectThread={setActiveThreadId} />
108-
<CopilotChat threadId={activeThreadId} /> // [!code highlight]
108+
<CopilotChat threadId={activeThreadId} /> {/* [!code highlight] */}
109109
</div>
110110
);
111111
}

docs/content/docs/learn/threads.mdx

Lines changed: 64 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
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."
44
icon: "lucide/MessageSquareMore"
55
doc_type: explanation
66
---
@@ -17,130 +17,116 @@ Threads are a platform-level concept, not tied to any specific agent framework.
1717

1818
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.
1919

20-
### The three layers
20+
### How the pieces fit together
2121

22-
Thread functionality spans all three layers of the CopilotKit architecture:
22+
From a developer's perspective, threads involve three things:
2323

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

3132
### Auto-naming
3233

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

3536
Auto-naming is enabled by default. Disable it with `generateThreadNames: false` on the runtime. Users can always override the generated name via `renameThread()`.
3637

3738
### Archive vs. delete
3839

3940
Threads support two removal operations with different semantics:
4041

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

44-
Neither operation has a built-in confirmation flow — your application should implement its own if needed.
47+
## How it works
4548

46-
### Event sequencing
49+
### Starting a new conversation
4750

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

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).
5357

54-
## How it works
58+
### Resuming a conversation
5559

56-
### Starting a new conversation (run flow)
57-
58-
```mermaid
59-
sequenceDiagram
60-
participant Client as Browser
61-
participant Agent as IntelligenceAgent
62-
participant Runtime as AgentRunner
63-
participant Platform as Intelligence Platform
64-
65-
Client->>Agent: run(threadId, messages)
66-
Agent->>Platform: POST /agent/{id}/run
67-
Platform-->>Agent: joinToken
68-
Agent->>Platform: WebSocket connect (joinToken)
69-
Agent->>Platform: Join channel thread:{threadId}
70-
Runtime->>Platform: Join same channel
71-
Runtime->>Runtime: Execute agent
72-
Runtime->>Platform: Push events (with cpki_event_id, cpki_event_seq)
73-
Platform->>Agent: Relay events via WebSocket
74-
Agent->>Client: Fire AG-UI events
75-
```
76-
77-
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.
8963
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.
9367

9468
### Switching threads
9569

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

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

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
10482

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

10785
### Pessimistic updates
10886

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 modelthe client waits for the server to confirm via WebSocket before updating the thread list. This means:
11088

11189
- The thread list doesn't change until the server confirms the operation
11290
- 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
11492

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
11694

117-
### Realtime thread metadata
95+
### Mutation failures
11896

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

121-
Supported metadata operations: `created`, `renamed`, `archived`, `unarchived`, `updated`, `deleted`.
99+
- **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
122103

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):
124109

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

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
131118

132119
### Why event replay instead of message snapshots?
133120

134121
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
137123
- **Faithful reproduction** — streaming tokens, tool calls, and state changes replay exactly as they originally occurred
138124

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

141127
### When threads are the wrong tool
142128

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`.
144130
- **Client-only state** — if you need local-only chat history without server persistence, manage messages in React state or localStorage instead.
145131

146132
## Next steps

docs/content/docs/learn/tutorials/multi-conversation-chat.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ A chat application with a thread sidebar — similar to ChatGPT or Claude's conv
209209

210210
return (
211211
<CopilotChat
212-
threadId={activeThreadId} // [!code highlight]
212+
threadId={activeThreadId} {/* [!code highlight] */}
213213
className="h-full"
214214
/>
215215
);
@@ -254,12 +254,12 @@ A chat application with a thread sidebar — similar to ChatGPT or Claude's conv
254254

255255
## What's next
256256

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

259259
- **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
261260
- **Unread indicators** — track which threads have new messages since the user last viewed them
262261
- **Drag to reorder** — let users pin important threads to the top
262+
- **Archive view** — add a toggle to show archived threads using `includeArchived: true`
263263

264264
## Next steps
265265

docs/snippets/shared/threads/threads.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ CopilotKit threads enable persistent, resumable multi-turn conversations. The `u
9898
return (
9999
<div className="flex">
100100
<ThreadSidebar onSelectThread={setActiveThreadId} />
101-
<CopilotChat threadId={activeThreadId} /> // [!code highlight]
101+
<CopilotChat threadId={activeThreadId} /> {/* [!code highlight] */}
102102
</div>
103103
);
104104
}

0 commit comments

Comments
 (0)