Skip to content

Commit bd345bb

Browse files
committed
docs: add onChatSuspend/onChatResume, exitAfterPreloadIdle, withClientData, ChatBuilder docs
1 parent a90fead commit bd345bb

File tree

3 files changed

+237
-35
lines changed

3 files changed

+237
-35
lines changed

docs/ai-chat/backend.mdx

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ description: "Three approaches to building your chat backend — chat.task(), se
99
The highest-level approach. Handles message accumulation, stop signals, turn lifecycle, and auto-piping automatically.
1010

1111
<Tip>
12-
To fix a **custom** `UIMessage` subtype (typed custom data parts, tool map, etc.), use [`chat.withUIMessage<...>().task({...})`](/ai-chat/types) instead of `chat.task({...})`. Options are the same; defaults for `toUIMessageStream()` can be set on `withUIMessage`.
12+
To fix a **custom** `UIMessage` subtype or typed client data schema, use the [ChatBuilder](/ai-chat/types#chatbuilder) via `chat.withUIMessage<...>()` and/or `chat.withClientData({ schema })`. Builder-level hooks can also be chained before `.task()`. See [Types](/ai-chat/types).
1313
</Tip>
1414

1515
### Simple: return a StreamTextResult
@@ -71,7 +71,9 @@ async function runAgentLoop(messages: ModelMessage[]) {
7171

7272
Every chat lifecycle callback and the **`run`** payload include **`ctx`**: the same run context object as `task({ run: (payload, { ctx }) => ... })`. Import the type with **`import type { TaskRunContext } from "@trigger.dev/sdk"`** (the **`Context`** export is the same type). Use **`ctx`** for tags, metadata, or any API that needs the full run record. The string **`runId`** on chat events is always **`ctx.run.id`** (both are provided for convenience). See [Task context (`ctx`)](/ai-chat/reference#task-context-ctx) in the API reference.
7373

74-
Standard **[task lifecycle hooks](/tasks/overview)****`onWait`**, **`onResume`**, **`onComplete`**, **`onFailure`**, etc. — are also available on **`chat.task()`** with the same shapes as on a normal `task()`. For example, tear down an external sandbox **right before the run suspends** waiting for the next message using **`onWait`** when **`wait.type === "token"`**. See the [Code execution sandbox](/ai-chat/patterns/code-sandbox) pattern.
74+
Standard **[task lifecycle hooks](/tasks/overview)****`onWait`**, **`onResume`**, **`onComplete`**, **`onFailure`**, etc. — are also available on **`chat.task()`** with the same shapes as on a normal `task()`.
75+
76+
Chat tasks also have two dedicated suspension hooks — **`onChatSuspend`** and **`onChatResume`** — that fire at the idle-to-suspended transition with full chat context. Use them for resource cleanup (e.g. tearing down sandboxes) and re-initialization. See [onChatSuspend / onChatResume](#onchatsuspend--onchatresume) and the [Code execution sandbox](/ai-chat/patterns/code-sandbox) pattern.
7577

7678
#### onPreload
7779

@@ -276,6 +278,69 @@ export const myChat = chat.task({
276278
For a full **conversation + session** persistence pattern (including preload, continuation, and token renewal), see [Database persistence](/ai-chat/patterns/database-persistence).
277279
</Tip>
278280

281+
#### onChatSuspend / onChatResume
282+
283+
Chat-specific hooks that fire at the **idle-to-suspended** transition — the moment the run stops using compute and waits for the next message. These replace the need for the generic `onWait` / `onResume` task hooks for chat-specific work.
284+
285+
The `phase` discriminator tells you **when** the suspend/resume happened:
286+
287+
- `"preload"` — after `onPreload`, waiting for the first message
288+
- `"turn"` — after `onTurnComplete`, waiting for the next message
289+
290+
```ts
291+
export const myChat = chat.task({
292+
id: "my-chat",
293+
onChatSuspend: async (event) => {
294+
// Tear down expensive resources before suspending
295+
await disposeCodeSandbox(event.ctx.run.id);
296+
if (event.phase === "turn") {
297+
logger.info("Suspending after turn", { turn: event.turn });
298+
}
299+
},
300+
onChatResume: async (event) => {
301+
// Re-initialize after waking up
302+
logger.info("Resumed", { phase: event.phase });
303+
},
304+
run: async ({ messages, signal }) => {
305+
return streamText({ model: openai("gpt-4o"), messages, abortSignal: signal });
306+
},
307+
});
308+
```
309+
310+
| Field | Type | Description |
311+
| ------------ | ---------------- | ------------------------------------------------------------ |
312+
| `phase` | `"preload" \| "turn"` | Whether this is a preload or post-turn suspension |
313+
| `ctx` | `TaskRunContext` | Full task run context |
314+
| `chatId` | `string` | Chat session ID |
315+
| `runId` | `string` | The Trigger.dev run ID |
316+
| `clientData` | Typed by `clientDataSchema` | Custom data from the frontend |
317+
| `turn` | `number` | Turn number (**`"turn"` phase only**) |
318+
| `messages` | `ModelMessage[]` | Accumulated model messages (**`"turn"` phase only**) |
319+
| `uiMessages` | `UIMessage[]` | Accumulated UI messages (**`"turn"` phase only**) |
320+
321+
<Tip>
322+
Unlike `onWait` (which fires for all wait types — duration, task, batch, token), `onChatSuspend` fires only at chat suspension points with full chat context. No need to filter on `wait.type`.
323+
</Tip>
324+
325+
#### exitAfterPreloadIdle
326+
327+
When set to `true`, a preloaded run completes successfully after the idle timeout elapses instead of suspending. Use this for "fire and forget" preloads — if the user doesn't send a message during the idle window, the run ends cleanly.
328+
329+
```ts
330+
export const myChat = chat.task({
331+
id: "my-chat",
332+
preloadIdleTimeoutInSeconds: 10,
333+
exitAfterPreloadIdle: true,
334+
onPreload: async ({ chatId, clientData }) => {
335+
// Eagerly set up state — if no message comes, the run just ends
336+
await initializeChat(chatId, clientData);
337+
},
338+
run: async ({ messages, signal }) => {
339+
return streamText({ model: openai("gpt-4o"), messages, abortSignal: signal });
340+
},
341+
});
342+
```
343+
279344
### Using prompts
280345

281346
Use [AI Prompts](/ai/prompts) to manage your system prompt as versioned, overridable config. Store the resolved prompt in a lifecycle hook with `chat.prompt.set()`, then spread `chat.toStreamTextOptions()` into `streamText` — it includes the system prompt, model, config, and telemetry automatically.

docs/ai-chat/reference.mdx

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ Options for `chat.task()`.
2929
| `preloadIdleTimeoutInSeconds` | `number` | Same as `idleTimeoutInSeconds` | Idle timeout after `onPreload` fires |
3030
| `preloadTimeout` | `string` | Same as `turnTimeout` | Suspend timeout for preloaded runs |
3131
| `uiMessageStreamOptions` | `ChatUIMessageStreamOptions` || Default options for `toUIMessageStream()`. Per-turn override via `chat.setUIMessageStreamOptions()` |
32+
| `onChatSuspend` | `(event: ChatSuspendEvent) => Promise<void> \| void` || Fires right before the run suspends. See [onChatSuspend](/ai-chat/backend#onchatsuspend--onchatresume) |
33+
| `onChatResume` | `(event: ChatResumeEvent) => Promise<void> \| void` || Fires right after the run resumes from suspension |
34+
| `exitAfterPreloadIdle` | `boolean` | `false` | Exit run after preload idle timeout instead of suspending. See [exitAfterPreloadIdle](/ai-chat/backend#exitafterpreloadidle) |
3235

3336
Plus all standard [TaskOptions](/tasks/overview)`retry`, `queue`, `machine`, `maxDuration`, **`onWait`**, **`onResume`**, **`onComplete`**, and other lifecycle hooks. Those hooks use the same parameter shapes as on a normal `task()` (including `ctx`).
3437

@@ -148,6 +151,36 @@ Passed to the `onBeforeTurnComplete` callback. Same fields as `TurnCompleteEvent
148151
| _(all TurnCompleteEvent fields)_ | | See [TurnCompleteEvent](#turncompleteevent) (includes `ctx`) |
149152
| `writer` | [`ChatWriter`](#chatwriter) | Stream writer — the stream is still open so chunks appear in the current turn |
150153

154+
## ChatSuspendEvent
155+
156+
Passed to the `onChatSuspend` callback. A discriminated union on `phase`.
157+
158+
| Field | Type | Description |
159+
| ------------ | --------------------------- | -------------------------------------------------------- |
160+
| `phase` | `"preload" \| "turn"` | Whether this is a preload or post-turn suspension |
161+
| `ctx` | `TaskRunContext` | Full task run context |
162+
| `chatId` | `string` | Chat session ID |
163+
| `runId` | `string` | The Trigger.dev run ID |
164+
| `clientData` | Typed by `clientDataSchema` | Custom data from the frontend |
165+
| `turn` | `number` | Turn number (**`"turn"` phase only**) |
166+
| `messages` | `ModelMessage[]` | Accumulated model messages (**`"turn"` phase only**) |
167+
| `uiMessages` | `UIMessage[]` | Accumulated UI messages (**`"turn"` phase only**) |
168+
169+
## ChatResumeEvent
170+
171+
Passed to the `onChatResume` callback. Same discriminated union shape as `ChatSuspendEvent`.
172+
173+
| Field | Type | Description |
174+
| ------------ | --------------------------- | -------------------------------------------------------- |
175+
| `phase` | `"preload" \| "turn"` | Whether this is a preload or post-turn resumption |
176+
| `ctx` | `TaskRunContext` | Full task run context |
177+
| `chatId` | `string` | Chat session ID |
178+
| `runId` | `string` | The Trigger.dev run ID |
179+
| `clientData` | Typed by `clientDataSchema` | Custom data from the frontend |
180+
| `turn` | `number` | Turn number (**`"turn"` phase only**) |
181+
| `messages` | `ModelMessage[]` | Accumulated model messages (**`"turn"` phase only**) |
182+
| `uiMessages` | `UIMessage[]` | Accumulated UI messages (**`"turn"` phase only**) |
183+
151184
## ChatWriter
152185

153186
A stream writer passed to lifecycle callbacks. Write custom `UIMessageChunk` parts (e.g. `data-*` parts) to the chat stream.
@@ -322,16 +355,15 @@ All methods available on the `chat` object from `@trigger.dev/sdk/ai`.
322355
| `chat.cleanupAbortedParts(message)` | Remove incomplete parts from a stopped response message |
323356
| `chat.stream` | Typed chat output stream — use `.writer()`, `.pipe()`, `.append()`, `.read()` |
324357
| `chat.MessageAccumulator` | Class that accumulates conversation messages across turns |
325-
| `chat.withUIMessage(config?).task(options)` | Same as `chat.task`, but fixes a custom `UIMessage` subtype and optional default stream options. See [Types](/ai-chat/types) |
358+
| `chat.withUIMessage(config?)` | Returns a [ChatBuilder](/ai-chat/types#chatbuilder) with a fixed `UIMessage` subtype. See [Types](/ai-chat/types) |
359+
| `chat.withClientData({ schema })` | Returns a [ChatBuilder](/ai-chat/types#chatbuilder) with a fixed client data schema. See [Types](/ai-chat/types#typed-client-data-with-chatwithclientdata) |
326360

327361
## `chat.withUIMessage`
328362

329-
Returns `{ task }`, where `task` is like [`chat.task`](#chat-namespace) but parameterized on a UI message type `TUIM`.
363+
Returns a [`ChatBuilder`](/ai-chat/types#chatbuilder) with a fixed `UIMessage` subtype. Chain `.withClientData()`, hook methods, and `.task()`.
330364

331365
```ts
332-
chat.withUIMessage<TUIM>(config?: ChatWithUIMessageConfig<TUIM>): {
333-
task: (options: ChatTaskOptions<..., ..., TUIM>) => Task<...>;
334-
};
366+
chat.withUIMessage<TUIM>(config?: ChatWithUIMessageConfig<TUIM>): ChatBuilder<TUIM>;
335367
```
336368

337369
| Parameter | Type | Description |
@@ -340,6 +372,20 @@ chat.withUIMessage<TUIM>(config?: ChatWithUIMessageConfig<TUIM>): {
340372

341373
Use this when you need [`InferChatUIMessage`](#inferchatuimessage) / typed `data-*` parts / `InferUITools` to line up across backend hooks and `useChat`. Full guide: [Types](/ai-chat/types).
342374

375+
## `chat.withClientData`
376+
377+
Returns a [`ChatBuilder`](/ai-chat/types#chatbuilder) with a fixed client data schema. All hooks and `run` get typed `clientData` without passing `clientDataSchema` in `.task()` options.
378+
379+
```ts
380+
chat.withClientData<TSchema>({ schema: TSchema }): ChatBuilder<UIMessage, TSchema>;
381+
```
382+
383+
| Parameter | Type | Description |
384+
| --------- | ------------ | -------------------------------------------------- |
385+
| `schema` | `TaskSchema` | Zod, ArkType, Valibot, or any supported schema lib |
386+
387+
Full guide: [Typed client data](/ai-chat/types#typed-client-data-with-chatwithclientdata).
388+
343389
## `ChatWithUIMessageConfig`
344390

345391
| Field | Type | Description |

0 commit comments

Comments
 (0)