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
docs(ai-chat): clarify setup, steering, recovery, and branching
Clarify chat.agent setup and recovery with examples that check chat
ownership before starting sessions or refreshing tokens, use managed
streamText for steering, and preserve partial responses after failures.
Explain how to load transcripts before resuming the frontend and stop a
resumed generation.
Update the branching guide to use transcript storage and an explicit
active branch, and add checks for transport behavior, error recovery,
and branch isolation.
Mono-RevId: 071c63091f9605d84806381d97311f2fb1583eea
`run()` is your code — wrap it in try/catch for full control. This is the right place to save partial state to your DB before the error chunk goes out:
105
-
106
-
```ts
107
-
run: async ({ messages, chatId, signal }) => {
108
-
try {
109
-
returnstreamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
Catch errors from work you await inside `run()`, such as loading application data. Errors emitted after `streamText()` returns reach the response stream instead. Record those in `onTurnComplete`, which receives the partial response and the error.
returnstreamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
163
146
},
164
147
});
165
148
```
166
149
167
150
<Info>
168
-
`chat.agent`uses `retry: { maxAttempts: 1 }` internally, so the run never retries on failure. To add run-level retries, wrap the agent in a parent task or implement your own retry logic in the frontend (re-send the message).
151
+
`chat.agent`defaults to one run attempt. Setting `oomMachine` enables one additional attempt on the larger machine after an out-of-memory failure. Recover from a failed turn by sending another message or calling `regenerate()`.
`regenerate()` removes the last assistant response and re-sends. Combined with `onValidateMessages` or `hydrateMessages`, you can reload the canonical state from your DB before retrying.
208
+
`regenerate()` removes the last assistant response and re-sends. If your storage implements `loadContext`, handle `trigger === "regenerate-message"` there: remove trailing assistant messages from the active branch so the model receives the last user message as its prompt.
223
209
224
210
### Pattern 3: Save partial responses
225
211
226
-
When a stream errors mid-response, the `responseMessage` in `onBeforeTurnComplete` and `onTurnComplete` contains the partial output. Save it as a "draft" so the user can see what was generated before the error:
212
+
When a stream fails or you stop a response, transcript storage preserves the partial assistant message with `final: false`. Keep that flag in your storage adapter so a reload doesn't present an incomplete answer as finished.
if (!responseMessage|| (!error&&!stopped)) return;
244
219
245
-
If the primary model errors, try a fallback model in the same turn:
246
-
247
-
```ts
248
-
run: async ({ messages, signal }) => {
249
-
try {
250
-
returnstreamText({
251
-
model: anthropic("claude-sonnet-4-5"),
252
-
messages,
253
-
abortSignal: signal,
254
-
stopWhen: stepCountIs(15),
255
-
});
256
-
} catch (err) {
257
-
console.warn("Primary model failed, falling back:", err);
258
-
returnstreamText({
259
-
model: anthropic("claude-sonnet-4-6"),
260
-
messages,
261
-
abortSignal: signal,
262
-
stopWhen: stepCountIs(15),
263
-
});
264
-
}
220
+
awaitdb.partialResponse.upsert({
221
+
where: { id: responseMessage.id },
222
+
create: {
223
+
id: responseMessage.id,
224
+
chatId,
225
+
message: responseMessage,
226
+
reason: stopped?"stopped":"errored",
227
+
},
228
+
update: {
229
+
message: responseMessage,
230
+
reason: stopped?"stopped":"errored",
231
+
},
232
+
});
265
233
},
266
234
```
267
235
268
-
<Note>
269
-
This only catches errors thrown synchronously by `streamText` setup. Errors that happen mid-stream go through `uiMessageStreamOptions.onError`, not your try/catch.
270
-
</Note>
236
+
### Pattern 4: Retry with another model
237
+
238
+
`streamText()` returns before the provider finishes streaming. A `try/catch` around that call only catches setup failures; it doesn't catch errors that arrive later in the stream.
239
+
240
+
For a streamed provider failure, let the failed turn finish, choose a fallback model on your server, then regenerate the response. The partial response remains available until you replace it. Check whether tools already performed side effects before retrying, and make those operations idempotent.
271
241
272
242
## What gets written to the stream on error
273
243
@@ -283,7 +253,7 @@ The AI SDK's `useChat` processes this and:
283
253
284
254
1. Sets `useChat`'s `error` field to an `Error` with `message = errorText`
285
255
2. Calls the user's `onError` callback (if set)
286
-
3.Marks the turn as complete (`status`returns to `"ready"`)
256
+
3.Sets `status` to `"error"`. Sending another message or regenerating starts a new request.
287
257
288
258
```tsx
289
259
const { messages, error, status } =useChat({
@@ -375,13 +345,11 @@ const transport = useTriggerChatTransport({
375
345
376
346
## Run-level retries
377
347
378
-
`chat.agent`uses `retry: { maxAttempts: 1 }` — the run **never retries** on unhandled failure. This is intentional: each turn is conversation-preserving, so a true run failure is severe and shouldn't silently retry (which could send duplicate API calls or mutate state twice).
348
+
`chat.agent`defaults to `retry: { maxAttempts: 1 }`. Turn errors are handled within the conversation, so another message can succeed in the same run.
379
349
380
-
To add retry-like behavior:
350
+
If you configure `oomMachine`, the agent uses up to 2 attempts and retries an out-of-memory failure on that machine. This doesn't enable retries for ordinary provider or tool errors.
381
351
382
-
-**Per-turn retries**: handle inside `run()` with try/catch and a fallback model
383
-
-**Per-message retries**: re-send from the frontend (call `sendMessage` or `regenerate` again)
384
-
-**Whole-run retries**: wrap `chat.agent` with a parent task that has `retry` configured, and call the agent's task internally
352
+
Use the AI SDK's provider retry settings for transient model-call failures. After a turn has failed, let the user send another message or call `regenerate()`. Retrying a conversation can repeat tool side effects, so use idempotency keys for writes.
Copy file name to clipboardExpand all lines: docs/ai-chat/frontend.mdx
+15-16Lines changed: 15 additions & 16 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -139,6 +139,10 @@ Because the underlying Session row outlives individual runs, a chat you were in
139
139
140
140
If you do not keep your own copy of the conversation, load it from the agent's [transcript storage](/ai-chat/transcript-storage#reading-the-transcript) instead: `chat.createLoadTranscriptAction(storage)` on the server and `useLoadTranscript(chatId, action, { transport })` in the browser return the messages and seed the transport's resume cursor, for the default storage and your own alike.
141
141
142
+
Mount the component that calls `useChat` after transcript loading finishes. Its initial `messages` and the transport's cursor must describe the same saved transcript. Transcript loading can seed the cursor before session hydration: the transport keeps it until the session is created or hydrated. Reconnecting the live stream requires a known session.
143
+
144
+
To stop a generation after a reload, stop the local subscription and send the backend stop command together. See [Stop generation](#stop-generation) for the handler.
145
+
142
146
```tsx app/chat/[chatId]/ChatPage.tsx
143
147
"use client";
144
148
@@ -250,11 +254,13 @@ const transport = useTriggerChatTransport<typeof myChat>({
The transport threads `clientData` through three places automatically: into `startSession`'s `params.clientData` for the first run's `payload.metadata`, into per-turn `metadata` on every `.in/append` chunk, and live-updates if the option value changes between renders (so React-driven values like the current user work without reconstructing the transport).
261
+
The transport threads `clientData` through three places automatically: into `startSession`'s `params.clientData` for the first run's `payload.metadata`, into per-turn `metadata` on every `.in/append` chunk, and live-updates if the option value changes between renders (so React-driven values like the selected language work without reconstructing the transport).
262
+
263
+
Browser-supplied `clientData` is input, including after schema validation. Use it for preferences such as language. Derive ownership, permissions, and plan limits from authenticated server state. See [trusted edge signals](/ai-chat/patterns/trusted-edge-signals).
258
264
259
265
### Per-message metadata
260
266
@@ -270,26 +276,19 @@ Instead of manually parsing `clientData` with Zod in every hook, pass a `clientD
270
276
271
277
```ts
272
278
import { chat } from"@trigger.dev/sdk/ai";
273
-
import { streamText, stepCountIs } from"ai";
279
+
import { stepCountIs } from"ai";
274
280
import { anthropic } from"@ai-sdk/anthropic";
275
281
import { z } from"zod";
276
282
277
283
exportconst myChat =chat.agent({
278
284
id: "my-chat",
279
285
clientDataSchema: z.object({
280
-
model: z.string().optional(),
281
-
userId: z.string(),
286
+
locale: z.enum(["en", "fr"]).default("en"),
282
287
}),
283
-
onChatStart: async ({ chatId, clientData }) => {
284
-
// clientData is typed as { model?: string; userId: string }
285
-
awaitdb.chat.create({
286
-
data: { id: chatId, userId: clientData.userId },
287
-
});
288
-
},
289
-
run: async ({ messages, clientData, signal }) => {
290
-
// Same typed clientData — no manual parsing needed
system: clientData?.locale==="fr"?"Reply in French.":"Reply in English.",
293
292
messages,
294
293
abortSignal: signal,
295
294
stopWhen: stepCountIs(15),
@@ -317,15 +316,15 @@ Supports Zod, ArkType, Valibot, and other schema libraries supported by the SDK.
317
316
318
317
Use `transport.stopGeneration(chatId)` to stop the current generation. This sends a stop signal to the running task via input streams, aborting the current `streamText` call while keeping the run alive for the next message.
319
318
320
-
`stopGeneration` works in all scenarios — including after a page refresh when the stream was reconnected via `resume`. Call it alongside `useChat`'s `stop()`to also update the frontend state:
319
+
`stopGeneration`also works after a page refresh when the stream was reconnected via `resume`. Call `useChat`'s `stop()`immediately, then send the backend stop command without waiting for the local stop to finish:
321
320
322
321
```tsx
323
322
const { messages, sendMessage, stop: aiStop, status } =useChat({ transport });
0 commit comments