@trigger.dev/sdk 0.0.0-prerelease-20260908122921 → 0.0.0-prerelease-20260909071038

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 (76) hide show
  1. package/dist/commonjs/v3/ai-shared.d.ts +15 -0
  2. package/dist/commonjs/v3/ai-shared.js +35 -1
  3. package/dist/commonjs/v3/ai-shared.js.map +1 -1
  4. package/dist/commonjs/v3/ai.d.ts +76 -9
  5. package/dist/commonjs/v3/ai.js +316 -222
  6. package/dist/commonjs/v3/ai.js.map +1 -1
  7. package/dist/commonjs/v3/chat-client.d.ts +8 -0
  8. package/dist/commonjs/v3/chat-client.js +199 -109
  9. package/dist/commonjs/v3/chat-client.js.map +1 -1
  10. package/dist/commonjs/v3/chat-react.d.ts +54 -0
  11. package/dist/commonjs/v3/chat-react.js +83 -0
  12. package/dist/commonjs/v3/chat-react.js.map +1 -1
  13. package/dist/commonjs/v3/chat.d.ts +11 -0
  14. package/dist/commonjs/v3/chat.js +36 -27
  15. package/dist/commonjs/v3/chat.js.map +1 -1
  16. package/dist/commonjs/v3/chatSnapshotIo.d.ts +2 -0
  17. package/dist/commonjs/v3/chatSnapshotIo.js +174 -0
  18. package/dist/commonjs/v3/chatSnapshotIo.js.map +1 -0
  19. package/dist/commonjs/v3/test/index.d.ts +1 -0
  20. package/dist/commonjs/v3/test/index.js +3 -1
  21. package/dist/commonjs/v3/test/index.js.map +1 -1
  22. package/dist/commonjs/v3/test/mock-chat-agent.d.ts +4 -4
  23. package/dist/commonjs/v3/test/mock-chat-agent.js +25 -4
  24. package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
  25. package/dist/commonjs/v3/test/transcript-storage-tests.d.ts +40 -0
  26. package/dist/commonjs/v3/test/transcript-storage-tests.js +258 -0
  27. package/dist/commonjs/v3/test/transcript-storage-tests.js.map +1 -0
  28. package/dist/commonjs/v3/transcriptStorage.d.ts +229 -0
  29. package/dist/commonjs/v3/transcriptStorage.js +321 -0
  30. package/dist/commonjs/v3/transcriptStorage.js.map +1 -0
  31. package/dist/commonjs/version.js +1 -1
  32. package/dist/esm/v3/ai-shared.d.ts +15 -0
  33. package/dist/esm/v3/ai-shared.js +33 -0
  34. package/dist/esm/v3/ai-shared.js.map +1 -1
  35. package/dist/esm/v3/ai.d.ts +76 -9
  36. package/dist/esm/v3/ai.js +306 -217
  37. package/dist/esm/v3/ai.js.map +1 -1
  38. package/dist/esm/v3/chat-client.d.ts +8 -0
  39. package/dist/esm/v3/chat-client.js +200 -110
  40. package/dist/esm/v3/chat-client.js.map +1 -1
  41. package/dist/esm/v3/chat-react.d.ts +54 -0
  42. package/dist/esm/v3/chat-react.js +81 -0
  43. package/dist/esm/v3/chat-react.js.map +1 -1
  44. package/dist/esm/v3/chat.d.ts +11 -0
  45. package/dist/esm/v3/chat.js +36 -27
  46. package/dist/esm/v3/chat.js.map +1 -1
  47. package/dist/esm/v3/chatSnapshotIo.d.ts +2 -0
  48. package/dist/esm/v3/chatSnapshotIo.js +166 -0
  49. package/dist/esm/v3/chatSnapshotIo.js.map +1 -0
  50. package/dist/esm/v3/test/index.d.ts +1 -0
  51. package/dist/esm/v3/test/index.js +1 -0
  52. package/dist/esm/v3/test/index.js.map +1 -1
  53. package/dist/esm/v3/test/mock-chat-agent.d.ts +4 -4
  54. package/dist/esm/v3/test/mock-chat-agent.js +25 -4
  55. package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
  56. package/dist/esm/v3/test/transcript-storage-tests.d.ts +40 -0
  57. package/dist/esm/v3/test/transcript-storage-tests.js +255 -0
  58. package/dist/esm/v3/test/transcript-storage-tests.js.map +1 -0
  59. package/dist/esm/v3/transcriptStorage.d.ts +229 -0
  60. package/dist/esm/v3/transcriptStorage.js +309 -0
  61. package/dist/esm/v3/transcriptStorage.js.map +1 -0
  62. package/dist/esm/version.js +1 -1
  63. package/docs/ai-chat/actions.mdx +2 -2
  64. package/docs/ai-chat/background-injection.mdx +2 -0
  65. package/docs/ai-chat/compaction.mdx +2 -0
  66. package/docs/ai-chat/frontend.mdx +2 -0
  67. package/docs/ai-chat/lifecycle-hooks.mdx +6 -2
  68. package/docs/ai-chat/patterns/database-persistence.mdx +6 -2
  69. package/docs/ai-chat/patterns/persistence-and-replay.mdx +34 -23
  70. package/docs/ai-chat/reference.mdx +56 -5
  71. package/docs/ai-chat/transcript-storage.mdx +233 -0
  72. package/docs/config/extensions/syncEnvVars.mdx +6 -0
  73. package/docs/deploy-environment-variables.mdx +23 -2
  74. package/package.json +2 -2
  75. package/skills/trigger-authoring-chat-agent/SKILL.md +3 -2
  76. package/skills/trigger-chat-agent-advanced/SKILL.md +5 -5
@@ -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
 
@@ -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 — durable JSON snapshot in object storage plus session.out replay, with a hydrateMessages short-circuit for backend-owned history."
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."
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 **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.
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.
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, lastOutTimestamp }
35
+ Run1->>Snapshot: write { messages: [u1, a1], lastOutEventId, lastInEventId }
36
36
  Note over Run1: idle suspend (or exit)
37
37
 
38
38
  User->>Run2: u2 (delta only)
@@ -52,15 +52,21 @@ The accumulator starts empty. The wire delivers `u1`. After the model finishes,
52
52
 
53
53
  ```json
54
54
  {
55
- "version": 1,
55
+ "version": 2,
56
56
  "savedAt": 1715180400000,
57
- "messages": [u1, a1],
57
+ "messages": [
58
+ { "id": "u1", "final": true, "message": u1 },
59
+ { "id": "a1", "final": true, "message": a1 }
60
+ ],
61
+ "state": null,
58
62
  "lastOutEventId": "42",
59
- "lastOutTimestamp": 1715180399000
63
+ "lastInEventId": "7"
60
64
  }
61
65
  ```
62
66
 
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.
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.
64
70
 
65
71
  ### Run 2 — boot
66
72
 
@@ -110,21 +116,25 @@ Replay carries the conversation across the crash boundary with zero customer cod
110
116
 
111
117
  ## OOM-retry interaction
112
118
 
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.
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.
114
120
 
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.
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.
116
122
 
117
123
  If no snapshot exists (first turn, or `hydrateMessages` registered), the path falls back to the scan.
118
124
 
119
- ## Action turns — no snapshot write
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
120
130
 
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.
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.
122
132
 
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.
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`.
124
134
 
125
- ## The `hydrateMessages` short-circuit
135
+ ### The `hydrateMessages` hook
126
136
 
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.
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.
128
138
 
129
139
  ```ts
130
140
  import { chat, upsertIncomingMessage } from "@trigger.dev/sdk/ai";
@@ -160,24 +170,24 @@ export const myChat = chat.agent({
160
170
 
161
171
  What you gain:
162
172
 
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.
173
+ - **Zero object-store traffic per turn.** No snapshot read, no snapshot write. `OBJECT_STORE_*` env vars don't have to be set.
164
174
  - **Branching, undo, edit, abuse prevention** — patterns that need a backend-side single source of truth work naturally because the customer mediates every read.
165
175
 
166
176
  What you give up:
167
177
 
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.)
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.
170
180
 
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.
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.
172
182
 
173
- ## When neither is configured
183
+ ## When no storage is configured
174
184
 
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.
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.
176
186
 
177
187
  For local development this is sometimes fine — you're not testing continuations. For production it isn't. Configure one of:
178
188
 
179
189
  - **Object store** (`OBJECT_STORE_*` env vars on your webapp) — easiest, default behavior.
180
- - **`hydrateMessages` + your own database** — stronger control, suits multi-tenant apps with audit needs.
190
+ - **A transcript storage over your own database** — stronger control, suits multi-tenant apps with audit needs.
181
191
 
182
192
  ## Snapshot key & lifecycle
183
193
 
@@ -188,7 +198,7 @@ For local development this is sometimes fine — you're not testing continuation
188
198
  | Key suffix | `sessions/{sessionId}/snapshot.json` |
189
199
  | Final key | `packets/{projectRef}/{envSlug}/sessions/{sessionId}/snapshot.json` |
190
200
  | Size | Tens of KB typical, capped only by object-store limits |
191
- | Cadence | Overwritten after every successful `onTurnComplete` |
201
+ | Cadence | Overwritten after every successful `onTurnComplete`, and after a history-changing action |
192
202
 
193
203
  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.
194
204
 
@@ -201,7 +211,8 @@ For local development against `pnpm run docker`, the bundled MinIO container is
201
211
  ## See also
202
212
 
203
213
  - [Client Protocol](/ai-chat/client-protocol#how-history-is-rebuilt) — the wire-level view of the same model
204
- - [`hydrateMessages`](/ai-chat/lifecycle-hooks#hydratemessages) — the short-circuit hook
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
205
216
  - [OOM resilience](/ai-chat/patterns/oom-resilience) — how `session.in` cutoffs interact with snapshots
206
217
  - [Database persistence](/ai-chat/patterns/database-persistence) — the canonical persistence pattern using `onTurnComplete`
207
218
  - [v4.5 upgrade guide](/ai-chat/upgrade-guide#v45-wire-format-change) — when this model landed and what changed
@@ -44,9 +44,10 @@ Options for `chat.agent()`.
44
44
  | `onPreload` | `(event: PreloadEvent) => Promise<void> \| void` | — | Fires on preloaded runs before the first message |
45
45
  | `onChatStart` | `(event: ChatStartEvent) => Promise<void> \| void` | — | Fires once per chat, on the very first user message. Does NOT fire on continuation runs or OOM-retries — see [onChatStart](/ai-chat/lifecycle-hooks#onchatstart). |
46
46
  | `onValidateMessages` | `(event: ValidateMessagesEvent) => UIMessage[] \| Promise<UIMessage[]>` | — | Validate/transform UIMessages before model conversion. See [onValidateMessages](/ai-chat/lifecycle-hooks#onvalidatemessages) |
47
- | `hydrateMessages` | `(event: HydrateMessagesEvent) => UIMessage[] \| Promise<UIMessage[]>` | — | Load message history from backend, replacing the linear accumulator. See [hydrateMessages](/ai-chat/lifecycle-hooks#hydratemessages) |
47
+ | `storage` | `TranscriptStorage` | `defaultStorage` | Where the conversation is persisted and read back. The platform snapshot by default; bring your own to write each change to your database. See [Transcript storage](/ai-chat/transcript-storage) |
48
+ | `hydrateMessages` | `(event: HydrateMessagesEvent) => UIMessage[] \| Promise<UIMessage[]>` | — | **Deprecated.** Load message history from backend, replacing the linear accumulator. Use `loadContext` on a `storage` instead; cannot be combined with `storage`. See [hydrateMessages](/ai-chat/lifecycle-hooks#hydratemessages) |
48
49
  | `actionSchema` | `TaskSchema` | — | Schema for validating custom actions sent via `transport.sendAction()`. See [Actions](/ai-chat/actions) |
49
- | `onAction` | `(event: ActionEvent) => Promise<void \| ActionTurn> \| void \| ActionTurn` | — | Handle custom actions. Actions are state edits: only `hydrateMessages` + `onAction` fire. Return `chat.turn()` to run a turn on the edited history, or nothing for an edit only. See [Actions](/ai-chat/actions) |
50
+ | `onAction` | `(event: ActionEvent) => Promise<void \| ActionTurn> \| void \| ActionTurn` | — | Handle custom actions. Actions are state edits: only `hydrateMessages` (or a storage's `loadContext`) + `onAction` fire. Return `chat.turn()` to run a turn on the edited history, or nothing for an edit only. See [Actions](/ai-chat/actions) |
50
51
  | `onTurnStart` | `(event: TurnStartEvent) => Promise<void> \| void` | — | Fires every turn before `run()` |
51
52
  | `onBeforeTurnComplete` | `(event: BeforeTurnCompleteEvent) => Promise<void> \| void` | — | Fires after response but before stream closes. Includes `writer`. |
52
53
  | `onTurnComplete` | `(event: TurnCompleteEvent) => Promise<void> \| void` | — | Fires after each turn completes (stream closed) |
@@ -218,9 +219,38 @@ Passed to the `tools` function form on `chat.agent`, once per turn, to resolve t
218
219
  | `continuation` | `boolean` | Whether this run is continuing an existing chat |
219
220
  | `clientData` | Typed by `clientDataSchema` | Custom data from the frontend |
220
221
 
222
+ ## TranscriptStorage
223
+
224
+ The persistence adapter set through `chat.agent({ storage })`. See [Transcript storage](/ai-chat/transcript-storage). All types below are exported from `@trigger.dev/sdk/ai`.
225
+
226
+ | Member | Signature | Description |
227
+ | --- | --- | --- |
228
+ | `load` | `(scope: TranscriptScope, opts?: TranscriptLoadOptions) => Promise<TranscriptLoadResult>` | The conversation, in order. Called once at a continuation boot and by `chat.createLoadTranscriptAction` for rendering. |
229
+ | `save` | `(ctx: TranscriptStorageContext, changeset: TranscriptChangeset) => Promise<void>` | Apply the changes since the last save. Called after every turn, failed turn and history-changing action. |
230
+ | `loadContext?` | `(scope: TranscriptScope, event: LoadContextEvent) => Promise<UIMessage[]>` | Optional. When present, the storage owns the model's context: called on every turn and action in place of the runtime's transcript. Same event shape as `HydrateMessagesEvent`. |
231
+
232
+ | Type | Shape |
233
+ | --- | --- |
234
+ | `TranscriptScope<TClientData>` | `{ chatId: string; clientData: TClientData }` |
235
+ | `TranscriptStorageContext<TClientData>` | `TranscriptScope` plus `{ turn: number; trigger: "submit-message" \| "regenerate-message" \| "action"; runId: string; ctx: TaskRunContext }` |
236
+ | `TranscriptChange` | `{ op: "put"; message: UIMessage; final?: boolean }` \| `{ op: "remove"; id: string }` \| `{ op: "truncateAfter"; afterId: string }` \| `{ op: "state"; value: unknown \| null }` |
237
+ | `TranscriptChangeset` | `{ reason: "turn-complete" \| "turn-error" \| "action" \| "compaction" \| "recovery"; changes: TranscriptChange[]; transcript: TranscriptState; cursors?: TranscriptCursors }` |
238
+ | `TranscriptState` | `{ entries: Array<{ id: string; final: boolean; message: UIMessage }>; state: unknown \| null }`: the whole conversation after the changeset's changes, for stores that write one document |
239
+ | `TranscriptCursors` | `{ lastOutEventId?: string; lastInEventId?: string }` |
240
+ | `TranscriptLoadOptions` | `{ limit?: number; before?: string }` |
241
+ | `TranscriptLoadResult` | `{ messages: UIMessage[]; state: unknown \| null; cursors?: TranscriptCursors; nextCursor?: string }` |
242
+
243
+ | Export | Description |
244
+ | --- | --- |
245
+ | `defaultStorage` | The platform snapshot storage the agent uses when `storage` is not set. Equal to `snapshotTranscriptStorage()`. |
246
+ | `snapshotTranscriptStorage()` | Factory for the platform snapshot storage. |
247
+ | `memoryTranscriptStorage()` | An in-process storage that also records every changeset it receives. The reference implementation. |
248
+ | `reduceTranscriptChanges(state, changes)` | Pure reducer that applies changes to `{ entries, state }`. Useful for building a storage over a document store. |
249
+ | `runTranscriptStorageTests(makeStorage, options?)` | From `@trigger.dev/sdk/ai/test`. The conformance suite for a storage implementation. Pass `{ api: { describe, it, expect } }` when test globals are off, and `clientData` when your storage scopes by it. |
250
+
221
251
  ## HydrateMessagesEvent
222
252
 
223
- Passed to the `hydrateMessages` callback. See [hydrateMessages](/ai-chat/lifecycle-hooks#hydratemessages).
253
+ Passed to the `hydrateMessages` callback. See [hydrateMessages](/ai-chat/lifecycle-hooks#hydratemessages). `hydrateMessages` is deprecated; `LoadContextEvent`, passed to a storage's `loadContext`, has the same shape.
224
254
 
225
255
  | Field | Type | Description |
226
256
  | ------------------ | ----------------------------------------------------- | --------------------------------------------------------- |
@@ -369,8 +399,8 @@ Passed to `compactUIMessages` and `compactModelMessages` callbacks.
369
399
  | Field | Type | Description |
370
400
  | --------------- | -------------------- | ---------------------------------------------------- |
371
401
  | `summary` | `string` | The generated summary text |
372
- | `uiMessages` | `UIMessage[]` | Current UI messages (full conversation) |
373
- | `modelMessages` | `ModelMessage[]` | Current model messages (full conversation) |
402
+ | `uiMessages` | `UIMessage[]` | Current UI messages (the transcript) |
403
+ | `modelMessages` | `ModelMessage[]` | Current model messages (the lane the model is sent; after a compaction this is the summary plus what followed it, not the whole transcript) |
374
404
  | `chatId` | `string` | Chat session ID |
375
405
  | `turn` | `number` | Current turn (0-indexed) |
376
406
  | `clientData` | `unknown` | Custom data from the frontend |
@@ -514,6 +544,7 @@ All methods available on the `chat` object from `@trigger.dev/sdk/ai`.
514
544
  | `chat.messages` | Incoming message mailbox; supports non-consuming `.peek()` / `.hasPending()`, single-record `.next()`, `.on()`, and suspend-aware `.waitWithIdleTimeout()` |
515
545
  | `chat.local<T>({ id })` | Create a per-run typed local (see [`chat.local`](/ai-chat/chat-local)) |
516
546
  | `chat.createStartSessionAction(taskId, options?)` | Returns a server action that creates a chat Session + triggers the first run + returns a session-scoped PAT. Idempotent on `(env, externalId)`. |
547
+ | `chat.createLoadTranscriptAction(storage, options?)` | Returns a server action that reads a conversation from a transcript storage (`{ chatId, clientData?, limit?, before? }` → `TranscriptLoadResult`). Pair with `useLoadTranscript`. Performs no application authorization: authorize `chatId` for the signed-in user in your own code. See [Transcript storage](/ai-chat/transcript-storage#reading-the-transcript) |
517
548
  | `chat.waitForHandover(options)` | Wait for a [`chat.headStart`](/ai-chat/fast-starts#handover-with-custom-agents) handover signal in a custom loop. Returns the signal or `null`. `chat.MessageAccumulator` wraps this as `consumeHandover()` / `applyHandover()` |
518
549
  | `chat.requestUpgrade()` | End the current run after this turn so the next message starts on the latest agent version. Server-orchestrated handoff. |
519
550
  | `chat.close({ reason })` | End the conversation permanently: close the session row, write a terminal `session-closed` record, and exit without a continuation. Decide it before the turn ends (`onBeforeTurnComplete`, not `onTurnComplete`) so the client sees the closed state on that turn. |
@@ -876,6 +907,26 @@ Second argument to `chat.createStartSessionAction(taskId, options?)`. Controls h
876
907
  | `baseURL` | `string \| (ctx: { endpoint: "sessions" \| "auth"; chatId: string }) => string` | `apiClientManager.baseURL` | API base URL. `endpoint` is `"sessions"` for `POST /api/v1/sessions` or `"auth"` for `POST /api/v1/auth/jwt/claims` (only fires when `tokenTTL` is set). |
877
908
  | `fetch` | `(url: string, init: RequestInit, ctx: { endpoint: "sessions" \| "auth"; chatId: string }) => Promise<Response>` | — | Per-request fetch override. Use to route session-create through a trusted edge proxy so `basePayload.metadata` is rewritten before reaching `api.trigger.dev`. |
878
909
 
910
+ ## createLoadTranscriptAction options
911
+
912
+ Second argument to `chat.createLoadTranscriptAction(storage, options?)`.
913
+
914
+ | Option | Type | Default | Description |
915
+ | ----------- | ------------------------ | -------------------------- | --------------------------------------------------------------------------- |
916
+ | `limit` | `number` | — | Page size when the caller passes none. Returns the most recent messages and a `nextCursor`. |
917
+ | `apiClient` | `ApiClientConfiguration` | `apiClientManager` config | Scope the read to a specific API client (secret key, base URL). The default storage reads through it. |
918
+
919
+ ## useLoadTranscript
920
+
921
+ `useLoadTranscript(chatId, load, options?)` from `@trigger.dev/sdk/chat/react`. Loads a conversation through a `chat.createLoadTranscriptAction` action, for rendering before the chat connects. Re-runs when `chatId` changes.
922
+
923
+ | Option | Type | Description |
924
+ | ----------- | ---------------------- | -------------------------------------------------------------------------------------------- |
925
+ | `transport` | `TriggerChatTransport` | Seed the transport's resume cursor for this chat from the transcript, once it knows the session. |
926
+ | `limit` | `number` | Page size passed to the action. |
927
+
928
+ Returns `{ messages, isLoading, error, nextCursor }`. `nextCursor` is the id to pass as `before` to the action for the page before this one.
929
+
879
930
  ## useMultiTabChat
880
931
 
881
932
  React hook for multi-tab message coordination. Import from `@trigger.dev/sdk/chat/react`.
@@ -0,0 +1,233 @@
1
+ ---
2
+ title: "Transcript storage"
3
+ sidebarTitle: "Transcript storage"
4
+ description: "Where a chat.agent conversation is kept: the UIMessages the runtime saves, the platform default, reading history back, and bringing your own database through the TranscriptStorage adapter."
5
+ ---
6
+
7
+ ## Why a conversation needs a home
8
+
9
+ A `chat.agent` conversation outlives a single run. One run answers many turns and survives the idle gaps between them, but a run does end eventually (a version upgrade, its turn limit, a crash), and the next message then boots a fresh run with nothing in memory (see [How it works](/ai-chat/how-it-works)). For that new run to answer in context, the conversation so far has to be read back from somewhere durable. The same store is what a page reload and the dashboard read to show history.
10
+
11
+ That somewhere is a **transcript storage**. You get one by default with no setup: the platform keeps the conversation as a snapshot in object storage, the same blob the Sessions view in the dashboard renders. Bring your own when you want the conversation in your own database instead.
12
+
13
+ ## What gets saved
14
+
15
+ The transcript is a list of **`UIMessage`s**, keyed by `chatId`. A `UIMessage` is the rich, renderable message the frontend works with: an `id`, a `role`, and an array of `parts` (text, reasoning, tool calls and their results, and any custom `data-*` parts). It is the same shape your React app holds and the same shape the dashboard renders, so what you store is exactly what a user sees.
16
+
17
+ <Note>
18
+ `UIMessage`s are not what the model reads. Each turn the runtime derives a `ModelMessage[]` from the transcript, the flattened `{ role, content }` form an LLM takes, and hands it to your `run()` as `messages`. The transcript storage never deals in `ModelMessage`s. It holds the UI messages; the model's view is derived from them.
19
+ </Note>
20
+
21
+ Keeping the UI shape is deliberate. It is lossless (a tool call and its result survive as parts), it is what renders, and the model's view can be rebuilt from it. Two things cannot be rebuilt from the messages alone, so the runtime hands them to the storage as well:
22
+
23
+ - **`state`**: an opaque record for what the model saw that the transcript does not capture, a [compaction](/ai-chat/compaction) summary and [injected context](/ai-chat/background-injection). Store it as-is and give it back on load.
24
+ - **cursors**: the stream positions the next run resumes from. Persist them opaquely; a storage never reads them.
25
+
26
+ So a save is: the messages, a `state` blob, and two cursors. Nothing else.
27
+
28
+ ## The default storage
29
+
30
+ Do nothing and you get the platform snapshot: the whole conversation written to object storage after each change, read back when a run continues. It is the blob the dashboard's Sessions view renders, and it needs no configuration.
31
+
32
+ ```ts
33
+ import { chat } from "@trigger.dev/sdk/ai";
34
+ import { anthropic } from "@ai-sdk/anthropic";
35
+
36
+ export const myChat = chat.agent({
37
+ id: "my-chat",
38
+ run: async ({ messages, signal, streamText }) =>
39
+ streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal }),
40
+ });
41
+ ```
42
+
43
+ The default rewrites the whole conversation on every turn. That is fine for most chats and costs one write. When it stops being fine, or when you want the conversation in a database you already run, you bring your own.
44
+
45
+ ## Bring your own storage
46
+
47
+ Set `storage` on the agent to persist the conversation yourself:
48
+
49
+ ```ts
50
+ import { chat } from "@trigger.dev/sdk/ai";
51
+ import { anthropic } from "@ai-sdk/anthropic";
52
+ import { myTranscriptStorage } from "./transcript-storage";
53
+
54
+ export const myChat = chat.agent({
55
+ id: "my-chat",
56
+ storage: myTranscriptStorage,
57
+ run: async ({ messages, signal, streamText }) =>
58
+ streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal }),
59
+ });
60
+ ```
61
+
62
+ Reasons to:
63
+
64
+ - **Your database is the source of truth.** History lives next to the rest of your data, queryable, backed up, and deletable on your terms.
65
+ - **Cheaper writes on long chats.** A row-per-message store writes only what changed on a turn instead of rewriting the whole conversation.
66
+ - **Render history in one query** from your own tables, the same `load` the runtime uses.
67
+ - **Own the model's context** for branching, trust boundaries, or rollback (see [Owning the model's context](#owning-the-models-context)).
68
+
69
+ The runtime drives the storage. You never decide when to write, what a regenerate means for your rows, or how a crash mid-answer is recovered. Those decisions are the same for every backend, so they live in the runtime; your job is to store what it hands you and give it back.
70
+
71
+ ## The interface
72
+
73
+ ```ts
74
+ type TranscriptStorage<TClientData = unknown> = {
75
+ load(
76
+ scope: { chatId: string; clientData: TClientData },
77
+ opts?: { limit?: number; before?: string }
78
+ ): Promise<{
79
+ messages: UIMessage[];
80
+ state: unknown | null;
81
+ cursors?: { lastOutEventId?: string; lastInEventId?: string };
82
+ nextCursor?: string;
83
+ }>;
84
+
85
+ save(
86
+ ctx: {
87
+ chatId: string;
88
+ clientData: TClientData;
89
+ turn: number;
90
+ trigger: "submit-message" | "regenerate-message" | "action";
91
+ runId: string;
92
+ ctx: TaskRunContext;
93
+ },
94
+ changeset: {
95
+ reason: "turn-complete" | "turn-error" | "action" | "compaction" | "recovery";
96
+ changes: TranscriptChange[];
97
+ transcript: { entries: Array<{ id: string; final: boolean; message: UIMessage }>; state: unknown | null };
98
+ cursors?: { lastOutEventId?: string; lastInEventId?: string };
99
+ }
100
+ ): Promise<void>;
101
+
102
+ loadContext?(
103
+ scope: { chatId: string; clientData: TClientData },
104
+ event: LoadContextEvent
105
+ ): Promise<UIMessage[]>;
106
+ };
107
+
108
+ type TranscriptChange =
109
+ | { op: "put"; message: UIMessage; final?: boolean }
110
+ | { op: "remove"; id: string }
111
+ | { op: "truncateAfter"; afterId: string }
112
+ | { op: "state"; value: unknown | null };
113
+ ```
114
+
115
+ `load` returns the conversation. `save` records a change to it. `loadContext` is optional and covered [below](#owning-the-models-context). All the types are exported from `@trigger.dev/sdk/ai`.
116
+
117
+ `scope` is the tenant of a read: the `chatId` and the `clientData` your app passed. `ctx` on a save is the same plus the run it happened in. `clientData` is how the runtime hands you the tenant; use it to scope or authorize where your backend needs to.
118
+
119
+ ## What the runtime hands `save`
120
+
121
+ A changeset carries the same save two ways, and a storage uses whichever suits its shape.
122
+
123
+ `changes` is the ordered list of what changed since the last save. A row-per-message store applies them, as one transaction where the backend supports one:
124
+
125
+ | Change | Meaning |
126
+ | --- | --- |
127
+ | `put` | Upsert by `message.id`. An unknown id appends at the end; a known id is replaced in place. `final` is `false` for a partial answer captured from a turn that failed or was stopped, and `true` otherwise. |
128
+ | `remove` | Delete by id. A no-op for an unknown id. |
129
+ | `truncateAfter` | Drop every message ordered after `afterId`. This is what an undo or a regenerate becomes. A no-op for an unknown id. |
130
+ | `state` | Replace the runtime's opaque record; `null` clears it. |
131
+
132
+ `transcript` is the whole conversation as it stands after those changes, `entries` plus `state`. A store that keeps the conversation as one document (object storage, a key-value store, a JSON column) writes it as-is and keeps no state of its own between saves. The default storage is exactly that: it serialises `transcript` and rewrites the blob.
133
+
134
+ The changes are the intent, spelled out. A normal turn is two `put`s, the user's message and the assistant's answer. A steering message the user sent mid-turn is another `put` in the same changeset. An undo through `chat.history.slice(0, -2)` is one `truncateAfter`. A regenerate is a `truncateAfter` and a `put`. A tool approval that updates the assistant message in place is one `put` for that id. Messages are addressed by id; how you order rows is your concern.
135
+
136
+ A few properties worth knowing:
137
+
138
+ - Saves happen after the turn's answer has reached the browser, so they never delay the response. The runtime awaits each `save` before the run suspends.
139
+ - A `save` that throws is logged and the turn continues. The changes fold into the next changeset, and every change is idempotent, so a retried changeset converges on the same result.
140
+ - A `load` that throws boots the run from the durable stream's recent tail rather than failing.
141
+
142
+ ## Reading the transcript
143
+
144
+ `load` is the one read for every backend, the default included. Call it on your server, scoped to the signed-in user through `clientData`, and pass the result to the browser:
145
+
146
+ ```ts app/actions.ts
147
+ "use server";
148
+ import { chat, defaultStorage } from "@trigger.dev/sdk/ai";
149
+
150
+ export const loadTranscript = chat.createLoadTranscriptAction(defaultStorage, { limit: 50 });
151
+ ```
152
+
153
+ ```tsx app/chat/[chatId]/ChatPage.tsx
154
+ "use client";
155
+ import { useLoadTranscript, useTriggerChatTransport } from "@trigger.dev/sdk/chat/react";
156
+ import { loadTranscript } from "@/app/actions";
157
+
158
+ export function ChatPage({ chatId }: { chatId: string }) {
159
+ const transport = useTriggerChatTransport({ task: "my-chat", accessToken, startSession });
160
+ const { messages, isLoading, nextCursor } = useLoadTranscript(chatId, loadTranscript, {
161
+ transport,
162
+ });
163
+ if (isLoading) return <Spinner />;
164
+ return <ChatView chatId={chatId} initialMessages={messages} transport={transport} />;
165
+ }
166
+ ```
167
+
168
+ <Warning>
169
+ The action receives `chatId` from the browser, so authorize it before returning: check that the signed-in user owns this chat. `defaultStorage` loads purely by `chatId` and does no tenant check of its own, so an exported action with no authorization lets any authenticated user read any chat's transcript. A custom storage can enforce tenancy inside `load` using `clientData`, but the server action is still the place to reject a `chatId` the caller may not read.
170
+ </Warning>
171
+
172
+ `limit` returns the most recent messages and a `nextCursor`; pass it as `before` for the page before that one. With the default storage, a paged read is served by the platform, so a long conversation is not downloaded in full to render its last fifty messages. When you pass `transport` and it already knows the session, the hook seeds its resume cursor from the transcript, so the live subscription opens just past the persisted history instead of replaying it.
173
+
174
+ Swap `defaultStorage` for your own storage and nothing else about the read changes.
175
+
176
+ ## Owning the model's context
177
+
178
+ By default the model's context each turn is the transcript the runtime accumulated, converted to `ModelMessage`s. A storage that declares `loadContext` takes that over: the runtime calls it on every turn and action, with the messages the frontend sent and the transcript the runtime had, and uses the `UIMessage`s it returns as the conversation (converting them to `ModelMessage`s the same way). Reach for it when your database decides what the model sees, for branching conversations, a trust boundary where the browser's history is not to be believed, or a curated context window.
179
+
180
+ ```ts
181
+ const storage: TranscriptStorage<{ userId: string }> = {
182
+ load: (scope, opts) => rows.load(scope, opts),
183
+ save: (ctx, changeset) => rows.save(ctx, changeset),
184
+ loadContext: async ({ chatId, clientData }, { incomingMessages }) => {
185
+ const branch = await rows.activeBranch(chatId, clientData.userId);
186
+ return [...branch, ...incomingMessages];
187
+ },
188
+ };
189
+ ```
190
+
191
+ `save` keeps receiving every change, and crash recovery keeps running. This is the replacement for the deprecated [`hydrateMessages`](/ai-chat/lifecycle-hooks#hydratemessages) hook; setting both `hydrateMessages` and `storage` on an agent is a startup error.
192
+
193
+ ## Writing your own storage
194
+
195
+ The contract is small and the conformance suite checks it. Point the suite at a factory for your storage and run it under vitest or jest:
196
+
197
+ ```ts transcript-storage.test.ts
198
+ import { runTranscriptStorageTests } from "@trigger.dev/sdk/ai/test";
199
+ import { postgresTranscriptStorage } from "./transcript-storage";
200
+
201
+ runTranscriptStorageTests(() => postgresTranscriptStorage(process.env.TEST_DATABASE_URL!));
202
+ ```
203
+
204
+ The suite covers appends and in-place replacement, idempotent `remove` and `truncateAfter`, `state` round-trips, cursors, replaying the same changeset twice, paging, and chat isolation. `memoryTranscriptStorage()` is the reference implementation, and it is handy in your own tests to see exactly what the runtime hands a storage.
205
+
206
+ A few things to get right:
207
+
208
+ - Pick one view and stay with it. Apply `changes` if you store rows, write `transcript` if you store a document; don't mix them within one save.
209
+ - `put` for a known id replaces the message in place; position and ordering don't change.
210
+ - `truncateAfter` and `remove` are idempotent. Applying a changeset twice gives the same result as applying it once.
211
+ - `load` with no options returns the whole conversation in order. With `limit`, return the most recent messages and a `nextCursor` (the id of the oldest returned message) when earlier messages exist.
212
+ - Scope reads and writes by `clientData` where your backend enforces tenancy.
213
+
214
+ ## Guarantees and limits
215
+
216
+ - Crash recovery of a half-written answer is runtime-owned in every configuration. It comes from the durable session stream, which no application database can reconstruct. A storage holds settled turns; the runtime overlays the recovered tail and hands it to `save` like any other change.
217
+ - Bringing your own database does not remove platform custody. Session streams still hold message content for their retention window.
218
+ - The default storage rewrites the whole conversation each turn. A row-per-message storage writes only what changed. That is the reason to plug in your own.
219
+
220
+ ## Migrating from hydrateMessages
221
+
222
+ `hydrateMessages` keeps working with a one-time deprecation warning. Crash recovery runs for it, but the runtime does not write to your store on its behalf. To move:
223
+
224
+ 1. Implement `TranscriptStorage` over your existing tables. Your `hydrateMessages` body becomes `loadContext`; the writes you did in hooks become `save`.
225
+ 2. Set `storage` on the agent and remove `hydrateMessages`. Setting both is an error.
226
+ 3. Run `runTranscriptStorageTests` against your implementation.
227
+
228
+ ## See also
229
+
230
+ - [Persistence and replay](/ai-chat/patterns/persistence-and-replay): how the runtime rebuilds a conversation when a new run boots
231
+ - [Database persistence](/ai-chat/patterns/database-persistence): the hook-based pattern and how it relates
232
+ - [Actions](/ai-chat/actions#actions-and-persistence): what an undo or regenerate becomes in the changeset
233
+ - [Compaction](/ai-chat/compaction): the summary the runtime keeps in `state`
@@ -63,6 +63,12 @@ export default defineConfig({
63
63
 
64
64
  In this example we're using env vars from [Infisical](https://infisical.com).
65
65
 
66
+ <Tip>
67
+ Infisical also offers a native [Secret Sync](/deploy-environment-variables#infisical-secret-sync)
68
+ that pushes secrets to Trigger.dev without a build extension or a redeploy. Use `syncEnvVars` when
69
+ you want to resolve secrets at deploy time in code, or for a service without a native sync.
70
+ </Tip>
71
+
66
72
  ```ts trigger.config.ts
67
73
  import { defineConfig } from "@trigger.dev/sdk";
68
74
  import { syncEnvVars } from "@trigger.dev/build/extensions/core";
@@ -184,14 +184,35 @@ For more information about the context object, see the [Context documentation](/
184
184
 
185
185
  ### Sync env vars from another service
186
186
 
187
- You could use the SDK functions above but it's much easier to use our `syncEnvVars` build extension in your `trigger.config` file.
187
+ There are two ways to pull secrets from another service into Trigger.dev: a native **Secret Sync** (currently [Infisical](https://infisical.com)), or the `syncEnvVars` build extension for any other service.
188
+
189
+ #### Infisical Secret Sync
190
+
191
+ If your secrets live in [Infisical](https://infisical.com), sync them natively, without a build extension or a redeploy. You configure the sync in the Infisical dashboard, and it pushes secrets straight to your [Environment Variables page](#in-the-dashboard) in Trigger.dev. When a secret changes in Infisical the sync updates the matching variable, and your tasks pick up the new value on their next run.
192
+
193
+ Set it up in Infisical in two steps:
194
+
195
+ 1. Add a **Trigger.dev App Connection** using a Trigger.dev Personal Access Token from your account settings. Self-hosted instances are supported.
196
+ 2. Create a **Secret Sync**: choose the connection, pick the target organization, project and environment (Production, Staging, Development, or Preview), and set the secret path to sync.
197
+
198
+ Follow the [Trigger.dev Secret Sync guide](https://infisical.com/docs/integrations/secret-syncs/trigger-dev) in the Infisical docs for the full walkthrough.
199
+
200
+ <Note>
201
+ A Secret Sync only overwrites the keys it manages, leaving variables you set manually untouched.
202
+ Synced variables are marked as [secret](#secret-environment-variables) by default, so they appear
203
+ redacted on the Environment Variables page.
204
+ </Note>
205
+
206
+ #### Using the `syncEnvVars` build extension
207
+
208
+ For any other service, use our `syncEnvVars` build extension in your `trigger.config` file to resolve secrets at deploy time.
188
209
 
189
210
  <Note>
190
211
  To use the `syncEnvVars` build extension, you should first install the `@trigger.dev/build`
191
212
  package into your devDependencies.
192
213
  </Note>
193
214
 
194
- In this example we're using env vars from [Infisical](https://infisical.com).
215
+ In this example we're using env vars from [Infisical](https://infisical.com), but you can adapt it to any secrets manager.
195
216
 
196
217
  ```ts trigger.config.ts
197
218
  import { defineConfig } from "@trigger.dev/sdk";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trigger.dev/sdk",
3
- "version": "0.0.0-prerelease-20260908122921",
3
+ "version": "0.0.0-prerelease-20260909071038",
4
4
  "description": "trigger.dev Node.JS SDK",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -69,7 +69,7 @@
69
69
  "dependencies": {
70
70
  "@opentelemetry/api": "1.9.1",
71
71
  "@opentelemetry/semantic-conventions": "1.41.1",
72
- "@trigger.dev/core": "0.0.0-prerelease-20260908122921",
72
+ "@trigger.dev/core": "0.0.0-prerelease-20260909071038",
73
73
  "uncrypto": "^0.1.3"
74
74
  },
75
75
  "devDependencies": {
@@ -222,8 +222,9 @@ frontend, narrow `useChat` with `InferChatUIMessage<typeof myChat>` from `@trigg
222
222
  `chat.agent` accepts hooks that fire in a fixed per-turn order:
223
223
 
224
224
  ```text
225
- onValidateMessages -> hydrateMessages -> onChatStart (chat's first message only)
226
- -> onTurnStart -> run() -> onBeforeTurnComplete -> onTurnComplete
225
+ onValidateMessages -> storage.loadContext (or the deprecated hydrateMessages)
226
+ -> onChatStart (chat's first message only)
227
+ -> onTurnStart -> run() -> onBeforeTurnComplete -> onTurnComplete -> storage.save
227
228
  ```
228
229
 
229
230
  `onBoot` fires once per worker process (every fresh boot, including continuation runs) and is where
@@ -193,11 +193,11 @@ compaction: {
193
193
  to also emit a model response, built with the `streamText` from `onAction`'s own argument so it
194
194
  carries the agent's prompt and tools like any other turn.
195
195
 
196
- Persistence splits by model. Without `hydrateMessages` the runtime snapshots the conversation after
197
- an action that changed it, so a rollback or a returned response survives the run ending. With
198
- `hydrateMessages` your store is the source of truth and the runtime does not write, so mirror every
199
- mutation yourself: a regenerate is a delete and an insert, and `chat.pipeAndCapture` hands back the
200
- same assistant message the runtime would have captured.
196
+ Persistence goes through the agent's transcript storage (`storage` on `chat.agent`; the platform
197
+ snapshot by default). After an action that changed the conversation the runtime hands the storage a
198
+ changeset: an undo is one `truncateAfter`, a regenerate is a `truncateAfter` plus the new answer's
199
+ `put`. With the deprecated `hydrateMessages` your store is the source of truth and the runtime does
200
+ not write, so mirror every mutation yourself: a regenerate is a delete and an insert.
201
201
 
202
202
  ```ts
203
203
  export const myChat = chat.agent({