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): 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.
Copy file name to clipboardExpand all lines: docs/ai-chat/backend.mdx
+9-27Lines changed: 9 additions & 27 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -12,32 +12,6 @@ import RcBanner from "/snippets/ai-chat-rc-banner.mdx";
12
12
13
13
The highest-level approach. Handles message accumulation, stop signals, turn lifecycle, and auto-piping automatically.
14
14
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
-
41
15
### Simple: return a StreamTextResult
42
16
43
17
Return the `streamText` result from `run` and it's automatically piped to the frontend:
**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
+
65
43
### Using chat.pipe() for complex flows
66
44
67
45
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();
173
151
174
152
For piping streams from subtasks to the parent chat (via `target: "root"`), see the [Sub-agents pattern](/ai-chat/patterns/sub-agents).
175
153
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
+
176
158
### Lifecycle hooks
177
159
178
160
`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:
Copy file name to clipboardExpand all lines: docs/ai-chat/tools.mdx
+13-13Lines changed: 13 additions & 13 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -8,7 +8,7 @@ import RcBanner from "/snippets/ai-chat-rc-banner.mdx";
8
8
9
9
<RcBanner />
10
10
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**:
12
12
13
13
```ts
14
14
import { chat } from"@trigger.dev/sdk/ai";
@@ -51,19 +51,19 @@ There are three places a tool set shows up. Declare once, reuse:
51
51
| --- | --- |
52
52
|`chat.agent({ tools })`| Re-applies `toModelOutput` on prior-turn history; hands the set back typed on the `run()` payload. |
53
53
|`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. |
55
55
56
56
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.
57
57
58
58
<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`.
60
60
</Tip>
61
61
62
62
## `toModelOutput` across turns
63
63
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.
65
65
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.
67
67
68
68
Declaring `tools` on the config fixes this: the SDK threads them into the conversion, so `toModelOutput` is re-applied on every turn.
`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:
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.
150
150
151
151
## Typing messages from your tools
152
152
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`:
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).
164
164
165
165
## Skills
166
166
167
167
[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.
168
168
169
169
## Manual turn loops (`chat.customAgent`)
170
170
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:
0 commit comments