@trigger.dev/sdk 0.0.0-prerelease-20260909071038 → 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 (142) 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-shared.d.ts +0 -15
  4. package/dist/commonjs/v3/ai-shared.js +1 -35
  5. package/dist/commonjs/v3/ai-shared.js.map +1 -1
  6. package/dist/commonjs/v3/ai.d.ts +25 -275
  7. package/dist/commonjs/v3/ai.js +308 -1283
  8. package/dist/commonjs/v3/ai.js.map +1 -1
  9. package/dist/commonjs/v3/chat-client.d.ts +2 -11
  10. package/dist/commonjs/v3/chat-client.js +111 -227
  11. package/dist/commonjs/v3/chat-client.js.map +1 -1
  12. package/dist/commonjs/v3/chat-react.d.ts +0 -88
  13. package/dist/commonjs/v3/chat-react.js +1 -130
  14. package/dist/commonjs/v3/chat-react.js.map +1 -1
  15. package/dist/commonjs/v3/chat-server.d.ts +6 -42
  16. package/dist/commonjs/v3/chat-server.js +7 -52
  17. package/dist/commonjs/v3/chat-server.js.map +1 -1
  18. package/dist/commonjs/v3/chat.d.ts +10 -92
  19. package/dist/commonjs/v3/chat.js +73 -328
  20. package/dist/commonjs/v3/chat.js.map +1 -1
  21. package/dist/commonjs/v3/sessions.d.ts +2 -15
  22. package/dist/commonjs/v3/sessions.js +1 -12
  23. package/dist/commonjs/v3/sessions.js.map +1 -1
  24. package/dist/commonjs/v3/shared.js +36 -30
  25. package/dist/commonjs/v3/shared.js.map +1 -1
  26. package/dist/commonjs/v3/test/index.d.ts +0 -1
  27. package/dist/commonjs/v3/test/index.js +1 -3
  28. package/dist/commonjs/v3/test/index.js.map +1 -1
  29. package/dist/commonjs/v3/test/mock-chat-agent.d.ts +4 -47
  30. package/dist/commonjs/v3/test/mock-chat-agent.js +4 -115
  31. package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
  32. package/dist/commonjs/v3/test/test-session-handle.js +0 -6
  33. package/dist/commonjs/v3/test/test-session-handle.js.map +1 -1
  34. package/dist/commonjs/version.js +1 -1
  35. package/dist/esm/imports/ai-runtime.d.ts +2 -2
  36. package/dist/esm/imports/ai-runtime.js +2 -2
  37. package/dist/esm/imports/ai-runtime.js.map +1 -1
  38. package/dist/esm/v3/ai-shared.d.ts +0 -15
  39. package/dist/esm/v3/ai-shared.js +0 -33
  40. package/dist/esm/v3/ai-shared.js.map +1 -1
  41. package/dist/esm/v3/ai.d.ts +25 -275
  42. package/dist/esm/v3/ai.js +305 -1275
  43. package/dist/esm/v3/ai.js.map +1 -1
  44. package/dist/esm/v3/chat-client.d.ts +2 -11
  45. package/dist/esm/v3/chat-client.js +112 -228
  46. package/dist/esm/v3/chat-client.js.map +1 -1
  47. package/dist/esm/v3/chat-react.d.ts +0 -88
  48. package/dist/esm/v3/chat-react.js +1 -127
  49. package/dist/esm/v3/chat-react.js.map +1 -1
  50. package/dist/esm/v3/chat-server.d.ts +6 -42
  51. package/dist/esm/v3/chat-server.js +8 -53
  52. package/dist/esm/v3/chat-server.js.map +1 -1
  53. package/dist/esm/v3/chat.d.ts +10 -92
  54. package/dist/esm/v3/chat.js +74 -329
  55. package/dist/esm/v3/chat.js.map +1 -1
  56. package/dist/esm/v3/sessions.d.ts +2 -15
  57. package/dist/esm/v3/sessions.js +1 -11
  58. package/dist/esm/v3/sessions.js.map +1 -1
  59. package/dist/esm/v3/shared.js +23 -17
  60. package/dist/esm/v3/shared.js.map +1 -1
  61. package/dist/esm/v3/test/index.d.ts +0 -1
  62. package/dist/esm/v3/test/index.js +0 -1
  63. package/dist/esm/v3/test/index.js.map +1 -1
  64. package/dist/esm/v3/test/mock-chat-agent.d.ts +4 -47
  65. package/dist/esm/v3/test/mock-chat-agent.js +6 -117
  66. package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
  67. package/dist/esm/v3/test/test-session-handle.js +0 -6
  68. package/dist/esm/v3/test/test-session-handle.js.map +1 -1
  69. package/dist/esm/version.js +1 -1
  70. package/docs/ai-chat/actions.mdx +23 -55
  71. package/docs/ai-chat/anatomy.mdx +3 -3
  72. package/docs/ai-chat/backend.mdx +48 -125
  73. package/docs/ai-chat/background-injection.mdx +19 -69
  74. package/docs/ai-chat/client-protocol.mdx +4 -5
  75. package/docs/ai-chat/compaction.mdx +7 -13
  76. package/docs/ai-chat/custom-agents.mdx +0 -23
  77. package/docs/ai-chat/fast-starts.mdx +20 -27
  78. package/docs/ai-chat/frontend.mdx +14 -19
  79. package/docs/ai-chat/lifecycle-hooks.mdx +2 -6
  80. package/docs/ai-chat/migrating-from-a-route-handler.mdx +14 -16
  81. package/docs/ai-chat/patterns/database-persistence.mdx +2 -6
  82. package/docs/ai-chat/patterns/persistence-and-replay.mdx +23 -34
  83. package/docs/ai-chat/patterns/skills.mdx +10 -7
  84. package/docs/ai-chat/patterns/version-upgrades.mdx +6 -79
  85. package/docs/ai-chat/pending-messages.mdx +3 -3
  86. package/docs/ai-chat/prompt-caching.mdx +25 -23
  87. package/docs/ai-chat/quick-start.mdx +11 -11
  88. package/docs/ai-chat/reference.mdx +9 -67
  89. package/docs/ai-chat/sessions.mdx +1 -6
  90. package/docs/ai-chat/testing.mdx +1 -2
  91. package/docs/ai-chat/tools.mdx +13 -18
  92. package/docs/ai-chat/upgrade-guide.mdx +2 -2
  93. package/docs/apikeys.mdx +45 -27
  94. package/docs/config/extensions/syncEnvVars.mdx +0 -6
  95. package/docs/deploy-environment-variables.mdx +2 -23
  96. package/docs/deployment/overview.mdx +8 -4
  97. package/docs/deployment/preview-branches.mdx +4 -4
  98. package/docs/deployment/version-skew-protection.mdx +0 -62
  99. package/docs/manual-setup.mdx +7 -7
  100. package/docs/mcp-tools.mdx +0 -9
  101. package/docs/quick-start.mdx +3 -3
  102. package/docs/realtime/auth.mdx +1 -1
  103. package/docs/self-hosting/security.mdx +0 -5
  104. package/docs/tasks/scheduled.mdx +0 -24
  105. package/docs/triggering.mdx +1 -1
  106. package/package.json +4 -4
  107. package/skills/trigger-authoring-chat-agent/SKILL.md +29 -41
  108. package/skills/trigger-chat-agent-advanced/SKILL.md +12 -31
  109. package/dist/commonjs/v3/chatSnapshotIo.d.ts +0 -2
  110. package/dist/commonjs/v3/chatSnapshotIo.js +0 -174
  111. package/dist/commonjs/v3/chatSnapshotIo.js.map +0 -1
  112. package/dist/commonjs/v3/chatVersionSkew.d.ts +0 -12
  113. package/dist/commonjs/v3/chatVersionSkew.js +0 -30
  114. package/dist/commonjs/v3/chatVersionSkew.js.map +0 -1
  115. package/dist/commonjs/v3/externalDeploymentId.d.ts +0 -23
  116. package/dist/commonjs/v3/externalDeploymentId.js +0 -43
  117. package/dist/commonjs/v3/externalDeploymentId.js.map +0 -1
  118. package/dist/commonjs/v3/test/transcript-storage-tests.d.ts +0 -40
  119. package/dist/commonjs/v3/test/transcript-storage-tests.js +0 -258
  120. package/dist/commonjs/v3/test/transcript-storage-tests.js.map +0 -1
  121. package/dist/commonjs/v3/transcriptStorage.d.ts +0 -229
  122. package/dist/commonjs/v3/transcriptStorage.js +0 -321
  123. package/dist/commonjs/v3/transcriptStorage.js.map +0 -1
  124. package/dist/esm/v3/chatSnapshotIo.d.ts +0 -2
  125. package/dist/esm/v3/chatSnapshotIo.js +0 -166
  126. package/dist/esm/v3/chatSnapshotIo.js.map +0 -1
  127. package/dist/esm/v3/chatVersionSkew.d.ts +0 -12
  128. package/dist/esm/v3/chatVersionSkew.js +0 -27
  129. package/dist/esm/v3/chatVersionSkew.js.map +0 -1
  130. package/dist/esm/v3/externalDeploymentId.d.ts +0 -23
  131. package/dist/esm/v3/externalDeploymentId.js +0 -38
  132. package/dist/esm/v3/externalDeploymentId.js.map +0 -1
  133. package/dist/esm/v3/test/transcript-storage-tests.d.ts +0 -40
  134. package/dist/esm/v3/test/transcript-storage-tests.js +0 -255
  135. package/dist/esm/v3/test/transcript-storage-tests.js.map +0 -1
  136. package/dist/esm/v3/transcriptStorage.d.ts +0 -229
  137. package/dist/esm/v3/transcriptStorage.js +0 -309
  138. package/dist/esm/v3/transcriptStorage.js.map +0 -1
  139. package/docs/ai-chat/patterns/native-compaction.mdx +0 -310
  140. package/docs/ai-chat/transcript-storage.mdx +0 -233
  141. package/docs/reports.mdx +0 -157
  142. package/docs/troubleshooting-zod.mdx +0 -158
@@ -1,14 +1,14 @@
1
1
  ---
2
2
  title: "Background injection"
3
3
  sidebarTitle: "Background injection"
4
- description: "Inject context from background work into the agent's conversation: self-review, RAG augmentation, or any async analysis."
4
+ description: "Inject context from background work into the agent's conversation — self-review, RAG augmentation, or any async analysis."
5
5
  ---
6
6
 
7
7
  ## Overview
8
8
 
9
9
  `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).
10
10
 
11
- This is the backend counterpart to [pending messages](/ai-chat/pending-messages). Pending messages come from the user via the frontend, while `chat.inject()` comes from your task code.
11
+ This is the backend counterpart to [pending messages](/ai-chat/pending-messages) — pending messages come from the user via the frontend, while `chat.inject()` comes from your task code.
12
12
 
13
13
  ## Basic usage
14
14
 
@@ -33,9 +33,8 @@ The most powerful pattern combines `chat.defer()` (background work) with `chat.i
33
33
  ```ts
34
34
  export const myChat = chat.agent({
35
35
  id: "my-chat",
36
- registry,
37
36
  onTurnComplete: async ({ messages }) => {
38
- // Kick off background analysis, doesn't block the turn
37
+ // Kick off background analysis — doesn't block the turn
39
38
  chat.defer(
40
39
  (async () => {
41
40
  const analysis = await analyzeConversation(messages);
@@ -48,8 +47,9 @@ export const myChat = chat.agent({
48
47
  })()
49
48
  );
50
49
  },
51
- run: async ({ messages, signal, streamText }) => {
50
+ run: async ({ messages, signal }) => {
52
51
  return streamText({
52
+ ...chat.toStreamTextOptions({ registry }),
53
53
  messages,
54
54
  abortSignal: signal,
55
55
  stopWhen: stepCountIs(15),
@@ -77,7 +77,7 @@ A cheap model reviews the agent's response after each turn and injects coaching
77
77
  ```ts
78
78
  import { chat } from "@trigger.dev/sdk/ai";
79
79
  import { prompts } from "@trigger.dev/sdk";
80
- import { generateObject, createProviderRegistry, stepCountIs } from "ai";
80
+ import { streamText, generateObject, createProviderRegistry, stepCountIs } from "ai";
81
81
  import { anthropic } from "@ai-sdk/anthropic";
82
82
  import { z } from "zod";
83
83
 
@@ -98,7 +98,6 @@ Be concise. Only flag issues worth fixing.`,
98
98
 
99
99
  export const myChat = chat.agent({
100
100
  id: "my-chat",
101
- registry,
102
101
  onTurnComplete: async ({ messages }) => {
103
102
  chat.defer(
104
103
  (async () => {
@@ -140,8 +139,9 @@ export const myChat = chat.agent({
140
139
  })()
141
140
  );
142
141
  },
143
- run: async ({ messages, signal, streamText }) => {
142
+ run: async ({ messages, signal }) => {
144
143
  return streamText({
144
+ ...chat.toStreamTextOptions({ registry }),
145
145
  messages,
146
146
  abortSignal: signal,
147
147
  stopWhen: stepCountIs(15),
@@ -150,7 +150,7 @@ export const myChat = chat.agent({
150
150
  });
151
151
  ```
152
152
 
153
- The self-review runs on `claude-haiku-4-5` (fast, cheap) in the background. If the user sends another message before it completes, the coaching is still injected, because `chat.inject()` persists across the idle wait.
153
+ The self-review runs on `claude-haiku-4-5` (fast, cheap) in the background. If the user sends another message before it completes, the coaching is still injected — `chat.inject()` persists across the idle wait.
154
154
 
155
155
  ## Other use cases
156
156
 
@@ -161,25 +161,25 @@ The self-review runs on `claude-haiku-4-5` (fast, cheap) in the background. If t
161
161
 
162
162
  ## `chat.defer` standalone
163
163
 
164
- `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.
164
+ `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.
165
165
 
166
166
  ```ts
167
167
  export const myChat = chat.agent({
168
168
  id: "my-chat",
169
169
  onTurnStart: async ({ chatId, runId }) => {
170
- // Analytics: fire-and-forget, irrelevant to resume.
170
+ // Analytics — fire-and-forget, irrelevant to resume.
171
171
  chat.defer(analytics.track("turn_started", { chatId, runId }));
172
172
  },
173
- run: async ({ messages, signal, streamText }) => {
173
+ run: async ({ messages, signal }) => {
174
174
  return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
175
175
  },
176
176
  });
177
177
  ```
178
178
 
179
- `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`.
179
+ `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`.
180
180
 
181
181
  <Warning>
182
- **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.
182
+ **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.
183
183
  </Warning>
184
184
 
185
185
  ## How it differs from pending messages
@@ -189,57 +189,9 @@ export const myChat = chat.agent({
189
189
  | **Source** | Backend task code | Frontend user input |
190
190
  | **Triggered by** | Your code (e.g. `onTurnComplete` + `chat.defer()`) | User sending a message during streaming |
191
191
  | **Injection point** | Start of next turn, or next `prepareStep` boundary | Next `prepareStep` boundary only |
192
- | **Message role** | Any. `system` becomes an instruction, others join the conversation (see below) | Typically `user` |
192
+ | **Message role** | Any (`system`, `user`, `assistant`) | Typically `user` |
193
193
  | **Frontend visibility** | Not visible unless you write custom `data-*` chunks | Visible via `usePendingMessages` hook |
194
194
 
195
- ## Two lanes: trusted and untrusted
196
-
197
- The role you inject with decides more than position. It decides whether the model
198
- treats the content as trustworthy.
199
-
200
- **`role: "system"` goes to the instructions lane.** The block is appended to the
201
- system instructions for subsequent inference calls, so it carries the same standing
202
- as your system prompt. This is the lane for context the agent should believe:
203
- entitlements, plan changes, operational notices.
204
-
205
- It has to work this way. On AI SDK 7 a system message inside `messages` is rejected
206
- for every provider. `standardizePrompt` throws before any provider is called, and
207
- its own advice is to use the instructions option, so the injected block goes there
208
- rather than into the transcript.
209
-
210
- <Warning>
211
- The instructions lane is delivered by `chat.toStreamTextOptions()`, because that
212
- is the only place the SDK can set `streamText`'s instructions for you. If your
213
- `run()` calls `streamText({ model, messages, abortSignal })` without spreading
214
- `chat.toStreamTextOptions()`, a `role: "system"` injection never reaches the
215
- model. The conversational lane has no such requirement: it arrives through
216
- `messages` either way.
217
- </Warning>
218
-
219
- - An injection applies to the next turn only. A block injected in `onTurnComplete`
220
- shapes the following turn and is cleared after it, so it is not repeated on every
221
- turn from then on. Within that turn it is consumed once rather than once per read,
222
- so a `run()` that builds options more than once sees the same instructions in
223
- every build.
224
- - The injected text is merged into a single instruction rather than added as a
225
- second block, because AI SDK 5 rejects an array of system blocks while accepting
226
- one structured block. Merging changes the cached prefix, so a cached system prompt
227
- gets no cache hit for as long as an injection is live. If you rely on prompt
228
- caching, inject sparingly and prefer facts that go stale, so the injection clears.
229
-
230
- **Any other role joins the conversation, and is untrusted by construction.** A
231
- message injected as `user` is indistinguishable from something the user typed, and a
232
- well-aligned model treats it accordingly, and may say so and re-derive the answer
233
- from tools instead of taking it at face value:
234
-
235
- > "that text arrived embedded in your message, not from a tool I called, so I
236
- > verified it myself rather than trusting it"
237
-
238
- That is correct behaviour, not a bug. So inject **checkable facts** in the
239
- conversational lane and put **directives** in the instructions lane. A conclusion
240
- injected as a user message is the worst of both: the model neither trusts it nor
241
- ignores it, and may contradict it in front of the user.
242
-
243
195
  ## API reference
244
196
 
245
197
  ### chat.inject()
@@ -248,9 +200,7 @@ ignores it, and may contradict it in front of the user.
248
200
  chat.inject(messages: ModelMessage[]): void
249
201
  ```
250
202
 
251
- Queue model messages for injection at the next opportunity. Messages persist across the idle wait between turns, and are not reset when a new turn starts.
252
-
253
- Lifetime: a conversational message (`role: "user"` or `"assistant"`) becomes part of the model's context from the next turn onward, for the rest of the conversation. It is written to the [transcript storage](/ai-chat/transcript-storage)'s `state`, anchored to the message it followed, so it survives a continuation run and comes back in the same place. It does not appear in the UI transcript. A history edit that rebuilds the context drops it. This holds however the message reached the model: drained before `run()` or at a step boundary inside a multi-step turn. A message that is still queued when the run ends (injected from the last `onTurnComplete` before an exit, for example) is carried in the storage's `state` too and is queued again when the next run boots, so it reaches the next turn. A `role: "system"` message is appended to the instructions for the next turn only and is consumed once. Injecting the same notice every turn adds a copy every turn; dedupe on your side.
203
+ Queue model messages for injection at the next opportunity. Messages persist across the idle wait between turns — they are not reset when a new turn starts.
254
204
 
255
205
  **Parameters:**
256
206
 
@@ -259,9 +209,9 @@ Lifetime: a conversational message (`role: "user"` or `"assistant"`) becomes par
259
209
  | `messages` | `ModelMessage[]` | Model messages to inject (from the `ai` package) |
260
210
 
261
211
  Messages are drained (consumed) when:
262
- 1. A new turn starts, before `run()` executes
263
- 2. A `prepareStep` boundary is reached, between tool-call steps during streaming
212
+ 1. A new turn starts — before `run()` executes
213
+ 2. A `prepareStep` boundary is reached — between tool-call steps during streaming
264
214
 
265
215
  <Note>
266
- `chat.inject()` writes to an in-memory queue in the current process. It works from any code running in the same task: lifecycle hooks, deferred work, tool execute functions, etc. It does not work from subtasks or other runs.
216
+ `chat.inject()` writes to an in-memory queue in the current process. It works from any code running in the same task — lifecycle hooks, deferred work, tool execute functions, etc. It does not work from subtasks or other runs.
267
217
  </Note>
@@ -54,7 +54,7 @@ A single-shell walk-through of the whole protocol — copy, fill in `BASE_URL` /
54
54
 
55
55
  ```bash
56
56
  BASE_URL="https://api.trigger.dev" # or your local webapp
57
- SECRET_KEY="tr_dev_sk_..." # secret API key for the env
57
+ SECRET_KEY="tr_dev_..." # secret API key for the env
58
58
  TASK_ID="ai-chat" # your chat.agent task id
59
59
  CHAT_ID=$(uuidgen | tr '[:upper:]' '[:lower:]')
60
60
 
@@ -210,7 +210,6 @@ Pick `"preload"` when the UI has rendered but the user hasn't typed (warms the a
210
210
  | `triggerConfig.maxAttempts` | `number` | Per-run retry cap (1–10). |
211
211
  | `triggerConfig.maxDuration` | `number` | Per-run wall-clock cap, seconds. |
212
212
  | `triggerConfig.lockToVersion` | `string` | Pin every run to a specific worker version. |
213
- | `triggerConfig.externalDeploymentId` | `string \| null` | Pin every run to the deployment carrying this [external deployment id](/deployment/version-skew-protection#chat-sessions). Discovered from the environment when omitted; `null` opts the chat out. |
214
213
  | `triggerConfig.region` | `string` | Region preference. |
215
214
  | `triggerConfig.idleTimeoutInSeconds` | `number` | Surfaced to the agent through the wire payload (1–3600). |
216
215
 
@@ -267,7 +266,7 @@ x-trigger-jwt-claims: {"sub":"...","scopes":["read:runs:run_abc123","write:input
267
266
  Re-calling `POST /api/v1/sessions` with the same `(taskIdentifier, externalId)` pair is **idempotent for the lifetime of the session**:
268
267
 
269
268
  - If the session is still alive: returns the existing row with `isCached: true`, `runId` unchanged, and a **fresh** 60-minute `publicAccessToken`. No duplicate run is triggered. (Idle/exited runs are different — see [Continuations](#continuations).)
270
- - If the session has been closed (`POST /api/v1/sessions/{id}/close`, or `chat.close()` from inside the agent): returns **HTTP 409**. Closed is one-way; reuse a different `externalId` to start a new conversation.
269
+ - If the session has been closed (`POST /api/v1/sessions/{id}/close`): returns **HTTP 409**. Closed is one-way; reuse a different `externalId` to start a new conversation.
271
270
  - Any tags / metadata / expiresAt / triggerConfig fields you send on the cached path are written through to the row, so you can update e.g. `triggerConfig.basePayload.metadata` mid-conversation. The new fields apply to **future** runs (continuations); the currently-live run keeps its original config.
272
271
 
273
272
  <Warning>
@@ -694,7 +693,7 @@ The body is a JSON-serialized [`ChatInputChunk`](#chatinputchunk), a tagged unio
694
693
  | --- | --- |
695
694
  | `401` | Missing or invalid `Authorization` header. |
696
695
  | `403` | Token doesn't carry `write:sessions:{externalId}`. |
697
- | `409` | The session is closed: `{ "ok": false, "error": "Cannot append to a closed session", "code": "session_closed", "closedReason": "<reason or null>" }`. Key on `code`, not the message. Terminal: do not retry, and stop reconnecting to `.out`. |
696
+ | `409` | The session is closed — `{ "ok": false, "error": "Cannot append to a closed session" }`. |
698
697
  | `413` | Body exceeds 1 MiB **or** the wrapped record would exceed S2's ~1 MiB per-record metered ceiling. A normal `kind: "message"` payload is a few KB; if you hit this you're shipping more than one message per record or pushing a single tool output that's itself oversized. Carries CORS headers so browser fetches can read the status. |
699
698
  | `500` | Transient backend failure on the durable stream. Safe to retry — appends are idempotent on `(externalId, X-Part-Id)` if you set the optional `X-Part-Id` request header (the built-in clients set it from a UUID). |
700
699
 
@@ -833,7 +832,7 @@ Custom actions (undo, rollback, edit) ride on the same `.in` channel using `kind
833
832
  }
834
833
  ```
835
834
 
836
- For managed `chat.agent()` tasks, actions wake the agent from suspension (same as messages) and fire the `onAction` hook — they are not turns, so `run()` and turn lifecycle hooks do not fire. If `onAction` returns `chat.turn()`, a turn runs on the edited history and its chunks follow on `.out` like any turn's.
835
+ For managed `chat.agent()` tasks, actions wake the agent from suspension (same as messages) and fire the `onAction` hook — they are not turns, so `run()` and turn lifecycle hooks do not fire. If `onAction` returns a `StreamTextResult`, the response is auto-piped to the frontend (but still no `run()` or `onTurnComplete`). The `action` payload is validated against the agent's `actionSchema`. If the agent didn't register an `actionSchema` (or your `action` payload doesn't match it), validation fails the same way `metadata` does — `.in/append` returns `200 OK`, but the run trace shows `chat turn N [ERROR]` and the wire emits a `turn-complete` control record with no other chunks. See [Actions](/ai-chat/actions) for the agent-side schema setup.
837
836
 
838
837
  Raw `chat.customAgent()` tasks receive `action` as `unknown` and must validate it in their own loop.
839
838
 
@@ -19,12 +19,11 @@ Provide `shouldCompact` to decide when to compact and `summarize` to generate th
19
19
 
20
20
  ```ts
21
21
  import { chat } from "@trigger.dev/sdk/ai";
22
- import { generateText, stepCountIs } from "ai";
22
+ import { streamText, generateText, stepCountIs } from "ai";
23
23
  import { anthropic } from "@ai-sdk/anthropic";
24
24
 
25
25
  export const myChat = chat.agent({
26
26
  id: "my-chat",
27
- registry,
28
27
  compaction: {
29
28
  shouldCompact: ({ totalTokens }) => (totalTokens ?? 0) > 80_000,
30
29
  summarize: async ({ messages }) => {
@@ -35,8 +34,9 @@ export const myChat = chat.agent({
35
34
  return result.text;
36
35
  },
37
36
  },
38
- run: async ({ messages, signal, streamText }) => {
37
+ run: async ({ messages, signal }) => {
39
38
  return streamText({
39
+ ...chat.toStreamTextOptions({ registry }),
40
40
  messages,
41
41
  abortSignal: signal,
42
42
  stopWhen: stepCountIs(15),
@@ -61,12 +61,6 @@ After each turn completes:
61
61
 
62
62
  On the next turn, the LLM receives the compact summary instead of the full history — dramatically reducing token usage while preserving context.
63
63
 
64
- The compacted context is durable. The runtime writes it to the [transcript storage](/ai-chat/transcript-storage)'s `state` alongside the messages, so a new run that boots to continue the conversation starts from the summary rather than re-reading the whole transcript and summarising it again. An undo or edit that reaches into the summarised part of the conversation clears the stored summary, and compaction runs again from the edited history when the threshold is next crossed.
65
-
66
- <Note>
67
- This is Trigger.dev's provider-agnostic compaction. To persist a **provider's own** compaction across turns instead (Anthropic context editing or OpenAI stored responses), and to fall back between providers without re-sending history, see [Native compaction & provider fallback](/ai-chat/patterns/native-compaction).
68
- </Note>
69
-
70
64
  ## Customizing what gets persisted
71
65
 
72
66
  By default, compaction only affects model messages — UI messages stay intact so users see the full conversation after a page refresh. You can customize this with `compactUIMessages`:
@@ -97,7 +91,7 @@ export const myChat = chat.agent({
97
91
  ...uiMessages.slice(-4), // Keep the last 4 messages
98
92
  ],
99
93
  },
100
- run: async ({ messages, signal, streamText }) => {
94
+ run: async ({ messages, signal }) => {
101
95
  return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
102
96
  },
103
97
  });
@@ -191,7 +185,7 @@ export const myChat = chat.agent({
191
185
  data: { chatId, summary, totalTokens, messageCount },
192
186
  });
193
187
  },
194
- run: async ({ messages, signal, streamText }) => {
188
+ run: async ({ messages, signal }) => {
195
189
  return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
196
190
  },
197
191
  });
@@ -207,7 +201,7 @@ Define a `compact` action that reuses your existing `summarize` function:
207
201
 
208
202
  ```ts
209
203
  import { chat } from "@trigger.dev/sdk/ai";
210
- import { generateText, generateId, convertToModelMessages } from "ai";
204
+ import { streamText, generateText, generateId, convertToModelMessages } from "ai";
211
205
  import { anthropic } from "@ai-sdk/anthropic";
212
206
  import { z } from "zod";
213
207
 
@@ -249,7 +243,7 @@ export const myChat = chat.agent({
249
243
  ]);
250
244
  },
251
245
 
252
- run: async ({ messages, signal, streamText }) => {
246
+ run: async ({ messages, signal }) => {
253
247
  return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
254
248
  },
255
249
  });
@@ -290,29 +290,6 @@ Read `turn.stopped` to tell a user stop from a full run cancel:
290
290
 
291
291
  A hand-rolled loop wires this itself with `chat.createStopSignal()` and `chat.cleanupAbortedParts()`. Two things `createSession` handles for you are easy to get wrong there — see the [hand-rolled loop checklist](#hand-rolled-loop-checklist).
292
292
 
293
- ### Ending the conversation
294
-
295
- `chat.close({ reason })` works in a custom agent exactly as it does in [`chat.agent`](/ai-chat/backend#ending-the-conversation): the session row is closed, further sends are refused with HTTP 409, and no continuation run is scheduled. Call it from anywhere in your loop.
296
-
297
- ```ts trigger/my-chat.ts
298
- for await (const turn of session) {
299
- const result = streamText({ model, messages: turn.messages, abortSignal: turn.signal });
300
-
301
- // Close BEFORE turn.complete(): that call writes the turn boundary the
302
- // browser stops reading at, and it carries the closed state out with it.
303
- if (await overBudget(turn.chatId)) {
304
- chat.close({ reason: "Monthly budget reached" });
305
- }
306
-
307
- await turn.complete(result);
308
- if (turn.stopped) break;
309
- }
310
- ```
311
-
312
- The close is performed when your `run()` returns, so it lands whether you `break` out of the loop, return early, or keep iterating. A hand-rolled loop with no iterator at all works the same way, as long as you call `chat.close()` before the `chat.writeTurnComplete()` that ends the turn.
313
-
314
- Only `chat.agent` and `chat.customAgent` bind the run to its Session, so `chat.close()` throws in a plain `task()`.
315
-
316
293
  ## Hand-rolled loop with primitives
317
294
 
318
295
  For full control, skip `createSession` and compose the primitives directly:
@@ -239,11 +239,11 @@ This is an **import-chain** problem, not a runtime one. A "we'll strip the execu
239
239
 
240
240
  export const myChat = chat.agent({
241
241
  id: "my-chat",
242
- run: async ({ messages, signal, streamText }) =>
242
+ run: async ({ messages, signal }) =>
243
243
  streamText({
244
+ ...chat.toStreamTextOptions({ tools: chatTools }),
244
245
  model: anthropic("claude-sonnet-4-6"),
245
246
  messages,
246
- tools: chatTools,
247
247
  stopWhen: stepCountIs(10),
248
248
  abortSignal: signal,
249
249
  }),
@@ -251,7 +251,7 @@ This is an **import-chain** problem, not a runtime one. A "we'll strip the execu
251
251
  ```
252
252
  </Step>
253
253
  <Step title="Build the head-start handler">
254
- Call `chat.headStart({ agentId, run })`. It returns a standard Web Fetch handler: `(req: Request) => Promise<Response>`. The `run` callback receives a `streamText` that already carries the SDK-owned wiring: the converted messages, `stopWhen: stepCountIs(1)` and the abort signal. Pass your schema-only tools to it explicitly, and add your own `model` and `system` on top.
254
+ Call `chat.headStart({ agentId, run })`. It returns a standard Web Fetch handler: `(req: Request) => Promise<Response>`. Inside the `run` callback you call `streamText` yourself and spread `chat.toStreamTextOptions({ tools })` to inherit the SDK-owned wiring (messages, schema-only tools, `stopWhen: stepCountIs(1)`, abort signal). Add your own `model` and `system` on top.
255
255
 
256
256
  ```ts lib/chat-handler.ts
257
257
  import { chat } from "@trigger.dev/sdk/chat-server";
@@ -261,23 +261,18 @@ This is an **import-chain** problem, not a runtime one. A "we'll strip the execu
261
261
 
262
262
  export const chatHandler = chat.headStart({
263
263
  agentId: "my-chat",
264
- run: async ({ streamText }) =>
264
+ run: async ({ chat: helper }) =>
265
265
  streamText({
266
+ ...helper.toStreamTextOptions({ tools: headStartTools }),
266
267
  model: anthropic("claude-sonnet-4-6"),
267
268
  system: "You are a helpful assistant.",
268
- tools: headStartTools,
269
269
  }),
270
270
  });
271
271
  ```
272
272
 
273
- <Note>
274
- That `streamText` is the SDK's, not the one from `ai`. It pins `messages`,
275
- `prompt`, `stopWhen: stepCountIs(1)` and `abortSignal`, which the handover depends on:
276
- running past step 1 would splice a stream the agent is supposed to own. Setting
277
- any of the four at the call site is a type error, and a throw if you get past
278
- the types, rather than breaking the handover quietly. `chat.toStreamTextOptions()` is still there if you want to build the
279
- options yourself.
280
- </Note>
273
+ <Warning>
274
+ Don't set `stopWhen` here. The spread pins it to `stepCountIs(1)`, and overriding it makes the handler run steps the agent is supposed to own — the handover then splices a stream that has already moved past step 1.
275
+ </Warning>
281
276
 
282
277
  <Tip>
283
278
  Use the **same model** on both sides (route handler and `chat.agent`) to avoid a tone or style shift between step 1 and step 2+. Your LLM provider keys stay server-side in your warm process — Trigger.dev never holds them in this design.
@@ -630,27 +625,27 @@ chat.headStart<TTools>({
630
625
  export const chatHandler = chat.headStart({
631
626
  agentId: "my-chat",
632
627
  triggerConfig: { tags: ["org:acme"], queue: "chat", machine: "small-2x" },
633
- run: async ({ streamText }) => streamText({ model, system, tools: headStartTools }),
628
+ run: async ({ chat: helper }) =>
629
+ streamText({ ...helper.toStreamTextOptions({ tools: headStartTools }), model, system }),
634
630
  });
635
631
  ```
636
632
 
637
633
  The `run` callback receives:
638
634
 
639
- - `messages: UIMessage[]`: user messages parsed from the request body.
640
- - `signal: AbortSignal`: fires when the request closes or the SDK times out the handover.
641
- - `streamText`: the AI SDK's `streamText` with the keys below already applied. Prefer it.
642
- - `chat: HeadStartChatHelper<TTools>`: exposes `chat.toStreamTextOptions({ tools })` for building the options by hand, plus a `chat.session` escape hatch for power users.
635
+ - `messages: UIMessage[]` — user messages parsed from the request body.
636
+ - `signal: AbortSignal` — fires when the request closes or the SDK times out the handover.
637
+ - `chat: HeadStartChatHelper<TTools>` — exposes `chat.toStreamTextOptions({ tools })` and a `chat.session` escape hatch for power users.
643
638
 
644
- The SDK owns these keys. Passing one to the callback's `streamText` is a type error, and a throw behind that; re-setting one after a `chat.toStreamTextOptions()` spread breaks the protocol with no error at all:
639
+ `chat.toStreamTextOptions({ tools })` returns options to spread into `streamText`. The SDK owns these keys — overriding them will break the protocol:
645
640
 
646
641
  | Key | What the SDK sets | Why |
647
642
  | --- | --- | --- |
648
643
  | `messages` | `convertToModelMessages(uiMessages)` | First-turn user history |
649
- | `prompt` | Nothing, and rejects yours | `messages` already carries the history |
650
- | `stopWhen` | `stepCountIs(1)` | Step 1 only, the agent picks up step 2 onward |
644
+ | `tools` | What you pass | Schema-only tools for step 1 |
645
+ | `stopWhen` | `stepCountIs(1)` | Step 1 only — agent picks up step 2+ |
651
646
  | `abortSignal` | Combined request + idle timeout | Safe cleanup on disconnect |
652
647
 
653
- You bring `model`, `system`, `providerOptions`, `prepareStep`, anything else `streamText` accepts. `tools` is yours too: pass your schema-only set to the callback's `streamText` (or to `chat.toStreamTextOptions({ tools })`), since the SDK cannot know it.
648
+ You bring `model`, `system`, `providerOptions`, `prepareStep`, anything else `streamText` accepts.
654
649
 
655
650
  #### The transport option
656
651
 
@@ -689,11 +684,11 @@ export async function POST(req: Request) {
689
684
  agentId: "my-chat",
690
685
  chatId, // session externalId; reuse it on the destination page
691
686
  messages, // first-turn user history
692
- run: async ({ streamText }) =>
687
+ run: async ({ chat: helper }) =>
693
688
  streamText({
689
+ ...helper.toStreamTextOptions({ tools: headStartTools }),
694
690
  model: anthropic("claude-sonnet-4-6"),
695
691
  system: "You are a helpful assistant.",
696
- tools: headStartTools,
697
692
  }),
698
693
  });
699
694
 
@@ -740,13 +735,11 @@ chat.startHeadStart<TTools>({
740
735
  triggerConfig?: Partial<SessionTriggerConfig>, // tags, queue, machine, …
741
736
  apiClient?: ApiClientConfiguration, // when the agent lives in another project/env
742
737
  metadata?: Record<string, unknown>, // merged into the run payload; never sent to the browser
743
- }): Promise<{ chatId: string; pendingVersion: boolean; completion: Promise<void> }>
738
+ }): Promise<{ chatId: string; completion: Promise<void> }>
744
739
  ```
745
740
 
746
741
  `completion` resolves once the head start finishes; `await` it or hand it to `waitUntil`. It rejects if the warm step or the dispatch fails.
747
742
 
748
- `pendingVersion` is `true` when the agent run is parked waiting for the deployment carrying the session's [external deployment id](/deployment/version-skew-protection#chat-sessions). Step 1 still runs in your process and still reaches the browser, so pass the flag to the destination page if you want it to say a deploy is in progress rather than appear to stall on step 2.
749
-
750
743
  ### Limitations
751
744
 
752
745
  - **First turn only.** Step 2+ and turn 2+ run on the trigger side. There's no per-turn "head start every turn" mode — the win comes from amortizing agent boot across the LLM call once.
@@ -137,8 +137,6 @@ On page load, fetch both the messages and the session state from your database,
137
137
 
138
138
  Because the underlying Session row outlives individual runs, a chat you were in yesterday resumes against the same chat — even if the original run has long since exited. The transport hydrates from the persisted state and uses `lastEventId` to resubscribe; if the client tries to send a new message and no run is alive, the server triggers a fresh continuation run on the same session before the message is appended.
139
139
 
140
- If you do not keep your own copy of the conversation, load it from the agent's [transcript storage](/ai-chat/transcript-storage#reading-the-transcript) instead: `chat.createLoadTranscriptAction(storage)` on the server and `useLoadTranscript(chatId, action, { transport })` in the browser return the messages and seed the transport's resume cursor, for the default storage and your own alike.
141
-
142
140
  ```tsx app/chat/[chatId]/ChatPage.tsx
143
141
  "use client";
144
142
 
@@ -444,48 +442,45 @@ function Chat({ chatId, transport }) {
444
442
 
445
443
  ## Sending actions
446
444
 
447
- Send custom actions (undo, rollback, edit, regenerate) as `useChat` requests, with the action in the request `body`. The transport recognises `body.action` and sends it as an action rather than a message, and because `useChat` made the request it owns the response: an action that returns `chat.turn()` on the server streams its answer into the message list like any turn, with `status`, `error` and `stop` behaving as for a message. An action that returns nothing completes with no message added.
445
+ Send custom actions (undo, rollback, edit) to the agent via `transport.sendAction()`. Actions wake the agent and fire only `hydrateMessages` (if configured) and `onAction` — they're not turns, so `onTurnStart` / `prepareMessages` / `onBeforeTurnComplete` / `onTurnComplete` and `run()` do not fire.
448
446
 
449
- ```tsx
450
- import { useChat } from "@ai-sdk/react";
451
- import { useChatActions, useTriggerChatTransport } from "@trigger.dev/sdk/chat/react";
447
+ For optimistic UI, mirror the action's effect on the `useChat` state via `setMessages` while the request is in flight:
452
448
 
449
+ ```tsx
453
450
  function ChatControls({ chatId }: { chatId: string }) {
454
451
  const transport = useTriggerChatTransport({
455
452
  task: "my-chat",
456
453
  accessToken: ({ chatId }) => mintChatAccessToken(chatId),
457
- startSession: ({ chatId, clientData }) => startChatSession({ chatId, clientData }),
454
+ startSession: ({ chatId, clientData }) =>
455
+ startChatSession({ chatId, clientData }),
458
456
  });
459
- const { sendMessage, regenerate, setMessages } = useChat({ id: chatId, transport });
460
- const { sendAction } = useChatActions({ sendMessage });
457
+
458
+ const { setMessages } = useChat({ transport });
461
459
 
462
460
  return (
463
461
  <div>
464
462
  <button
465
463
  onClick={() => {
466
- // Mirror the server-side edit optimistically; the server does not
467
- // push history changes back.
464
+ void transport.sendAction(chatId, { type: "undo" });
468
465
  setMessages((prev) => prev.slice(0, -2));
469
- void sendAction({ type: "undo" });
470
466
  }}
471
467
  >
472
468
  Undo last exchange
473
469
  </button>
474
- {/* regenerate() removes the trailing answer locally; the server does the same before its turn */}
475
- <button onClick={() => regenerate({ body: { action: { type: "regenerate" } } })}>
476
- Regenerate
470
+ <button
471
+ onClick={() => transport.sendAction(chatId, { type: "rollback", targetMessageId: "msg-5" })}
472
+ >
473
+ Rollback to message
477
474
  </button>
478
475
  </div>
479
476
  );
480
477
  }
481
478
  ```
482
479
 
483
- `useChatActions` is a two-line convenience over `sendMessage(undefined, { body: { action } })`. Any `useChat` request can carry an action the same way, `regenerate({ body })` included.
484
-
485
- The action payload is validated against the agent's `actionSchema` on the backend; invalid actions are rejected. See [Actions](/ai-chat/actions) for the backend setup.
480
+ The action payload is validated against the agent's `actionSchema` on the backend — invalid actions are rejected. See [Actions](/ai-chat/actions) for the backend setup.
486
481
 
487
482
  <Note>
488
- `transport.sendAction()` still exists for callers outside `useChat` (server to server, or a custom client). It returns the response as a raw `ReadableStream<UIMessageChunk>` that the caller must read; `useChat` does not consume it.
483
+ `sendAction` returns a `ReadableStream<UIMessageChunk>`. For side-effect-only actions (where `onAction` returns `void`), the stream completes immediately with `trigger:turn-complete`. For actions where `onAction` returns a `StreamTextResult`, the stream carries the assistant chunks the same way `sendMessages` does — `useChat` consumes them automatically.
489
484
  </Note>
490
485
 
491
486
  For server-to-server usage, `AgentChat` has the same method:
@@ -86,7 +86,7 @@ export const myChat = chat.agent({
86
86
 
87
87
  Fires once on a continuation boot when the dead predecessor was mid-stream — a partial assistant survives on `session.out`. The runtime reconstructs context automatically via a smart default; this hook is the override path for policies that need something different.
88
88
 
89
- The hook does NOT fire when there's no partial — clean continuations after `chat.endRun()` or `chat.requestUpgrade()`, fresh chats, OOM retries on top of a complete snapshot. Those paths dispatch any in-flight user message as a normal turn on the new run without involving the hook. It fires regardless of whether [`hydrateMessages`](#hydratemessages) or a [transcript storage](/ai-chat/transcript-storage) is registered: crash recovery is runtime-owned for every agent.
89
+ The hook does NOT fire when there's no partial — clean continuations after `chat.endRun()` or `chat.requestUpgrade()`, fresh chats, OOM retries on top of a complete snapshot. Those paths dispatch any in-flight user message as a normal turn on the new run without involving the hook. It also does NOT fire when [`hydrateMessages`](#hydratemessages) is registered (the customer owns persistence).
90
90
 
91
91
  ```ts
92
92
  export const myChat = chat.agent({
@@ -260,10 +260,6 @@ export const myChat = chat.agent({
260
260
 
261
261
  ## hydrateMessages
262
262
 
263
- <Warning>
264
- `hydrateMessages` is deprecated. Give the agent a [transcript storage](/ai-chat/transcript-storage) instead: `loadContext` on the storage decides the model's context, and `save` persists every change, so crash recovery and durable compaction cover it. Existing agents keep working with a one-time warning. Setting `hydrateMessages` together with `storage` is an error.
265
- </Warning>
266
-
267
263
  Load the full message history from your backend on every turn, replacing the built-in linear accumulator. When set, the hook's return value becomes the accumulated state; the normal accumulation logic (append for submit, replace for regenerate) is skipped entirely.
268
264
 
269
265
  Use this when the backend should be the source of truth for message history: abuse prevention, branching conversations (DAGs), or rollback/undo support.
@@ -319,7 +315,7 @@ After the hook returns, the runtime overlays the wire's tool-state advances (`ou
319
315
  </Note>
320
316
 
321
317
  <Tip>
322
- Registering `hydrateMessages` turns off the runtime's transcript reads and writes: your hook is the source of truth for history, and the runtime does not read or write its [snapshot](/ai-chat/patterns/persistence-and-replay). Crash recovery still runs. When a new run boots, the dead run's unfinished answer and unacknowledged messages are replayed from the session streams, `onRecoveryBoot` fires, and the hook receives the recovered tail in `previousMessages`. Persisting that tail is yours to do; a [transcript storage](/ai-chat/transcript-storage) does it for you.
318
+ Registering `hydrateMessages` short-circuits the runtime's [snapshot + replay](/ai-chat/patterns/persistence-and-replay) reconstruction at run boot — your hook is the single source of truth for history, so the runtime skips reading or writing the snapshot entirely. No object storage traffic, no replay cost. The trade-off is that you own persistence end-to-end.
323
319
  </Tip>
324
320
 
325
321
  <Note>