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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/dist/commonjs/imports/ai-runtime-cjs.cjs.map +1 -1
  2. package/dist/commonjs/imports/ai-runtime.js +0 -2
  3. package/dist/commonjs/v3/ai-shared.d.ts +0 -15
  4. package/dist/commonjs/v3/ai-shared.js +1 -35
  5. package/dist/commonjs/v3/ai-shared.js.map +1 -1
  6. package/dist/commonjs/v3/ai.d.ts +25 -275
  7. package/dist/commonjs/v3/ai.js +308 -1283
  8. package/dist/commonjs/v3/ai.js.map +1 -1
  9. package/dist/commonjs/v3/chat-client.d.ts +2 -11
  10. package/dist/commonjs/v3/chat-client.js +111 -227
  11. package/dist/commonjs/v3/chat-client.js.map +1 -1
  12. package/dist/commonjs/v3/chat-react.d.ts +0 -88
  13. package/dist/commonjs/v3/chat-react.js +1 -130
  14. package/dist/commonjs/v3/chat-react.js.map +1 -1
  15. package/dist/commonjs/v3/chat-server.d.ts +6 -42
  16. package/dist/commonjs/v3/chat-server.js +7 -52
  17. package/dist/commonjs/v3/chat-server.js.map +1 -1
  18. package/dist/commonjs/v3/chat.d.ts +10 -92
  19. package/dist/commonjs/v3/chat.js +73 -328
  20. package/dist/commonjs/v3/chat.js.map +1 -1
  21. package/dist/commonjs/v3/sessions.d.ts +2 -15
  22. package/dist/commonjs/v3/sessions.js +1 -12
  23. package/dist/commonjs/v3/sessions.js.map +1 -1
  24. package/dist/commonjs/v3/shared.js +36 -30
  25. package/dist/commonjs/v3/shared.js.map +1 -1
  26. package/dist/commonjs/v3/test/index.d.ts +0 -1
  27. package/dist/commonjs/v3/test/index.js +1 -3
  28. package/dist/commonjs/v3/test/index.js.map +1 -1
  29. package/dist/commonjs/v3/test/mock-chat-agent.d.ts +4 -47
  30. package/dist/commonjs/v3/test/mock-chat-agent.js +4 -115
  31. package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
  32. package/dist/commonjs/v3/test/test-session-handle.js +0 -6
  33. package/dist/commonjs/v3/test/test-session-handle.js.map +1 -1
  34. package/dist/commonjs/version.js +1 -1
  35. package/dist/esm/imports/ai-runtime.d.ts +2 -2
  36. package/dist/esm/imports/ai-runtime.js +2 -2
  37. package/dist/esm/imports/ai-runtime.js.map +1 -1
  38. package/dist/esm/v3/ai-shared.d.ts +0 -15
  39. package/dist/esm/v3/ai-shared.js +0 -33
  40. package/dist/esm/v3/ai-shared.js.map +1 -1
  41. package/dist/esm/v3/ai.d.ts +25 -275
  42. package/dist/esm/v3/ai.js +305 -1275
  43. package/dist/esm/v3/ai.js.map +1 -1
  44. package/dist/esm/v3/chat-client.d.ts +2 -11
  45. package/dist/esm/v3/chat-client.js +112 -228
  46. package/dist/esm/v3/chat-client.js.map +1 -1
  47. package/dist/esm/v3/chat-react.d.ts +0 -88
  48. package/dist/esm/v3/chat-react.js +1 -127
  49. package/dist/esm/v3/chat-react.js.map +1 -1
  50. package/dist/esm/v3/chat-server.d.ts +6 -42
  51. package/dist/esm/v3/chat-server.js +8 -53
  52. package/dist/esm/v3/chat-server.js.map +1 -1
  53. package/dist/esm/v3/chat.d.ts +10 -92
  54. package/dist/esm/v3/chat.js +74 -329
  55. package/dist/esm/v3/chat.js.map +1 -1
  56. package/dist/esm/v3/sessions.d.ts +2 -15
  57. package/dist/esm/v3/sessions.js +1 -11
  58. package/dist/esm/v3/sessions.js.map +1 -1
  59. package/dist/esm/v3/shared.js +23 -17
  60. package/dist/esm/v3/shared.js.map +1 -1
  61. package/dist/esm/v3/test/index.d.ts +0 -1
  62. package/dist/esm/v3/test/index.js +0 -1
  63. package/dist/esm/v3/test/index.js.map +1 -1
  64. package/dist/esm/v3/test/mock-chat-agent.d.ts +4 -47
  65. package/dist/esm/v3/test/mock-chat-agent.js +6 -117
  66. package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
  67. package/dist/esm/v3/test/test-session-handle.js +0 -6
  68. package/dist/esm/v3/test/test-session-handle.js.map +1 -1
  69. package/dist/esm/version.js +1 -1
  70. package/docs/ai-chat/actions.mdx +23 -55
  71. package/docs/ai-chat/anatomy.mdx +3 -3
  72. package/docs/ai-chat/backend.mdx +48 -125
  73. package/docs/ai-chat/background-injection.mdx +19 -69
  74. package/docs/ai-chat/client-protocol.mdx +4 -5
  75. package/docs/ai-chat/compaction.mdx +7 -13
  76. package/docs/ai-chat/custom-agents.mdx +0 -23
  77. package/docs/ai-chat/fast-starts.mdx +20 -27
  78. package/docs/ai-chat/frontend.mdx +14 -19
  79. package/docs/ai-chat/lifecycle-hooks.mdx +2 -6
  80. package/docs/ai-chat/migrating-from-a-route-handler.mdx +14 -16
  81. package/docs/ai-chat/patterns/database-persistence.mdx +2 -6
  82. package/docs/ai-chat/patterns/persistence-and-replay.mdx +23 -34
  83. package/docs/ai-chat/patterns/skills.mdx +10 -7
  84. package/docs/ai-chat/patterns/version-upgrades.mdx +6 -79
  85. package/docs/ai-chat/pending-messages.mdx +3 -3
  86. package/docs/ai-chat/prompt-caching.mdx +25 -23
  87. package/docs/ai-chat/quick-start.mdx +11 -11
  88. package/docs/ai-chat/reference.mdx +9 -67
  89. package/docs/ai-chat/sessions.mdx +1 -6
  90. package/docs/ai-chat/testing.mdx +1 -2
  91. package/docs/ai-chat/tools.mdx +13 -18
  92. package/docs/ai-chat/upgrade-guide.mdx +2 -2
  93. package/docs/apikeys.mdx +45 -27
  94. package/docs/config/extensions/syncEnvVars.mdx +0 -6
  95. package/docs/deploy-environment-variables.mdx +2 -23
  96. package/docs/deployment/overview.mdx +8 -4
  97. package/docs/deployment/preview-branches.mdx +4 -4
  98. package/docs/deployment/version-skew-protection.mdx +0 -62
  99. package/docs/manual-setup.mdx +7 -7
  100. package/docs/mcp-tools.mdx +0 -9
  101. package/docs/quick-start.mdx +3 -3
  102. package/docs/realtime/auth.mdx +1 -1
  103. package/docs/self-hosting/security.mdx +0 -5
  104. package/docs/tasks/scheduled.mdx +0 -24
  105. package/docs/triggering.mdx +1 -1
  106. package/package.json +4 -4
  107. package/skills/trigger-authoring-chat-agent/SKILL.md +29 -41
  108. package/skills/trigger-chat-agent-advanced/SKILL.md +12 -31
  109. package/dist/commonjs/v3/chatSnapshotIo.d.ts +0 -2
  110. package/dist/commonjs/v3/chatSnapshotIo.js +0 -174
  111. package/dist/commonjs/v3/chatSnapshotIo.js.map +0 -1
  112. package/dist/commonjs/v3/chatVersionSkew.d.ts +0 -12
  113. package/dist/commonjs/v3/chatVersionSkew.js +0 -30
  114. package/dist/commonjs/v3/chatVersionSkew.js.map +0 -1
  115. package/dist/commonjs/v3/externalDeploymentId.d.ts +0 -23
  116. package/dist/commonjs/v3/externalDeploymentId.js +0 -43
  117. package/dist/commonjs/v3/externalDeploymentId.js.map +0 -1
  118. package/dist/commonjs/v3/test/transcript-storage-tests.d.ts +0 -40
  119. package/dist/commonjs/v3/test/transcript-storage-tests.js +0 -258
  120. package/dist/commonjs/v3/test/transcript-storage-tests.js.map +0 -1
  121. package/dist/commonjs/v3/transcriptStorage.d.ts +0 -229
  122. package/dist/commonjs/v3/transcriptStorage.js +0 -321
  123. package/dist/commonjs/v3/transcriptStorage.js.map +0 -1
  124. package/dist/esm/v3/chatSnapshotIo.d.ts +0 -2
  125. package/dist/esm/v3/chatSnapshotIo.js +0 -166
  126. package/dist/esm/v3/chatSnapshotIo.js.map +0 -1
  127. package/dist/esm/v3/chatVersionSkew.d.ts +0 -12
  128. package/dist/esm/v3/chatVersionSkew.js +0 -27
  129. package/dist/esm/v3/chatVersionSkew.js.map +0 -1
  130. package/dist/esm/v3/externalDeploymentId.d.ts +0 -23
  131. package/dist/esm/v3/externalDeploymentId.js +0 -38
  132. package/dist/esm/v3/externalDeploymentId.js.map +0 -1
  133. package/dist/esm/v3/test/transcript-storage-tests.d.ts +0 -40
  134. package/dist/esm/v3/test/transcript-storage-tests.js +0 -255
  135. package/dist/esm/v3/test/transcript-storage-tests.js.map +0 -1
  136. package/dist/esm/v3/transcriptStorage.d.ts +0 -229
  137. package/dist/esm/v3/transcriptStorage.js +0 -309
  138. package/dist/esm/v3/transcriptStorage.js.map +0 -1
  139. package/docs/ai-chat/patterns/native-compaction.mdx +0 -310
  140. package/docs/ai-chat/transcript-storage.mdx +0 -233
  141. package/docs/reports.mdx +0 -157
  142. package/docs/troubleshooting-zod.mdx +0 -158
@@ -1,310 +0,0 @@
1
- ---
2
- title: "Native compaction & provider fallback"
3
- sidebarTitle: "Native compaction"
4
- description: "Persist provider-native compaction (Anthropic context editing, OpenAI stored responses) across chat.agent turns so history is never re-sent, and fall back between providers without losing the conversation."
5
- ---
6
-
7
- Providers compact a conversation within a single request. Anthropic's [context editing](https://docs.anthropic.com/en/docs/build-with-claude/context-editing) clears old tool-use blocks server-side, and OpenAI's [stored responses](https://platform.openai.com/docs/guides/conversation-state) keep the thread server-side so you only send the delta. Neither changes what your agent has accumulated, so on its own the next turn re-sends the whole transcript again and the token saving is lost.
8
-
9
- This is the gap this page closes. After each turn, mirror what the provider compacted into the agent's stored history with [`chat.history.set()`](/ai-chat/reference#chat-namespace), so the next turn is derived from the already-reduced conversation. And because a native handle is provider-specific, this page also shows how a provider-agnostic [Trigger.dev compaction](/ai-chat/compaction) summary lets you fall back between providers without re-expanding the context.
10
-
11
- <Note>
12
- The full runnable example is [`triggerdotdev/resilient-chat-example`](https://github.com/triggerdotdev/resilient-chat-example). See `native-persist.ts` for the Anthropic persistence flow and `resilient-chat.ts` for OpenAI stored responses plus provider fallback.
13
- </Note>
14
-
15
- ## Two kinds of compaction
16
-
17
- They are not competing; they compose. Native compaction is the per-turn optimization, and Trigger.dev compaction is the durable, portable checkpoint.
18
-
19
- | | Native (provider) | Trigger.dev `compaction` |
20
- | --- | --- | --- |
21
- | Runs | Inside one provider request | Between steps / turns, in your run |
22
- | Scope | Provider-specific (Anthropic edits, OpenAI stored thread) | Provider-agnostic |
23
- | Portable across a provider switch | No, the handle is a cache miss on the other provider | Yes, `summarize` returns a plain string |
24
- | Persisted by default | No, you mirror it in `onTurnComplete` | Yes, replaces model messages and keeps UI messages |
25
-
26
- ## Persist Anthropic native context editing
27
-
28
- Anthropic's `contextManagement` clears old tool-use/tool-result blocks server-side per request, and reports how many it cleared in `providerMetadata.anthropic.contextManagement.appliedEdits` (`clearedToolUses`, `clearedInputTokens`). It does not touch your accumulated history, so on its own the next turn still re-sends everything.
29
-
30
- The fix: read the `appliedEdits` counts as they stream in `onStepFinish`, then after the turn mirror that clearing into stored history with `chat.history.set()`. No custom summarizer is involved, since the provider's native editing drives what gets persisted.
31
-
32
- ```ts /trigger/native-persist.ts
33
- import { chat } from "@trigger.dev/sdk/ai";
34
- import { streamText, stepCountIs, tool, type UIMessage } from "ai";
35
- import { anthropic } from "@ai-sdk/anthropic";
36
- import { z } from "zod";
37
-
38
- const fetchRecord = tool({
39
- description: "Fetch the full text of a record by its numeric id.",
40
- inputSchema: z.object({ id: z.number() }),
41
- execute: async ({ id }) => ({ id, text: `RECORD ${id}: ...` }),
42
- });
43
-
44
- // How many tool-uses Anthropic cleared this turn, per chat. Captured in run(),
45
- // applied in onTurnComplete. An in-memory Map is enough because the run stays
46
- // alive across turns (idleTimeoutInSeconds).
47
- const clearedByChat = new Map<string, number>();
48
-
49
- const isToolPart = (p: { type?: string }) =>
50
- typeof p?.type === "string" && (p.type.startsWith("tool-") || p.type === "dynamic-tool");
51
-
52
- // Drop the oldest n tool parts, mirroring what the provider cleared. A tool call
53
- // and its result live in one part, so pairing stays intact.
54
- function pruneOldestToolParts(messages: UIMessage[], n: number): UIMessage[] {
55
- let toRemove = n;
56
- const out: UIMessage[] = [];
57
- for (const m of messages) {
58
- if (toRemove <= 0 || m.role !== "assistant" || !m.parts) {
59
- out.push(m);
60
- continue;
61
- }
62
- const kept = m.parts.filter((p) => {
63
- if (toRemove > 0 && isToolPart(p)) {
64
- toRemove--;
65
- return false;
66
- }
67
- return true;
68
- });
69
- if (kept.length > 0) out.push({ ...m, parts: kept });
70
- }
71
- return out;
72
- }
73
-
74
- export const nativePersist = chat.agent({
75
- id: "native-persist",
76
- idleTimeoutInSeconds: 120,
77
- tools: { fetchRecord },
78
- run: async ({ messages, chatId, tools, signal }) => {
79
- return streamText({
80
- model: anthropic("claude-sonnet-4-5"),
81
- messages,
82
- tools,
83
- abortSignal: signal,
84
- stopWhen: stepCountIs(12),
85
- providerOptions: {
86
- anthropic: {
87
- contextManagement: {
88
- edits: [
89
- {
90
- type: "clear_tool_uses_20250919",
91
- trigger: { type: "tool_uses", value: 2 },
92
- keep: { type: "tool_uses", value: 1 },
93
- clearToolInputs: true,
94
- },
95
- ],
96
- },
97
- },
98
- },
99
- onStepFinish: ({ providerMetadata }) => {
100
- const cm = providerMetadata?.anthropic?.contextManagement as
101
- | { appliedEdits?: Array<{ type?: string; clearedToolUses?: number }> }
102
- | undefined;
103
- let stepCleared = 0;
104
- for (const e of cm?.appliedEdits ?? []) {
105
- if (e.type === "clear_tool_uses_20250919") stepCleared += e.clearedToolUses ?? 0;
106
- }
107
- if (stepCleared > 0) {
108
- clearedByChat.set(chatId, (clearedByChat.get(chatId) ?? 0) + stepCleared);
109
- }
110
- },
111
- });
112
- },
113
- // After the turn, mirror the server-side clearing into stored history.
114
- onTurnComplete: async ({ chatId, uiMessages }) => {
115
- const cleared = clearedByChat.get(chatId) ?? 0;
116
- if (cleared <= 0) return;
117
- chat.history.set(pruneOldestToolParts(uiMessages, cleared));
118
- clearedByChat.set(chatId, 0);
119
- },
120
- });
121
- ```
122
-
123
- Turn 1 sends the user message and accumulates six tool results. Anthropic clears four of them server-side. `onTurnComplete` prunes those four from stored history, so turn 2 re-sends the smaller conversation (one tool result, not six) instead of the full transcript.
124
-
125
- <Note>
126
- `onTurnComplete` is where persistence happens. Action turns fire `onAction` only, and a `chat.history.set()` inside `run()` is overwritten by the accumulator at turn end. See [Persistence and replay](/ai-chat/patterns/persistence-and-replay#action-turns-no-snapshot-write).
127
- </Note>
128
-
129
- ## Persist OpenAI stored responses
130
-
131
- OpenAI's `store: true` keeps the thread server-side and returns a `responseId`. Pass that back as `previousResponseId` on the next turn and send only the messages since the last assistant reply; everything before it lives on OpenAI's side.
132
-
133
- ```ts /trigger/openai-store.ts
134
- import { chat } from "@trigger.dev/sdk/ai";
135
- import { streamText, stepCountIs, type ModelMessage } from "ai";
136
- import { openai } from "@ai-sdk/openai";
137
-
138
- // Persist the stored-response handle between turns. Replace with your database.
139
- const nativeStore = new Map<string, { previousResponseId: string }>();
140
-
141
- // When OpenAI already holds the thread, send only what is new since the last
142
- // assistant reply. Everything before that lives server-side.
143
- function messagesSinceLastAssistant(messages: ModelMessage[]): ModelMessage[] {
144
- let last = -1;
145
- for (let i = 0; i < messages.length; i++) {
146
- if (messages[i]!.role === "assistant") last = i;
147
- }
148
- return last === -1 ? messages : messages.slice(last + 1);
149
- }
150
-
151
- export const openaiStore = chat.agent({
152
- id: "openai-store",
153
- idleTimeoutInSeconds: 120,
154
- run: async ({ messages, chatId, signal }) => {
155
- const native = nativeStore.get(chatId);
156
- const outbound = native ? messagesSinceLastAssistant(messages) : messages;
157
-
158
- const result = streamText({
159
- model: openai("gpt-4o"),
160
- messages: outbound,
161
- abortSignal: signal,
162
- stopWhen: stepCountIs(5),
163
- providerOptions: {
164
- openai: native ? { store: true, previousResponseId: native.previousResponseId } : { store: true },
165
- },
166
- });
167
-
168
- // Capture the response id off the metadata for the next turn.
169
- void result.providerMetadata.then((meta) => {
170
- const rid = typeof meta?.openai?.responseId === "string" ? meta.openai.responseId : undefined;
171
- if (rid) nativeStore.set(chatId, { previousResponseId: rid });
172
- });
173
-
174
- return result;
175
- },
176
- });
177
- ```
178
-
179
- Turn 1 stores the thread and sends all three messages. Turn 2 sends only the new user message (`1/3`), because OpenAI already has the rest.
180
-
181
- ## Fall back between providers without losing history
182
-
183
- A native handle is a per-provider cache. An OpenAI `previousResponseId` means nothing to Anthropic, and Anthropic's server-side edits don't exist on OpenAI. So when a provider is down and you fall back to another, the native optimization is a cache miss, and a naive fallback re-sends the entire raw transcript to the new provider.
184
-
185
- [Trigger.dev's `compaction`](/ai-chat/compaction) is the portable checkpoint that closes this gap. `summarize` returns a plain string and `compactModelMessages` returns neutral `ModelMessage[]`, so the summary survives any provider switch. Tag each native handle with the provider that produced it. On a switch it's a cache miss, and you rebuild from the summary instead of re-expanding the context.
186
-
187
- ```ts /trigger/resilient-chat.ts
188
- import { chat } from "@trigger.dev/sdk/ai";
189
- import { streamText, generateText, stepCountIs, generateId, type ModelMessage } from "ai";
190
- import { anthropic } from "@ai-sdk/anthropic";
191
- import { openai } from "@ai-sdk/openai";
192
-
193
- type Provider = "anthropic" | "openai";
194
- const FALLBACK_ORDER: Provider[] = ["anthropic", "openai"];
195
-
196
- // Native handle, tagged with the provider that produced it. Replace with your DB.
197
- type NativeState = { provider: "openai"; previousResponseId: string };
198
- const nativeStore = new Map<string, NativeState>();
199
-
200
- // Provider-agnostic summary: a plain string, portable across any provider.
201
- async function summarizeConversation(messages: ModelMessage[]): Promise<string> {
202
- const { text } = await generateText({
203
- model: openai("gpt-4o-mini"),
204
- messages: [
205
- ...messages,
206
- {
207
- role: "user",
208
- content:
209
- "Summarize this conversation so it can continue with ANY model. " +
210
- "Preserve decisions made, facts established, open questions, and the user's intent.",
211
- },
212
- ],
213
- });
214
- return text;
215
- }
216
-
217
- export const resilientChat = chat.agent({
218
- id: "resilient-chat",
219
- idleTimeoutInSeconds: 120,
220
-
221
- compaction: {
222
- shouldCompact: ({ totalTokens }) => (totalTokens ?? 0) > 80_000,
223
- summarize: ({ messages }) => summarizeConversation(messages),
224
- compactModelMessages: ({ modelMessages, summary }) => [
225
- { role: "user", content: `Summary of the conversation so far:\n\n${summary}` },
226
- ...modelMessages.slice(-2),
227
- ],
228
- compactUIMessages: ({ uiMessages, summary }) => [
229
- {
230
- id: generateId(),
231
- role: "assistant",
232
- parts: [{ type: "text", text: `[Conversation summary]\n\n${summary}` }],
233
- },
234
- ...uiMessages.slice(-2),
235
- ],
236
- },
237
-
238
- // A Trigger.dev compaction is the reset point: the provider's server-side thread
239
- // no longer matches the compacted baseline, so invalidate the native handle.
240
- onCompacted: async ({ chatId }) => {
241
- if (chatId) nativeStore.delete(chatId);
242
- },
243
-
244
- run: async ({ messages, chatId, signal }) => {
245
- let lastError: unknown;
246
- for (const providerId of FALLBACK_ORDER) {
247
- const native = nativeStore.get(chatId);
248
- try {
249
- if (providerId === "openai") {
250
- // On a switch to OpenAI with no matching handle, `messages` is already the
251
- // compacted baseline (summary + recent), so raw history is not re-sent.
252
- const useHandle = native?.provider === "openai";
253
- const result = streamText({
254
- model: openai("gpt-4o"),
255
- messages,
256
- abortSignal: signal,
257
- stopWhen: stepCountIs(5),
258
- providerOptions: {
259
- openai: useHandle
260
- ? { store: true, previousResponseId: native!.previousResponseId }
261
- : { store: true },
262
- },
263
- });
264
- void result.providerMetadata.then((meta) => {
265
- const rid = typeof meta?.openai?.responseId === "string" ? meta.openai.responseId : undefined;
266
- if (rid) nativeStore.set(chatId, { provider: "openai", previousResponseId: rid });
267
- });
268
- return result;
269
- }
270
-
271
- return streamText({
272
- model: anthropic("claude-sonnet-4-5"),
273
- messages,
274
- abortSignal: signal,
275
- stopWhen: stepCountIs(5),
276
- providerOptions: {
277
- anthropic: {
278
- contextManagement: {
279
- edits: [{ type: "clear_tool_uses_20250919", trigger: { type: "input_tokens", value: 80_000 }, keep: { type: "tool_uses", value: 3 } }],
280
- },
281
- },
282
- },
283
- });
284
- } catch (error) {
285
- lastError = error; // Provider failed, try the next one in the order.
286
- }
287
- }
288
- throw lastError;
289
- },
290
- });
291
- ```
292
-
293
- When Anthropic is down, the loop falls through to OpenAI. Because `compaction` has already reduced `messages` to a summary plus the last couple of exchanges, the switch sends the portable baseline, not megabytes of raw transcript.
294
-
295
- <Warning>
296
- Fallback here retries a turn that hasn't started streaming yet. Once a response is streaming to the client, a mid-stream provider failure can't be swapped transparently. Surface the error and let the frontend regenerate the turn. See [Error handling](/ai-chat/error-handling).
297
- </Warning>
298
-
299
- ## Production notes
300
-
301
- - **Persist the handles.** The `Map`s above (`nativeStore`, `clearedByChat`) work in the example because the run stays alive across turns, but they don't survive a run boundary. Store native handles and summaries in your database keyed by `chatId`, alongside your [message persistence](/ai-chat/patterns/database-persistence).
302
- - **No cross-provider translation.** Native compaction from one provider never transfers to another. The Trigger.dev `compaction` summary is the only portable baseline across a switch.
303
- - **Native compaction is opt-in per turn.** It applies only for the provider whose `providerOptions` you set on that turn's `streamText` call.
304
-
305
- ## See also
306
-
307
- - [Compaction](/ai-chat/compaction): the provider-agnostic `compaction` option, `onCompacted`, and manual `chat.compact()`.
308
- - [Prompt caching](/ai-chat/prompt-caching): the other per-turn token optimization, and how it interacts with a growing history.
309
- - [Database persistence](/ai-chat/patterns/database-persistence): where to store native handles and summaries for real.
310
- - [Lifecycle hooks](/ai-chat/lifecycle-hooks): `onTurnComplete` and `onCompacted` in the broader hook taxonomy.
@@ -1,233 +0,0 @@
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`