@trigger.dev/sdk 4.5.16 → 4.6.0

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 (156) hide show
  1. package/dist/commonjs/imports/ai-runtime-cjs.cjs.map +1 -1
  2. package/dist/commonjs/imports/ai-runtime.js +2 -0
  3. package/dist/commonjs/v3/ai-shared.d.ts +15 -0
  4. package/dist/commonjs/v3/ai-shared.js +35 -1
  5. package/dist/commonjs/v3/ai-shared.js.map +1 -1
  6. package/dist/commonjs/v3/ai.d.ts +311 -25
  7. package/dist/commonjs/v3/ai.js +1633 -380
  8. package/dist/commonjs/v3/ai.js.map +1 -1
  9. package/dist/commonjs/v3/auth.d.ts +10 -4
  10. package/dist/commonjs/v3/auth.js.map +1 -1
  11. package/dist/commonjs/v3/chat-client.d.ts +11 -2
  12. package/dist/commonjs/v3/chat-client.js +227 -111
  13. package/dist/commonjs/v3/chat-client.js.map +1 -1
  14. package/dist/commonjs/v3/chat-react.d.ts +88 -0
  15. package/dist/commonjs/v3/chat-react.js +130 -1
  16. package/dist/commonjs/v3/chat-react.js.map +1 -1
  17. package/dist/commonjs/v3/chat-server.d.ts +42 -6
  18. package/dist/commonjs/v3/chat-server.js +52 -7
  19. package/dist/commonjs/v3/chat-server.js.map +1 -1
  20. package/dist/commonjs/v3/chat.d.ts +92 -10
  21. package/dist/commonjs/v3/chat.js +328 -73
  22. package/dist/commonjs/v3/chat.js.map +1 -1
  23. package/dist/commonjs/v3/chatSnapshotIo.d.ts +2 -0
  24. package/dist/commonjs/v3/chatSnapshotIo.js +173 -0
  25. package/dist/commonjs/v3/chatSnapshotIo.js.map +1 -0
  26. package/dist/commonjs/v3/chatVersionSkew.d.ts +12 -0
  27. package/dist/commonjs/v3/chatVersionSkew.js +30 -0
  28. package/dist/commonjs/v3/chatVersionSkew.js.map +1 -0
  29. package/dist/commonjs/v3/externalDeploymentId.d.ts +23 -0
  30. package/dist/commonjs/v3/externalDeploymentId.js +43 -0
  31. package/dist/commonjs/v3/externalDeploymentId.js.map +1 -0
  32. package/dist/commonjs/v3/retry.d.ts +4 -0
  33. package/dist/commonjs/v3/retry.js +22 -13
  34. package/dist/commonjs/v3/retry.js.map +1 -1
  35. package/dist/commonjs/v3/sessions.d.ts +15 -2
  36. package/dist/commonjs/v3/sessions.js +12 -1
  37. package/dist/commonjs/v3/sessions.js.map +1 -1
  38. package/dist/commonjs/v3/shared.js +30 -36
  39. package/dist/commonjs/v3/shared.js.map +1 -1
  40. package/dist/commonjs/v3/test/index.d.ts +1 -0
  41. package/dist/commonjs/v3/test/index.js +3 -1
  42. package/dist/commonjs/v3/test/index.js.map +1 -1
  43. package/dist/commonjs/v3/test/mock-chat-agent.d.ts +47 -4
  44. package/dist/commonjs/v3/test/mock-chat-agent.js +116 -10
  45. package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
  46. package/dist/commonjs/v3/test/test-session-handle.js +6 -0
  47. package/dist/commonjs/v3/test/test-session-handle.js.map +1 -1
  48. package/dist/commonjs/v3/test/transcript-storage-tests.d.ts +40 -0
  49. package/dist/commonjs/v3/test/transcript-storage-tests.js +258 -0
  50. package/dist/commonjs/v3/test/transcript-storage-tests.js.map +1 -0
  51. package/dist/commonjs/v3/transcriptStorage.d.ts +261 -0
  52. package/dist/commonjs/v3/transcriptStorage.js +374 -0
  53. package/dist/commonjs/v3/transcriptStorage.js.map +1 -0
  54. package/dist/commonjs/version.js +1 -1
  55. package/dist/esm/imports/ai-runtime.d.ts +2 -2
  56. package/dist/esm/imports/ai-runtime.js +2 -2
  57. package/dist/esm/imports/ai-runtime.js.map +1 -1
  58. package/dist/esm/v3/ai-shared.d.ts +15 -0
  59. package/dist/esm/v3/ai-shared.js +33 -0
  60. package/dist/esm/v3/ai-shared.js.map +1 -1
  61. package/dist/esm/v3/ai.d.ts +311 -25
  62. package/dist/esm/v3/ai.js +1625 -377
  63. package/dist/esm/v3/ai.js.map +1 -1
  64. package/dist/esm/v3/auth.d.ts +10 -4
  65. package/dist/esm/v3/auth.js.map +1 -1
  66. package/dist/esm/v3/chat-client.d.ts +11 -2
  67. package/dist/esm/v3/chat-client.js +228 -112
  68. package/dist/esm/v3/chat-client.js.map +1 -1
  69. package/dist/esm/v3/chat-react.d.ts +88 -0
  70. package/dist/esm/v3/chat-react.js +127 -1
  71. package/dist/esm/v3/chat-react.js.map +1 -1
  72. package/dist/esm/v3/chat-server.d.ts +42 -6
  73. package/dist/esm/v3/chat-server.js +53 -8
  74. package/dist/esm/v3/chat-server.js.map +1 -1
  75. package/dist/esm/v3/chat.d.ts +92 -10
  76. package/dist/esm/v3/chat.js +329 -74
  77. package/dist/esm/v3/chat.js.map +1 -1
  78. package/dist/esm/v3/chatSnapshotIo.d.ts +2 -0
  79. package/dist/esm/v3/chatSnapshotIo.js +165 -0
  80. package/dist/esm/v3/chatSnapshotIo.js.map +1 -0
  81. package/dist/esm/v3/chatVersionSkew.d.ts +12 -0
  82. package/dist/esm/v3/chatVersionSkew.js +27 -0
  83. package/dist/esm/v3/chatVersionSkew.js.map +1 -0
  84. package/dist/esm/v3/externalDeploymentId.d.ts +23 -0
  85. package/dist/esm/v3/externalDeploymentId.js +38 -0
  86. package/dist/esm/v3/externalDeploymentId.js.map +1 -0
  87. package/dist/esm/v3/retry.d.ts +4 -0
  88. package/dist/esm/v3/retry.js +14 -8
  89. package/dist/esm/v3/retry.js.map +1 -1
  90. package/dist/esm/v3/sessions.d.ts +15 -2
  91. package/dist/esm/v3/sessions.js +11 -1
  92. package/dist/esm/v3/sessions.js.map +1 -1
  93. package/dist/esm/v3/shared.js +17 -23
  94. package/dist/esm/v3/shared.js.map +1 -1
  95. package/dist/esm/v3/test/index.d.ts +1 -0
  96. package/dist/esm/v3/test/index.js +1 -0
  97. package/dist/esm/v3/test/index.js.map +1 -1
  98. package/dist/esm/v3/test/mock-chat-agent.d.ts +47 -4
  99. package/dist/esm/v3/test/mock-chat-agent.js +118 -12
  100. package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
  101. package/dist/esm/v3/test/test-session-handle.js +6 -0
  102. package/dist/esm/v3/test/test-session-handle.js.map +1 -1
  103. package/dist/esm/v3/test/transcript-storage-tests.d.ts +40 -0
  104. package/dist/esm/v3/test/transcript-storage-tests.js +255 -0
  105. package/dist/esm/v3/test/transcript-storage-tests.js.map +1 -0
  106. package/dist/esm/v3/transcriptStorage.d.ts +261 -0
  107. package/dist/esm/v3/transcriptStorage.js +360 -0
  108. package/dist/esm/v3/transcriptStorage.js.map +1 -0
  109. package/dist/esm/version.js +1 -1
  110. package/docs/ai-chat/actions.mdx +55 -23
  111. package/docs/ai-chat/anatomy.mdx +47 -27
  112. package/docs/ai-chat/backend.mdx +125 -48
  113. package/docs/ai-chat/background-injection.mdx +122 -19
  114. package/docs/ai-chat/client-protocol.mdx +5 -4
  115. package/docs/ai-chat/compaction.mdx +13 -7
  116. package/docs/ai-chat/custom-agents.mdx +23 -0
  117. package/docs/ai-chat/fast-starts.mdx +27 -20
  118. package/docs/ai-chat/frontend.mdx +19 -14
  119. package/docs/ai-chat/how-it-works.mdx +1 -1
  120. package/docs/ai-chat/lifecycle-hooks.mdx +6 -2
  121. package/docs/ai-chat/migrating-from-a-route-handler.mdx +16 -14
  122. package/docs/ai-chat/migrating-from-hydrate-messages.mdx +299 -0
  123. package/docs/ai-chat/patterns/database-persistence.mdx +24 -9
  124. package/docs/ai-chat/patterns/native-compaction.mdx +310 -0
  125. package/docs/ai-chat/patterns/persistence-and-replay.mdx +34 -23
  126. package/docs/ai-chat/patterns/skills.mdx +7 -10
  127. package/docs/ai-chat/patterns/version-upgrades.mdx +79 -6
  128. package/docs/ai-chat/pending-messages.mdx +3 -3
  129. package/docs/ai-chat/prompt-caching.mdx +23 -25
  130. package/docs/ai-chat/quick-start.mdx +11 -11
  131. package/docs/ai-chat/reference.mdx +69 -10
  132. package/docs/ai-chat/sessions.mdx +6 -1
  133. package/docs/ai-chat/side-channels.mdx +1 -1
  134. package/docs/ai-chat/testing.mdx +2 -1
  135. package/docs/ai-chat/tools.mdx +18 -13
  136. package/docs/ai-chat/transcript-storage.mdx +247 -0
  137. package/docs/ai-chat/upgrade-guide.mdx +2 -2
  138. package/docs/apikeys.mdx +27 -45
  139. package/docs/config/extensions/syncEnvVars.mdx +6 -0
  140. package/docs/deploy-environment-variables.mdx +23 -2
  141. package/docs/deployment/overview.mdx +4 -8
  142. package/docs/deployment/preview-branches.mdx +4 -4
  143. package/docs/deployment/version-skew-protection.mdx +62 -0
  144. package/docs/introduction.mdx +58 -152
  145. package/docs/manual-setup.mdx +7 -7
  146. package/docs/mcp-tools.mdx +9 -0
  147. package/docs/quick-start.mdx +3 -3
  148. package/docs/realtime/auth.mdx +1 -1
  149. package/docs/reports.mdx +157 -0
  150. package/docs/self-hosting/security.mdx +17 -0
  151. package/docs/tasks/scheduled.mdx +24 -0
  152. package/docs/triggering.mdx +1 -1
  153. package/docs/troubleshooting-zod.mdx +158 -0
  154. package/package.json +4 -4
  155. package/skills/trigger-authoring-chat-agent/SKILL.md +41 -29
  156. package/skills/trigger-chat-agent-advanced/SKILL.md +54 -19
@@ -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_..." # secret API key for the env
57
+ SECRET_KEY="tr_dev_sk_..." # 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,6 +210,7 @@ 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. |
213
214
  | `triggerConfig.region` | `string` | Region preference. |
214
215
  | `triggerConfig.idleTimeoutInSeconds` | `number` | Surfaced to the agent through the wire payload (1–3600). |
215
216
 
@@ -266,7 +267,7 @@ x-trigger-jwt-claims: {"sub":"...","scopes":["read:runs:run_abc123","write:input
266
267
  Re-calling `POST /api/v1/sessions` with the same `(taskIdentifier, externalId)` pair is **idempotent for the lifetime of the session**:
267
268
 
268
269
  - 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).)
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.
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.
270
271
  - 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.
271
272
 
272
273
  <Warning>
@@ -693,7 +694,7 @@ The body is a JSON-serialized [`ChatInputChunk`](#chatinputchunk), a tagged unio
693
694
  | --- | --- |
694
695
  | `401` | Missing or invalid `Authorization` header. |
695
696
  | `403` | Token doesn't carry `write:sessions:{externalId}`. |
696
- | `409` | The session is closed `{ "ok": false, "error": "Cannot append to a closed session" }`. |
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`. |
697
698
  | `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. |
698
699
  | `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). |
699
700
 
@@ -832,7 +833,7 @@ Custom actions (undo, rollback, edit) ride on the same `.in` channel using `kind
832
833
  }
833
834
  ```
834
835
 
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.
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.
836
837
 
837
838
  Raw `chat.customAgent()` tasks receive `action` as `unknown` and must validate it in their own loop.
838
839
 
@@ -19,11 +19,12 @@ 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 { streamText, generateText, stepCountIs } from "ai";
22
+ import { 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,
27
28
  compaction: {
28
29
  shouldCompact: ({ totalTokens }) => (totalTokens ?? 0) > 80_000,
29
30
  summarize: async ({ messages }) => {
@@ -34,9 +35,8 @@ export const myChat = chat.agent({
34
35
  return result.text;
35
36
  },
36
37
  },
37
- run: async ({ messages, signal }) => {
38
+ run: async ({ messages, signal, streamText }) => {
38
39
  return streamText({
39
- ...chat.toStreamTextOptions({ registry }),
40
40
  messages,
41
41
  abortSignal: signal,
42
42
  stopWhen: stepCountIs(15),
@@ -61,6 +61,12 @@ 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
+
64
70
  ## Customizing what gets persisted
65
71
 
66
72
  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`:
@@ -91,7 +97,7 @@ export const myChat = chat.agent({
91
97
  ...uiMessages.slice(-4), // Keep the last 4 messages
92
98
  ],
93
99
  },
94
- run: async ({ messages, signal }) => {
100
+ run: async ({ messages, signal, streamText }) => {
95
101
  return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
96
102
  },
97
103
  });
@@ -185,7 +191,7 @@ export const myChat = chat.agent({
185
191
  data: { chatId, summary, totalTokens, messageCount },
186
192
  });
187
193
  },
188
- run: async ({ messages, signal }) => {
194
+ run: async ({ messages, signal, streamText }) => {
189
195
  return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
190
196
  },
191
197
  });
@@ -201,7 +207,7 @@ Define a `compact` action that reuses your existing `summarize` function:
201
207
 
202
208
  ```ts
203
209
  import { chat } from "@trigger.dev/sdk/ai";
204
- import { streamText, generateText, generateId, convertToModelMessages } from "ai";
210
+ import { generateText, generateId, convertToModelMessages } from "ai";
205
211
  import { anthropic } from "@ai-sdk/anthropic";
206
212
  import { z } from "zod";
207
213
 
@@ -243,7 +249,7 @@ export const myChat = chat.agent({
243
249
  ]);
244
250
  },
245
251
 
246
- run: async ({ messages, signal }) => {
252
+ run: async ({ messages, signal, streamText }) => {
247
253
  return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
248
254
  },
249
255
  });
@@ -290,6 +290,29 @@ 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
+
293
316
  ## Hand-rolled loop with primitives
294
317
 
295
318
  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 }) =>
242
+ run: async ({ messages, signal, streamText }) =>
243
243
  streamText({
244
- ...chat.toStreamTextOptions({ tools: chatTools }),
245
244
  model: anthropic("claude-sonnet-4-6"),
246
245
  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>`. 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.
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.
255
255
 
256
256
  ```ts lib/chat-handler.ts
257
257
  import { chat } from "@trigger.dev/sdk/chat-server";
@@ -261,18 +261,23 @@ 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 ({ chat: helper }) =>
264
+ run: async ({ streamText }) =>
265
265
  streamText({
266
- ...helper.toStreamTextOptions({ tools: headStartTools }),
267
266
  model: anthropic("claude-sonnet-4-6"),
268
267
  system: "You are a helpful assistant.",
268
+ tools: headStartTools,
269
269
  }),
270
270
  });
271
271
  ```
272
272
 
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>
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>
276
281
 
277
282
  <Tip>
278
283
  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.
@@ -625,27 +630,27 @@ chat.headStart<TTools>({
625
630
  export const chatHandler = chat.headStart({
626
631
  agentId: "my-chat",
627
632
  triggerConfig: { tags: ["org:acme"], queue: "chat", machine: "small-2x" },
628
- run: async ({ chat: helper }) =>
629
- streamText({ ...helper.toStreamTextOptions({ tools: headStartTools }), model, system }),
633
+ run: async ({ streamText }) => streamText({ model, system, tools: headStartTools }),
630
634
  });
631
635
  ```
632
636
 
633
637
  The `run` callback receives:
634
638
 
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.
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.
638
643
 
639
- `chat.toStreamTextOptions({ tools })` returns options to spread into `streamText`. The SDK owns these keys overriding them will break the protocol:
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:
640
645
 
641
646
  | Key | What the SDK sets | Why |
642
647
  | --- | --- | --- |
643
648
  | `messages` | `convertToModelMessages(uiMessages)` | First-turn user history |
644
- | `tools` | What you pass | Schema-only tools for step 1 |
645
- | `stopWhen` | `stepCountIs(1)` | Step 1 only agent picks up step 2+ |
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 |
646
651
  | `abortSignal` | Combined request + idle timeout | Safe cleanup on disconnect |
647
652
 
648
- You bring `model`, `system`, `providerOptions`, `prepareStep`, anything else `streamText` accepts.
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.
649
654
 
650
655
  #### The transport option
651
656
 
@@ -684,11 +689,11 @@ export async function POST(req: Request) {
684
689
  agentId: "my-chat",
685
690
  chatId, // session externalId; reuse it on the destination page
686
691
  messages, // first-turn user history
687
- run: async ({ chat: helper }) =>
692
+ run: async ({ streamText }) =>
688
693
  streamText({
689
- ...helper.toStreamTextOptions({ tools: headStartTools }),
690
694
  model: anthropic("claude-sonnet-4-6"),
691
695
  system: "You are a helpful assistant.",
696
+ tools: headStartTools,
692
697
  }),
693
698
  });
694
699
 
@@ -735,11 +740,13 @@ chat.startHeadStart<TTools>({
735
740
  triggerConfig?: Partial<SessionTriggerConfig>, // tags, queue, machine, …
736
741
  apiClient?: ApiClientConfiguration, // when the agent lives in another project/env
737
742
  metadata?: Record<string, unknown>, // merged into the run payload; never sent to the browser
738
- }): Promise<{ chatId: string; completion: Promise<void> }>
743
+ }): Promise<{ chatId: string; pendingVersion: boolean; completion: Promise<void> }>
739
744
  ```
740
745
 
741
746
  `completion` resolves once the head start finishes; `await` it or hand it to `waitUntil`. It rejects if the warm step or the dispatch fails.
742
747
 
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
+
743
750
  ### Limitations
744
751
 
745
752
  - **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,6 +137,8 @@ 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
+
140
142
  ```tsx app/chat/[chatId]/ChatPage.tsx
141
143
  "use client";
142
144
 
@@ -442,45 +444,48 @@ function Chat({ chatId, transport }) {
442
444
 
443
445
  ## Sending actions
444
446
 
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.
446
-
447
- For optimistic UI, mirror the action's effect on the `useChat` state via `setMessages` while the request is in flight:
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.
448
448
 
449
449
  ```tsx
450
+ import { useChat } from "@ai-sdk/react";
451
+ import { useChatActions, useTriggerChatTransport } from "@trigger.dev/sdk/chat/react";
452
+
450
453
  function ChatControls({ chatId }: { chatId: string }) {
451
454
  const transport = useTriggerChatTransport({
452
455
  task: "my-chat",
453
456
  accessToken: ({ chatId }) => mintChatAccessToken(chatId),
454
- startSession: ({ chatId, clientData }) =>
455
- startChatSession({ chatId, clientData }),
457
+ startSession: ({ chatId, clientData }) => startChatSession({ chatId, clientData }),
456
458
  });
457
-
458
- const { setMessages } = useChat({ transport });
459
+ const { sendMessage, regenerate, setMessages } = useChat({ id: chatId, transport });
460
+ const { sendAction } = useChatActions({ sendMessage });
459
461
 
460
462
  return (
461
463
  <div>
462
464
  <button
463
465
  onClick={() => {
464
- void transport.sendAction(chatId, { type: "undo" });
466
+ // Mirror the server-side edit optimistically; the server does not
467
+ // push history changes back.
465
468
  setMessages((prev) => prev.slice(0, -2));
469
+ void sendAction({ type: "undo" });
466
470
  }}
467
471
  >
468
472
  Undo last exchange
469
473
  </button>
470
- <button
471
- onClick={() => transport.sendAction(chatId, { type: "rollback", targetMessageId: "msg-5" })}
472
- >
473
- Rollback to message
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
474
477
  </button>
475
478
  </div>
476
479
  );
477
480
  }
478
481
  ```
479
482
 
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.
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.
481
486
 
482
487
  <Note>
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.
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.
484
489
  </Note>
485
490
 
486
491
  For server-to-server usage, `AgentChat` has the same method:
@@ -90,7 +90,7 @@ Here is a typical cold turn — user opens the page, types "What's the weather?"
90
90
  Immediately after writing the new turn-complete marker, the agent issues an S2 trim command targeting the *previous* turn-complete's sequence number. This bounds the stream's storage to roughly one turn of chunks plus the latest control record.
91
91
  </Step>
92
92
  <Step title="Task: fires onTurnComplete, writes snapshot to S3">
93
- `onTurnComplete` runs (your hook for persistence). Then the agent writes `ChatSnapshotV1` `{ version: 1, messages, lastOutEventId, lastOutTimestamp }` to S3 at `sessions/:chatId/snapshot.json`. This write is awaited, not fire-and-forget, so the next run is guaranteed to find it.
93
+ `onTurnComplete` runs (your hook for persistence). Then the agent writes the transcript to S3 at `sessions/:chatId/snapshot.json`: a header line carrying the stream cursors and the runtime's own state, one line per message, and an index of the messages at the end so a page of history can be read without downloading the whole conversation. Once [compaction](/ai-chat/compaction) has run, only the most recent messages are kept, so the file stops growing with the conversation. If you need the full history, keep your own [transcript storage](/ai-chat/transcript-storage). This write is awaited, not fire-and-forget, so the next run is guaranteed to find it.
94
94
  </Step>
95
95
  <Step title="Task: goes idle, then suspends">
96
96
  The agent re-enters the waitpoint on `.in`. After `idleTimeoutInSeconds` of nothing arriving, `onChatSuspend` fires and the engine snapshots the run. Compute is freed.
@@ -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 also does NOT fire when [`hydrateMessages`](#hydratemessages) is registered (the customer owns persistence).
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.
90
90
 
91
91
  ```ts
92
92
  export const myChat = chat.agent({
@@ -260,6 +260,10 @@ 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
+
263
267
  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.
264
268
 
265
269
  Use this when the backend should be the source of truth for message history: abuse prevention, branching conversations (DAGs), or rollback/undo support.
@@ -315,7 +319,7 @@ After the hook returns, the runtime overlays the wire's tool-state advances (`ou
315
319
  </Note>
316
320
 
317
321
  <Tip>
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.
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.
319
323
  </Tip>
320
324
 
321
325
  <Note>
@@ -56,7 +56,7 @@ Then make these changes:
56
56
  - Create a `chat.agent` task in `trigger/chat.ts`. Move the existing `streamText` call
57
57
  into its `run` function UNCHANGED — same model, same `system`, same `temperature`,
58
58
  same `stopWhen`, same provider options. Do not rewrite the prompt or swap the model.
59
- - Spread `...chat.toStreamTextOptions({ tools })` as the FIRST property of that
59
+ - Take `streamText` from the `run` argument, so the managed options apply to that
60
60
  `streamText` call, so the explicit options after it still win.
61
61
  - `run` receives `ModelMessage[]` already. Delete the `convertToModelMessages` call.
62
62
  - Forward the `signal` from `run` as `abortSignal` so Stop works.
@@ -145,10 +145,9 @@ import { tools } from "@/lib/tools";
145
145
  export const myChat = chat.agent({
146
146
  id: "my-chat",
147
147
  tools,
148
- run: async ({ messages, tools, signal }) =>
148
+ run: async ({ messages, tools, signal, streamText }) =>
149
149
  streamText({
150
- // Spread first, so every option below still wins.
151
- ...chat.toStreamTextOptions({ tools }),
150
+ tools,
152
151
  model: anthropic("claude-sonnet-4-5"),
153
152
  system: "You are a helpful assistant.",
154
153
  messages,
@@ -163,10 +162,10 @@ Four things changed inside the `streamText` call, and `tools` moved onto the age
163
162
  - **`messages` arrives as `ModelMessage[]`.** The runtime converts the frontend's `UIMessage[]` for you, so `convertToModelMessages` is gone.
164
163
  - **`abortSignal` comes from `signal` on the payload**, not `req.signal`. It fires on stop and on cancel.
165
164
  - **Return the `StreamTextResult`.** It's piped to the frontend automatically — no `toUIMessageStreamResponse`. If `streamText` is buried in a helper, call `await chat.pipe(result)` from anywhere in the task instead and let `run` resolve `void`.
166
- - **`...chat.toStreamTextOptions()` is spread first.** It wires up the `prepareStep` callback behind [compaction](/ai-chat/compaction), [mid-turn steering](/ai-chat/pending-messages), and [background injection](/ai-chat/background-injection), plus the system prompt set via [`chat.prompt()`](/ai-chat/backend#using-prompts) and telemetry.
165
+ - **`streamText` comes from the `run` argument, not from `ai`.** It carries the `prepareStep` behind [compaction](/ai-chat/compaction), [mid-turn steering](/ai-chat/pending-messages) and [background injection](/ai-chat/background-injection), plus the system prompt set via [`chat.prompt()`](/ai-chat/backend#using-prompts) and telemetry.
167
166
 
168
167
  <Warning>
169
- Omitting `...chat.toStreamTextOptions()` throws no error compaction, steering, and background injection just silently never run. Spread it first so any explicit override you write after it takes precedence.
168
+ Importing `streamText` from `ai` instead throws no error: compaction, steering and background injection never run. Spreading `chat.toStreamTextOptions()` into the imported one is the equivalent, and is what a custom agent has to do, since it has no `run` argument. A `chat.headStart` route gets a bound `streamText` of its own.
170
169
  </Warning>
171
170
 
172
171
  There's no `maxDuration` equivalent to set. A turn isn't bounded by a function timeout; a run stays alive across turns and suspends when nothing is happening.
@@ -357,9 +356,9 @@ export const myChat = chat.agent({
357
356
  }),
358
357
  ]);
359
358
  },
360
- run: async ({ messages, tools, signal }) =>
359
+ run: async ({ messages, tools, signal, streamText }) =>
361
360
  streamText({
362
- ...chat.toStreamTextOptions({ tools }),
361
+ tools,
363
362
  model: anthropic("claude-sonnet-4-5"),
364
363
  system: "You are a helpful assistant.",
365
364
  messages,
@@ -468,18 +467,21 @@ Head Start brings the route handler back for exactly that first turn. It runs st
468
467
 
469
468
  export const chatHandler = chat.headStart({
470
469
  agentId: "my-chat",
471
- run: async ({ chat: helper }) =>
470
+ run: async ({ streamText }) =>
472
471
  streamText({
473
- ...helper.toStreamTextOptions({ tools: headStartTools }),
474
472
  model: anthropic("claude-sonnet-4-5"),
475
473
  system: "You are a helpful assistant.",
474
+ tools: headStartTools,
476
475
  }),
477
476
  });
478
477
  ```
479
478
 
480
- <Warning>
481
- Spread `toStreamTextOptions()` first and add only your own keys after it. It owns `messages`, `tools`, `abortSignal`, and `stopWhen` and unlike the agent-side spread, re-setting any of those breaks the handover rather than degrading it. `stopWhen` in particular is pinned to `stepCountIs(1)`: the agent, not the handler, runs step 2 onward.
482
- </Warning>
479
+ <Note>
480
+ That `streamText` owns `messages`, `prompt`, `abortSignal` and `stopWhen`. Passing one is
481
+ a type error, and a runtime throw if you get past the types. Unlike the agent side, re-setting one breaks the
482
+ handover rather than degrading it: `stopWhen` is pinned to `stepCountIs(1)`
483
+ because the agent, not the handler, runs step 2 onward.
484
+ </Note>
483
485
 
484
486
  Your provider keys never leave your server — the first-turn model call runs in your process, so that environment needs whatever the model requires.
485
487
  </Step>
@@ -558,7 +560,7 @@ The shape is identical outside Next.js. The agent task and the React component d
558
560
 
559
561
  **The head-start route dies mid-turn on Vercel.** The handler holds the SSE response open until the agent signals turn-complete, so the function timeout has to cover the whole turn, not just step 1. Set `maxDuration` on that route segment.
560
562
 
561
- **Compaction and steering do nothing.** The `...chat.toStreamTextOptions()` spread is missing, or something before it in the object is overwriting `prepareStep`. Spread it as the first property.
563
+ **Compaction and steering do nothing.** `run` is calling the `streamText` imported from `ai` rather than the one in its argument. Destructure `streamText` from the `run` argument, or spread `chat.toStreamTextOptions()` into the imported one.
562
564
 
563
565
  **`toModelOutput` works on the first turn, then stops.** Tools are declared only on `streamText`. Declare the same set on `chat.agent({ tools })` too, and read it back off the `run` payload.
564
566