Skip to content

Commit 69cf5b3

Browse files
committed
docs(ai-chat): prep for v4.5 release candidate
Comprehensive pass on the AI Agents + Prompts docs ahead of cutting v4.5.0-rc.0: - RC banner across every AI chat + Prompts page (single snippet) with links to a new Compatibility table covering ai SDK v5/v6, React 18/19, and Node 18.20+ peer ranges. - Restructured the Agents sidebar into four sub-groups (Building agents, Features, Patterns, Reference) and hoisted Agents to the top level of the docs nav. - New Sessions reference page documenting sessions.start / retrieve / list / open, SessionHandle.in / .out, control records, and the authorization model. - Split the catch-all Features page into proper homes: chat.local moves to its own page under Building agents, chat.defer joins background injection, custom data parts + raw chat.stream move into Backend, and the target:"root" + ai.toolExecute walkthroughs move into the Sub-agents pattern. - Fixed broken code examples and cross-link anchors: auth.createPublicToken scopes shape, useTriggerChatTransport import path, AgentChatOptions table, runInMockTaskContext re-export, and anchor slugs that didn't match Mintlify's slugifier.
1 parent 53a2d94 commit 69cf5b3

41 files changed

Lines changed: 927 additions & 1001 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/ai-chat/actions.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ sidebarTitle: "Actions"
44
description: "Custom commands sent from the frontend that mutate chat state without consuming a turn — undo, rollback, edit, regenerate."
55
---
66

7+
import RcBanner from "/snippets/ai-chat-rc-banner.mdx";
8+
9+
<RcBanner />
10+
711
## Overview
812

913
Custom actions let the frontend send structured commands (undo, rollback, edit, regenerate) that modify the conversation state. **Actions are not turns**: they fire `hydrateMessages` (if set) and `onAction` only. No turn lifecycle hooks (`onTurnStart` / `prepareMessages` / `onBeforeTurnComplete` / `onTurnComplete`), no `run()`, no turn-counter increment. The trace span is named `chat action`.

docs/ai-chat/backend.mdx

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ sidebarTitle: "Backend"
44
description: "Three approaches to building your chat backend — chat.agent(), session iterator, or raw task primitives."
55
---
66

7+
import RcBanner from "/snippets/ai-chat-rc-banner.mdx";
8+
9+
<RcBanner />
10+
711
## chat.agent()
812

913
The highest-level approach. Handles message accumulation, stop signals, turn lifecycle, and auto-piping automatically.
@@ -87,6 +91,85 @@ async function runAgentLoop(messages: ModelMessage[]) {
8791
}
8892
```
8993

94+
### Custom data parts
95+
96+
Add custom `data-*` parts to the assistant's response message via `chat.response.write()` (from `run()`) or the `writer` parameter in lifecycle hooks. Non-transient `data-*` chunks are automatically added to `responseMessage.parts` and surface in `onTurnComplete` for persistence:
97+
98+
```ts
99+
export const myChat = chat.agent({
100+
id: "my-chat",
101+
onBeforeTurnComplete: async ({ writer, turn }) => {
102+
// This data part will be in responseMessage.parts in onTurnComplete
103+
writer.write({
104+
type: "data-metadata",
105+
data: { turn, model: "gpt-4o", timestamp: Date.now() },
106+
});
107+
},
108+
onTurnComplete: async ({ responseMessage }) => {
109+
// responseMessage.parts includes the data-metadata part
110+
await db.messages.save(responseMessage);
111+
},
112+
run: async ({ messages, signal }) => {
113+
// Also works from run() via chat.response
114+
chat.response.write({
115+
type: "data-context",
116+
data: { searchResults: results },
117+
});
118+
119+
return streamText({ model: openai("gpt-4o"), messages, abortSignal: signal });
120+
},
121+
});
122+
```
123+
124+
Add `transient: true` to data chunks that should stream to the frontend but NOT persist in the response message. Use this for progress indicators, loading states, and other temporary UI:
125+
126+
```ts
127+
// Transient — frontend sees it, but NOT in onTurnComplete's responseMessage
128+
writer.write({
129+
type: "data-progress",
130+
id: "search",
131+
data: { percent: 50 },
132+
transient: true,
133+
});
134+
```
135+
136+
<Info>
137+
This matches the AI SDK's semantics: `data-*` chunks persist to `message.parts` by default. Only `transient: true` chunks are ephemeral. Non-data chunks (`text-delta`, `tool-*`, etc.) are handled by `streamText` and captured via `onFinish` — they don't need `chat.response`.
138+
</Info>
139+
140+
<Note>
141+
`chat.response` and the `writer` accumulation behavior work with `chat.agent` and `chat.createSession`. If you're using [`chat.customAgent`](#raw-task-with-primitives), you own the accumulator — see the raw-task example for the manual pattern.
142+
</Note>
143+
144+
### Raw streaming with `chat.stream`
145+
146+
For low-level stream access (piping from subtasks, reading streams by run ID), use `chat.stream`. Chunks written via `chat.stream` go directly to the realtime output — they are **NOT** accumulated into the response message regardless of the `transient` flag.
147+
148+
```ts
149+
// Raw stream — always ephemeral, never in responseMessage
150+
const { waitUntilComplete } = chat.stream.writer({
151+
execute: ({ write }) => {
152+
write({ type: "data-status", data: { message: "Processing..." } });
153+
},
154+
});
155+
await waitUntilComplete();
156+
```
157+
158+
<Tip>
159+
Use `data-*` chunk types (e.g. `data-status`, `data-progress`) for custom data. The AI SDK processes these into `DataUIPart` objects in `message.parts` on the frontend. Writing the same `type` + `id` again updates the existing part instead of creating a new one — useful for live progress.
160+
</Tip>
161+
162+
`chat.stream` exposes the full stream API:
163+
164+
| Method | Description |
165+
|--------|-------------|
166+
| `chat.stream.writer(options)` | Write individual chunks via a callback |
167+
| `chat.stream.pipe(stream, options?)` | Pipe a `ReadableStream` or `AsyncIterable` |
168+
| `chat.stream.append(value, options?)` | Append raw data |
169+
| `chat.stream.read(runId, options?)` | Read the stream by run ID |
170+
171+
For piping streams from subtasks to the parent chat (via `target: "root"`), see the [Sub-agents pattern](/ai-chat/patterns/sub-agents).
172+
90173
### Lifecycle hooks
91174

92175
`chat.agent({ ... })` accepts hooks that fire in a fixed order around each turn, plus dedicated suspend/resume hooks. The full reference lives on its own page:

docs/ai-chat/background-injection.mdx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ sidebarTitle: "Background injection"
44
description: "Inject context from background work into the agent's conversation — self-review, RAG augmentation, or any async analysis."
55
---
66

7+
import RcBanner from "/snippets/ai-chat-rc-banner.mdx";
8+
9+
<RcBanner />
10+
711
## Overview
812

913
`chat.inject()` queues model messages for injection into the conversation. Messages are picked up at the start of the next turn or at the next `prepareStep` boundary (between tool-call steps).
@@ -157,6 +161,29 @@ The self-review runs on `gpt-4o-mini` (fast, cheap) in the background. If the us
157161
- **Fact-checking**: Verify claims in the response using search tools, inject corrections
158162
- **Context enrichment**: Look up user/account data based on what was discussed, inject it as system context
159163

164+
## `chat.defer` standalone
165+
166+
`chat.defer()` is also useful on its own, without `chat.inject()`. Any work whose timing has no resume implication — analytics, audit logs, search-index writes, cache warming — can run in parallel with streaming instead of in the critical path. All deferred promises are awaited (with a 5s timeout) before `onTurnComplete` fires.
167+
168+
```ts
169+
export const myChat = chat.agent({
170+
id: "my-chat",
171+
onTurnStart: async ({ chatId, runId }) => {
172+
// Analytics — fire-and-forget, irrelevant to resume.
173+
chat.defer(analytics.track("turn_started", { chatId, runId }));
174+
},
175+
run: async ({ messages, signal }) => {
176+
return streamText({ model: openai("gpt-4o"), messages, abortSignal: signal });
177+
},
178+
});
179+
```
180+
181+
`chat.defer()` can be called from anywhere during a turn — hooks, `run()`, or nested helpers. All deferred promises are collected and awaited together before `onTurnComplete`.
182+
183+
<Warning>
184+
**Don't use `chat.defer()` for the message-history write in `onTurnStart`.** That write must land *before* the model starts streaming, otherwise a mid-stream page refresh will read `[]` from your DB and lose the user's message from the rendered conversation. See [Database persistence — `onTurnStart`](/ai-chat/patterns/database-persistence#onturnstart). Reserve `chat.defer` for writes whose timing has no resume implication.
185+
</Warning>
186+
160187
## How it differs from pending messages
161188

162189
| | `chat.inject()` | [Pending messages](/ai-chat/pending-messages) |

docs/ai-chat/changelog.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ See the [Sessions Upgrade Guide](/ai-chat/upgrade-guide) for the full step-by-st
364364
- Rewritten [Client Protocol](/ai-chat/client-protocol) — full wire format for the new `/realtime/v1/sessions/{sessionId}/...` endpoints, JWT scopes, S2 direct-write credentials, and `Last-Event-ID` resume.
365365
- [Database persistence pattern](/ai-chat/patterns/database-persistence) — new `chatId`-keyed `ChatSession` shape (no more `runId`) and a warning on the `onTurnComplete` race that requires a single atomic write of `messages` + `lastEventId`.
366366
- [Reference](/ai-chat/reference) — added `chat.createStartSessionAction`, `chat.createAccessToken`, `ChatInputChunk`, `TriggerChatTaskResult.sessionId`, `ChatTaskRunPayload.sessionId`. The old run-scoped stream-ID constants are gone.
367-
- Refreshed [Backend](/ai-chat/backend), [Frontend](/ai-chat/frontend), [Server Chat](/ai-chat/server-chat), [Quick start](/ai-chat/quick-start), [Overview](/ai-chat/overview), [Features](/ai-chat/features), [Types](/ai-chat/types), [Error handling](/ai-chat/error-handling), and [Testing](/ai-chat/testing) for the session-based wiring.
367+
- Refreshed [Backend](/ai-chat/backend), [Frontend](/ai-chat/frontend), [Server Chat](/ai-chat/server-chat), [Quick start](/ai-chat/quick-start), [Overview](/ai-chat/overview), [Types](/ai-chat/types), [Error handling](/ai-chat/error-handling), and [Testing](/ai-chat/testing) for the session-based wiring.
368368

369369
</Update>
370370

@@ -592,7 +592,7 @@ writer.write({ type: "data-progress", data: { percent: 50 }, transient: true });
592592

593593
Non-transient `data-*` chunks written via lifecycle hook `writer.write()` now automatically persist to the response message, matching the AI SDK's default semantics. Add `transient: true` for ephemeral chunks (progress indicators, status updates).
594594

595-
See [Custom data parts](/ai-chat/features#custom-data-parts).
595+
See [Custom data parts](/ai-chat/backend#custom-data-parts).
596596

597597
## Tool approvals
598598

docs/ai-chat/chat-local.mdx

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
---
2+
title: "chat.local"
3+
sidebarTitle: "chat.local"
4+
description: "Typed, run-scoped data accessible from hooks, run(), tools, and subtasks. Survives across turns, auto-cleared between runs, auto-hydrated into subtasks."
5+
---
6+
7+
import RcBanner from "/snippets/ai-chat-rc-banner.mdx";
8+
9+
<RcBanner />
10+
11+
Use `chat.local` to create typed, run-scoped data that persists across turns and is accessible from anywhere — the run function, tools, nested helpers. Each run gets its own isolated copy, and locals are automatically cleared between runs.
12+
13+
Lifecycle hooks and **`run`** also receive **`ctx`** ([`TaskRunContext`](/ai-chat/reference#task-context-ctx)) — the same object as on a standard `task()` — for tags, metadata, and cleanup that needs the full run record.
14+
15+
When a subtask is invoked via `ai.toolExecute()` (or the deprecated `ai.tool()`), initialized locals are automatically serialized into the subtask's metadata and hydrated on first access — no extra code needed. Subtask changes to hydrated locals are local to the subtask and don't propagate back to the parent.
16+
17+
## Declaring and initializing
18+
19+
Declare locals at module level with a unique `id`, then initialize them inside a lifecycle hook where you have context (chatId, clientData, etc.):
20+
21+
```ts
22+
import { chat } from "@trigger.dev/sdk/ai";
23+
import { streamText, tool } from "ai";
24+
import { openai } from "@ai-sdk/openai";
25+
import { z } from "zod";
26+
import { db } from "@/lib/db";
27+
28+
// Declare at module level — each local needs a unique id
29+
const userContext = chat.local<{
30+
userId: string;
31+
name: string;
32+
plan: "free" | "pro";
33+
messageCount: number;
34+
}>({ id: "userContext" });
35+
36+
export const myChat = chat.agent({
37+
id: "my-chat",
38+
clientDataSchema: z.object({ userId: z.string() }),
39+
onBoot: async ({ clientData }) => {
40+
// Initialize with real data from your database
41+
const user = await db.user.findUnique({
42+
where: { id: clientData.userId },
43+
});
44+
userContext.init({
45+
userId: clientData.userId,
46+
name: user.name,
47+
plan: user.plan,
48+
messageCount: user.messageCount,
49+
});
50+
},
51+
run: async ({ messages, signal }) => {
52+
userContext.messageCount++;
53+
54+
return streamText({
55+
model: openai("gpt-4o"),
56+
system: `Helping ${userContext.name} (${userContext.plan} plan).`,
57+
messages,
58+
abortSignal: signal,
59+
});
60+
},
61+
});
62+
```
63+
64+
<Warning>
65+
Initialize `chat.local` in [`onBoot`](/ai-chat/lifecycle-hooks#onboot), not `onChatStart`. `onBoot` fires on every fresh worker — including continuation runs (post-cancel, crash, `endRun`, `requestUpgrade`, OOM retry) — whereas `onChatStart` only fires on the chat's very first message. Initializing in `onChatStart` means `run()` will crash on continuation runs with `chat.local can only be modified after initialization`.
66+
</Warning>
67+
68+
## Accessing from tools
69+
70+
Locals are accessible from anywhere during task execution — including AI SDK tools:
71+
72+
```ts
73+
const userContext = chat.local<{ plan: "free" | "pro" }>({ id: "userContext" });
74+
75+
const premiumTool = tool({
76+
description: "Access premium features",
77+
inputSchema: z.object({ feature: z.string() }),
78+
execute: async ({ feature }) => {
79+
if (userContext.plan !== "pro") {
80+
return { error: "This feature requires a Pro plan." };
81+
}
82+
// ... premium logic
83+
},
84+
});
85+
```
86+
87+
## Accessing from subtasks
88+
89+
When you use `ai.toolExecute()` inside AI SDK `tool()` to expose a subtask, chat locals are automatically available read-only:
90+
91+
```ts
92+
import { chat, ai } from "@trigger.dev/sdk/ai";
93+
import { schemaTask } from "@trigger.dev/sdk";
94+
import { streamText, tool } from "ai";
95+
import { openai } from "@ai-sdk/openai";
96+
import { z } from "zod";
97+
98+
const userContext = chat.local<{ name: string; plan: "free" | "pro" }>({ id: "userContext" });
99+
100+
export const analyzeDataTask = schemaTask({
101+
id: "analyze-data",
102+
schema: z.object({ query: z.string() }),
103+
run: async ({ query }) => {
104+
// userContext.name just works — auto-hydrated from parent metadata
105+
console.log(`Analyzing for ${userContext.name}`);
106+
// Changes here are local to this subtask and don't propagate back
107+
},
108+
});
109+
110+
const analyzeData = tool({
111+
description: analyzeDataTask.description ?? "",
112+
inputSchema: analyzeDataTask.schema!,
113+
execute: ai.toolExecute(analyzeDataTask),
114+
});
115+
116+
export const myChat = chat.agent({
117+
id: "my-chat",
118+
onBoot: async ({ clientData }) => {
119+
userContext.init({ name: "Alice", plan: "pro" });
120+
},
121+
run: async ({ messages, signal }) => {
122+
return streamText({
123+
model: openai("gpt-4o"),
124+
messages,
125+
tools: { analyzeData },
126+
abortSignal: signal,
127+
});
128+
},
129+
});
130+
```
131+
132+
<Note>
133+
Values must be JSON-serializable for subtask access. Non-serializable values (functions, class instances, etc.) will be lost during transfer.
134+
</Note>
135+
136+
## Dirty tracking and persistence
137+
138+
The `hasChanged()` method returns `true` if any property was set since the last check, then resets the flag. Use it in lifecycle hooks to only persist when data actually changed:
139+
140+
```ts
141+
onTurnComplete: async ({ chatId }) => {
142+
if (userContext.hasChanged()) {
143+
await db.user.update({
144+
where: { id: userContext.get().userId },
145+
data: {
146+
messageCount: userContext.messageCount,
147+
},
148+
});
149+
}
150+
},
151+
```
152+
153+
## API
154+
155+
| Method | Description |
156+
|--------|-------------|
157+
| `chat.local<T>({ id })` | Create a typed local with a unique id (declare at module level) |
158+
| `local.init(value)` | Initialize with a value (call in hooks or `run`) |
159+
| `local.hasChanged()` | Returns `true` if modified since last check, resets flag |
160+
| `local.get()` | Returns a plain object copy (for serialization) |
161+
| `local.property` | Direct property access (read/write via Proxy) |
162+
163+
<Note>
164+
Locals use shallow proxying. Nested object mutations like `local.prefs.theme = "dark"` won't trigger the dirty flag. Instead, replace the whole property: `local.prefs = { ...local.prefs, theme: "dark" }`.
165+
</Note>
166+
167+
## See also
168+
169+
- [Lifecycle hooks](/ai-chat/lifecycle-hooks)`onBoot` is the canonical init site for `chat.local`.
170+
- [Database persistence pattern](/ai-chat/patterns/database-persistence) — full per-hook breakdown using `chat.local` alongside DB rows.
171+
- [Code execution sandbox pattern](/ai-chat/patterns/code-sandbox) — example of using `chat.local` to hold a sandbox handle across turns.

docs/ai-chat/client-protocol.mdx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ sidebarTitle: "Client Protocol"
44
description: "The wire protocol for building custom chat transports — how clients communicate with chat agents over Sessions and SSE."
55
---
66

7+
import RcBanner from "/snippets/ai-chat-rc-banner.mdx";
8+
9+
<RcBanner />
10+
711
This page documents the protocol that chat clients use to communicate with `chat.agent()` tasks. Use this if you're building a custom transport (e.g., for a Slack bot, CLI tool, or native app) instead of using the built-in `TriggerChatTransport` or `AgentChat`.
812

913
<Note>
@@ -1037,7 +1041,7 @@ The `publicAccessToken` returned in the body of `POST /api/v1/sessions` carries
10371041
10381042
## FAQ
10391043
1040-
<Expandable title="After sending `kind: \"stop\"`, can I immediately send the next message?">
1044+
<Expandable title="After sending `kind: stop`, can I immediately send the next message?">
10411045
Yes. `.in` records are processed in arrival order — the agent's stop handler aborts the in-flight `streamText`, emits a `turn-complete` control record, and reads the next record. You don't have to wait for `turn-complete` on the wire before posting the next `.in/append`. In practice you usually do anyway, because your UI is gated on the stream coming back to ready.
10421046
</Expandable>
10431047

docs/ai-chat/compaction.mdx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ sidebarTitle: "Compaction"
44
description: "Automatic context compaction to keep long conversations within token limits."
55
---
66

7+
import RcBanner from "/snippets/ai-chat-rc-banner.mdx";
8+
9+
<RcBanner />
10+
711
## Overview
812

913
Long conversations accumulate tokens across turns. Eventually the context window fills up, causing errors or degraded responses. Compaction solves this by automatically summarizing the conversation when token usage exceeds a threshold, then using that summary as the context for future turns.
@@ -258,11 +262,16 @@ Actions fire `onAction`, apply any `chat.history.*` mutations, then call `run()`
258262
Call `transport.sendAction()` from a button or slash command:
259263

260264
```tsx
261-
import { useTriggerChatTransport } from "@trigger.dev/react-hooks";
265+
import { useTriggerChatTransport } from "@trigger.dev/sdk/chat/react";
262266
import { useChat } from "@ai-sdk/react";
263267

264-
function ChatView({ chatId, accessToken }: { chatId: string; accessToken: string }) {
265-
const transport = useTriggerChatTransport({ task: "my-chat", accessToken });
268+
function ChatView({ chatId }: { chatId: string }) {
269+
const transport = useTriggerChatTransport({
270+
task: "my-chat",
271+
accessToken: ({ chatId }) => mintChatAccessToken(chatId),
272+
startSession: ({ chatId, taskId, clientData }) =>
273+
startChatSession({ chatId, taskId, clientData }),
274+
});
266275
const { messages } = useChat({ id: chatId, transport });
267276

268277
return (
@@ -294,7 +303,7 @@ onAction: async ({ action, uiMessages }) => {
294303
},
295304
```
296305

297-
See [Raw streaming with chat.stream](/ai-chat/features#raw-streaming-with-chatstream) for the full API.
306+
See [Raw streaming with `chat.stream`](/ai-chat/backend#raw-streaming-with-chat-stream) for the full API.
298307

299308
## Using with chat.createSession()
300309

0 commit comments

Comments
 (0)