@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
@@ -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
- - Take `streamText` from the `run` argument, so the managed options apply to that
59
+ - Spread `...chat.toStreamTextOptions({ tools })` as the FIRST property of 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,9 +145,10 @@ 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, streamText }) =>
148
+ run: async ({ messages, tools, signal }) =>
149
149
  streamText({
150
- tools,
150
+ // Spread first, so every option below still wins.
151
+ ...chat.toStreamTextOptions({ tools }),
151
152
  model: anthropic("claude-sonnet-4-5"),
152
153
  system: "You are a helpful assistant.",
153
154
  messages,
@@ -162,10 +163,10 @@ Four things changed inside the `streamText` call, and `tools` moved onto the age
162
163
  - **`messages` arrives as `ModelMessage[]`.** The runtime converts the frontend's `UIMessage[]` for you, so `convertToModelMessages` is gone.
163
164
  - **`abortSignal` comes from `signal` on the payload**, not `req.signal`. It fires on stop and on cancel.
164
165
  - **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`.
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.
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.
166
167
 
167
168
  <Warning>
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.
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.
169
170
  </Warning>
170
171
 
171
172
  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.
@@ -356,9 +357,9 @@ export const myChat = chat.agent({
356
357
  }),
357
358
  ]);
358
359
  },
359
- run: async ({ messages, tools, signal, streamText }) =>
360
+ run: async ({ messages, tools, signal }) =>
360
361
  streamText({
361
- tools,
362
+ ...chat.toStreamTextOptions({ tools }),
362
363
  model: anthropic("claude-sonnet-4-5"),
363
364
  system: "You are a helpful assistant.",
364
365
  messages,
@@ -467,21 +468,18 @@ Head Start brings the route handler back for exactly that first turn. It runs st
467
468
 
468
469
  export const chatHandler = chat.headStart({
469
470
  agentId: "my-chat",
470
- run: async ({ streamText }) =>
471
+ run: async ({ chat: helper }) =>
471
472
  streamText({
473
+ ...helper.toStreamTextOptions({ tools: headStartTools }),
472
474
  model: anthropic("claude-sonnet-4-5"),
473
475
  system: "You are a helpful assistant.",
474
- tools: headStartTools,
475
476
  }),
476
477
  });
477
478
  ```
478
479
 
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>
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>
485
483
 
486
484
  Your provider keys never leave your server — the first-turn model call runs in your process, so that environment needs whatever the model requires.
487
485
  </Step>
@@ -560,7 +558,7 @@ The shape is identical outside Next.js. The agent task and the React component d
560
558
 
561
559
  **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.
562
560
 
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.
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.
564
562
 
565
563
  **`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.
566
564
 
@@ -9,10 +9,6 @@ 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
-
16
12
  This page describes a **hook mapping** that works with any database. Adapt table and column names to your stack.
17
13
 
18
14
  ## Conceptual data model
@@ -171,9 +167,9 @@ chat.agent({
171
167
  });
172
168
  ```
173
169
 
174
- ## Alternative: `hydrateMessages` (deprecated)
170
+ ## Alternative: `hydrateMessages`
175
171
 
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.
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.
177
173
 
178
174
  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`):
179
175
 
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  title: "Persistence and replay"
3
3
  sidebarTitle: "Persistence and replay"
4
- description: "How chat.agent rebuilds conversation history at run boot — the transcript storage's persisted conversation plus session.out replay, and what changes when your app owns the model's context."
4
+ description: "How chat.agent rebuilds conversation history at run boot — durable JSON snapshot in object storage plus session.out replay, with a hydrateMessages short-circuit for backend-owned history."
5
5
  ---
6
6
 
7
7
  `chat.agent` runs are processes — they boot, stream a turn, and either suspend (waiting for the next message) or exit. When the next message arrives at a session whose previous run already exited, a **fresh** run boots with no in-memory state. Something has to rebuild the conversation history before that turn can produce a coherent response.
8
8
 
9
- This page walks through the **storage + replay** model. The persisted conversation comes from the agent's [transcript storage](/ai-chat/transcript-storage); the default storage is the snapshot in object storage described below, and a storage you bring is read the same way. Replay of the session streams covers what happened after the last save, and it runs for every agent, including one that owns the model's context.
9
+ This page walks through the **snapshot + replay** model the runtime uses by default, and the [`hydrateMessages`](/ai-chat/lifecycle-hooks#hydratemessages) short-circuit that turns the whole thing off when the customer owns history.
10
10
 
11
11
  ## Why a snapshot at all
12
12
 
@@ -32,7 +32,7 @@ sequenceDiagram
32
32
  User->>Run1: u1
33
33
  Run1->>SessionOut: assistant chunks for a1
34
34
  Run1->>Run1: onTurnComplete
35
- Run1->>Snapshot: write { messages: [u1, a1], lastOutEventId, lastInEventId }
35
+ Run1->>Snapshot: write { messages: [u1, a1], lastOutEventId, lastOutTimestamp }
36
36
  Note over Run1: idle suspend (or exit)
37
37
 
38
38
  User->>Run2: u2 (delta only)
@@ -52,21 +52,15 @@ The accumulator starts empty. The wire delivers `u1`. After the model finishes,
52
52
 
53
53
  ```json
54
54
  {
55
- "version": 2,
55
+ "version": 1,
56
56
  "savedAt": 1715180400000,
57
- "messages": [
58
- { "id": "u1", "final": true, "message": u1 },
59
- { "id": "a1", "final": true, "message": a1 }
60
- ],
61
- "state": null,
57
+ "messages": [u1, a1],
62
58
  "lastOutEventId": "42",
63
- "lastInEventId": "7"
59
+ "lastOutTimestamp": 1715180399000
64
60
  }
65
61
  ```
66
62
 
67
- `state` holds what the runtime cannot rebuild from the messages, such as a [compaction](/ai-chat/compaction) summary; `final` is false for a partial answer captured from a failed turn. Snapshots written by older SDK versions have `version: 1` and are read as if every message were final with no state.
68
-
69
- The key is `packets/{projectRef}/{envSlug}/sessions/{sessionId}/snapshot.json` — overwritten every turn, never appended. With your own storage, the equivalent is whatever `save` writes: the runtime hands it the two new messages as `put` changes and the same cursors, and a row-per-message store writes two rows instead of the whole conversation. The write is **awaited**, not fire-and-forget — if the run idle-suspends immediately after, in-flight promises don't reliably complete and the snapshot would be lost.
63
+ The key is `packets/{projectRef}/{envSlug}/sessions/{sessionId}/snapshot.json` — overwritten every turn, never appended. The write is **awaited**, not fire-and-forget — if the run idle-suspends immediately after, in-flight promises don't reliably complete and the snapshot would be lost.
70
64
 
71
65
  ### Run 2 — boot
72
66
 
@@ -116,25 +110,21 @@ Replay carries the conversation across the crash boundary with zero customer cod
116
110
 
117
111
  ## OOM-retry interaction
118
112
 
119
- The runtime already had an OOM-retry path that scans `session.out` for the latest `trigger:turn-complete` timestamp to use as a cutoff for `session.in` (so the retry doesn't re-process completed turns — see [OOM resilience](/ai-chat/patterns/oom-resilience)). The snapshot's `lastInEventId` field is exactly that committed `.in` cursor.
113
+ The runtime already had an OOM-retry path that scans `session.out` for the latest `trigger:turn-complete` timestamp to use as a cutoff for `session.in` (so the retry doesn't re-process completed turns — see [OOM resilience](/ai-chat/patterns/oom-resilience)). The snapshot includes a `lastOutTimestamp` field that is exactly that high-water mark.
120
114
 
121
- When a snapshot exists, the OOM-retry path reads `lastInEventId` directly instead of scanning `session.out`. One fewer stream subscription per retry. Free win.
115
+ When a snapshot exists, the OOM-retry path reads `lastOutTimestamp` directly instead of scanning `session.out`. One fewer stream subscription per retry. Free win.
122
116
 
123
117
  If no snapshot exists (first turn, or `hydrateMessages` registered), the path falls back to the scan.
124
118
 
125
- ## Action turns
126
-
127
- [Actions](/ai-chat/actions) (`trigger: "action"`) don't fire `onTurnComplete` — they fire `onAction` only. An action that changed the conversation is saved on its own, with `reason: "action"` and the same resume cursors as the last turn, so an undo survives the run ending. See [Actions and persistence](/ai-chat/actions#actions-and-persistence).
128
-
129
- ## When your app owns the model's context
119
+ ## Action turns — no snapshot write
130
120
 
131
- A storage with [`loadContext`](/ai-chat/transcript-storage#owning-the-models-context), or the deprecated [`hydrateMessages`](/ai-chat/lifecycle-hooks#hydratemessages) hook, decides what the model sees on every turn instead of the runtime's accumulated transcript. That changes the boot sequence in one place: with `hydrateMessages` the storage read is skipped, because the hook is the source of truth. Everything else still runs. The `session.out` and `session.in` tails are replayed, a partial answer and unacknowledged messages are recovered, and `onRecoveryBoot` fires. The hook then receives the recovered tail in `previousMessages`, so it can persist an answer a crashed run had already started.
121
+ [Action turns](/ai-chat/actions) (`trigger: "action"`) don't fire `onTurnComplete` — they fire `onAction` only. The snapshot write site is gated on `onTurnComplete`, so action turns don't snapshot.
132
122
 
133
- With `loadContext` on a storage, the storage is still read and written: `load` restores the cursors and the runtime's `state` (a compaction summary survives), `save` still receives every change, and only the model's context comes from `loadContext`.
123
+ If `onAction` mutates `chat.history.*` and then the run crashes before the next regular turn, the mutation is lost. The user re-fires the action. This matches `chat.history` semantics in general — mutations are persisted at turn boundaries, not action boundaries.
134
124
 
135
- ### The `hydrateMessages` hook
125
+ ## The `hydrateMessages` short-circuit
136
126
 
137
- When the customer registers a [`hydrateMessages`](/ai-chat/lifecycle-hooks#hydratemessages) hook, the runtime trusts the hook to be the source of truth for history. The snapshot is neither read nor written. The hook fires per turn, returns the canonical chain from the customer's database, and the accumulator is set to whatever the hook returned.
127
+ When the customer registers a [`hydrateMessages`](/ai-chat/lifecycle-hooks#hydratemessages) hook, the runtime trusts the hook to be the source of truth for history. Snapshot read and replay are **skipped entirely** at boot. The hook fires per turn, returns the canonical chain from the customer's database, and the accumulator is set to whatever the hook returned.
138
128
 
139
129
  ```ts
140
130
  import { chat, upsertIncomingMessage } from "@trigger.dev/sdk/ai";
@@ -170,24 +160,24 @@ export const myChat = chat.agent({
170
160
 
171
161
  What you gain:
172
162
 
173
- - **Zero object-store traffic per turn.** No snapshot read, no snapshot write. `OBJECT_STORE_*` env vars don't have to be set.
163
+ - **Zero object-store traffic per turn.** No snapshot read, no snapshot write, no replay subscription. `OBJECT_STORE_*` env vars don't have to be set.
174
164
  - **Branching, undo, edit, abuse prevention** — patterns that need a backend-side single source of truth work naturally because the customer mediates every read.
175
165
 
176
166
  What you give up:
177
167
 
178
- - **You own persistence end-to-end.** A bug in `hydrateMessages` that returns the wrong chain corrupts the conversation visible to the model, and a compaction summary has nowhere durable to live.
179
- - **OOM-retry needs a `session.out` scan again** because there's no snapshot to short-circuit it.
168
+ - **You own persistence end-to-end.** A bug in `hydrateMessages` that returns the wrong chain corrupts the conversation visible to the model.
169
+ - **OOM-retry needs a `session.out` scan again** because there's no snapshot to short-circuit it. (Same as the pre-snapshot baseline — not a regression, just a missed optimization.)
180
170
 
181
- A [transcript storage](/ai-chat/transcript-storage) with `loadContext` gives you the same ownership of the model's context while the runtime keeps writing every change and its own state to your store. It is the recommended path; `hydrateMessages` is deprecated.
171
+ The runtime's snapshot+replay is the safer default. `hydrateMessages` is the right choice when you already have authoritative storage for messages and want one consistent persistence path.
182
172
 
183
- ## When no storage is configured
173
+ ## When neither is configured
184
174
 
185
- If no object store is configured and the agent has no `storage` of its own, conversations don't survive run boundaries. A continuation boots empty. The runtime logs a warning at agent registration time so you see this at deploy time, not at user-traffic time.
175
+ If `hydrateMessages` is not registered **and** no object store is configured, conversations don't survive run boundaries. A continuation boots empty. The runtime logs a warning at agent registration time so you see this at deploy time, not at user-traffic time.
186
176
 
187
177
  For local development this is sometimes fine — you're not testing continuations. For production it isn't. Configure one of:
188
178
 
189
179
  - **Object store** (`OBJECT_STORE_*` env vars on your webapp) — easiest, default behavior.
190
- - **A transcript storage over your own database** — stronger control, suits multi-tenant apps with audit needs.
180
+ - **`hydrateMessages` + your own database** — stronger control, suits multi-tenant apps with audit needs.
191
181
 
192
182
  ## Snapshot key & lifecycle
193
183
 
@@ -198,7 +188,7 @@ For local development this is sometimes fine — you're not testing continuation
198
188
  | Key suffix | `sessions/{sessionId}/snapshot.json` |
199
189
  | Final key | `packets/{projectRef}/{envSlug}/sessions/{sessionId}/snapshot.json` |
200
190
  | Size | Tens of KB typical, capped only by object-store limits |
201
- | Cadence | Overwritten after every successful `onTurnComplete`, and after a history-changing action |
191
+ | Cadence | Overwritten after every successful `onTurnComplete` |
202
192
 
203
193
  Snapshots accumulate per-session forever unless you set a lifecycle policy on the bucket. A 90-day expiry on `packets/*/sessions/*/snapshot.json` is a reasonable default if your chats don't typically resume after that window. Closed sessions are not auto-cleaned today.
204
194
 
@@ -211,8 +201,7 @@ For local development against `pnpm run docker`, the bundled MinIO container is
211
201
  ## See also
212
202
 
213
203
  - [Client Protocol](/ai-chat/client-protocol#how-history-is-rebuilt) — the wire-level view of the same model
214
- - [Transcript storage](/ai-chat/transcript-storage) — the adapter the runtime persists through, and how to bring your own
215
- - [`hydrateMessages`](/ai-chat/lifecycle-hooks#hydratemessages) — the deprecated context hook
204
+ - [`hydrateMessages`](/ai-chat/lifecycle-hooks#hydratemessages) — the short-circuit hook
216
205
  - [OOM resilience](/ai-chat/patterns/oom-resilience) — how `session.in` cutoffs interact with snapshots
217
206
  - [Database persistence](/ai-chat/patterns/database-persistence) — the canonical persistence pattern using `onTurnComplete`
218
207
  - [v4.5 upgrade guide](/ai-chat/upgrade-guide#v45-wire-format-change) — when this model landed and what changed
@@ -79,7 +79,7 @@ The **body** is loaded on demand via the `loadSkill` tool when the agent decides
79
79
  ```ts trigger/chat.ts
80
80
  import { chat } from "@trigger.dev/sdk/ai";
81
81
  import { skills } from "@trigger.dev/sdk";
82
- import { stepCountIs } from "ai";
82
+ import { streamText, stepCountIs } from "ai";
83
83
  import { anthropic } from "@ai-sdk/anthropic";
84
84
 
85
85
  const timeUtilsSkill = skills.define({
@@ -92,11 +92,12 @@ export const agent = chat.agent({
92
92
  onChatStart: async () => {
93
93
  chat.skills.set([await timeUtilsSkill.local()]);
94
94
  },
95
- run: async ({ messages, signal, streamText }) => {
95
+ run: async ({ messages, signal }) => {
96
96
  return streamText({
97
97
  model: anthropic("claude-sonnet-4-5"),
98
98
  messages,
99
99
  abortSignal: signal,
100
+ ...chat.toStreamTextOptions(),
100
101
  stopWhen: stepCountIs(15),
101
102
  });
102
103
  },
@@ -110,7 +111,7 @@ export const agent = chat.agent({
110
111
 
111
112
  `skill.local()` reads the bundled `SKILL.md` from disk and returns a `ResolvedSkill` with the parsed frontmatter + body + on-disk path.
112
113
 
113
- `chat.skills.set([...])` stores the resolved skills for the current run. The `streamText` from `run`'s argument picks them up automatically:
114
+ `chat.skills.set([...])` stores the resolved skills for the current run. `chat.toStreamTextOptions()` spreads them into `streamText` automatically:
114
115
 
115
116
  - The frontmatter `description` lands in the system prompt under "Available skills:".
116
117
  - Three tools are added: `loadSkill`, `readFile`, `bash` — scoped per skill.
@@ -168,10 +169,12 @@ return streamText({
168
169
  model: anthropic("claude-sonnet-4-5"),
169
170
  messages,
170
171
  abortSignal: signal,
171
- tools: {
172
- webFetch, // your tool
173
- deepResearch, // your tool
174
- },
172
+ ...chat.toStreamTextOptions({
173
+ tools: {
174
+ webFetch, // your tool
175
+ deepResearch, // your tool
176
+ },
177
+ }),
175
178
  stopWhen: stepCountIs(15),
176
179
  });
177
180
  ```
@@ -8,17 +8,6 @@ Chat agent runs are pinned to the worker version they started on. When you deplo
8
8
 
9
9
  `chat.requestUpgrade()` is the managed upgrade signal for `chat.agent()` and the `chat.createSession()` iterator. Fully hand-rolled custom agents use `chat.endAndContinue()` between turns to immediately hand the Session to a new run.
10
10
 
11
- <Note>
12
- If your sessions are pinned by [version skew
13
- protection](/deployment/version-skew-protection#chat-sessions), you do not need this page to move a
14
- conversation onto a new deployment. A pinned session follows its pin on its own: when the stored
15
- `externalDeploymentId` stops naming the deployment a run is on, the agent hands over at the next
16
- turn boundary. Set [`versionSkew: "hold"`](#staying-put) to turn that off for one agent.
17
-
18
- Read on for the cases that are still yours to decide — leaving a pin for a version nobody named,
19
- a session that was never pinned, and hand-rolled custom agents.
20
- </Note>
21
-
22
11
  ## How it works
23
12
 
24
13
  When `chat.requestUpgrade()` is called in `onTurnStart` or `onValidateMessages`:
@@ -30,44 +19,22 @@ When `chat.requestUpgrade()` is called in `onTurnStart` or `onValidateMessages`:
30
19
 
31
20
  The new run lives on the **same Session** as the old one. `chatId` is the durable identity; only the underlying `currentRunId` rotates. The audit log records the new run with `reason: "upgrade"`.
32
21
 
33
- ### What "the latest deployment" means
34
-
35
- The handoff clears the session's [external deployment id](/deployment/version-skew-protection#chat-sessions) so the new run can land on the current version — re-applying the pin the agent just rejected would make the upgrade impossible. The cleared pin is persisted on the session, so the next continuation doesn't fall back to it either.
36
-
37
- To move to a specific deployment rather than to whatever is current, name it:
38
-
39
- ```ts
40
- chat.requestUpgrade({ externalDeploymentId: clientData.commitSha });
41
- ```
42
-
43
- That is usually what you want when the client told you which build it is on: it upgrades to the version the client expects instead of merely to the newest one.
44
-
45
- <Warning>
46
- `lockToVersion` is a different thing and is **never** cleared. A session started with an explicit
47
- `lockToVersion` re-applies it on every run including upgrade handoffs, so `chat.requestUpgrade()`
48
- cannot escape it — the new run lands on the same version the old one did. Use the external
49
- deployment id if you want a pin an agent can opt out of.
50
- </Warning>
51
-
52
22
  When called from inside `run()` or `chat.defer()`, the current turn completes normally first and the run exits afterward. The next message triggers the continuation on the same session.
53
23
 
54
24
  ```mermaid
55
25
  sequenceDiagram
56
26
  participant User
57
27
  participant Transport
58
- participant Session as session.in
59
28
  participant RunV1 as Run (v1)
60
29
  participant RunV2 as Run (v2)
61
30
 
62
31
  User->>Transport: send message
63
- Transport->>Session: append message
64
- Session->>RunV1: input stream
32
+ Transport->>RunV1: input stream
65
33
  RunV1->>RunV1: onTurnStart → requestUpgrade()
66
- RunV1->>RunV2: end-and-continue triggers the successor
67
- RunV1-->>Transport: trigger:upgrade-required (filtered, no re-send)
68
- RunV1->>RunV1: exit (run() never called, message left unacknowledged)
69
- Session->>RunV2: same message, replayed on boot
70
- RunV2-->>Transport: response stream (same session.out)
34
+ RunV1-->>Transport: trigger:upgrade-required
35
+ RunV1->>RunV1: exit (run() never called)
36
+ Transport->>RunV2: trigger new run (continuation, same message)
37
+ RunV2-->>Transport: response stream
71
38
  Transport-->>User: response (seamless)
72
39
  ```
73
40
 
@@ -136,13 +103,6 @@ This pattern is useful when:
136
103
 
137
104
  ## Auto-detect from build ID (Next.js / Vercel)
138
105
 
139
- <Warning>
140
- You probably don't need this any more. If your sessions are pinned, following the pin is the
141
- built-in behaviour and it needs no `clientData` and no `chat.local`. Reach for the recipe below
142
- only when you want to upgrade on a signal the pin doesn't carry — a frontend build id that moves
143
- independently of the deployment your app names.
144
- </Warning>
145
-
146
106
  For automatic upgrade on every deploy, pass your platform's build ID via `clientData` instead of a manual version. The agent stores the ID from the first message and upgrades when it changes:
147
107
 
148
108
  ```tsx title="app/components/Chat.tsx"
@@ -191,38 +151,6 @@ export const myChat = chat
191
151
 
192
152
  This upgrades on **every** deploy, not just breaking changes. Good for fast-moving projects where you always want the latest code.
193
153
 
194
- ## Staying put
195
-
196
- A pinned session follows its pin by default. To keep one agent where it is — a long tool chain you
197
- don't want interrupted, or a conversation you'd rather move on your own terms — set `versionSkew`:
198
-
199
- ```ts
200
- export const myChat = chat.agent({
201
- id: "my-chat",
202
- versionSkew: "hold",
203
- run: async ({ messages, signal }) => { ... },
204
- });
205
- ```
206
-
207
- `"hold"` only stops the automatic handoff. `chat.requestUpgrade()` still works, so you can keep the
208
- decision and still get the seamless swap.
209
-
210
- Two cases never hand over automatically, whatever `versionSkew` says:
211
-
212
- - **A session with no pin.** There is nothing to compare against, and an unpinned session already
213
- lands on the current version every time it starts a run.
214
- - **A session using `lockToVersion`.** That pin outranks the external deployment id and
215
- `chat.requestUpgrade()` cannot escape it, so handing over would land on the same version and
216
- repeat.
217
-
218
- <Note>
219
- Following the pin costs one session read per turn on pinned chats, and the handoff happens at a
220
- turn boundary — never mid-turn. If the pin names a deployment that hasn't landed yet, the successor
221
- parks: your messages stay durable, and the transport emits `run-pending-version` with
222
- `source: "upgrade"` so you can say so in the UI. See [parked
223
- chats](/deployment/version-skew-protection#chat-sessions).
224
- </Note>
225
-
226
154
  ## Custom agents
227
155
 
228
156
  Use `chat.requestUpgrade()` with `chat.agent()`. With `chat.createSession()`, call `chat.requestUpgrade()`, then advance the iterator once more so it can exit normally. For an immediate handoff, close the iterator before calling `chat.endAndContinue()`. In a fully hand-rolled `chat.customAgent()` task, detach input listeners, persist the completed turn, write its boundary, then call `chat.endAndContinue()` and return immediately:
@@ -238,7 +166,7 @@ await chat.endAndContinue();
238
166
  return;
239
167
  ```
240
168
 
241
- The continuation uses the same durable Session and receives `.in` records that the old run has not consumed. It starts on the latest deployed task version only if the Session is unpinned — a Session carrying `lockToVersion` or an `externalDeploymentId` re-applies that pin, so the continuation lands where the Session says rather than on the newest build.
169
+ The continuation uses the same durable Session and receives `.in` records that the old run has not consumed. It starts on the latest deployed task version unless the Session's trigger configuration sets `lockToVersion`.
242
170
 
243
171
  If input has been dispatched to the old run but should be processed by the continuation, detach the old listeners and skip the final `chat.writeTurnComplete()`. A turn-complete boundary acknowledges the latest input dispatched to the old run, so writing one after that dispatch would cause the continuation to resume past the input.
244
172
 
@@ -254,7 +182,6 @@ Both are graceful exits. [`onRecoveryBoot`](/ai-chat/patterns/recovery-boot) doe
254
182
 
255
183
  ## See also
256
184
 
257
- - [Version skew protection](/deployment/version-skew-protection#chat-sessions) — pin a session to the deployment matching the app build that started it
258
185
  - [Lifecycle hooks](/ai-chat/lifecycle-hooks) — where `onTurnStart` and `onChatResume` fit in the turn cycle
259
186
  - [Recovery boot](/ai-chat/patterns/recovery-boot) — the sibling hook for mid-stream interruptions (does NOT fire on `requestUpgrade`)
260
187
  - [Database persistence](/ai-chat/patterns/database-persistence) — how continuations interact with session state
@@ -30,18 +30,18 @@ Add `pendingMessages` to your `chat.agent` configuration:
30
30
 
31
31
  ```ts
32
32
  import { chat } from "@trigger.dev/sdk/ai";
33
- import { stepCountIs } from "ai";
33
+ import { streamText, stepCountIs } from "ai";
34
34
  import { anthropic } from "@ai-sdk/anthropic";
35
35
 
36
36
  export const myChat = chat.agent({
37
37
  id: "my-chat",
38
- registry,
39
38
  pendingMessages: {
40
39
  // Only inject when there are completed steps (tool calls happened)
41
40
  shouldInject: ({ steps }) => steps.length > 0,
42
41
  },
43
- run: async ({ messages, signal, streamText }) => {
42
+ run: async ({ messages, signal }) => {
44
43
  return streamText({
44
+ ...chat.toStreamTextOptions({ registry }),
45
45
  messages,
46
46
  tools: { /* ... */ },
47
47
  abortSignal: signal,
@@ -16,7 +16,7 @@ A request renders as `tools` → `system` → `messages`. There are three prefix
16
16
 
17
17
  | Region | How to cache it | Stability |
18
18
  | --- | --- | --- |
19
- | System prompt (+ tools) | `cacheControl` / `systemProviderOptions` on `chat.agent()`, or `providerOptions` on `chat.prompt.set()` | Set once, never changes — the highest-value target |
19
+ | System prompt (+ tools) | `cacheControl` / `systemProviderOptions` on `chat.toStreamTextOptions()`, or `providerOptions` on `chat.prompt.set()` | Set once, never changes — the highest-value target |
20
20
  | Conversation history | `prepareMessages` adds a breakpoint to the last message | Grows append-only across turns |
21
21
  | Tool definitions | Stable as long as your tool set doesn't change between turns | Render at position 0 — changing them invalidates everything |
22
22
 
@@ -32,22 +32,23 @@ The system prompt (your `chat.prompt` text plus any skills preamble) is usually
32
32
 
33
33
  Three ways to opt in, depending on where you'd rather express it.
34
34
 
35
- **`cacheControl` on the agent** — the Anthropic-flavored one-liner:
35
+ **`cacheControl` at the `streamText` call site** — the Anthropic-flavored one-liner:
36
36
 
37
37
  ```ts /trigger/chat.ts
38
38
  import { chat } from "@trigger.dev/sdk/ai";
39
+ import { streamText } from "ai";
39
40
  import { anthropic } from "@ai-sdk/anthropic";
40
41
 
41
42
  export const myChat = chat.agent({
42
43
  id: "my-chat",
43
- cacheControl: { type: "ephemeral" },
44
44
  onChatStart: async () => {
45
45
  chat.prompt.set(SYSTEM_PROMPT); // a large, stable instruction block
46
46
  },
47
- run: async ({ messages, signal, streamText }) => {
47
+ run: async ({ messages, signal }) => {
48
48
  return streamText({
49
49
  model: anthropic("claude-sonnet-4-6"),
50
50
  // Caches the system block with a 5-minute breakpoint.
51
+ ...chat.toStreamTextOptions({ cacheControl: { type: "ephemeral" } }),
51
52
  messages,
52
53
  abortSignal: signal,
53
54
  });
@@ -58,19 +59,17 @@ export const myChat = chat.agent({
58
59
  **`systemProviderOptions`** is the provider-agnostic form — pass the raw `providerOptions` so it composes with any provider:
59
60
 
60
61
  ```ts /trigger/chat.ts
61
- export const myChat = chat.agent({
62
- id: "my-chat",
63
- systemProviderOptions: { anthropic: { cacheControl: { type: "ephemeral" } } },
64
- run: async ({ messages, signal, streamText }) =>
65
- streamText({
66
- model: anthropic("claude-sonnet-4-6"),
67
- messages,
68
- abortSignal: signal,
69
- }),
62
+ return streamText({
63
+ model: anthropic("claude-sonnet-4-6"),
64
+ ...chat.toStreamTextOptions({
65
+ systemProviderOptions: { anthropic: { cacheControl: { type: "ephemeral" } } },
66
+ }),
67
+ messages,
68
+ abortSignal: signal,
70
69
  });
71
70
  ```
72
71
 
73
- **`providerOptions` on `chat.prompt.set()`** co-locates the intent with where the prompt is defined. It carries through to the managed `streamText` with no call-site change:
72
+ **`providerOptions` on `chat.prompt.set()`** co-locates the intent with where the prompt is defined. It carries through to `toStreamTextOptions()` with no call-site change:
74
73
 
75
74
  ```ts /trigger/chat.ts
76
75
  onChatStart: async () => {
@@ -78,16 +77,17 @@ onChatStart: async () => {
78
77
  providerOptions: { anthropic: { cacheControl: { type: "ephemeral" } } },
79
78
  });
80
79
  },
81
- run: async ({ messages, signal, streamText }) => {
80
+ run: async ({ messages, signal }) => {
82
81
  return streamText({
83
82
  model: anthropic("claude-sonnet-4-6"),
83
+ ...chat.toStreamTextOptions(), // already cached
84
84
  messages,
85
85
  abortSignal: signal,
86
86
  });
87
87
  },
88
88
  ```
89
89
 
90
- If more than one is set, the most specific wins: `systemProviderOptions` overrides `cacheControl`, and both override `chat.prompt.set`'s `providerOptions`. There's no deep merge — the most specific option replaces the rest.
90
+ If more than one is set, the call-site option wins: `systemProviderOptions` overrides `cacheControl`, and both override `chat.prompt.set`'s `providerOptions`. There's no deep merge — the most specific option replaces the rest.
91
91
 
92
92
  <Note>
93
93
  Use the 1-hour cache for prefixes that sit idle longer than 5 minutes between turns: `cacheControl: { type: "ephemeral", ttl: "1h" }`. Writes cost more (2× vs 1.25×), so it pays off only when reads span the longer window.
@@ -100,7 +100,6 @@ Place a breakpoint on the last message and the entire conversation prefix up to
100
100
  ```ts /trigger/chat.ts
101
101
  export const myChat = chat.agent({
102
102
  id: "my-chat",
103
- cacheControl: { type: "ephemeral" },
104
103
  prepareMessages: async ({ messages }) => {
105
104
  if (messages.length === 0) return messages;
106
105
  const last = messages[messages.length - 1];
@@ -115,9 +114,10 @@ export const myChat = chat.agent({
115
114
  },
116
115
  ];
117
116
  },
118
- run: async ({ messages, signal, streamText }) => {
117
+ run: async ({ messages, signal }) => {
119
118
  return streamText({
120
119
  model: anthropic("claude-sonnet-4-6"),
120
+ ...chat.toStreamTextOptions({ cacheControl: { type: "ephemeral" } }),
121
121
  messages,
122
122
  abortSignal: signal,
123
123
  });
@@ -149,10 +149,11 @@ Caching is provider-specific, and most providers don't use per-block breakpoints
149
149
 
150
150
  ```ts /trigger/chat.ts
151
151
  // Amazon Bedrock
152
- export const myChat = chat.agent({
153
- id: "my-chat",
154
- systemProviderOptions: { bedrock: { cachePoint: { type: "default" } } },
155
- run: async ({ messages, streamText }) => streamText({ messages }),
152
+ return streamText({
153
+ ...chat.toStreamTextOptions({
154
+ systemProviderOptions: { bedrock: { cachePoint: { type: "default" } } },
155
+ }),
156
+ messages,
156
157
  });
157
158
  ```
158
159
 
@@ -165,13 +166,14 @@ Usage reporting is normalized. Each provider reports cache tokens under its own
165
166
  The turn's usage carries cache token counts. `chat.agent` accumulates them across turns and hands them to `run` as `previousTurnUsage` (last turn) and `totalUsage` (whole chat), both `LanguageModelUsage`:
166
167
 
167
168
  ```ts /trigger/chat.ts
168
- run: async ({ messages, signal, previousTurnUsage, streamText }) => {
169
+ run: async ({ messages, signal, previousTurnUsage }) => {
169
170
  // After turn 1, cacheReadTokens should be > 0 on a stable prefix.
170
171
  console.log("cache read", previousTurnUsage?.inputTokenDetails?.cacheReadTokens);
171
172
  console.log("cache write", previousTurnUsage?.inputTokenDetails?.cacheWriteTokens);
172
173
 
173
174
  return streamText({
174
175
  model: anthropic("claude-sonnet-4-6"),
176
+ ...chat.toStreamTextOptions({ cacheControl: { type: "ephemeral" } }),
175
177
  messages,
176
178
  abortSignal: signal,
177
179
  });
@@ -16,16 +16,19 @@ The chat surface works with Vercel AI SDK **v5, v6, or v7**; install whichever m
16
16
 
17
17
  ```ts trigger/chat.ts
18
18
  import { chat } from "@trigger.dev/sdk/ai";
19
- import { stepCountIs } from "ai";
19
+ import { streamText, stepCountIs } from "ai";
20
20
  import { anthropic } from "@ai-sdk/anthropic";
21
21
 
22
22
  export const myChat = chat.agent({
23
23
  id: "my-chat",
24
- // `streamText` here is the SDK's, not the one from `ai`: it carries
25
- // compaction, steering, background injection, the system prompt and
26
- // telemetry, so none of them have to be wired up by hand.
27
- run: async ({ messages, signal, streamText }) => {
24
+ run: async ({ messages, signal }) => {
28
25
  return streamText({
26
+ // Spread chat.toStreamTextOptions() FIRST — it wires up
27
+ // prepareStep (compaction, steering, background injection),
28
+ // the system prompt set via chat.prompt(), and telemetry.
29
+ // Skipping this is the single most common cause of subtle
30
+ // bugs (silent broken compaction, missing steering, etc.).
31
+ ...chat.toStreamTextOptions(),
29
32
  model: anthropic("claude-sonnet-4-5"),
30
33
  messages,
31
34
  abortSignal: signal,
@@ -35,12 +38,9 @@ The chat surface works with Vercel AI SDK **v5, v6, or v7**; install whichever m
35
38
  });
36
39
  ```
37
40
 
38
- <Note>
39
- Take `streamText` from `run`'s argument rather than importing it from `ai`. The
40
- imported one drives no `prepareStep`, so compaction, mid-turn steering and
41
- background injection never run, and nothing reports it. Spreading
42
- `chat.toStreamTextOptions()` into the imported one does the same job by hand.
43
- </Note>
41
+ <Warning>
42
+ **Always spread `chat.toStreamTextOptions()` into your `streamText` call.** It wires up the `prepareStep` callback that drives compaction, mid-turn steering, and background injection — features that silently no-op if the spread is missing. Spread it **first** so any explicit overrides (e.g. a custom `prepareStep`) win.
43
+ </Warning>
44
44
 
45
45
  <Tip>
46
46
  For a **custom** [`UIMessage`](https://sdk.vercel.ai/docs/reference/ai-sdk-core/ui-message) subtype (typed `data-*` parts, tool map, etc.), define the agent with [`chat.withUIMessage<...>().agent({...})`](/ai-chat/types) instead of `chat.agent`.