Skip to content

Commit 6c27ede

Browse files
committed
docs(ai-chat): unstack the callouts under chat.agent()
Three callouts (Tip, Info, Warning) were stacked back-to-back right under the chat.agent() header before any content. Lead with the intro and first example instead: the toStreamTextOptions warning now sits with the example it's about, and the durable-Session note moved to its own short section.
1 parent 77a2d14 commit 6c27ede

2 files changed

Lines changed: 22 additions & 40 deletions

File tree

docs/ai-chat/backend.mdx

Lines changed: 9 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,32 +12,6 @@ import RcBanner from "/snippets/ai-chat-rc-banner.mdx";
1212

1313
The highest-level approach. Handles message accumulation, stop signals, turn lifecycle, and auto-piping automatically.
1414

15-
<Tip>
16-
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 `.agent()`. See [Types](/ai-chat/types).
17-
</Tip>
18-
19-
<Info>
20-
Every `chat.agent` conversation is backed by a durable Session — `externalId` is your `chatId`, `type` is `"chat.agent"`, `taskIdentifier` is the agent's task ID. The session is the run manager: it owns the chat's runs, persists across run lifecycles, and orchestrates handoffs (idle continuation, `chat.requestUpgrade`). You rarely need to touch the session directly (`chat.stream`, `chat.messages`, `chat.stopSignal` wrap everything), but `payload.sessionId` is available if you want to reach in — e.g. `sessions.open(payload.sessionId)` to write from a sub-agent or from outside the turn loop.
21-
</Info>
22-
23-
<Warning>
24-
**Always spread `chat.toStreamTextOptions()` into every `streamText` call.** It wires up the `prepareStep` callback that drives [compaction](/ai-chat/compaction), [steering](/ai-chat/pending-messages), and [background injection](/ai-chat/background-injection) — features that silently no-op if the spread is missing. It also injects the system prompt set via `chat.prompt()`, the resolved model (when a registry is provided), and telemetry metadata.
25-
26-
Spread it **first** in the options object so any explicit overrides win:
27-
28-
```ts
29-
streamText({
30-
...chat.toStreamTextOptions(), // or: chat.toStreamTextOptions({ registry, tools }) — see below
31-
messages,
32-
abortSignal: signal,
33-
// any explicit overrides go here
34-
stopWhen: stepCountIs(15),
35-
});
36-
```
37-
38-
Examples in this doc keep the spread implicit for brevity, but you should include it in real code.
39-
</Warning>
40-
4115
### Simple: return a StreamTextResult
4216

4317
Return the `streamText` result from `run` and it's automatically piped to the frontend:
@@ -51,7 +25,7 @@ export const simpleChat = chat.agent({
5125
id: "simple-chat",
5226
run: async ({ messages, signal }) => {
5327
return streamText({
54-
...chat.toStreamTextOptions(), // prepareStep, system, telemetry see callout above
28+
...chat.toStreamTextOptions(), // prepareStep, system, telemetry (see note below)
5529
model: anthropic("claude-sonnet-4-5"),
5630
system: "You are a helpful assistant.",
5731
messages,
@@ -62,6 +36,10 @@ export const simpleChat = chat.agent({
6236
});
6337
```
6438

39+
<Warning>
40+
**Always spread `chat.toStreamTextOptions()` first** (as above) so your explicit overrides win. It wires up the `prepareStep` callback behind [compaction](/ai-chat/compaction), [steering](/ai-chat/pending-messages), and [background injection](/ai-chat/background-injection), all of which silently no-op without it, and injects the system prompt from `chat.prompt()`, the resolved model (when you pass a `registry`), and telemetry metadata. Examples below keep the spread implicit for brevity, so include it in real code.
41+
</Warning>
42+
6543
### Using chat.pipe() for complex flows
6644

6745
For complex agent flows where `streamText` is called deep inside your code, use `chat.pipe()`. It works from **anywhere inside a task** — even nested function calls.
@@ -173,6 +151,10 @@ await waitUntilComplete();
173151

174152
For piping streams from subtasks to the parent chat (via `target: "root"`), see the [Sub-agents pattern](/ai-chat/patterns/sub-agents).
175153

154+
### Backed by a Session
155+
156+
Every `chat.agent` conversation is backed by a durable [Session](/ai-chat/sessions): `externalId` is your `chatId`, `type` is `"chat.agent"`, and `taskIdentifier` is the agent's task ID. The session is the run manager. It owns the chat's runs, persists across run lifecycles, and orchestrates handoffs (idle continuation, `chat.requestUpgrade`). You rarely touch it directly, since `chat.stream`, `chat.messages`, and `chat.stopSignal` wrap everything, but `payload.sessionId` is there when you need to reach in, e.g. `sessions.open(payload.sessionId)` to write from a sub-agent or from outside the turn loop.
157+
176158
### Lifecycle hooks
177159

178160
`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/tools.mdx

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import RcBanner from "/snippets/ai-chat-rc-banner.mdx";
88

99
<RcBanner />
1010

11-
`chat.agent` doesn't call the model for you — your tools still go to [`streamText`](https://sdk.vercel.ai/docs/ai-sdk-core/tools-and-tool-calling) inside `run()`. But you should **also declare them on the agent config**:
11+
`chat.agent` doesn't call the model for you. Your tools still go to [`streamText`](https://sdk.vercel.ai/docs/ai-sdk-core/tools-and-tool-calling) inside `run()`. But you should **also declare them on the agent config**:
1212

1313
```ts
1414
import { chat } from "@trigger.dev/sdk/ai";
@@ -51,19 +51,19 @@ There are three places a tool set shows up. Declare once, reuse:
5151
| --- | --- |
5252
| `chat.agent({ tools })` | Re-applies `toModelOutput` on prior-turn history; hands the set back typed on the `run()` payload. |
5353
| `chat.toStreamTextOptions({ tools })` | Detects which tool calls need [HITL approval](/ai-chat/patterns/human-in-the-loop) (`needsApproval`) and merges any auto-injected [skill](/ai-chat/patterns/skills) tools. |
54-
| `streamText({ tools })` | What the model actually calls. `chat.toStreamTextOptions({ tools })` already sets this spread it instead of passing `tools` twice. |
54+
| `streamText({ tools })` | What the model actually calls. `chat.toStreamTextOptions({ tools })` already sets this, so spread it instead of passing `tools` twice. |
5555

5656
The canonical pattern: declare `tools` on the config, read them back from the `run()` payload, and pass that to `chat.toStreamTextOptions({ tools })`. One declaration flows everywhere.
5757

5858
<Tip>
59-
Conversion only reads each tool's `inputSchema` and `toModelOutput` never `execute`. If you keep heavy `execute` dependencies out of a module (for bundle reasons), you can declare a lightweight schema-only tool map on the config and add the executes where you call `streamText`.
59+
Conversion only reads each tool's `inputSchema` and `toModelOutput`, never `execute`. If you keep heavy `execute` dependencies out of a module (for bundle reasons), you can declare a lightweight schema-only tool map on the config and add the executes where you call `streamText`.
6060
</Tip>
6161

6262
## `toModelOutput` across turns
6363

64-
`toModelOutput` transforms a tool's result before it enters the model's context turning raw image bytes into an image content part, or compressing a long sub-agent transcript into a one-line summary. The full result still streams to the frontend; the model only sees the transformed version.
64+
`toModelOutput` transforms a tool's result before it enters the model's context, turning raw image bytes into an image content part, or compressing a long sub-agent transcript into a one-line summary. The full result still streams to the frontend; the model only sees the transformed version.
6565

66-
The catch is multi-turn. After each turn, `chat.agent` persists the conversation as `UIMessage[]` and re-converts it to model messages at the start of the next turn. That conversion needs your tools to find each `toModelOutput`. **If you only pass tools to `streamText` and not to the config, the transform runs on turn 1 but is skipped on every later turn** — the raw output gets stringified back into the prompt instead, and the model loses the transformed view.
66+
The catch is multi-turn. After each turn, `chat.agent` persists the conversation as `UIMessage[]` and re-converts it to model messages at the start of the next turn. That conversion needs your tools to find each `toModelOutput`. **If you only pass tools to `streamText` and not to the config, the transform runs on turn 1 but is skipped on every later turn.** The raw output gets stringified back into the prompt instead, and the model loses the transformed view.
6767

6868
Declaring `tools` on the config fixes this: the SDK threads them into the conversion, so `toModelOutput` is re-applied on every turn.
6969

@@ -97,7 +97,7 @@ export const chartChat = chat.agent({
9797

9898
## Static or per-turn tools
9999

100-
`tools` accepts either a static `ToolSet` or a function that returns one per turn for tools that depend on the user, a feature flag, or anything in the turn context:
100+
`tools` accepts either a static `ToolSet` or a function that returns one per turn, for tools that depend on the user, a feature flag, or anything in the turn context:
101101

102102
```ts
103103
export const myChat = chat
@@ -146,11 +146,11 @@ run: async ({ messages, tools, signal }) => {
146146
};
147147
```
148148

149-
When no `tools` are declared, the payload's `tools` is an empty object and behaves exactly as before declaring tools is fully opt-in.
149+
When no `tools` are declared, the payload's `tools` is an empty object and behaves exactly as before, so declaring tools is fully opt-in.
150150

151151
## Typing messages from your tools
152152

153-
To get typed tool parts (`tool-${name}` with typed input/output) on your `UIMessage`in hooks like `onTurnComplete` and on the frontend derive the message type from your tool set with `InferChatUIMessageFromTools`:
153+
To get typed tool parts (`tool-${name}` with typed input/output) on your `UIMessage`, in hooks like `onTurnComplete` and on the frontend, derive the message type from your tool set with `InferChatUIMessageFromTools`:
154154

155155
```ts
156156
import type { InferChatUIMessageFromTools } from "@trigger.dev/sdk/ai";
@@ -160,15 +160,15 @@ const tools = { searchDocs, renderChart };
160160
export type ChatUiMessage = InferChatUIMessageFromTools<typeof tools>;
161161
```
162162

163-
This is shorthand for `UIMessage<unknown, UIDataTypes, InferUITools<typeof tools>>`. Pin it on the agent with [`chat.withUIMessage<ChatUiMessage>()`](/ai-chat/types#custom-uimessage-with-chat-withuimessage) and reuse it on the client. If you also have custom `data-*` parts, build the `UIMessage` generic directly instead — see [Types](/ai-chat/types).
163+
This is shorthand for `UIMessage<unknown, UIDataTypes, InferUITools<typeof tools>>`. Pin it on the agent with [`chat.withUIMessage<ChatUiMessage>()`](/ai-chat/types#custom-uimessage-with-chat-withuimessage) and reuse it on the client. If you also have custom `data-*` parts, build the `UIMessage` generic directly instead. See [Types](/ai-chat/types).
164164

165165
## Skills
166166

167167
[Agent skills](/ai-chat/patterns/skills) are auto-injected as tools (`loadSkill`, `readFile`, `bash`) by `chat.toStreamTextOptions()`. They're separate from your config `tools`: declare your own tools on the config (so their `toModelOutput` survives across turns), and let `toStreamTextOptions` merge the skill tools on top at call time. Skill tools don't define `toModelOutput`, so they don't need to be on the config.
168168

169169
## Manual turn loops (`chat.customAgent`)
170170

171-
The `tools` config option belongs to the managed [`chat.agent`](/ai-chat/backend#chat-agent). When you drive the loop yourself with [`chat.customAgent`](/ai-chat/backend#raw-task-primitives) (or build messages from `chat.history`), you own the conversion so pass your tools to `convertToModelMessages` directly to get the same cross-turn `toModelOutput` behavior:
171+
The `tools` config option belongs to the managed [`chat.agent`](/ai-chat/backend#chat-agent). When you drive the loop yourself with [`chat.customAgent`](/ai-chat/backend#raw-task-primitives) (or build messages from `chat.history`), you own the conversion, so pass your tools to `convertToModelMessages` directly to get the same cross-turn `toModelOutput` behavior:
172172

173173
```ts
174174
import { convertToModelMessages, streamText } from "ai";
@@ -185,7 +185,7 @@ return streamText({ model: anthropic("claude-sonnet-4-5"), messages, tools });
185185

186186
## Learn more
187187

188-
- [Human-in-the-loop](/ai-chat/patterns/human-in-the-loop) tools that pause for approval.
189-
- [Sub-agents](/ai-chat/patterns/sub-agents) tools that delegate to other agents and compress their output with `toModelOutput`.
190-
- [Tool result auditing](/ai-chat/patterns/tool-result-auditing) logging tool results as they resolve.
188+
- [Human-in-the-loop](/ai-chat/patterns/human-in-the-loop): tools that pause for approval.
189+
- [Sub-agents](/ai-chat/patterns/sub-agents): tools that delegate to other agents and compress their output with `toModelOutput`.
190+
- [Tool result auditing](/ai-chat/patterns/tool-result-auditing): logging tool results as they resolve.
191191
- [AI SDK: Tools and tool calling](https://sdk.vercel.ai/docs/ai-sdk-core/tools-and-tool-calling).

0 commit comments

Comments
 (0)