@trigger.dev/sdk 4.6.4 → 4.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commonjs/v3/ai.d.ts +5 -2
- package/dist/commonjs/v3/ai.js +189 -266
- package/dist/commonjs/v3/ai.js.map +1 -1
- package/dist/commonjs/v3/chat-client.js +7 -0
- package/dist/commonjs/v3/chat-client.js.map +1 -1
- package/dist/commonjs/v3/chat-server.d.ts +1 -0
- package/dist/commonjs/v3/chat-server.js +8 -0
- package/dist/commonjs/v3/chat-server.js.map +1 -1
- package/dist/commonjs/v3/chat.d.ts +28 -4
- package/dist/commonjs/v3/chat.js +41 -9
- package/dist/commonjs/v3/chat.js.map +1 -1
- package/dist/commonjs/v3/chatRouteWait.d.ts +21 -0
- package/dist/commonjs/v3/chatRouteWait.js +43 -0
- package/dist/commonjs/v3/chatRouteWait.js.map +1 -0
- package/dist/commonjs/v3/compactionResponse.js +5 -0
- package/dist/commonjs/v3/compactionResponse.js.map +1 -1
- package/dist/commonjs/v3/concurrency-shared.d.ts +13 -0
- package/dist/commonjs/v3/concurrency-shared.js +35 -0
- package/dist/commonjs/v3/concurrency-shared.js.map +1 -0
- package/dist/commonjs/v3/concurrencyLimits.d.ts +73 -0
- package/dist/commonjs/v3/concurrencyLimits.js +166 -0
- package/dist/commonjs/v3/concurrencyLimits.js.map +1 -0
- package/dist/commonjs/v3/index.d.ts +2 -1
- package/dist/commonjs/v3/index.js +3 -1
- package/dist/commonjs/v3/index.js.map +1 -1
- package/dist/commonjs/v3/managedChatResponse.d.ts +44 -0
- package/dist/commonjs/v3/managedChatResponse.js +233 -0
- package/dist/commonjs/v3/managedChatResponse.js.map +1 -0
- package/dist/commonjs/v3/queues.d.ts +31 -0
- package/dist/commonjs/v3/queues.js +31 -0
- package/dist/commonjs/v3/queues.js.map +1 -1
- package/dist/commonjs/v3/shared.d.ts +18 -1
- package/dist/commonjs/v3/shared.js +137 -47
- package/dist/commonjs/v3/shared.js.map +1 -1
- package/dist/commonjs/v3/steeringContext.d.ts +41 -0
- package/dist/commonjs/v3/steeringContext.js +118 -0
- package/dist/commonjs/v3/steeringContext.js.map +1 -0
- package/dist/commonjs/v3/transcriptStorage.d.ts +4 -1
- package/dist/commonjs/v3/transcriptStorage.js +51 -4
- package/dist/commonjs/v3/transcriptStorage.js.map +1 -1
- package/dist/commonjs/version.js +1 -1
- package/dist/esm/v3/ai.d.ts +5 -2
- package/dist/esm/v3/ai.js +189 -266
- package/dist/esm/v3/ai.js.map +1 -1
- package/dist/esm/v3/chat-client.js +7 -0
- package/dist/esm/v3/chat-client.js.map +1 -1
- package/dist/esm/v3/chat-server.d.ts +1 -0
- package/dist/esm/v3/chat-server.js +8 -0
- package/dist/esm/v3/chat-server.js.map +1 -1
- package/dist/esm/v3/chat.d.ts +28 -4
- package/dist/esm/v3/chat.js +41 -9
- package/dist/esm/v3/chat.js.map +1 -1
- package/dist/esm/v3/chatRouteWait.d.ts +21 -0
- package/dist/esm/v3/chatRouteWait.js +40 -0
- package/dist/esm/v3/chatRouteWait.js.map +1 -0
- package/dist/esm/v3/compactionResponse.js +5 -0
- package/dist/esm/v3/compactionResponse.js.map +1 -1
- package/dist/esm/v3/concurrency-shared.d.ts +13 -0
- package/dist/esm/v3/concurrency-shared.js +31 -0
- package/dist/esm/v3/concurrency-shared.js.map +1 -0
- package/dist/esm/v3/concurrencyLimits.d.ts +73 -0
- package/dist/esm/v3/concurrencyLimits.js +158 -0
- package/dist/esm/v3/concurrencyLimits.js.map +1 -0
- package/dist/esm/v3/index.d.ts +2 -1
- package/dist/esm/v3/index.js +2 -1
- package/dist/esm/v3/index.js.map +1 -1
- package/dist/esm/v3/managedChatResponse.d.ts +44 -0
- package/dist/esm/v3/managedChatResponse.js +228 -0
- package/dist/esm/v3/managedChatResponse.js.map +1 -0
- package/dist/esm/v3/queues.d.ts +31 -0
- package/dist/esm/v3/queues.js +31 -0
- package/dist/esm/v3/queues.js.map +1 -1
- package/dist/esm/v3/shared.d.ts +18 -1
- package/dist/esm/v3/shared.js +136 -47
- package/dist/esm/v3/shared.js.map +1 -1
- package/dist/esm/v3/steeringContext.d.ts +41 -0
- package/dist/esm/v3/steeringContext.js +113 -0
- package/dist/esm/v3/steeringContext.js.map +1 -0
- package/dist/esm/v3/transcriptStorage.d.ts +4 -1
- package/dist/esm/v3/transcriptStorage.js +51 -4
- package/dist/esm/v3/transcriptStorage.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/docs/ai-chat/client-protocol.mdx +3 -1
- package/docs/ai-chat/error-handling.mdx +44 -76
- package/docs/ai-chat/fast-starts.mdx +1 -1
- package/docs/ai-chat/frontend.mdx +27 -21
- package/docs/ai-chat/patterns/branching-conversations.mdx +95 -230
- package/docs/ai-chat/patterns/human-in-the-loop.mdx +166 -164
- package/docs/ai-chat/patterns/tool-result-auditing.mdx +28 -27
- package/docs/ai-chat/patterns/version-upgrades.mdx +4 -4
- package/docs/ai-chat/pending-messages.mdx +19 -5
- package/docs/ai-chat/quick-start.mdx +26 -20
- package/docs/ai-chat/reference.mdx +21 -3
- package/docs/ai-chat/sessions.mdx +1 -1
- package/docs/ai-chat/testing.mdx +16 -4
- package/docs/concurrency.mdx +384 -0
- package/docs/database-connections.mdx +3 -3
- package/docs/deploy-environment-variables.mdx +6 -0
- package/docs/deployment/atomic-deployment.mdx +416 -132
- package/docs/deployment/overview.mdx +2 -2
- package/docs/github-actions.mdx +2 -2
- package/docs/github-integration.mdx +2 -2
- package/docs/idempotency.mdx +43 -5
- package/docs/introduction.mdx +1 -1
- package/docs/limits.mdx +16 -6
- package/docs/observability/query.mdx +25 -0
- package/docs/queues.mdx +271 -0
- package/docs/reports.mdx +1 -1
- package/docs/runs/priority.mdx +2 -25
- package/docs/self-hosting/env/webapp.mdx +7 -0
- package/docs/tasks/overview.mdx +3 -5
- package/docs/troubleshooting-alerts.mdx +124 -1
- package/docs/troubleshooting.mdx +12 -0
- package/docs/vercel-integration.mdx +6 -7
- package/docs/versioning.mdx +1 -1
- package/docs/writing-tasks-introduction.mdx +2 -1
- package/package.json +2 -2
- package/docs/deployment/version-skew-protection.mdx +0 -492
- package/docs/queue-concurrency.mdx +0 -358
|
@@ -1,280 +1,145 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "Branching conversations"
|
|
3
3
|
sidebarTitle: "Branching conversations"
|
|
4
|
-
description: "
|
|
4
|
+
description: "Persist an active conversation branch and use transcript storage and actions to switch the agent's context."
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Keep an explicit active branch for each chat. Use your storage adapter's `loadContext` to give the model that branch, and use actions to fork or switch it.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
A fork copies the conversation up to a selected message. Later messages belong to that branch only. For example, one branch can keep Oslo as a destination while another changes it to Tokyo.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
## Store the active branch
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
// Each message is a node in the tree
|
|
15
|
-
type ChatNode = {
|
|
16
|
-
id: string;
|
|
17
|
-
chatId: string;
|
|
18
|
-
parentId: string | null; // null for root
|
|
19
|
-
role: "user" | "assistant";
|
|
20
|
-
message: UIMessage; // the full AI SDK message
|
|
21
|
-
createdAt: Date;
|
|
22
|
-
};
|
|
23
|
-
```
|
|
13
|
+
Your database needs a chat record with an `activeBranchId`, plus messages for each branch. You can store each branch as a snapshot or use parent pointers to share message prefixes. Persist the selected branch explicitly; choosing the newest leaf loses the selection when someone switches to an older branch.
|
|
24
14
|
|
|
25
|
-
|
|
15
|
+
Keep the transcript's `final` flags, opaque runtime state, and stream cursors. A partial assistant response must stay partial after a reload. See [transcript storage](/ai-chat/transcript-storage) for the adapter contract and pagination helpers.
|
|
26
16
|
|
|
27
|
-
|
|
28
|
-
root
|
|
29
|
-
├── user: "Hello"
|
|
30
|
-
│ └── assistant: "Hi there!"
|
|
31
|
-
│ ├── user: "What's the weather?" ← branch A
|
|
32
|
-
│ │ └── assistant: "It's sunny!"
|
|
33
|
-
│ └── user: "Tell me a joke" ← branch B (active)
|
|
34
|
-
│ └── assistant: "Why did the..."
|
|
35
|
-
```
|
|
17
|
+
The examples below use application helpers from `@/lib/branches`. Implement them against your database with these guarantees:
|
|
36
18
|
|
|
37
|
-
|
|
19
|
+
| Helper | Behavior |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| `readActiveMessages(chatId)` | Return the selected branch in message order. |
|
|
22
|
+
| `forkBranch(chatId, messageId)` | Copy the active prefix through the selected message, create a branch, and select it in one transaction. Reject messages outside this chat. |
|
|
23
|
+
| `selectBranch(chatId, branchId)` | Select an existing branch belonging to this chat. |
|
|
24
|
+
| `branchStorage.load` | Return the active transcript, runtime state, final flags, and resume cursors. |
|
|
25
|
+
| `branchStorage.save` | Save the supplied transcript to the active branch and retain the supplied runtime state and cursors. |
|
|
38
26
|
|
|
39
|
-
|
|
27
|
+
Make repeated saves idempotent by message ID. Serialize branch changes with transcript writes so a turn can't save its response into a different branch.
|
|
40
28
|
|
|
41
|
-
|
|
29
|
+
## Load the selected context
|
|
42
30
|
|
|
43
|
-
|
|
31
|
+
A storage adapter with `loadContext` owns the model's context on every turn and action. Include incoming user messages in the selected branch. Handle regeneration explicitly by removing trailing assistant messages before returning the context.
|
|
44
32
|
|
|
45
|
-
```ts
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
33
|
+
```ts lib/branch-storage.ts
|
|
34
|
+
import type { UIMessage } from "ai";
|
|
35
|
+
import type {
|
|
36
|
+
LoadContextEvent,
|
|
37
|
+
TranscriptScope,
|
|
38
|
+
TranscriptStorage,
|
|
39
|
+
} from "@trigger.dev/sdk/ai";
|
|
40
|
+
import { branchStorage, readActiveMessages } from "@/lib/branches";
|
|
50
41
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
42
|
+
export const storage: TranscriptStorage = {
|
|
43
|
+
...branchStorage,
|
|
44
|
+
async loadContext<T extends UIMessage>(
|
|
45
|
+
{ chatId }: TranscriptScope,
|
|
46
|
+
event: LoadContextEvent<unknown, T>
|
|
47
|
+
): Promise<T[]> {
|
|
48
|
+
const messages = [...(await readActiveMessages(chatId))] as T[];
|
|
56
49
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
current = current.parentId ? byId.get(current.parentId) : undefined;
|
|
63
|
-
}
|
|
64
|
-
return path;
|
|
65
|
-
}
|
|
50
|
+
if (event.trigger === "regenerate-message") {
|
|
51
|
+
while (messages.length && messages.at(-1)!.role !== "user") {
|
|
52
|
+
messages.pop();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
66
55
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
56
|
+
for (const incoming of event.incomingMessages) {
|
|
57
|
+
if (incoming.role === "user" && !messages.some((m) => m.id === incoming.id)) {
|
|
58
|
+
messages.push(incoming);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
71
61
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
62
|
+
return messages;
|
|
63
|
+
},
|
|
64
|
+
};
|
|
76
65
|
```
|
|
77
66
|
|
|
78
|
-
|
|
67
|
+
This pattern supports plain user messages. For tools that wait for browser results or approval, retain the canonical assistant message in your returned context. The SDK merges incoming tool-result state into that message by ID.
|
|
68
|
+
|
|
69
|
+
`loadContext` also receives `previousMessages`, including any tail recovered from the session stream. If your database missed a save, reconcile that tail with the active branch before returning context. Recovery needs to preserve branch ownership; appending every recovered message to whichever branch is selected can mix conversations.
|
|
70
|
+
|
|
71
|
+
## Fork and switch through actions
|
|
72
|
+
|
|
73
|
+
Perform branch changes in `onAction` and update `chat.history` to match the selected branch. Returning without `chat.turn()` completes the action without asking the model for a response. The runtime still calls `storage.save` with reason `action`.
|
|
79
74
|
|
|
80
|
-
```ts
|
|
75
|
+
```ts trigger/chat.ts
|
|
81
76
|
import { chat } from "@trigger.dev/sdk/ai";
|
|
82
|
-
import { streamText, stepCountIs } from "ai";
|
|
83
77
|
import { anthropic } from "@ai-sdk/anthropic";
|
|
84
78
|
import { z } from "zod";
|
|
79
|
+
import { storage } from "@/lib/branch-storage";
|
|
80
|
+
import { forkBranch, selectBranch, readActiveMessages } from "@/lib/branches";
|
|
85
81
|
|
|
86
82
|
export const myChat = chat.agent({
|
|
87
83
|
id: "branching-chat",
|
|
88
|
-
|
|
89
|
-
// Load the active branch from the DB on every turn.
|
|
90
|
-
// The frontend's message array is ignored — the tree is the source of truth.
|
|
91
|
-
hydrateMessages: async ({ chatId, trigger, incomingMessages }) => {
|
|
92
|
-
if (trigger === "submit-message" && incomingMessages.length > 0) {
|
|
93
|
-
await appendMessage(chatId, incomingMessages[incomingMessages.length - 1]!);
|
|
94
|
-
}
|
|
95
|
-
return getActiveBranch(chatId);
|
|
96
|
-
},
|
|
97
|
-
|
|
84
|
+
storage,
|
|
98
85
|
actionSchema: z.discriminatedUnion("type", [
|
|
99
|
-
|
|
100
|
-
z.object({ type: z.literal("
|
|
101
|
-
// Switch to a different branch by selecting a leaf node
|
|
102
|
-
z.object({ type: z.literal("switch-branch"), leafId: z.string() }),
|
|
103
|
-
// Undo the last user + assistant exchange
|
|
104
|
-
z.object({ type: z.literal("undo") }),
|
|
86
|
+
z.object({ type: z.literal("fork"), messageId: z.string() }),
|
|
87
|
+
z.object({ type: z.literal("switch"), branchId: z.string() }),
|
|
105
88
|
]),
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const original = await db.chatNode.findUnique({ where: { id: action.messageId } });
|
|
112
|
-
if (!original) break;
|
|
113
|
-
|
|
114
|
-
const newId = generateId();
|
|
115
|
-
await db.chatNode.create({
|
|
116
|
-
data: {
|
|
117
|
-
id: newId,
|
|
118
|
-
chatId,
|
|
119
|
-
parentId: original.parentId, // same parent = sibling
|
|
120
|
-
role: "user",
|
|
121
|
-
message: { id: newId, role: "user", parts: [{ type: "text", text: action.text }] },
|
|
122
|
-
createdAt: new Date(),
|
|
123
|
-
},
|
|
124
|
-
});
|
|
125
|
-
// Active branch now resolves through the new sibling (most recent leaf)
|
|
126
|
-
break;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
case "switch-branch": {
|
|
130
|
-
// Mark this leaf as the most recently accessed so getActiveBranch picks it
|
|
131
|
-
await db.chatNode.update({
|
|
132
|
-
where: { id: action.leafId },
|
|
133
|
-
data: { createdAt: new Date() },
|
|
134
|
-
});
|
|
135
|
-
break;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
case "undo": {
|
|
139
|
-
// Remove the last two nodes (user + assistant) from the active branch
|
|
140
|
-
const branch = await getActiveBranch(chatId);
|
|
141
|
-
if (branch.length >= 2) {
|
|
142
|
-
const lastTwo = branch.slice(-2);
|
|
143
|
-
await db.chatNode.deleteMany({
|
|
144
|
-
where: { id: { in: lastTwo.map((m) => m.id) } },
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
89
|
+
onAction: async ({ chatId, action }) => {
|
|
90
|
+
if (action.type === "fork") {
|
|
91
|
+
await forkBranch(chatId, action.messageId);
|
|
92
|
+
} else {
|
|
93
|
+
await selectBranch(chatId, action.branchId);
|
|
149
94
|
}
|
|
150
95
|
|
|
151
|
-
|
|
152
|
-
const updated = await getActiveBranch(chatId);
|
|
153
|
-
chat.history.set(updated);
|
|
154
|
-
},
|
|
155
|
-
|
|
156
|
-
onTurnComplete: async ({ chatId, responseMessage }) => {
|
|
157
|
-
// Persist the assistant's response as a new node
|
|
158
|
-
if (responseMessage) {
|
|
159
|
-
await appendMessage(chatId, responseMessage);
|
|
160
|
-
}
|
|
96
|
+
chat.history.set(await readActiveMessages(chatId));
|
|
161
97
|
},
|
|
162
|
-
|
|
163
|
-
run: async ({ messages, signal }) => {
|
|
98
|
+
run: async ({ messages, signal, streamText }) => {
|
|
164
99
|
return streamText({
|
|
165
100
|
model: anthropic("claude-sonnet-4-5"),
|
|
166
101
|
messages,
|
|
167
102
|
abortSignal: signal,
|
|
168
|
-
stopWhen: stepCountIs(15),
|
|
169
103
|
});
|
|
170
104
|
},
|
|
171
105
|
});
|
|
172
106
|
```
|
|
173
107
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
### Sending actions
|
|
177
|
-
|
|
178
|
-
Wire up edit, undo, and branch switching to the transport:
|
|
179
|
-
|
|
180
|
-
```tsx
|
|
181
|
-
function MessageActions({ message, chatId }: { message: UIMessage; chatId: string }) {
|
|
182
|
-
const transport = useTransport();
|
|
183
|
-
const [editing, setEditing] = useState(false);
|
|
184
|
-
const [editText, setEditText] = useState("");
|
|
185
|
-
|
|
186
|
-
if (message.role !== "user") return null;
|
|
187
|
-
|
|
188
|
-
return (
|
|
189
|
-
<div>
|
|
190
|
-
{editing ? (
|
|
191
|
-
<form onSubmit={() => {
|
|
192
|
-
transport.sendAction(chatId, { type: "edit", messageId: message.id, text: editText });
|
|
193
|
-
setEditing(false);
|
|
194
|
-
}}>
|
|
195
|
-
<input value={editText} onChange={(e) => setEditText(e.target.value)} />
|
|
196
|
-
<button type="submit">Save</button>
|
|
197
|
-
</form>
|
|
198
|
-
) : (
|
|
199
|
-
<button onClick={() => { setEditText(getMessageText(message)); setEditing(true); }}>
|
|
200
|
-
Edit
|
|
201
|
-
</button>
|
|
202
|
-
)}
|
|
203
|
-
</div>
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
```
|
|
108
|
+
Wait for a response to finish before forking or switching. Start with forks at completed assistant messages. Forking inside an unresolved tool exchange needs a policy for pending tool calls and already-performed side effects.
|
|
207
109
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
To show the `< 2/3 >` sibling switcher, query the tree for siblings at each fork point. This is a frontend concern — the backend exposes the data, the UI navigates it.
|
|
211
|
-
|
|
212
|
-
```tsx
|
|
213
|
-
function BranchSwitcher({ message, chatId, siblings }: {
|
|
214
|
-
message: UIMessage;
|
|
215
|
-
chatId: string;
|
|
216
|
-
siblings: { id: string; createdAt: string }[];
|
|
217
|
-
}) {
|
|
218
|
-
const transport = useTransport();
|
|
219
|
-
if (siblings.length <= 1) return null;
|
|
220
|
-
|
|
221
|
-
const currentIndex = siblings.findIndex((s) => s.id === message.id);
|
|
222
|
-
|
|
223
|
-
return (
|
|
224
|
-
<div>
|
|
225
|
-
<button
|
|
226
|
-
disabled={currentIndex === 0}
|
|
227
|
-
onClick={() => {
|
|
228
|
-
// Find the leaf of the previous sibling's subtree
|
|
229
|
-
transport.sendAction(chatId, {
|
|
230
|
-
type: "switch-branch",
|
|
231
|
-
leafId: siblings[currentIndex - 1]!.id,
|
|
232
|
-
});
|
|
233
|
-
}}
|
|
234
|
-
>
|
|
235
|
-
<
|
|
236
|
-
</button>
|
|
237
|
-
<span>{currentIndex + 1}/{siblings.length}</span>
|
|
238
|
-
<button
|
|
239
|
-
disabled={currentIndex === siblings.length - 1}
|
|
240
|
-
onClick={() => {
|
|
241
|
-
transport.sendAction(chatId, {
|
|
242
|
-
type: "switch-branch",
|
|
243
|
-
leafId: siblings[currentIndex + 1]!.id,
|
|
244
|
-
});
|
|
245
|
-
}}
|
|
246
|
-
>
|
|
247
|
-
>
|
|
248
|
-
</button>
|
|
249
|
-
</div>
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
```
|
|
110
|
+
## Update the frontend after the action
|
|
253
111
|
|
|
254
|
-
|
|
255
|
-
The sibling data (which messages share the same parent) needs to come from your database — query it when loading the chat or include it as client data. The agent only returns the active branch via `hydrateMessages`.
|
|
256
|
-
</Note>
|
|
112
|
+
Use `useChatActions` with `useChat`'s `sendMessage`. It consumes the action's response stream and keeps request state in the chat hook. After the action completes, reload the selected transcript so the visible messages match the next model call.
|
|
257
113
|
|
|
258
|
-
|
|
114
|
+
```tsx app/chat/BranchPicker.tsx
|
|
115
|
+
import { useChat } from "@ai-sdk/react";
|
|
116
|
+
import { useChatActions } from "@trigger.dev/sdk/chat/react";
|
|
117
|
+
import { loadTranscript } from "@/app/actions";
|
|
259
118
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
119
|
+
// Inside your chat component, with its existing chatId and transport:
|
|
120
|
+
const { sendMessage, setMessages, status } = useChat({ id: chatId, transport });
|
|
121
|
+
const { sendAction } = useChatActions({ sendMessage });
|
|
122
|
+
const busy = status === "submitted" || status === "streaming";
|
|
123
|
+
|
|
124
|
+
async function switchBranch(branchId: string) {
|
|
125
|
+
await sendAction({ type: "switch", branchId });
|
|
126
|
+
const transcript = await loadTranscript({ chatId });
|
|
127
|
+
setMessages(transcript.messages);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
<button disabled={busy} onClick={() => switchBranch(branch.id)}>
|
|
131
|
+
{branch.label}
|
|
132
|
+
</button>
|
|
133
|
+
```
|
|
267
134
|
|
|
268
|
-
|
|
135
|
+
Authenticate `loadTranscript` and check ownership before reading the chat. Validate branch IDs on the server even when the frontend only offers branches from that chat. If you use `transport.sendAction` directly, consume its returned stream before sending another message.
|
|
269
136
|
|
|
270
|
-
|
|
271
|
-
- **The tree lives in your database** — the agent loads a linear path from it via `hydrateMessages`. The agent itself doesn't know about the tree structure.
|
|
272
|
-
- **`hydrateMessages` + `onAction` + `chat.history`** are the three primitives. Hydration loads the active path, actions modify the tree, and `chat.history.set()` syncs the accumulator after tree modifications.
|
|
273
|
-
- **Frontend owns navigation** — the `< 2/3 >` UI, sibling queries, and branch switching triggers are client-side concerns. The backend just processes actions and returns responses.
|
|
137
|
+
## Check branch isolation
|
|
274
138
|
|
|
275
|
-
|
|
139
|
+
1. Tell the agent the destination is Oslo and wait for the response.
|
|
140
|
+
2. Fork at that response, then change the destination to Tokyo.
|
|
141
|
+
3. Switch to the original branch and ask for the destination. It should answer Oslo.
|
|
142
|
+
4. Switch to the fork and ask again. It should answer Tokyo.
|
|
143
|
+
5. Reload the page and repeat the checks. The selected branch and its messages should survive the reload.
|
|
276
144
|
|
|
277
|
-
|
|
278
|
-
- [Actions](/ai-chat/actions) — custom actions with `actionSchema` and `onAction`
|
|
279
|
-
- [`chat.history`](/ai-chat/backend#chat-history) — imperative history mutations
|
|
280
|
-
- [Database persistence](/ai-chat/patterns/database-persistence) — basic persistence pattern (linear)
|
|
145
|
+
Also test regeneration, repeated storage saves, and continuation in a fresh run. An in-memory branch choice can pass the first four steps and still disappear when the worker restarts.
|