@trigger.dev/sdk 0.0.0-prerelease-20260908122921 → 0.0.0-prerelease-streamfix-20260909094302

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/dist/commonjs/imports/ai-runtime-cjs.cjs.map +1 -1
  2. package/dist/commonjs/imports/ai-runtime.js +0 -2
  3. package/dist/commonjs/v3/ai.d.ts +16 -199
  4. package/dist/commonjs/v3/ai.js +102 -983
  5. package/dist/commonjs/v3/ai.js.map +1 -1
  6. package/dist/commonjs/v3/chat-client.d.ts +2 -3
  7. package/dist/commonjs/v3/chat-client.js +5 -31
  8. package/dist/commonjs/v3/chat-client.js.map +1 -1
  9. package/dist/commonjs/v3/chat-react.d.ts +0 -34
  10. package/dist/commonjs/v3/chat-react.js +1 -47
  11. package/dist/commonjs/v3/chat-react.js.map +1 -1
  12. package/dist/commonjs/v3/chat-server.d.ts +6 -42
  13. package/dist/commonjs/v3/chat-server.js +7 -52
  14. package/dist/commonjs/v3/chat-server.js.map +1 -1
  15. package/dist/commonjs/v3/chat.d.ts +10 -81
  16. package/dist/commonjs/v3/chat.js +46 -292
  17. package/dist/commonjs/v3/chat.js.map +1 -1
  18. package/dist/commonjs/v3/sessions.d.ts +2 -15
  19. package/dist/commonjs/v3/sessions.js +1 -12
  20. package/dist/commonjs/v3/sessions.js.map +1 -1
  21. package/dist/commonjs/v3/shared.js +36 -30
  22. package/dist/commonjs/v3/shared.js.map +1 -1
  23. package/dist/commonjs/v3/test/mock-chat-agent.d.ts +0 -43
  24. package/dist/commonjs/v3/test/mock-chat-agent.js +0 -90
  25. package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
  26. package/dist/commonjs/v3/test/test-session-handle.js +0 -6
  27. package/dist/commonjs/v3/test/test-session-handle.js.map +1 -1
  28. package/dist/commonjs/version.js +1 -1
  29. package/dist/esm/imports/ai-runtime.d.ts +2 -2
  30. package/dist/esm/imports/ai-runtime.js +2 -2
  31. package/dist/esm/imports/ai-runtime.js.map +1 -1
  32. package/dist/esm/v3/ai.d.ts +16 -199
  33. package/dist/esm/v3/ai.js +103 -984
  34. package/dist/esm/v3/ai.js.map +1 -1
  35. package/dist/esm/v3/chat-client.d.ts +2 -3
  36. package/dist/esm/v3/chat-client.js +5 -31
  37. package/dist/esm/v3/chat-client.js.map +1 -1
  38. package/dist/esm/v3/chat-react.d.ts +0 -34
  39. package/dist/esm/v3/chat-react.js +1 -46
  40. package/dist/esm/v3/chat-react.js.map +1 -1
  41. package/dist/esm/v3/chat-server.d.ts +6 -42
  42. package/dist/esm/v3/chat-server.js +8 -53
  43. package/dist/esm/v3/chat-server.js.map +1 -1
  44. package/dist/esm/v3/chat.d.ts +10 -81
  45. package/dist/esm/v3/chat.js +47 -293
  46. package/dist/esm/v3/chat.js.map +1 -1
  47. package/dist/esm/v3/sessions.d.ts +2 -15
  48. package/dist/esm/v3/sessions.js +1 -11
  49. package/dist/esm/v3/sessions.js.map +1 -1
  50. package/dist/esm/v3/shared.js +23 -17
  51. package/dist/esm/v3/shared.js.map +1 -1
  52. package/dist/esm/v3/test/mock-chat-agent.d.ts +0 -43
  53. package/dist/esm/v3/test/mock-chat-agent.js +2 -92
  54. package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
  55. package/dist/esm/v3/test/test-session-handle.js +0 -6
  56. package/dist/esm/v3/test/test-session-handle.js.map +1 -1
  57. package/dist/esm/version.js +1 -1
  58. package/docs/ai-chat/actions.mdx +23 -55
  59. package/docs/ai-chat/anatomy.mdx +3 -3
  60. package/docs/ai-chat/backend.mdx +48 -125
  61. package/docs/ai-chat/background-injection.mdx +19 -67
  62. package/docs/ai-chat/client-protocol.mdx +4 -5
  63. package/docs/ai-chat/compaction.mdx +7 -11
  64. package/docs/ai-chat/custom-agents.mdx +0 -23
  65. package/docs/ai-chat/fast-starts.mdx +20 -27
  66. package/docs/ai-chat/frontend.mdx +14 -17
  67. package/docs/ai-chat/migrating-from-a-route-handler.mdx +14 -16
  68. package/docs/ai-chat/patterns/skills.mdx +10 -7
  69. package/docs/ai-chat/patterns/version-upgrades.mdx +6 -79
  70. package/docs/ai-chat/pending-messages.mdx +3 -3
  71. package/docs/ai-chat/prompt-caching.mdx +25 -23
  72. package/docs/ai-chat/quick-start.mdx +11 -11
  73. package/docs/ai-chat/reference.mdx +5 -12
  74. package/docs/ai-chat/sessions.mdx +1 -6
  75. package/docs/ai-chat/testing.mdx +1 -2
  76. package/docs/ai-chat/tools.mdx +13 -18
  77. package/docs/ai-chat/upgrade-guide.mdx +2 -2
  78. package/docs/apikeys.mdx +45 -27
  79. package/docs/deployment/overview.mdx +8 -4
  80. package/docs/deployment/preview-branches.mdx +4 -4
  81. package/docs/deployment/version-skew-protection.mdx +0 -62
  82. package/docs/manual-setup.mdx +7 -7
  83. package/docs/mcp-tools.mdx +0 -9
  84. package/docs/quick-start.mdx +3 -3
  85. package/docs/realtime/auth.mdx +1 -1
  86. package/docs/self-hosting/security.mdx +0 -5
  87. package/docs/tasks/scheduled.mdx +0 -24
  88. package/docs/triggering.mdx +1 -1
  89. package/package.json +4 -4
  90. package/skills/trigger-authoring-chat-agent/SKILL.md +27 -38
  91. package/skills/trigger-chat-agent-advanced/SKILL.md +12 -31
  92. package/dist/commonjs/v3/chatVersionSkew.d.ts +0 -12
  93. package/dist/commonjs/v3/chatVersionSkew.js +0 -30
  94. package/dist/commonjs/v3/chatVersionSkew.js.map +0 -1
  95. package/dist/commonjs/v3/externalDeploymentId.d.ts +0 -23
  96. package/dist/commonjs/v3/externalDeploymentId.js +0 -43
  97. package/dist/commonjs/v3/externalDeploymentId.js.map +0 -1
  98. package/dist/esm/v3/chatVersionSkew.d.ts +0 -12
  99. package/dist/esm/v3/chatVersionSkew.js +0 -27
  100. package/dist/esm/v3/chatVersionSkew.js.map +0 -1
  101. package/dist/esm/v3/externalDeploymentId.d.ts +0 -23
  102. package/dist/esm/v3/externalDeploymentId.js +0 -38
  103. package/dist/esm/v3/externalDeploymentId.js.map +0 -1
  104. package/docs/ai-chat/patterns/native-compaction.mdx +0 -310
  105. package/docs/reports.mdx +0 -157
  106. package/docs/troubleshooting-zod.mdx +0 -158
@@ -79,7 +79,7 @@ The **body** is loaded on demand via the `loadSkill` tool when the agent decides
79
79
  ```ts trigger/chat.ts
80
80
  import { chat } from "@trigger.dev/sdk/ai";
81
81
  import { skills } from "@trigger.dev/sdk";
82
- import { stepCountIs } from "ai";
82
+ import { streamText, stepCountIs } from "ai";
83
83
  import { anthropic } from "@ai-sdk/anthropic";
84
84
 
85
85
  const timeUtilsSkill = skills.define({
@@ -92,11 +92,12 @@ export const agent = chat.agent({
92
92
  onChatStart: async () => {
93
93
  chat.skills.set([await timeUtilsSkill.local()]);
94
94
  },
95
- run: async ({ messages, signal, streamText }) => {
95
+ run: async ({ messages, signal }) => {
96
96
  return streamText({
97
97
  model: anthropic("claude-sonnet-4-5"),
98
98
  messages,
99
99
  abortSignal: signal,
100
+ ...chat.toStreamTextOptions(),
100
101
  stopWhen: stepCountIs(15),
101
102
  });
102
103
  },
@@ -110,7 +111,7 @@ export const agent = chat.agent({
110
111
 
111
112
  `skill.local()` reads the bundled `SKILL.md` from disk and returns a `ResolvedSkill` with the parsed frontmatter + body + on-disk path.
112
113
 
113
- `chat.skills.set([...])` stores the resolved skills for the current run. The `streamText` from `run`'s argument picks them up automatically:
114
+ `chat.skills.set([...])` stores the resolved skills for the current run. `chat.toStreamTextOptions()` spreads them into `streamText` automatically:
114
115
 
115
116
  - The frontmatter `description` lands in the system prompt under "Available skills:".
116
117
  - Three tools are added: `loadSkill`, `readFile`, `bash` — scoped per skill.
@@ -168,10 +169,12 @@ return streamText({
168
169
  model: anthropic("claude-sonnet-4-5"),
169
170
  messages,
170
171
  abortSignal: signal,
171
- tools: {
172
- webFetch, // your tool
173
- deepResearch, // your tool
174
- },
172
+ ...chat.toStreamTextOptions({
173
+ tools: {
174
+ webFetch, // your tool
175
+ deepResearch, // your tool
176
+ },
177
+ }),
175
178
  stopWhen: stepCountIs(15),
176
179
  });
177
180
  ```
@@ -8,17 +8,6 @@ Chat agent runs are pinned to the worker version they started on. When you deplo
8
8
 
9
9
  `chat.requestUpgrade()` is the managed upgrade signal for `chat.agent()` and the `chat.createSession()` iterator. Fully hand-rolled custom agents use `chat.endAndContinue()` between turns to immediately hand the Session to a new run.
10
10
 
11
- <Note>
12
- If your sessions are pinned by [version skew
13
- protection](/deployment/version-skew-protection#chat-sessions), you do not need this page to move a
14
- conversation onto a new deployment. A pinned session follows its pin on its own: when the stored
15
- `externalDeploymentId` stops naming the deployment a run is on, the agent hands over at the next
16
- turn boundary. Set [`versionSkew: "hold"`](#staying-put) to turn that off for one agent.
17
-
18
- Read on for the cases that are still yours to decide — leaving a pin for a version nobody named,
19
- a session that was never pinned, and hand-rolled custom agents.
20
- </Note>
21
-
22
11
  ## How it works
23
12
 
24
13
  When `chat.requestUpgrade()` is called in `onTurnStart` or `onValidateMessages`:
@@ -30,44 +19,22 @@ When `chat.requestUpgrade()` is called in `onTurnStart` or `onValidateMessages`:
30
19
 
31
20
  The new run lives on the **same Session** as the old one. `chatId` is the durable identity; only the underlying `currentRunId` rotates. The audit log records the new run with `reason: "upgrade"`.
32
21
 
33
- ### What "the latest deployment" means
34
-
35
- The handoff clears the session's [external deployment id](/deployment/version-skew-protection#chat-sessions) so the new run can land on the current version — re-applying the pin the agent just rejected would make the upgrade impossible. The cleared pin is persisted on the session, so the next continuation doesn't fall back to it either.
36
-
37
- To move to a specific deployment rather than to whatever is current, name it:
38
-
39
- ```ts
40
- chat.requestUpgrade({ externalDeploymentId: clientData.commitSha });
41
- ```
42
-
43
- That is usually what you want when the client told you which build it is on: it upgrades to the version the client expects instead of merely to the newest one.
44
-
45
- <Warning>
46
- `lockToVersion` is a different thing and is **never** cleared. A session started with an explicit
47
- `lockToVersion` re-applies it on every run including upgrade handoffs, so `chat.requestUpgrade()`
48
- cannot escape it — the new run lands on the same version the old one did. Use the external
49
- deployment id if you want a pin an agent can opt out of.
50
- </Warning>
51
-
52
22
  When called from inside `run()` or `chat.defer()`, the current turn completes normally first and the run exits afterward. The next message triggers the continuation on the same session.
53
23
 
54
24
  ```mermaid
55
25
  sequenceDiagram
56
26
  participant User
57
27
  participant Transport
58
- participant Session as session.in
59
28
  participant RunV1 as Run (v1)
60
29
  participant RunV2 as Run (v2)
61
30
 
62
31
  User->>Transport: send message
63
- Transport->>Session: append message
64
- Session->>RunV1: input stream
32
+ Transport->>RunV1: input stream
65
33
  RunV1->>RunV1: onTurnStart → requestUpgrade()
66
- RunV1->>RunV2: end-and-continue triggers the successor
67
- RunV1-->>Transport: trigger:upgrade-required (filtered, no re-send)
68
- RunV1->>RunV1: exit (run() never called, message left unacknowledged)
69
- Session->>RunV2: same message, replayed on boot
70
- RunV2-->>Transport: response stream (same session.out)
34
+ RunV1-->>Transport: trigger:upgrade-required
35
+ RunV1->>RunV1: exit (run() never called)
36
+ Transport->>RunV2: trigger new run (continuation, same message)
37
+ RunV2-->>Transport: response stream
71
38
  Transport-->>User: response (seamless)
72
39
  ```
73
40
 
@@ -136,13 +103,6 @@ This pattern is useful when:
136
103
 
137
104
  ## Auto-detect from build ID (Next.js / Vercel)
138
105
 
139
- <Warning>
140
- You probably don't need this any more. If your sessions are pinned, following the pin is the
141
- built-in behaviour and it needs no `clientData` and no `chat.local`. Reach for the recipe below
142
- only when you want to upgrade on a signal the pin doesn't carry — a frontend build id that moves
143
- independently of the deployment your app names.
144
- </Warning>
145
-
146
106
  For automatic upgrade on every deploy, pass your platform's build ID via `clientData` instead of a manual version. The agent stores the ID from the first message and upgrades when it changes:
147
107
 
148
108
  ```tsx title="app/components/Chat.tsx"
@@ -191,38 +151,6 @@ export const myChat = chat
191
151
 
192
152
  This upgrades on **every** deploy, not just breaking changes. Good for fast-moving projects where you always want the latest code.
193
153
 
194
- ## Staying put
195
-
196
- A pinned session follows its pin by default. To keep one agent where it is — a long tool chain you
197
- don't want interrupted, or a conversation you'd rather move on your own terms — set `versionSkew`:
198
-
199
- ```ts
200
- export const myChat = chat.agent({
201
- id: "my-chat",
202
- versionSkew: "hold",
203
- run: async ({ messages, signal }) => { ... },
204
- });
205
- ```
206
-
207
- `"hold"` only stops the automatic handoff. `chat.requestUpgrade()` still works, so you can keep the
208
- decision and still get the seamless swap.
209
-
210
- Two cases never hand over automatically, whatever `versionSkew` says:
211
-
212
- - **A session with no pin.** There is nothing to compare against, and an unpinned session already
213
- lands on the current version every time it starts a run.
214
- - **A session using `lockToVersion`.** That pin outranks the external deployment id and
215
- `chat.requestUpgrade()` cannot escape it, so handing over would land on the same version and
216
- repeat.
217
-
218
- <Note>
219
- Following the pin costs one session read per turn on pinned chats, and the handoff happens at a
220
- turn boundary — never mid-turn. If the pin names a deployment that hasn't landed yet, the successor
221
- parks: your messages stay durable, and the transport emits `run-pending-version` with
222
- `source: "upgrade"` so you can say so in the UI. See [parked
223
- chats](/deployment/version-skew-protection#chat-sessions).
224
- </Note>
225
-
226
154
  ## Custom agents
227
155
 
228
156
  Use `chat.requestUpgrade()` with `chat.agent()`. With `chat.createSession()`, call `chat.requestUpgrade()`, then advance the iterator once more so it can exit normally. For an immediate handoff, close the iterator before calling `chat.endAndContinue()`. In a fully hand-rolled `chat.customAgent()` task, detach input listeners, persist the completed turn, write its boundary, then call `chat.endAndContinue()` and return immediately:
@@ -238,7 +166,7 @@ await chat.endAndContinue();
238
166
  return;
239
167
  ```
240
168
 
241
- The continuation uses the same durable Session and receives `.in` records that the old run has not consumed. It starts on the latest deployed task version only if the Session is unpinned — a Session carrying `lockToVersion` or an `externalDeploymentId` re-applies that pin, so the continuation lands where the Session says rather than on the newest build.
169
+ The continuation uses the same durable Session and receives `.in` records that the old run has not consumed. It starts on the latest deployed task version unless the Session's trigger configuration sets `lockToVersion`.
242
170
 
243
171
  If input has been dispatched to the old run but should be processed by the continuation, detach the old listeners and skip the final `chat.writeTurnComplete()`. A turn-complete boundary acknowledges the latest input dispatched to the old run, so writing one after that dispatch would cause the continuation to resume past the input.
244
172
 
@@ -254,7 +182,6 @@ Both are graceful exits. [`onRecoveryBoot`](/ai-chat/patterns/recovery-boot) doe
254
182
 
255
183
  ## See also
256
184
 
257
- - [Version skew protection](/deployment/version-skew-protection#chat-sessions) — pin a session to the deployment matching the app build that started it
258
185
  - [Lifecycle hooks](/ai-chat/lifecycle-hooks) — where `onTurnStart` and `onChatResume` fit in the turn cycle
259
186
  - [Recovery boot](/ai-chat/patterns/recovery-boot) — the sibling hook for mid-stream interruptions (does NOT fire on `requestUpgrade`)
260
187
  - [Database persistence](/ai-chat/patterns/database-persistence) — how continuations interact with session state
@@ -30,18 +30,18 @@ Add `pendingMessages` to your `chat.agent` configuration:
30
30
 
31
31
  ```ts
32
32
  import { chat } from "@trigger.dev/sdk/ai";
33
- import { stepCountIs } from "ai";
33
+ import { streamText, stepCountIs } from "ai";
34
34
  import { anthropic } from "@ai-sdk/anthropic";
35
35
 
36
36
  export const myChat = chat.agent({
37
37
  id: "my-chat",
38
- registry,
39
38
  pendingMessages: {
40
39
  // Only inject when there are completed steps (tool calls happened)
41
40
  shouldInject: ({ steps }) => steps.length > 0,
42
41
  },
43
- run: async ({ messages, signal, streamText }) => {
42
+ run: async ({ messages, signal }) => {
44
43
  return streamText({
44
+ ...chat.toStreamTextOptions({ registry }),
45
45
  messages,
46
46
  tools: { /* ... */ },
47
47
  abortSignal: signal,
@@ -16,7 +16,7 @@ A request renders as `tools` → `system` → `messages`. There are three prefix
16
16
 
17
17
  | Region | How to cache it | Stability |
18
18
  | --- | --- | --- |
19
- | System prompt (+ tools) | `cacheControl` / `systemProviderOptions` on `chat.agent()`, or `providerOptions` on `chat.prompt.set()` | Set once, never changes — the highest-value target |
19
+ | System prompt (+ tools) | `cacheControl` / `systemProviderOptions` on `chat.toStreamTextOptions()`, or `providerOptions` on `chat.prompt.set()` | Set once, never changes — the highest-value target |
20
20
  | Conversation history | `prepareMessages` adds a breakpoint to the last message | Grows append-only across turns |
21
21
  | Tool definitions | Stable as long as your tool set doesn't change between turns | Render at position 0 — changing them invalidates everything |
22
22
 
@@ -32,22 +32,23 @@ The system prompt (your `chat.prompt` text plus any skills preamble) is usually
32
32
 
33
33
  Three ways to opt in, depending on where you'd rather express it.
34
34
 
35
- **`cacheControl` on the agent** — the Anthropic-flavored one-liner:
35
+ **`cacheControl` at the `streamText` call site** — the Anthropic-flavored one-liner:
36
36
 
37
37
  ```ts /trigger/chat.ts
38
38
  import { chat } from "@trigger.dev/sdk/ai";
39
+ import { streamText } from "ai";
39
40
  import { anthropic } from "@ai-sdk/anthropic";
40
41
 
41
42
  export const myChat = chat.agent({
42
43
  id: "my-chat",
43
- cacheControl: { type: "ephemeral" },
44
44
  onChatStart: async () => {
45
45
  chat.prompt.set(SYSTEM_PROMPT); // a large, stable instruction block
46
46
  },
47
- run: async ({ messages, signal, streamText }) => {
47
+ run: async ({ messages, signal }) => {
48
48
  return streamText({
49
49
  model: anthropic("claude-sonnet-4-6"),
50
50
  // Caches the system block with a 5-minute breakpoint.
51
+ ...chat.toStreamTextOptions({ cacheControl: { type: "ephemeral" } }),
51
52
  messages,
52
53
  abortSignal: signal,
53
54
  });
@@ -58,19 +59,17 @@ export const myChat = chat.agent({
58
59
  **`systemProviderOptions`** is the provider-agnostic form — pass the raw `providerOptions` so it composes with any provider:
59
60
 
60
61
  ```ts /trigger/chat.ts
61
- export const myChat = chat.agent({
62
- id: "my-chat",
63
- systemProviderOptions: { anthropic: { cacheControl: { type: "ephemeral" } } },
64
- run: async ({ messages, signal, streamText }) =>
65
- streamText({
66
- model: anthropic("claude-sonnet-4-6"),
67
- messages,
68
- abortSignal: signal,
69
- }),
62
+ return streamText({
63
+ model: anthropic("claude-sonnet-4-6"),
64
+ ...chat.toStreamTextOptions({
65
+ systemProviderOptions: { anthropic: { cacheControl: { type: "ephemeral" } } },
66
+ }),
67
+ messages,
68
+ abortSignal: signal,
70
69
  });
71
70
  ```
72
71
 
73
- **`providerOptions` on `chat.prompt.set()`** co-locates the intent with where the prompt is defined. It carries through to the managed `streamText` with no call-site change:
72
+ **`providerOptions` on `chat.prompt.set()`** co-locates the intent with where the prompt is defined. It carries through to `toStreamTextOptions()` with no call-site change:
74
73
 
75
74
  ```ts /trigger/chat.ts
76
75
  onChatStart: async () => {
@@ -78,16 +77,17 @@ onChatStart: async () => {
78
77
  providerOptions: { anthropic: { cacheControl: { type: "ephemeral" } } },
79
78
  });
80
79
  },
81
- run: async ({ messages, signal, streamText }) => {
80
+ run: async ({ messages, signal }) => {
82
81
  return streamText({
83
82
  model: anthropic("claude-sonnet-4-6"),
83
+ ...chat.toStreamTextOptions(), // already cached
84
84
  messages,
85
85
  abortSignal: signal,
86
86
  });
87
87
  },
88
88
  ```
89
89
 
90
- If more than one is set, the most specific wins: `systemProviderOptions` overrides `cacheControl`, and both override `chat.prompt.set`'s `providerOptions`. There's no deep merge — the most specific option replaces the rest.
90
+ If more than one is set, the call-site option wins: `systemProviderOptions` overrides `cacheControl`, and both override `chat.prompt.set`'s `providerOptions`. There's no deep merge — the most specific option replaces the rest.
91
91
 
92
92
  <Note>
93
93
  Use the 1-hour cache for prefixes that sit idle longer than 5 minutes between turns: `cacheControl: { type: "ephemeral", ttl: "1h" }`. Writes cost more (2× vs 1.25×), so it pays off only when reads span the longer window.
@@ -100,7 +100,6 @@ Place a breakpoint on the last message and the entire conversation prefix up to
100
100
  ```ts /trigger/chat.ts
101
101
  export const myChat = chat.agent({
102
102
  id: "my-chat",
103
- cacheControl: { type: "ephemeral" },
104
103
  prepareMessages: async ({ messages }) => {
105
104
  if (messages.length === 0) return messages;
106
105
  const last = messages[messages.length - 1];
@@ -115,9 +114,10 @@ export const myChat = chat.agent({
115
114
  },
116
115
  ];
117
116
  },
118
- run: async ({ messages, signal, streamText }) => {
117
+ run: async ({ messages, signal }) => {
119
118
  return streamText({
120
119
  model: anthropic("claude-sonnet-4-6"),
120
+ ...chat.toStreamTextOptions({ cacheControl: { type: "ephemeral" } }),
121
121
  messages,
122
122
  abortSignal: signal,
123
123
  });
@@ -149,10 +149,11 @@ Caching is provider-specific, and most providers don't use per-block breakpoints
149
149
 
150
150
  ```ts /trigger/chat.ts
151
151
  // Amazon Bedrock
152
- export const myChat = chat.agent({
153
- id: "my-chat",
154
- systemProviderOptions: { bedrock: { cachePoint: { type: "default" } } },
155
- run: async ({ messages, streamText }) => streamText({ messages }),
152
+ return streamText({
153
+ ...chat.toStreamTextOptions({
154
+ systemProviderOptions: { bedrock: { cachePoint: { type: "default" } } },
155
+ }),
156
+ messages,
156
157
  });
157
158
  ```
158
159
 
@@ -165,13 +166,14 @@ Usage reporting is normalized. Each provider reports cache tokens under its own
165
166
  The turn's usage carries cache token counts. `chat.agent` accumulates them across turns and hands them to `run` as `previousTurnUsage` (last turn) and `totalUsage` (whole chat), both `LanguageModelUsage`:
166
167
 
167
168
  ```ts /trigger/chat.ts
168
- run: async ({ messages, signal, previousTurnUsage, streamText }) => {
169
+ run: async ({ messages, signal, previousTurnUsage }) => {
169
170
  // After turn 1, cacheReadTokens should be > 0 on a stable prefix.
170
171
  console.log("cache read", previousTurnUsage?.inputTokenDetails?.cacheReadTokens);
171
172
  console.log("cache write", previousTurnUsage?.inputTokenDetails?.cacheWriteTokens);
172
173
 
173
174
  return streamText({
174
175
  model: anthropic("claude-sonnet-4-6"),
176
+ ...chat.toStreamTextOptions({ cacheControl: { type: "ephemeral" } }),
175
177
  messages,
176
178
  abortSignal: signal,
177
179
  });
@@ -16,16 +16,19 @@ The chat surface works with Vercel AI SDK **v5, v6, or v7**; install whichever m
16
16
 
17
17
  ```ts trigger/chat.ts
18
18
  import { chat } from "@trigger.dev/sdk/ai";
19
- import { stepCountIs } from "ai";
19
+ import { streamText, stepCountIs } from "ai";
20
20
  import { anthropic } from "@ai-sdk/anthropic";
21
21
 
22
22
  export const myChat = chat.agent({
23
23
  id: "my-chat",
24
- // `streamText` here is the SDK's, not the one from `ai`: it carries
25
- // compaction, steering, background injection, the system prompt and
26
- // telemetry, so none of them have to be wired up by hand.
27
- run: async ({ messages, signal, streamText }) => {
24
+ run: async ({ messages, signal }) => {
28
25
  return streamText({
26
+ // Spread chat.toStreamTextOptions() FIRST — it wires up
27
+ // prepareStep (compaction, steering, background injection),
28
+ // the system prompt set via chat.prompt(), and telemetry.
29
+ // Skipping this is the single most common cause of subtle
30
+ // bugs (silent broken compaction, missing steering, etc.).
31
+ ...chat.toStreamTextOptions(),
29
32
  model: anthropic("claude-sonnet-4-5"),
30
33
  messages,
31
34
  abortSignal: signal,
@@ -35,12 +38,9 @@ The chat surface works with Vercel AI SDK **v5, v6, or v7**; install whichever m
35
38
  });
36
39
  ```
37
40
 
38
- <Note>
39
- Take `streamText` from `run`'s argument rather than importing it from `ai`. The
40
- imported one drives no `prepareStep`, so compaction, mid-turn steering and
41
- background injection never run, and nothing reports it. Spreading
42
- `chat.toStreamTextOptions()` into the imported one does the same job by hand.
43
- </Note>
41
+ <Warning>
42
+ **Always spread `chat.toStreamTextOptions()` into your `streamText` call.** It wires up the `prepareStep` callback that drives compaction, mid-turn steering, and background injection — features that silently no-op if the spread is missing. Spread it **first** so any explicit overrides (e.g. a custom `prepareStep`) win.
43
+ </Warning>
44
44
 
45
45
  <Tip>
46
46
  For a **custom** [`UIMessage`](https://sdk.vercel.ai/docs/reference/ai-sdk-core/ui-message) subtype (typed `data-*` parts, tool map, etc.), define the agent with [`chat.withUIMessage<...>().agent({...})`](/ai-chat/types) instead of `chat.agent`.
@@ -46,16 +46,12 @@ Options for `chat.agent()`.
46
46
  | `onValidateMessages` | `(event: ValidateMessagesEvent) => UIMessage[] \| Promise<UIMessage[]>` | — | Validate/transform UIMessages before model conversion. See [onValidateMessages](/ai-chat/lifecycle-hooks#onvalidatemessages) |
47
47
  | `hydrateMessages` | `(event: HydrateMessagesEvent) => UIMessage[] \| Promise<UIMessage[]>` | — | Load message history from backend, replacing the linear accumulator. See [hydrateMessages](/ai-chat/lifecycle-hooks#hydratemessages) |
48
48
  | `actionSchema` | `TaskSchema` | — | Schema for validating custom actions sent via `transport.sendAction()`. See [Actions](/ai-chat/actions) |
49
- | `onAction` | `(event: ActionEvent) => Promise<void \| ActionTurn> \| void \| ActionTurn` | — | Handle custom actions. Actions are state edits: only `hydrateMessages` + `onAction` fire. Return `chat.turn()` to run a turn on the edited history, or nothing for an edit only. See [Actions](/ai-chat/actions) |
49
+ | `onAction` | `(event: ActionEvent) => Promise<unknown> \| unknown` | — | Handle custom actions. Actions are not turns — only `hydrateMessages` + `onAction` fire. Return a `StreamTextResult` (or `string` / `UIMessage`) for a model response; return `void` for side-effect-only. See [Actions](/ai-chat/actions) |
50
50
  | `onTurnStart` | `(event: TurnStartEvent) => Promise<void> \| void` | — | Fires every turn before `run()` |
51
51
  | `onBeforeTurnComplete` | `(event: BeforeTurnCompleteEvent) => Promise<void> \| void` | — | Fires after response but before stream closes. Includes `writer`. |
52
52
  | `onTurnComplete` | `(event: TurnCompleteEvent) => Promise<void> \| void` | — | Fires after each turn completes (stream closed) |
53
53
  | `onCompacted` | `(event: CompactedEvent) => Promise<void> \| void` | — | Fires when compaction occurs. Includes `writer`. See [Compaction](/ai-chat/compaction) |
54
54
  | `compaction` | `ChatAgentCompactionOptions` | — | Automatic context compaction. See [Compaction](/ai-chat/compaction) |
55
- | `registry` | `{ languageModel(id: string): unknown }` | — | A provider registry, so the managed `streamText` can resolve a model set through `chat.prompt.set()` |
56
- | `system` | `string \| SystemModelMessage` | — | The agent's system prompt. Injected instructions append to it. Set it here, at the `streamText` call site, or through `chat.prompt.set()`, but only in one of them |
57
- | `cacheControl` | `SystemCacheControl` | — | Mark the system prompt for provider-side caching. See [Prompt caching](/ai-chat/prompt-caching) |
58
- | `systemProviderOptions` | `ProviderMetadata` | — | Raw provider options for the system block. Takes precedence over `cacheControl` |
59
55
  | `pendingMessages` | `PendingMessagesOptions` | — | Mid-execution message injection. See [Pending Messages](/ai-chat/pending-messages) |
60
56
  | `prepareMessages` | `(event: PrepareMessagesEvent) => ModelMessage[]` | — | Transform model messages before use (cache breaks, context injection, etc.) |
61
57
  | `tools` | `ToolSet \| ((event: ResolveToolsEvent) => ToolSet \| Promise<ToolSet>)` | — | Tools for this agent. Threads each tool's `toModelOutput` through cross-turn history re-conversion, and hands the resolved set back on the run payload. Static set or per-turn function. See [Tools](/ai-chat/tools). |
@@ -102,10 +98,9 @@ The payload passed to the `run` function.
102
98
  | `ctx` | `TaskRunContext` | Full task run context — same as `task` `run`’s `{ ctx }` |
103
99
  | `messages` | `ModelMessage[]` | Model-ready messages — pass directly to `streamText` |
104
100
  | `tools` | `ToolSet` | Resolved tools declared on the agent config (empty object when none). Pass straight to `streamText`. See [Tools](/ai-chat/tools). |
105
- | `streamText` | `typeof streamText` | The AI SDK's `streamText` with this agent's managed options already applied: the prompt, skill tools, telemetry, and the `prepareStep` that delivers steering, compaction and injected context. Prefer it over importing `streamText` from `ai`. See [The managed streamText](/ai-chat/backend#the-managed-streamtext). |
106
101
  | `chatId` | `string` | Your conversation ID (the session's `externalId`) |
107
102
  | `sessionId` | `string` | Friendly ID of the backing Session (`session_*`). Use with `sessions.open()` for advanced cases. Always set — every chat.agent run is bound to a Session. |
108
- | `trigger` | `"submit-message" \| "regenerate-message" \| "action-turn"` | What triggered the request; `"action-turn"` is a turn requested by `chat.turn()` |
103
+ | `trigger` | `"submit-message" \| "regenerate-message"` | What triggered the request |
109
104
  | `messageId` | `string \| undefined` | Message ID (for regenerate) |
110
105
  | `clientData` | Typed by `clientDataSchema` | Custom data from the frontend (typed when schema is provided) |
111
106
  | `continuation` | `boolean` | Whether this run is continuing an existing chat (previous run ended) |
@@ -495,9 +490,9 @@ Options for [`chat.headStart()`](/ai-chat/fast-starts#head-start), the warm-serv
495
490
  | `agentId` | `string` | required | The `chat.agent` / `chat.customAgent` id to hand off to |
496
491
  | `run` | `(args: HeadStartRunArgs) => Promise<StreamTextResult>` | required | First-turn callback. Call `streamText` and spread `chat.toStreamTextOptions({ tools })` |
497
492
  | `idleTimeoutInSeconds` | `number` | `60` | How long the agent waits for the handover signal |
498
- | `triggerConfig` | `Partial<SessionTriggerConfig>` | `undefined` | Run options (tags, queue, machine, maxAttempts, maxDuration, region, lockToVersion, externalDeploymentId) for the auto-triggered handover-prepare run. The `chat:{chatId}` tag is prepended automatically and counts toward the 10-tag limit |
493
+ | `triggerConfig` | `Partial<SessionTriggerConfig>` | `undefined` | Run options (tags, queue, machine, maxAttempts, maxDuration, region, lockToVersion) for the auto-triggered handover-prepare run. The `chat:{chatId}` tag is prepended automatically and counts toward the 10-tag limit |
499
494
 
500
- `chat.headStart(options)` returns the handler `(req: Request) => Promise<Response>`. The `run` callback receives `HeadStartRunArgs`: `{ messages: UIMessage[], signal: AbortSignal, chat: HeadStartChatHelper }`, where the helper exposes `chat.toStreamTextOptions({ tools })` and a `chat.session` escape hatch (whose `pendingVersion` says whether the agent run is parked waiting for its deployment). See [Head Start](/ai-chat/fast-starts#head-start) for the full guide.
495
+ `chat.headStart(options)` returns the handler `(req: Request) => Promise<Response>`. The `run` callback receives `HeadStartRunArgs`: `{ messages: UIMessage[], signal: AbortSignal, chat: HeadStartChatHelper }`, where the helper exposes `chat.toStreamTextOptions({ tools })` and a `chat.session` escape hatch. See [Head Start](/ai-chat/fast-starts#head-start) for the full guide.
501
496
 
502
497
  ## chat namespace
503
498
 
@@ -516,7 +511,6 @@ All methods available on the `chat` object from `@trigger.dev/sdk/ai`.
516
511
  | `chat.createStartSessionAction(taskId, options?)` | Returns a server action that creates a chat Session + triggers the first run + returns a session-scoped PAT. Idempotent on `(env, externalId)`. |
517
512
  | `chat.waitForHandover(options)` | Wait for a [`chat.headStart`](/ai-chat/fast-starts#handover-with-custom-agents) handover signal in a custom loop. Returns the signal or `null`. `chat.MessageAccumulator` wraps this as `consumeHandover()` / `applyHandover()` |
518
513
  | `chat.requestUpgrade()` | End the current run after this turn so the next message starts on the latest agent version. Server-orchestrated handoff. |
519
- | `chat.close({ reason })` | End the conversation permanently: close the session row, write a terminal `session-closed` record, and exit without a continuation. Decide it before the turn ends (`onBeforeTurnComplete`, not `onTurnComplete`) so the client sees the closed state on that turn. |
520
514
  | `chat.endAndContinue()` | In a hand-rolled custom agent, hand off the Session to a fresh continuation run. Call between turns after detaching input listeners, then return immediately. The promise rejects if the handoff fails. |
521
515
  | `chat.setTurnTimeout(duration)` | Override turn timeout at runtime (e.g. `"2h"`) |
522
516
  | `chat.setTurnTimeoutInSeconds(seconds)` | Override turn timeout at runtime (in seconds) |
@@ -665,7 +659,6 @@ The `onEvent` callback receives a `ChatTransportEvent` (exported from `@trigger.
665
659
  | --- | --- | --- |
666
660
  | `message-sent` | `messageId?`, `source`, `durationMs`, `partId?`, `bodyBytes?` | A send was durably acknowledged — a 2xx from the session input stream append (or the `headStart` POST), after any internal token-refresh retries. This means the message is durably written to the stream the agent consumes from, not merely "request accepted". `partId` is the append's idempotency key, also stored on the server-side record. |
667
661
  | `message-send-failed` | `messageId?`, `source`, `error`, `status?`, `durationMs`, `partId?`, `bodyBytes?` | A send definitively failed after internal retries. Fires in addition to `useChat`'s `onError`. |
668
- | `run-pending-version` | `source` | The chat's run is parked waiting for the deployment carrying its external deployment id ([version skew protection](/deployment/version-skew-protection#chat-sessions)). Everything already sent is durable and answered once the deployment lands. `source` is `"start"` (learned while starting the session), `"send"` (from a message append, re-emitted on every send while parked) `"head-start"` (from the `headStart` POST, where step 1 still streams from your server and only step 2 waits) or `"upgrade"` (an automatic version handover whose successor is parked on a deployment that has not landed). |
669
662
  | `stream-connected` | `resumed`, `lastEventId?`, `messageId?` | The SSE subscription to the session's output stream started delivering. `resumed: true` when reconnecting from a stored cursor (page reload) rather than following a fresh send. `lastEventId` is the cursor it connected from. |
670
663
  | `first-chunk` | `chunkType?`, `lastEventId?`, `messageId?`, `sinceSendMs?` | The first response chunk of a turn arrived. `sinceSendMs` is the delta from the last turn-producing send — time to first token without any bookkeeping. |
671
664
  | `turn-completed` | `lastEventId?`, `sessionInEventId?`, `messageId?`, `sinceSendMs?` | The agent's turn-complete control record arrived — the "finished answering" signal. `sinceSendMs` is the full turn latency; `sessionInEventId` is the cursor the agent can safely resume its input stream from. Treat it as a lower bound: it is held back behind any message still waiting to be handled, so it can be below the sequence of the record this turn answered. Do not use it to decide whether a turn boundary belongs to your own send. |
@@ -801,7 +794,7 @@ See [Stop generation](/ai-chat/frontend#stop-generation) for full details.
801
794
 
802
795
  ### transport.sendAction()
803
796
 
804
- Send a custom action to the agent, outside `useChat`. Actions wake the agent from suspension and fire `onAction`. An action that returns `chat.turn()` is followed by a turn; its answer arrives on the returned stream, which the caller must read. From a `useChat` app, send actions as requests instead (`sendMessage(undefined, { body: { action } })` or the `useChatActions` hook) so `useChat` renders the answer.
797
+ Send a custom action to the agent. Actions wake the agent from suspension and fire `onAction`. They are not turns — `run()` and turn lifecycle hooks do not fire. If `onAction` returns a `StreamTextResult`, the response is auto-piped to the frontend.
805
798
 
806
799
  ```ts
807
800
  transport.sendAction(chatId: string, action: unknown): Promise<ReadableStream<UIMessageChunk>>
@@ -111,7 +111,7 @@ const { id, runId, publicAccessToken, isCached } = await sessions.start({
111
111
  | `type` | `string` | Free-form discriminator. `chat.agent` uses `"chat.agent"`. |
112
112
  | `externalId` | `string?` | Your stable identity. Cannot start with `session_` (reserved). |
113
113
  | `taskIdentifier` | `string` | Task this session triggers runs against. |
114
- | `triggerConfig` | `SessionTriggerConfig` | Trigger options applied to every run: `tags` (up to 10, same as [run tags](/tags); the chat helpers such as `chat.createStartSessionAction` and `AgentChat` add a `chat:{chatId}` tag themselves, which uses one slot. Direct `sessions.start` callers get all 10 and must add any chat tag themselves), `queue`, `machine`, `maxAttempts`, `maxDuration`, `region`, `idleTimeoutInSeconds`, `basePayload`, and the version pins `lockToVersion` / [`externalDeploymentId`](/deployment/version-skew-protection#chat-sessions). |
114
+ | `triggerConfig` | `SessionTriggerConfig` | Trigger options applied to every run: `tags` (up to 10, same as [run tags](/tags); the chat helpers such as `chat.createStartSessionAction` and `AgentChat` add a `chat:{chatId}` tag themselves, which uses one slot. Direct `sessions.start` callers get all 10 and must add any chat tag themselves), `queue`, `machine`, `maxAttempts`, `idleTimeoutInSeconds`, `basePayload`. |
115
115
  | `tags` | `string[]?` | Up to 10 tags on the Session row (separate from `triggerConfig.tags`). |
116
116
  | `metadata` | `Record<string, unknown>?` | Arbitrary JSON. |
117
117
  | `expiresAt` | `Date?` | Hard retention deadline. |
@@ -146,11 +146,6 @@ Mark a Session as closed. Terminal and idempotent. The optional `reason` is stor
146
146
  await sessions.close(chatId, { reason: "user signed out" });
147
147
  ```
148
148
 
149
- Closing tells a live run too: the close lands on the session's input channel, so an idle or suspended agent exits its loop on the next wake rather than waiting out its idle timeout. After it lands, appends to `.in` are refused with HTTP 409 and `code: "session_closed"`.
150
-
151
- To close from inside the agent instead, call [`chat.close()`](/ai-chat/backend#ending-the-conversation). It writes a terminal record to the response stream so the browser learns the reason, then closes the row.
152
-
153
-
154
149
  ### `sessions.list(options?, requestOptions?)`
155
150
 
156
151
  Cursor-paginated list of Sessions in the current environment. Returns a `CursorPagePromise` you can iterate with `for await`.
@@ -203,7 +203,7 @@ Equivalent to the frontend's `useChat().regenerate()` — replays a turn with th
203
203
 
204
204
  ### sendAction
205
205
 
206
- Routes a payload through `actionSchema` + `onAction`. An action is a state edit: only `hydrateMessages` and `onAction` fire, unless `onAction` returns `chat.turn()`, in which case a turn runs on the edited history and the returned `turn.rawChunks` carries that turn's answer.
206
+ Routes a payload through `actionSchema` + `onAction`. Actions are not turns: only `hydrateMessages` and `onAction` fire on the agent side — no turn lifecycle hooks, no `run()`. The returned `turn.rawChunks` contains whatever `onAction` produced (a streamed model response if it returned a `StreamTextResult`, otherwise just `trigger:turn-complete`):
207
207
 
208
208
  ```ts
209
209
  const turn = await harness.sendAction({ type: "undo" });
@@ -634,7 +634,6 @@ The harness's initial wire payload depends on `mode`:
634
634
  | `sendHandover({ partialAssistantMessage, isFinal?, messageId? })` | Dispatch a `handover` signal — only meaningful when started with `mode: "handover-prepare"`. The agent picks up partial assistant messages and continues the turn. |
635
635
  | `sendHandoverSkip()` | Dispatch a `handover-skip` signal — only meaningful when started with `mode: "handover-prepare"`. The agent exits cleanly without firing turn hooks. |
636
636
  | `sendAction(action)` | Route a custom action through `actionSchema` + `onAction`. |
637
- | `sendPendingMessage(message)` | Append a user message mid-turn without waiting for a turn to complete, so it reaches the running turn as a steering message. Resolves once the record has landed on `session.in`. |
638
637
  | `sendStop(message?)` | Fire a stop signal. Does not wait for the turn — the run's `signal.aborted` becomes `true`. |
639
638
  | `seedSnapshot(snapshot)` | Pre-seed the snapshot read for the next boot. Effective on the next run boot only. |
640
639
  | `seedSessionOutTail(chunks?)` | Pre-seed `session.out` chunks for the next boot's replay. Reduces to settled assistant turns. |
@@ -8,7 +8,7 @@ description: "Declare tools on chat.agent so toModelOutput survives across turns
8
8
 
9
9
  ```ts
10
10
  import { chat } from "@trigger.dev/sdk/ai";
11
- import { stepCountIs, tool } from "ai";
11
+ import { streamText, stepCountIs, tool } from "ai";
12
12
  import { anthropic } from "@ai-sdk/anthropic";
13
13
  import { z } from "zod";
14
14
 
@@ -23,9 +23,9 @@ const tools = {
23
23
  export const myChat = chat.agent({
24
24
  id: "my-chat",
25
25
  tools, // ← declare here
26
- run: async ({ messages, tools, signal, streamText }) =>
26
+ run: async ({ messages, tools, signal }) =>
27
27
  streamText({
28
- tools,
28
+ ...chat.toStreamTextOptions({ tools }), // ← the same set, handed back on the payload
29
29
  model: anthropic("claude-sonnet-4-5"),
30
30
  messages,
31
31
  abortSignal: signal,
@@ -46,15 +46,10 @@ There are three places a tool set shows up. Declare once, reuse:
46
46
  | Surface | What it's for |
47
47
  | --- | --- |
48
48
  | `chat.agent({ tools })` | Re-applies `toModelOutput` on prior-turn history; hands the set back typed on the `run()` payload. |
49
- | `streamText({ tools })` on the `run` argument's `streamText` | What the model actually calls. Detects which calls need [HITL approval](/ai-chat/patterns/human-in-the-loop) (`needsApproval`) and merges the auto-injected [skill](/ai-chat/patterns/skills) tools on top. Naming `tools` replaces the config set for that call, so you can narrow it; omitting `tools` falls back to the config set. |
50
- | `chat.toStreamTextOptions({ tools })` | The same job by hand, for a [custom agent](#manual-turn-loops-chatcustomagent), which has no `run` argument. |
49
+ | `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. |
50
+ | `streamText({ tools })` | What the model actually calls. `chat.toStreamTextOptions({ tools })` already sets this, so spread it instead of passing `tools` twice. |
51
51
 
52
- The canonical pattern: declare `tools` on the config, read them back from the `run()` payload, and pass that set to the `streamText` the payload also carries.
53
-
54
- ```ts
55
- run: async ({ messages, tools, signal, streamText }) =>
56
- streamText({ model, messages, tools, abortSignal: signal }),
57
- ```
52
+ 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.
58
53
 
59
54
  <Tip>
60
55
  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`.
@@ -85,9 +80,9 @@ const tools = {
85
80
  export const chartChat = chat.agent({
86
81
  id: "chart-chat",
87
82
  tools, // ← without this, the image is "remembered" on turn 1 and gone from turn 2
88
- run: async ({ messages, tools, signal, streamText }) =>
83
+ run: async ({ messages, tools, signal }) =>
89
84
  streamText({
90
- tools,
85
+ ...chat.toStreamTextOptions({ tools }),
91
86
  model: anthropic("claude-sonnet-4-5"),
92
87
  messages,
93
88
  abortSignal: signal,
@@ -109,9 +104,9 @@ export const myChat = chat
109
104
  searchDocs,
110
105
  ...(clientData?.plan === "pro" ? { deepResearch } : {}),
111
106
  }),
112
- run: async ({ messages, tools, signal, streamText }) =>
107
+ run: async ({ messages, tools, signal }) =>
113
108
  streamText({
114
- tools,
109
+ ...chat.toStreamTextOptions({ tools }),
115
110
  model: anthropic("claude-sonnet-4-5"),
116
111
  messages,
117
112
  abortSignal: signal,
@@ -136,10 +131,10 @@ The resolved set is what lands on the `run()` payload's `tools`.
136
131
  The `run()` payload's `tools` is typed to whatever you declared, so you can pass it straight through without re-importing the map:
137
132
 
138
133
  ```ts
139
- run: async ({ messages, tools, signal, streamText }) => {
134
+ run: async ({ messages, tools, signal }) => {
140
135
  // `tools` is typed as your tool set, not a broad `ToolSet`
141
136
  return streamText({
142
- tools,
137
+ ...chat.toStreamTextOptions({ tools }),
143
138
  model: anthropic("claude-sonnet-4-5"),
144
139
  messages,
145
140
  abortSignal: signal,
@@ -165,7 +160,7 @@ This is shorthand for `UIMessage<unknown, UIDataTypes, InferUITools<typeof tools
165
160
 
166
161
  ## Skills
167
162
 
168
- [Agent skills](/ai-chat/patterns/skills) are auto-injected as tools (`loadSkill`, `readFile`, `bash`) by the managed `streamText`, or by `chat.toStreamTextOptions()` if you build the options yourself. They're separate from your config `tools`: declare your own tools on the config (so their `toModelOutput` survives across turns), and the merge happens at call time. Skill tools don't define `toModelOutput`, so they don't need to be on the config.
163
+ [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.
169
164
 
170
165
  ## Manual turn loops (`chat.customAgent`)
171
166
 
@@ -298,8 +298,8 @@ and direct API consumers.
298
298
  fire at the same lifecycle points.
299
299
  - `onAction` is still defined the same way, but its semantics changed
300
300
  in the [May 6 prerelease](/ai-chat/changelog) — actions are no longer
301
- turns. To answer after an action's edit, return `chat.turn()`; returning
302
- a `StreamTextResult` is no longer supported.
301
+ turns, and `onAction` returning a `StreamTextResult` produces a model
302
+ response.
303
303
  - `chat.customAgent({...})` and the `chat.createSession(payload, ...)`
304
304
  helper for building a session loop manually inside a custom agent.
305
305
  - `chat.defer` (deferred work) and `chat.history` (imperative history