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

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 +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 +275 -25
  7. package/dist/commonjs/v3/ai.js +1336 -340
  8. package/dist/commonjs/v3/ai.js.map +1 -1
  9. package/dist/commonjs/v3/chat-client.d.ts +11 -2
  10. package/dist/commonjs/v3/chat-client.js +227 -111
  11. package/dist/commonjs/v3/chat-client.js.map +1 -1
  12. package/dist/commonjs/v3/chat-react.d.ts +88 -0
  13. package/dist/commonjs/v3/chat-react.js +130 -1
  14. package/dist/commonjs/v3/chat-react.js.map +1 -1
  15. package/dist/commonjs/v3/chat-server.d.ts +42 -6
  16. package/dist/commonjs/v3/chat-server.js +52 -7
  17. package/dist/commonjs/v3/chat-server.js.map +1 -1
  18. package/dist/commonjs/v3/chat.d.ts +92 -10
  19. package/dist/commonjs/v3/chat.js +328 -73
  20. package/dist/commonjs/v3/chat.js.map +1 -1
  21. package/dist/commonjs/v3/chatSnapshotIo.d.ts +2 -0
  22. package/dist/commonjs/v3/chatSnapshotIo.js +174 -0
  23. package/dist/commonjs/v3/chatSnapshotIo.js.map +1 -0
  24. package/dist/commonjs/v3/chatVersionSkew.d.ts +12 -0
  25. package/dist/commonjs/v3/chatVersionSkew.js +30 -0
  26. package/dist/commonjs/v3/chatVersionSkew.js.map +1 -0
  27. package/dist/commonjs/v3/externalDeploymentId.d.ts +23 -0
  28. package/dist/commonjs/v3/externalDeploymentId.js +43 -0
  29. package/dist/commonjs/v3/externalDeploymentId.js.map +1 -0
  30. package/dist/commonjs/v3/sessions.d.ts +15 -2
  31. package/dist/commonjs/v3/sessions.js +12 -1
  32. package/dist/commonjs/v3/sessions.js.map +1 -1
  33. package/dist/commonjs/v3/shared.js +30 -36
  34. package/dist/commonjs/v3/shared.js.map +1 -1
  35. package/dist/commonjs/v3/test/index.d.ts +1 -0
  36. package/dist/commonjs/v3/test/index.js +3 -1
  37. package/dist/commonjs/v3/test/index.js.map +1 -1
  38. package/dist/commonjs/v3/test/mock-chat-agent.d.ts +47 -4
  39. package/dist/commonjs/v3/test/mock-chat-agent.js +116 -10
  40. package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
  41. package/dist/commonjs/v3/test/test-session-handle.js +6 -0
  42. package/dist/commonjs/v3/test/test-session-handle.js.map +1 -1
  43. package/dist/commonjs/v3/test/transcript-storage-tests.d.ts +40 -0
  44. package/dist/commonjs/v3/test/transcript-storage-tests.js +258 -0
  45. package/dist/commonjs/v3/test/transcript-storage-tests.js.map +1 -0
  46. package/dist/commonjs/v3/transcriptStorage.d.ts +224 -0
  47. package/dist/commonjs/v3/transcriptStorage.js +291 -0
  48. package/dist/commonjs/v3/transcriptStorage.js.map +1 -0
  49. package/dist/commonjs/version.js +1 -1
  50. package/dist/esm/imports/ai-runtime.d.ts +2 -2
  51. package/dist/esm/imports/ai-runtime.js +2 -2
  52. package/dist/esm/imports/ai-runtime.js.map +1 -1
  53. package/dist/esm/v3/ai-shared.d.ts +15 -0
  54. package/dist/esm/v3/ai-shared.js +33 -0
  55. package/dist/esm/v3/ai-shared.js.map +1 -1
  56. package/dist/esm/v3/ai.d.ts +275 -25
  57. package/dist/esm/v3/ai.js +1328 -337
  58. package/dist/esm/v3/ai.js.map +1 -1
  59. package/dist/esm/v3/chat-client.d.ts +11 -2
  60. package/dist/esm/v3/chat-client.js +228 -112
  61. package/dist/esm/v3/chat-client.js.map +1 -1
  62. package/dist/esm/v3/chat-react.d.ts +88 -0
  63. package/dist/esm/v3/chat-react.js +127 -1
  64. package/dist/esm/v3/chat-react.js.map +1 -1
  65. package/dist/esm/v3/chat-server.d.ts +42 -6
  66. package/dist/esm/v3/chat-server.js +53 -8
  67. package/dist/esm/v3/chat-server.js.map +1 -1
  68. package/dist/esm/v3/chat.d.ts +92 -10
  69. package/dist/esm/v3/chat.js +329 -74
  70. package/dist/esm/v3/chat.js.map +1 -1
  71. package/dist/esm/v3/chatSnapshotIo.d.ts +2 -0
  72. package/dist/esm/v3/chatSnapshotIo.js +166 -0
  73. package/dist/esm/v3/chatSnapshotIo.js.map +1 -0
  74. package/dist/esm/v3/chatVersionSkew.d.ts +12 -0
  75. package/dist/esm/v3/chatVersionSkew.js +27 -0
  76. package/dist/esm/v3/chatVersionSkew.js.map +1 -0
  77. package/dist/esm/v3/externalDeploymentId.d.ts +23 -0
  78. package/dist/esm/v3/externalDeploymentId.js +38 -0
  79. package/dist/esm/v3/externalDeploymentId.js.map +1 -0
  80. package/dist/esm/v3/sessions.d.ts +15 -2
  81. package/dist/esm/v3/sessions.js +11 -1
  82. package/dist/esm/v3/sessions.js.map +1 -1
  83. package/dist/esm/v3/shared.js +17 -23
  84. package/dist/esm/v3/shared.js.map +1 -1
  85. package/dist/esm/v3/test/index.d.ts +1 -0
  86. package/dist/esm/v3/test/index.js +1 -0
  87. package/dist/esm/v3/test/index.js.map +1 -1
  88. package/dist/esm/v3/test/mock-chat-agent.d.ts +47 -4
  89. package/dist/esm/v3/test/mock-chat-agent.js +118 -12
  90. package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
  91. package/dist/esm/v3/test/test-session-handle.js +6 -0
  92. package/dist/esm/v3/test/test-session-handle.js.map +1 -1
  93. package/dist/esm/v3/test/transcript-storage-tests.d.ts +40 -0
  94. package/dist/esm/v3/test/transcript-storage-tests.js +255 -0
  95. package/dist/esm/v3/test/transcript-storage-tests.js.map +1 -0
  96. package/dist/esm/v3/transcriptStorage.d.ts +224 -0
  97. package/dist/esm/v3/transcriptStorage.js +280 -0
  98. package/dist/esm/v3/transcriptStorage.js.map +1 -0
  99. package/dist/esm/version.js +1 -1
  100. package/docs/ai-chat/actions.mdx +55 -23
  101. package/docs/ai-chat/anatomy.mdx +3 -3
  102. package/docs/ai-chat/backend.mdx +125 -48
  103. package/docs/ai-chat/background-injection.mdx +69 -19
  104. package/docs/ai-chat/client-protocol.mdx +5 -4
  105. package/docs/ai-chat/compaction.mdx +13 -7
  106. package/docs/ai-chat/custom-agents.mdx +23 -0
  107. package/docs/ai-chat/fast-starts.mdx +27 -20
  108. package/docs/ai-chat/frontend.mdx +19 -14
  109. package/docs/ai-chat/lifecycle-hooks.mdx +6 -2
  110. package/docs/ai-chat/migrating-from-a-route-handler.mdx +16 -14
  111. package/docs/ai-chat/patterns/database-persistence.mdx +6 -2
  112. package/docs/ai-chat/patterns/native-compaction.mdx +310 -0
  113. package/docs/ai-chat/patterns/persistence-and-replay.mdx +34 -23
  114. package/docs/ai-chat/patterns/skills.mdx +7 -10
  115. package/docs/ai-chat/patterns/version-upgrades.mdx +79 -6
  116. package/docs/ai-chat/pending-messages.mdx +3 -3
  117. package/docs/ai-chat/prompt-caching.mdx +23 -25
  118. package/docs/ai-chat/quick-start.mdx +11 -11
  119. package/docs/ai-chat/reference.mdx +67 -9
  120. package/docs/ai-chat/sessions.mdx +6 -1
  121. package/docs/ai-chat/testing.mdx +2 -1
  122. package/docs/ai-chat/tools.mdx +18 -13
  123. package/docs/ai-chat/transcript-storage.mdx +233 -0
  124. package/docs/ai-chat/upgrade-guide.mdx +2 -2
  125. package/docs/apikeys.mdx +27 -45
  126. package/docs/config/extensions/syncEnvVars.mdx +6 -0
  127. package/docs/deploy-environment-variables.mdx +23 -2
  128. package/docs/deployment/overview.mdx +4 -8
  129. package/docs/deployment/preview-branches.mdx +4 -4
  130. package/docs/deployment/version-skew-protection.mdx +62 -0
  131. package/docs/manual-setup.mdx +7 -7
  132. package/docs/mcp-tools.mdx +9 -0
  133. package/docs/quick-start.mdx +3 -3
  134. package/docs/realtime/auth.mdx +1 -1
  135. package/docs/reports.mdx +157 -0
  136. package/docs/self-hosting/security.mdx +5 -0
  137. package/docs/tasks/scheduled.mdx +24 -0
  138. package/docs/triggering.mdx +1 -1
  139. package/docs/troubleshooting-zod.mdx +158 -0
  140. package/package.json +4 -4
  141. package/skills/trigger-authoring-chat-agent/SKILL.md +41 -29
  142. package/skills/trigger-chat-agent-advanced/SKILL.md +31 -12
@@ -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:
@@ -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
 
@@ -9,6 +9,10 @@ Durable chat runs can span **hours** and **many turns**. You usually want:
9
9
  1. **Conversation state** — full **`UIMessage[]`** (or equivalent) keyed by **`chatId`**, so reloads and history views work.
10
10
  2. **Live session state** — a **scoped access token** for the session and optionally **`lastEventId`** for stream resume.
11
11
 
12
+ <Note>
13
+ You can persist the conversation state through a [transcript storage](/ai-chat/transcript-storage) on the agent rather than the hook mapping below. Give `chat.agent` a `storage` that writes to your database and the runtime hands it every change (a new message, an undo, a regenerate, a compaction) as it happens, with the resume cursors, and reads it back when a run continues. Your own database is then the transcript. The hook mapping below still works, and it is the way to persist the live session state (the access token) and anything the transcript does not cover.
14
+ </Note>
15
+
12
16
  This page describes a **hook mapping** that works with any database. Adapt table and column names to your stack.
13
17
 
14
18
  ## Conceptual data model
@@ -167,9 +171,9 @@ chat.agent({
167
171
  });
168
172
  ```
169
173
 
170
- ## Alternative: `hydrateMessages`
174
+ ## Alternative: `hydrateMessages` (deprecated)
171
175
 
172
- For apps that need the backend to be the single source of truth for message history — abuse prevention, branching conversations, or rollback support — use [`hydrateMessages`](/ai-chat/lifecycle-hooks#hydratemessages) instead of relying on the frontend's accumulated state.
176
+ For apps that need the backend to be the single source of truth for message history — abuse prevention, branching conversations, or rollback support — the recommended path is a [transcript storage](/ai-chat/transcript-storage#owning-the-models-context) with `loadContext`. The deprecated [`hydrateMessages`](/ai-chat/lifecycle-hooks#hydratemessages) hook does the same job without the runtime writing to your store.
173
177
 
174
178
  With hydration, the hook loads messages from your database on every turn. The frontend's messages are ignored (except for the new user message, which arrives in `incomingMessages`):
175
179