@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.
- package/dist/commonjs/imports/ai-runtime-cjs.cjs.map +1 -1
- package/dist/commonjs/imports/ai-runtime.js +0 -2
- package/dist/commonjs/v3/ai-shared.d.ts +0 -15
- package/dist/commonjs/v3/ai-shared.js +1 -35
- package/dist/commonjs/v3/ai-shared.js.map +1 -1
- package/dist/commonjs/v3/ai.d.ts +25 -275
- package/dist/commonjs/v3/ai.js +308 -1283
- package/dist/commonjs/v3/ai.js.map +1 -1
- package/dist/commonjs/v3/chat-client.d.ts +2 -11
- package/dist/commonjs/v3/chat-client.js +111 -227
- package/dist/commonjs/v3/chat-client.js.map +1 -1
- package/dist/commonjs/v3/chat-react.d.ts +0 -88
- package/dist/commonjs/v3/chat-react.js +1 -130
- package/dist/commonjs/v3/chat-react.js.map +1 -1
- package/dist/commonjs/v3/chat-server.d.ts +6 -42
- package/dist/commonjs/v3/chat-server.js +7 -52
- package/dist/commonjs/v3/chat-server.js.map +1 -1
- package/dist/commonjs/v3/chat.d.ts +10 -92
- package/dist/commonjs/v3/chat.js +73 -328
- package/dist/commonjs/v3/chat.js.map +1 -1
- package/dist/commonjs/v3/sessions.d.ts +2 -15
- package/dist/commonjs/v3/sessions.js +1 -12
- package/dist/commonjs/v3/sessions.js.map +1 -1
- package/dist/commonjs/v3/shared.js +36 -30
- package/dist/commonjs/v3/shared.js.map +1 -1
- package/dist/commonjs/v3/test/index.d.ts +0 -1
- package/dist/commonjs/v3/test/index.js +1 -3
- package/dist/commonjs/v3/test/index.js.map +1 -1
- package/dist/commonjs/v3/test/mock-chat-agent.d.ts +4 -47
- package/dist/commonjs/v3/test/mock-chat-agent.js +4 -115
- package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
- package/dist/commonjs/v3/test/test-session-handle.js +0 -6
- package/dist/commonjs/v3/test/test-session-handle.js.map +1 -1
- package/dist/commonjs/version.js +1 -1
- package/dist/esm/imports/ai-runtime.d.ts +2 -2
- package/dist/esm/imports/ai-runtime.js +2 -2
- package/dist/esm/imports/ai-runtime.js.map +1 -1
- package/dist/esm/v3/ai-shared.d.ts +0 -15
- package/dist/esm/v3/ai-shared.js +0 -33
- package/dist/esm/v3/ai-shared.js.map +1 -1
- package/dist/esm/v3/ai.d.ts +25 -275
- package/dist/esm/v3/ai.js +305 -1275
- package/dist/esm/v3/ai.js.map +1 -1
- package/dist/esm/v3/chat-client.d.ts +2 -11
- package/dist/esm/v3/chat-client.js +112 -228
- package/dist/esm/v3/chat-client.js.map +1 -1
- package/dist/esm/v3/chat-react.d.ts +0 -88
- package/dist/esm/v3/chat-react.js +1 -127
- package/dist/esm/v3/chat-react.js.map +1 -1
- package/dist/esm/v3/chat-server.d.ts +6 -42
- package/dist/esm/v3/chat-server.js +8 -53
- package/dist/esm/v3/chat-server.js.map +1 -1
- package/dist/esm/v3/chat.d.ts +10 -92
- package/dist/esm/v3/chat.js +74 -329
- package/dist/esm/v3/chat.js.map +1 -1
- package/dist/esm/v3/sessions.d.ts +2 -15
- package/dist/esm/v3/sessions.js +1 -11
- package/dist/esm/v3/sessions.js.map +1 -1
- package/dist/esm/v3/shared.js +23 -17
- package/dist/esm/v3/shared.js.map +1 -1
- package/dist/esm/v3/test/index.d.ts +0 -1
- package/dist/esm/v3/test/index.js +0 -1
- package/dist/esm/v3/test/index.js.map +1 -1
- package/dist/esm/v3/test/mock-chat-agent.d.ts +4 -47
- package/dist/esm/v3/test/mock-chat-agent.js +6 -117
- package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
- package/dist/esm/v3/test/test-session-handle.js +0 -6
- package/dist/esm/v3/test/test-session-handle.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/docs/ai-chat/actions.mdx +23 -55
- package/docs/ai-chat/anatomy.mdx +3 -3
- package/docs/ai-chat/backend.mdx +48 -125
- package/docs/ai-chat/background-injection.mdx +19 -69
- package/docs/ai-chat/client-protocol.mdx +4 -5
- package/docs/ai-chat/compaction.mdx +7 -13
- package/docs/ai-chat/custom-agents.mdx +0 -23
- package/docs/ai-chat/fast-starts.mdx +20 -27
- package/docs/ai-chat/frontend.mdx +14 -19
- package/docs/ai-chat/lifecycle-hooks.mdx +2 -6
- package/docs/ai-chat/migrating-from-a-route-handler.mdx +14 -16
- package/docs/ai-chat/patterns/database-persistence.mdx +2 -6
- package/docs/ai-chat/patterns/persistence-and-replay.mdx +23 -34
- package/docs/ai-chat/patterns/skills.mdx +10 -7
- package/docs/ai-chat/patterns/version-upgrades.mdx +6 -79
- package/docs/ai-chat/pending-messages.mdx +3 -3
- package/docs/ai-chat/prompt-caching.mdx +25 -23
- package/docs/ai-chat/quick-start.mdx +11 -11
- package/docs/ai-chat/reference.mdx +9 -67
- package/docs/ai-chat/sessions.mdx +1 -6
- package/docs/ai-chat/testing.mdx +1 -2
- package/docs/ai-chat/tools.mdx +13 -18
- package/docs/ai-chat/upgrade-guide.mdx +2 -2
- package/docs/apikeys.mdx +45 -27
- package/docs/config/extensions/syncEnvVars.mdx +0 -6
- package/docs/deploy-environment-variables.mdx +2 -23
- package/docs/deployment/overview.mdx +8 -4
- package/docs/deployment/preview-branches.mdx +4 -4
- package/docs/deployment/version-skew-protection.mdx +0 -62
- package/docs/manual-setup.mdx +7 -7
- package/docs/mcp-tools.mdx +0 -9
- package/docs/quick-start.mdx +3 -3
- package/docs/realtime/auth.mdx +1 -1
- package/docs/self-hosting/security.mdx +0 -5
- package/docs/tasks/scheduled.mdx +0 -24
- package/docs/triggering.mdx +1 -1
- package/package.json +4 -4
- package/skills/trigger-authoring-chat-agent/SKILL.md +29 -41
- package/skills/trigger-chat-agent-advanced/SKILL.md +12 -31
- package/dist/commonjs/v3/chatSnapshotIo.d.ts +0 -2
- package/dist/commonjs/v3/chatSnapshotIo.js +0 -174
- package/dist/commonjs/v3/chatSnapshotIo.js.map +0 -1
- package/dist/commonjs/v3/chatVersionSkew.d.ts +0 -12
- package/dist/commonjs/v3/chatVersionSkew.js +0 -30
- package/dist/commonjs/v3/chatVersionSkew.js.map +0 -1
- package/dist/commonjs/v3/externalDeploymentId.d.ts +0 -23
- package/dist/commonjs/v3/externalDeploymentId.js +0 -43
- package/dist/commonjs/v3/externalDeploymentId.js.map +0 -1
- package/dist/commonjs/v3/test/transcript-storage-tests.d.ts +0 -40
- package/dist/commonjs/v3/test/transcript-storage-tests.js +0 -258
- package/dist/commonjs/v3/test/transcript-storage-tests.js.map +0 -1
- package/dist/commonjs/v3/transcriptStorage.d.ts +0 -229
- package/dist/commonjs/v3/transcriptStorage.js +0 -321
- package/dist/commonjs/v3/transcriptStorage.js.map +0 -1
- package/dist/esm/v3/chatSnapshotIo.d.ts +0 -2
- package/dist/esm/v3/chatSnapshotIo.js +0 -166
- package/dist/esm/v3/chatSnapshotIo.js.map +0 -1
- package/dist/esm/v3/chatVersionSkew.d.ts +0 -12
- package/dist/esm/v3/chatVersionSkew.js +0 -27
- package/dist/esm/v3/chatVersionSkew.js.map +0 -1
- package/dist/esm/v3/externalDeploymentId.d.ts +0 -23
- package/dist/esm/v3/externalDeploymentId.js +0 -38
- package/dist/esm/v3/externalDeploymentId.js.map +0 -1
- package/dist/esm/v3/test/transcript-storage-tests.d.ts +0 -40
- package/dist/esm/v3/test/transcript-storage-tests.js +0 -255
- package/dist/esm/v3/test/transcript-storage-tests.js.map +0 -1
- package/dist/esm/v3/transcriptStorage.d.ts +0 -229
- package/dist/esm/v3/transcriptStorage.js +0 -309
- package/dist/esm/v3/transcriptStorage.js.map +0 -1
- package/docs/ai-chat/patterns/native-compaction.mdx +0 -310
- package/docs/ai-chat/transcript-storage.mdx +0 -233
- package/docs/reports.mdx +0 -157
- package/docs/troubleshooting-zod.mdx +0 -158
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "Background injection"
|
|
3
3
|
sidebarTitle: "Background injection"
|
|
4
|
-
description: "Inject context from background work into the agent's conversation
|
|
4
|
+
description: "Inject context from background work into the agent's conversation — self-review, RAG augmentation, or any async analysis."
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## Overview
|
|
8
8
|
|
|
9
9
|
`chat.inject()` queues model messages for injection into the conversation. Messages are picked up at the start of the next turn or at the next `prepareStep` boundary (between tool-call steps).
|
|
10
10
|
|
|
11
|
-
This is the backend counterpart to [pending messages](/ai-chat/pending-messages)
|
|
11
|
+
This is the backend counterpart to [pending messages](/ai-chat/pending-messages) — pending messages come from the user via the frontend, while `chat.inject()` comes from your task code.
|
|
12
12
|
|
|
13
13
|
## Basic usage
|
|
14
14
|
|
|
@@ -33,9 +33,8 @@ The most powerful pattern combines `chat.defer()` (background work) with `chat.i
|
|
|
33
33
|
```ts
|
|
34
34
|
export const myChat = chat.agent({
|
|
35
35
|
id: "my-chat",
|
|
36
|
-
registry,
|
|
37
36
|
onTurnComplete: async ({ messages }) => {
|
|
38
|
-
// Kick off background analysis
|
|
37
|
+
// Kick off background analysis — doesn't block the turn
|
|
39
38
|
chat.defer(
|
|
40
39
|
(async () => {
|
|
41
40
|
const analysis = await analyzeConversation(messages);
|
|
@@ -48,8 +47,9 @@ export const myChat = chat.agent({
|
|
|
48
47
|
})()
|
|
49
48
|
);
|
|
50
49
|
},
|
|
51
|
-
run: async ({ messages, signal
|
|
50
|
+
run: async ({ messages, signal }) => {
|
|
52
51
|
return streamText({
|
|
52
|
+
...chat.toStreamTextOptions({ registry }),
|
|
53
53
|
messages,
|
|
54
54
|
abortSignal: signal,
|
|
55
55
|
stopWhen: stepCountIs(15),
|
|
@@ -77,7 +77,7 @@ A cheap model reviews the agent's response after each turn and injects coaching
|
|
|
77
77
|
```ts
|
|
78
78
|
import { chat } from "@trigger.dev/sdk/ai";
|
|
79
79
|
import { prompts } from "@trigger.dev/sdk";
|
|
80
|
-
import { generateObject, createProviderRegistry, stepCountIs } from "ai";
|
|
80
|
+
import { streamText, generateObject, createProviderRegistry, stepCountIs } from "ai";
|
|
81
81
|
import { anthropic } from "@ai-sdk/anthropic";
|
|
82
82
|
import { z } from "zod";
|
|
83
83
|
|
|
@@ -98,7 +98,6 @@ Be concise. Only flag issues worth fixing.`,
|
|
|
98
98
|
|
|
99
99
|
export const myChat = chat.agent({
|
|
100
100
|
id: "my-chat",
|
|
101
|
-
registry,
|
|
102
101
|
onTurnComplete: async ({ messages }) => {
|
|
103
102
|
chat.defer(
|
|
104
103
|
(async () => {
|
|
@@ -140,8 +139,9 @@ export const myChat = chat.agent({
|
|
|
140
139
|
})()
|
|
141
140
|
);
|
|
142
141
|
},
|
|
143
|
-
run: async ({ messages, signal
|
|
142
|
+
run: async ({ messages, signal }) => {
|
|
144
143
|
return streamText({
|
|
144
|
+
...chat.toStreamTextOptions({ registry }),
|
|
145
145
|
messages,
|
|
146
146
|
abortSignal: signal,
|
|
147
147
|
stopWhen: stepCountIs(15),
|
|
@@ -150,7 +150,7 @@ export const myChat = chat.agent({
|
|
|
150
150
|
});
|
|
151
151
|
```
|
|
152
152
|
|
|
153
|
-
The self-review runs on `claude-haiku-4-5` (fast, cheap) in the background. If the user sends another message before it completes, the coaching is still injected
|
|
153
|
+
The self-review runs on `claude-haiku-4-5` (fast, cheap) in the background. If the user sends another message before it completes, the coaching is still injected — `chat.inject()` persists across the idle wait.
|
|
154
154
|
|
|
155
155
|
## Other use cases
|
|
156
156
|
|
|
@@ -161,25 +161,25 @@ The self-review runs on `claude-haiku-4-5` (fast, cheap) in the background. If t
|
|
|
161
161
|
|
|
162
162
|
## `chat.defer` standalone
|
|
163
163
|
|
|
164
|
-
`chat.defer()` is also useful on its own, without `chat.inject()`. Any work whose timing has no resume implication
|
|
164
|
+
`chat.defer()` is also useful on its own, without `chat.inject()`. Any work whose timing has no resume implication — analytics, audit logs, search-index writes, cache warming — can run in parallel with streaming instead of in the critical path. All deferred promises are awaited (with a 5s timeout) before `onTurnComplete` fires.
|
|
165
165
|
|
|
166
166
|
```ts
|
|
167
167
|
export const myChat = chat.agent({
|
|
168
168
|
id: "my-chat",
|
|
169
169
|
onTurnStart: async ({ chatId, runId }) => {
|
|
170
|
-
// Analytics
|
|
170
|
+
// Analytics — fire-and-forget, irrelevant to resume.
|
|
171
171
|
chat.defer(analytics.track("turn_started", { chatId, runId }));
|
|
172
172
|
},
|
|
173
|
-
run: async ({ messages, signal
|
|
173
|
+
run: async ({ messages, signal }) => {
|
|
174
174
|
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
175
175
|
},
|
|
176
176
|
});
|
|
177
177
|
```
|
|
178
178
|
|
|
179
|
-
`chat.defer()` can be called from anywhere during a turn
|
|
179
|
+
`chat.defer()` can be called from anywhere during a turn — hooks, `run()`, or nested helpers. All deferred promises are collected and awaited together before `onTurnComplete`.
|
|
180
180
|
|
|
181
181
|
<Warning>
|
|
182
|
-
**Don't use `chat.defer()` for the message-history write in `onTurnStart`.** That write must land *before* the model starts streaming, otherwise a mid-stream page refresh will read `[]` from your DB and lose the user's message from the rendered conversation. See [Database persistence
|
|
182
|
+
**Don't use `chat.defer()` for the message-history write in `onTurnStart`.** That write must land *before* the model starts streaming, otherwise a mid-stream page refresh will read `[]` from your DB and lose the user's message from the rendered conversation. See [Database persistence — `onTurnStart`](/ai-chat/patterns/database-persistence#onturnstart). Reserve `chat.defer` for writes whose timing has no resume implication.
|
|
183
183
|
</Warning>
|
|
184
184
|
|
|
185
185
|
## How it differs from pending messages
|
|
@@ -189,57 +189,9 @@ export const myChat = chat.agent({
|
|
|
189
189
|
| **Source** | Backend task code | Frontend user input |
|
|
190
190
|
| **Triggered by** | Your code (e.g. `onTurnComplete` + `chat.defer()`) | User sending a message during streaming |
|
|
191
191
|
| **Injection point** | Start of next turn, or next `prepareStep` boundary | Next `prepareStep` boundary only |
|
|
192
|
-
| **Message role** | Any
|
|
192
|
+
| **Message role** | Any (`system`, `user`, `assistant`) | Typically `user` |
|
|
193
193
|
| **Frontend visibility** | Not visible unless you write custom `data-*` chunks | Visible via `usePendingMessages` hook |
|
|
194
194
|
|
|
195
|
-
## Two lanes: trusted and untrusted
|
|
196
|
-
|
|
197
|
-
The role you inject with decides more than position. It decides whether the model
|
|
198
|
-
treats the content as trustworthy.
|
|
199
|
-
|
|
200
|
-
**`role: "system"` goes to the instructions lane.** The block is appended to the
|
|
201
|
-
system instructions for subsequent inference calls, so it carries the same standing
|
|
202
|
-
as your system prompt. This is the lane for context the agent should believe:
|
|
203
|
-
entitlements, plan changes, operational notices.
|
|
204
|
-
|
|
205
|
-
It has to work this way. On AI SDK 7 a system message inside `messages` is rejected
|
|
206
|
-
for every provider. `standardizePrompt` throws before any provider is called, and
|
|
207
|
-
its own advice is to use the instructions option, so the injected block goes there
|
|
208
|
-
rather than into the transcript.
|
|
209
|
-
|
|
210
|
-
<Warning>
|
|
211
|
-
The instructions lane is delivered by `chat.toStreamTextOptions()`, because that
|
|
212
|
-
is the only place the SDK can set `streamText`'s instructions for you. If your
|
|
213
|
-
`run()` calls `streamText({ model, messages, abortSignal })` without spreading
|
|
214
|
-
`chat.toStreamTextOptions()`, a `role: "system"` injection never reaches the
|
|
215
|
-
model. The conversational lane has no such requirement: it arrives through
|
|
216
|
-
`messages` either way.
|
|
217
|
-
</Warning>
|
|
218
|
-
|
|
219
|
-
- An injection applies to the next turn only. A block injected in `onTurnComplete`
|
|
220
|
-
shapes the following turn and is cleared after it, so it is not repeated on every
|
|
221
|
-
turn from then on. Within that turn it is consumed once rather than once per read,
|
|
222
|
-
so a `run()` that builds options more than once sees the same instructions in
|
|
223
|
-
every build.
|
|
224
|
-
- The injected text is merged into a single instruction rather than added as a
|
|
225
|
-
second block, because AI SDK 5 rejects an array of system blocks while accepting
|
|
226
|
-
one structured block. Merging changes the cached prefix, so a cached system prompt
|
|
227
|
-
gets no cache hit for as long as an injection is live. If you rely on prompt
|
|
228
|
-
caching, inject sparingly and prefer facts that go stale, so the injection clears.
|
|
229
|
-
|
|
230
|
-
**Any other role joins the conversation, and is untrusted by construction.** A
|
|
231
|
-
message injected as `user` is indistinguishable from something the user typed, and a
|
|
232
|
-
well-aligned model treats it accordingly, and may say so and re-derive the answer
|
|
233
|
-
from tools instead of taking it at face value:
|
|
234
|
-
|
|
235
|
-
> "that text arrived embedded in your message, not from a tool I called, so I
|
|
236
|
-
> verified it myself rather than trusting it"
|
|
237
|
-
|
|
238
|
-
That is correct behaviour, not a bug. So inject **checkable facts** in the
|
|
239
|
-
conversational lane and put **directives** in the instructions lane. A conclusion
|
|
240
|
-
injected as a user message is the worst of both: the model neither trusts it nor
|
|
241
|
-
ignores it, and may contradict it in front of the user.
|
|
242
|
-
|
|
243
195
|
## API reference
|
|
244
196
|
|
|
245
197
|
### chat.inject()
|
|
@@ -248,9 +200,7 @@ ignores it, and may contradict it in front of the user.
|
|
|
248
200
|
chat.inject(messages: ModelMessage[]): void
|
|
249
201
|
```
|
|
250
202
|
|
|
251
|
-
Queue model messages for injection at the next opportunity. Messages persist across the idle wait between turns
|
|
252
|
-
|
|
253
|
-
Lifetime: a conversational message (`role: "user"` or `"assistant"`) becomes part of the model's context from the next turn onward, for the rest of the conversation. It is written to the [transcript storage](/ai-chat/transcript-storage)'s `state`, anchored to the message it followed, so it survives a continuation run and comes back in the same place. It does not appear in the UI transcript. A history edit that rebuilds the context drops it. This holds however the message reached the model: drained before `run()` or at a step boundary inside a multi-step turn. A message that is still queued when the run ends (injected from the last `onTurnComplete` before an exit, for example) is carried in the storage's `state` too and is queued again when the next run boots, so it reaches the next turn. A `role: "system"` message is appended to the instructions for the next turn only and is consumed once. Injecting the same notice every turn adds a copy every turn; dedupe on your side.
|
|
203
|
+
Queue model messages for injection at the next opportunity. Messages persist across the idle wait between turns — they are not reset when a new turn starts.
|
|
254
204
|
|
|
255
205
|
**Parameters:**
|
|
256
206
|
|
|
@@ -259,9 +209,9 @@ Lifetime: a conversational message (`role: "user"` or `"assistant"`) becomes par
|
|
|
259
209
|
| `messages` | `ModelMessage[]` | Model messages to inject (from the `ai` package) |
|
|
260
210
|
|
|
261
211
|
Messages are drained (consumed) when:
|
|
262
|
-
1. A new turn starts
|
|
263
|
-
2. A `prepareStep` boundary is reached
|
|
212
|
+
1. A new turn starts — before `run()` executes
|
|
213
|
+
2. A `prepareStep` boundary is reached — between tool-call steps during streaming
|
|
264
214
|
|
|
265
215
|
<Note>
|
|
266
|
-
`chat.inject()` writes to an in-memory queue in the current process. It works from any code running in the same task
|
|
216
|
+
`chat.inject()` writes to an in-memory queue in the current process. It works from any code running in the same task — lifecycle hooks, deferred work, tool execute functions, etc. It does not work from subtasks or other runs.
|
|
267
217
|
</Note>
|
|
@@ -54,7 +54,7 @@ A single-shell walk-through of the whole protocol — copy, fill in `BASE_URL` /
|
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
56
|
BASE_URL="https://api.trigger.dev" # or your local webapp
|
|
57
|
-
SECRET_KEY="
|
|
57
|
+
SECRET_KEY="tr_dev_..." # secret API key for the env
|
|
58
58
|
TASK_ID="ai-chat" # your chat.agent task id
|
|
59
59
|
CHAT_ID=$(uuidgen | tr '[:upper:]' '[:lower:]')
|
|
60
60
|
|
|
@@ -210,7 +210,6 @@ Pick `"preload"` when the UI has rendered but the user hasn't typed (warms the a
|
|
|
210
210
|
| `triggerConfig.maxAttempts` | `number` | Per-run retry cap (1–10). |
|
|
211
211
|
| `triggerConfig.maxDuration` | `number` | Per-run wall-clock cap, seconds. |
|
|
212
212
|
| `triggerConfig.lockToVersion` | `string` | Pin every run to a specific worker version. |
|
|
213
|
-
| `triggerConfig.externalDeploymentId` | `string \| null` | Pin every run to the deployment carrying this [external deployment id](/deployment/version-skew-protection#chat-sessions). Discovered from the environment when omitted; `null` opts the chat out. |
|
|
214
213
|
| `triggerConfig.region` | `string` | Region preference. |
|
|
215
214
|
| `triggerConfig.idleTimeoutInSeconds` | `number` | Surfaced to the agent through the wire payload (1–3600). |
|
|
216
215
|
|
|
@@ -267,7 +266,7 @@ x-trigger-jwt-claims: {"sub":"...","scopes":["read:runs:run_abc123","write:input
|
|
|
267
266
|
Re-calling `POST /api/v1/sessions` with the same `(taskIdentifier, externalId)` pair is **idempotent for the lifetime of the session**:
|
|
268
267
|
|
|
269
268
|
- If the session is still alive: returns the existing row with `isCached: true`, `runId` unchanged, and a **fresh** 60-minute `publicAccessToken`. No duplicate run is triggered. (Idle/exited runs are different — see [Continuations](#continuations).)
|
|
270
|
-
- If the session has been closed (`POST /api/v1/sessions/{id}/close
|
|
269
|
+
- If the session has been closed (`POST /api/v1/sessions/{id}/close`): returns **HTTP 409**. Closed is one-way; reuse a different `externalId` to start a new conversation.
|
|
271
270
|
- Any tags / metadata / expiresAt / triggerConfig fields you send on the cached path are written through to the row, so you can update e.g. `triggerConfig.basePayload.metadata` mid-conversation. The new fields apply to **future** runs (continuations); the currently-live run keeps its original config.
|
|
272
271
|
|
|
273
272
|
<Warning>
|
|
@@ -694,7 +693,7 @@ The body is a JSON-serialized [`ChatInputChunk`](#chatinputchunk), a tagged unio
|
|
|
694
693
|
| --- | --- |
|
|
695
694
|
| `401` | Missing or invalid `Authorization` header. |
|
|
696
695
|
| `403` | Token doesn't carry `write:sessions:{externalId}`. |
|
|
697
|
-
| `409` | The session is closed
|
|
696
|
+
| `409` | The session is closed — `{ "ok": false, "error": "Cannot append to a closed session" }`. |
|
|
698
697
|
| `413` | Body exceeds 1 MiB **or** the wrapped record would exceed S2's ~1 MiB per-record metered ceiling. A normal `kind: "message"` payload is a few KB; if you hit this you're shipping more than one message per record or pushing a single tool output that's itself oversized. Carries CORS headers so browser fetches can read the status. |
|
|
699
698
|
| `500` | Transient backend failure on the durable stream. Safe to retry — appends are idempotent on `(externalId, X-Part-Id)` if you set the optional `X-Part-Id` request header (the built-in clients set it from a UUID). |
|
|
700
699
|
|
|
@@ -833,7 +832,7 @@ Custom actions (undo, rollback, edit) ride on the same `.in` channel using `kind
|
|
|
833
832
|
}
|
|
834
833
|
```
|
|
835
834
|
|
|
836
|
-
For managed `chat.agent()` tasks, actions wake the agent from suspension (same as messages) and fire the `onAction` hook — they are not turns, so `run()` and turn lifecycle hooks do not fire. If `onAction` returns `
|
|
835
|
+
For managed `chat.agent()` tasks, actions wake the agent from suspension (same as messages) and fire the `onAction` hook — they are not turns, so `run()` and turn lifecycle hooks do not fire. If `onAction` returns a `StreamTextResult`, the response is auto-piped to the frontend (but still no `run()` or `onTurnComplete`). The `action` payload is validated against the agent's `actionSchema`. If the agent didn't register an `actionSchema` (or your `action` payload doesn't match it), validation fails the same way `metadata` does — `.in/append` returns `200 OK`, but the run trace shows `chat turn N [ERROR]` and the wire emits a `turn-complete` control record with no other chunks. See [Actions](/ai-chat/actions) for the agent-side schema setup.
|
|
837
836
|
|
|
838
837
|
Raw `chat.customAgent()` tasks receive `action` as `unknown` and must validate it in their own loop.
|
|
839
838
|
|
|
@@ -19,12 +19,11 @@ Provide `shouldCompact` to decide when to compact and `summarize` to generate th
|
|
|
19
19
|
|
|
20
20
|
```ts
|
|
21
21
|
import { chat } from "@trigger.dev/sdk/ai";
|
|
22
|
-
import { generateText, stepCountIs } from "ai";
|
|
22
|
+
import { streamText, generateText, stepCountIs } from "ai";
|
|
23
23
|
import { anthropic } from "@ai-sdk/anthropic";
|
|
24
24
|
|
|
25
25
|
export const myChat = chat.agent({
|
|
26
26
|
id: "my-chat",
|
|
27
|
-
registry,
|
|
28
27
|
compaction: {
|
|
29
28
|
shouldCompact: ({ totalTokens }) => (totalTokens ?? 0) > 80_000,
|
|
30
29
|
summarize: async ({ messages }) => {
|
|
@@ -35,8 +34,9 @@ export const myChat = chat.agent({
|
|
|
35
34
|
return result.text;
|
|
36
35
|
},
|
|
37
36
|
},
|
|
38
|
-
run: async ({ messages, signal
|
|
37
|
+
run: async ({ messages, signal }) => {
|
|
39
38
|
return streamText({
|
|
39
|
+
...chat.toStreamTextOptions({ registry }),
|
|
40
40
|
messages,
|
|
41
41
|
abortSignal: signal,
|
|
42
42
|
stopWhen: stepCountIs(15),
|
|
@@ -61,12 +61,6 @@ After each turn completes:
|
|
|
61
61
|
|
|
62
62
|
On the next turn, the LLM receives the compact summary instead of the full history — dramatically reducing token usage while preserving context.
|
|
63
63
|
|
|
64
|
-
The compacted context is durable. The runtime writes it to the [transcript storage](/ai-chat/transcript-storage)'s `state` alongside the messages, so a new run that boots to continue the conversation starts from the summary rather than re-reading the whole transcript and summarising it again. An undo or edit that reaches into the summarised part of the conversation clears the stored summary, and compaction runs again from the edited history when the threshold is next crossed.
|
|
65
|
-
|
|
66
|
-
<Note>
|
|
67
|
-
This is Trigger.dev's provider-agnostic compaction. To persist a **provider's own** compaction across turns instead (Anthropic context editing or OpenAI stored responses), and to fall back between providers without re-sending history, see [Native compaction & provider fallback](/ai-chat/patterns/native-compaction).
|
|
68
|
-
</Note>
|
|
69
|
-
|
|
70
64
|
## Customizing what gets persisted
|
|
71
65
|
|
|
72
66
|
By default, compaction only affects model messages — UI messages stay intact so users see the full conversation after a page refresh. You can customize this with `compactUIMessages`:
|
|
@@ -97,7 +91,7 @@ export const myChat = chat.agent({
|
|
|
97
91
|
...uiMessages.slice(-4), // Keep the last 4 messages
|
|
98
92
|
],
|
|
99
93
|
},
|
|
100
|
-
run: async ({ messages, signal
|
|
94
|
+
run: async ({ messages, signal }) => {
|
|
101
95
|
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
102
96
|
},
|
|
103
97
|
});
|
|
@@ -191,7 +185,7 @@ export const myChat = chat.agent({
|
|
|
191
185
|
data: { chatId, summary, totalTokens, messageCount },
|
|
192
186
|
});
|
|
193
187
|
},
|
|
194
|
-
run: async ({ messages, signal
|
|
188
|
+
run: async ({ messages, signal }) => {
|
|
195
189
|
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
196
190
|
},
|
|
197
191
|
});
|
|
@@ -207,7 +201,7 @@ Define a `compact` action that reuses your existing `summarize` function:
|
|
|
207
201
|
|
|
208
202
|
```ts
|
|
209
203
|
import { chat } from "@trigger.dev/sdk/ai";
|
|
210
|
-
import { generateText, generateId, convertToModelMessages } from "ai";
|
|
204
|
+
import { streamText, generateText, generateId, convertToModelMessages } from "ai";
|
|
211
205
|
import { anthropic } from "@ai-sdk/anthropic";
|
|
212
206
|
import { z } from "zod";
|
|
213
207
|
|
|
@@ -249,7 +243,7 @@ export const myChat = chat.agent({
|
|
|
249
243
|
]);
|
|
250
244
|
},
|
|
251
245
|
|
|
252
|
-
run: async ({ messages, signal
|
|
246
|
+
run: async ({ messages, signal }) => {
|
|
253
247
|
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
254
248
|
},
|
|
255
249
|
});
|
|
@@ -290,29 +290,6 @@ Read `turn.stopped` to tell a user stop from a full run cancel:
|
|
|
290
290
|
|
|
291
291
|
A hand-rolled loop wires this itself with `chat.createStopSignal()` and `chat.cleanupAbortedParts()`. Two things `createSession` handles for you are easy to get wrong there — see the [hand-rolled loop checklist](#hand-rolled-loop-checklist).
|
|
292
292
|
|
|
293
|
-
### Ending the conversation
|
|
294
|
-
|
|
295
|
-
`chat.close({ reason })` works in a custom agent exactly as it does in [`chat.agent`](/ai-chat/backend#ending-the-conversation): the session row is closed, further sends are refused with HTTP 409, and no continuation run is scheduled. Call it from anywhere in your loop.
|
|
296
|
-
|
|
297
|
-
```ts trigger/my-chat.ts
|
|
298
|
-
for await (const turn of session) {
|
|
299
|
-
const result = streamText({ model, messages: turn.messages, abortSignal: turn.signal });
|
|
300
|
-
|
|
301
|
-
// Close BEFORE turn.complete(): that call writes the turn boundary the
|
|
302
|
-
// browser stops reading at, and it carries the closed state out with it.
|
|
303
|
-
if (await overBudget(turn.chatId)) {
|
|
304
|
-
chat.close({ reason: "Monthly budget reached" });
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
await turn.complete(result);
|
|
308
|
-
if (turn.stopped) break;
|
|
309
|
-
}
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
The close is performed when your `run()` returns, so it lands whether you `break` out of the loop, return early, or keep iterating. A hand-rolled loop with no iterator at all works the same way, as long as you call `chat.close()` before the `chat.writeTurnComplete()` that ends the turn.
|
|
313
|
-
|
|
314
|
-
Only `chat.agent` and `chat.customAgent` bind the run to its Session, so `chat.close()` throws in a plain `task()`.
|
|
315
|
-
|
|
316
293
|
## Hand-rolled loop with primitives
|
|
317
294
|
|
|
318
295
|
For full control, skip `createSession` and compose the primitives directly:
|
|
@@ -239,11 +239,11 @@ This is an **import-chain** problem, not a runtime one. A "we'll strip the execu
|
|
|
239
239
|
|
|
240
240
|
export const myChat = chat.agent({
|
|
241
241
|
id: "my-chat",
|
|
242
|
-
run: async ({ messages, signal
|
|
242
|
+
run: async ({ messages, signal }) =>
|
|
243
243
|
streamText({
|
|
244
|
+
...chat.toStreamTextOptions({ tools: chatTools }),
|
|
244
245
|
model: anthropic("claude-sonnet-4-6"),
|
|
245
246
|
messages,
|
|
246
|
-
tools: chatTools,
|
|
247
247
|
stopWhen: stepCountIs(10),
|
|
248
248
|
abortSignal: signal,
|
|
249
249
|
}),
|
|
@@ -251,7 +251,7 @@ This is an **import-chain** problem, not a runtime one. A "we'll strip the execu
|
|
|
251
251
|
```
|
|
252
252
|
</Step>
|
|
253
253
|
<Step title="Build the head-start handler">
|
|
254
|
-
Call `chat.headStart({ agentId, run })`. It returns a standard Web Fetch handler: `(req: Request) => Promise<Response>`.
|
|
254
|
+
Call `chat.headStart({ agentId, run })`. It returns a standard Web Fetch handler: `(req: Request) => Promise<Response>`. Inside the `run` callback you call `streamText` yourself and spread `chat.toStreamTextOptions({ tools })` to inherit the SDK-owned wiring (messages, schema-only tools, `stopWhen: stepCountIs(1)`, abort signal). Add your own `model` and `system` on top.
|
|
255
255
|
|
|
256
256
|
```ts lib/chat-handler.ts
|
|
257
257
|
import { chat } from "@trigger.dev/sdk/chat-server";
|
|
@@ -261,23 +261,18 @@ This is an **import-chain** problem, not a runtime one. A "we'll strip the execu
|
|
|
261
261
|
|
|
262
262
|
export const chatHandler = chat.headStart({
|
|
263
263
|
agentId: "my-chat",
|
|
264
|
-
run: async ({
|
|
264
|
+
run: async ({ chat: helper }) =>
|
|
265
265
|
streamText({
|
|
266
|
+
...helper.toStreamTextOptions({ tools: headStartTools }),
|
|
266
267
|
model: anthropic("claude-sonnet-4-6"),
|
|
267
268
|
system: "You are a helpful assistant.",
|
|
268
|
-
tools: headStartTools,
|
|
269
269
|
}),
|
|
270
270
|
});
|
|
271
271
|
```
|
|
272
272
|
|
|
273
|
-
<
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
running past step 1 would splice a stream the agent is supposed to own. Setting
|
|
277
|
-
any of the four at the call site is a type error, and a throw if you get past
|
|
278
|
-
the types, rather than breaking the handover quietly. `chat.toStreamTextOptions()` is still there if you want to build the
|
|
279
|
-
options yourself.
|
|
280
|
-
</Note>
|
|
273
|
+
<Warning>
|
|
274
|
+
Don't set `stopWhen` here. The spread pins it to `stepCountIs(1)`, and overriding it makes the handler run steps the agent is supposed to own — the handover then splices a stream that has already moved past step 1.
|
|
275
|
+
</Warning>
|
|
281
276
|
|
|
282
277
|
<Tip>
|
|
283
278
|
Use the **same model** on both sides (route handler and `chat.agent`) to avoid a tone or style shift between step 1 and step 2+. Your LLM provider keys stay server-side in your warm process — Trigger.dev never holds them in this design.
|
|
@@ -630,27 +625,27 @@ chat.headStart<TTools>({
|
|
|
630
625
|
export const chatHandler = chat.headStart({
|
|
631
626
|
agentId: "my-chat",
|
|
632
627
|
triggerConfig: { tags: ["org:acme"], queue: "chat", machine: "small-2x" },
|
|
633
|
-
run: async ({
|
|
628
|
+
run: async ({ chat: helper }) =>
|
|
629
|
+
streamText({ ...helper.toStreamTextOptions({ tools: headStartTools }), model, system }),
|
|
634
630
|
});
|
|
635
631
|
```
|
|
636
632
|
|
|
637
633
|
The `run` callback receives:
|
|
638
634
|
|
|
639
|
-
- `messages: UIMessage[]
|
|
640
|
-
- `signal: AbortSignal
|
|
641
|
-
- `
|
|
642
|
-
- `chat: HeadStartChatHelper<TTools>`: exposes `chat.toStreamTextOptions({ tools })` for building the options by hand, plus a `chat.session` escape hatch for power users.
|
|
635
|
+
- `messages: UIMessage[]` — user messages parsed from the request body.
|
|
636
|
+
- `signal: AbortSignal` — fires when the request closes or the SDK times out the handover.
|
|
637
|
+
- `chat: HeadStartChatHelper<TTools>` — exposes `chat.toStreamTextOptions({ tools })` and a `chat.session` escape hatch for power users.
|
|
643
638
|
|
|
644
|
-
|
|
639
|
+
`chat.toStreamTextOptions({ tools })` returns options to spread into `streamText`. The SDK owns these keys — overriding them will break the protocol:
|
|
645
640
|
|
|
646
641
|
| Key | What the SDK sets | Why |
|
|
647
642
|
| --- | --- | --- |
|
|
648
643
|
| `messages` | `convertToModelMessages(uiMessages)` | First-turn user history |
|
|
649
|
-
| `
|
|
650
|
-
| `stopWhen` | `stepCountIs(1)` | Step 1 only
|
|
644
|
+
| `tools` | What you pass | Schema-only tools for step 1 |
|
|
645
|
+
| `stopWhen` | `stepCountIs(1)` | Step 1 only — agent picks up step 2+ |
|
|
651
646
|
| `abortSignal` | Combined request + idle timeout | Safe cleanup on disconnect |
|
|
652
647
|
|
|
653
|
-
You bring `model`, `system`, `providerOptions`, `prepareStep`, anything else `streamText` accepts.
|
|
648
|
+
You bring `model`, `system`, `providerOptions`, `prepareStep`, anything else `streamText` accepts.
|
|
654
649
|
|
|
655
650
|
#### The transport option
|
|
656
651
|
|
|
@@ -689,11 +684,11 @@ export async function POST(req: Request) {
|
|
|
689
684
|
agentId: "my-chat",
|
|
690
685
|
chatId, // session externalId; reuse it on the destination page
|
|
691
686
|
messages, // first-turn user history
|
|
692
|
-
run: async ({
|
|
687
|
+
run: async ({ chat: helper }) =>
|
|
693
688
|
streamText({
|
|
689
|
+
...helper.toStreamTextOptions({ tools: headStartTools }),
|
|
694
690
|
model: anthropic("claude-sonnet-4-6"),
|
|
695
691
|
system: "You are a helpful assistant.",
|
|
696
|
-
tools: headStartTools,
|
|
697
692
|
}),
|
|
698
693
|
});
|
|
699
694
|
|
|
@@ -740,13 +735,11 @@ chat.startHeadStart<TTools>({
|
|
|
740
735
|
triggerConfig?: Partial<SessionTriggerConfig>, // tags, queue, machine, …
|
|
741
736
|
apiClient?: ApiClientConfiguration, // when the agent lives in another project/env
|
|
742
737
|
metadata?: Record<string, unknown>, // merged into the run payload; never sent to the browser
|
|
743
|
-
}): Promise<{ chatId: string;
|
|
738
|
+
}): Promise<{ chatId: string; completion: Promise<void> }>
|
|
744
739
|
```
|
|
745
740
|
|
|
746
741
|
`completion` resolves once the head start finishes; `await` it or hand it to `waitUntil`. It rejects if the warm step or the dispatch fails.
|
|
747
742
|
|
|
748
|
-
`pendingVersion` is `true` when the agent run is parked waiting for the deployment carrying the session's [external deployment id](/deployment/version-skew-protection#chat-sessions). Step 1 still runs in your process and still reaches the browser, so pass the flag to the destination page if you want it to say a deploy is in progress rather than appear to stall on step 2.
|
|
749
|
-
|
|
750
743
|
### Limitations
|
|
751
744
|
|
|
752
745
|
- **First turn only.** Step 2+ and turn 2+ run on the trigger side. There's no per-turn "head start every turn" mode — the win comes from amortizing agent boot across the LLM call once.
|
|
@@ -137,8 +137,6 @@ On page load, fetch both the messages and the session state from your database,
|
|
|
137
137
|
|
|
138
138
|
Because the underlying Session row outlives individual runs, a chat you were in yesterday resumes against the same chat — even if the original run has long since exited. The transport hydrates from the persisted state and uses `lastEventId` to resubscribe; if the client tries to send a new message and no run is alive, the server triggers a fresh continuation run on the same session before the message is appended.
|
|
139
139
|
|
|
140
|
-
If you do not keep your own copy of the conversation, load it from the agent's [transcript storage](/ai-chat/transcript-storage#reading-the-transcript) instead: `chat.createLoadTranscriptAction(storage)` on the server and `useLoadTranscript(chatId, action, { transport })` in the browser return the messages and seed the transport's resume cursor, for the default storage and your own alike.
|
|
141
|
-
|
|
142
140
|
```tsx app/chat/[chatId]/ChatPage.tsx
|
|
143
141
|
"use client";
|
|
144
142
|
|
|
@@ -444,48 +442,45 @@ function Chat({ chatId, transport }) {
|
|
|
444
442
|
|
|
445
443
|
## Sending actions
|
|
446
444
|
|
|
447
|
-
Send custom actions (undo, rollback, edit
|
|
445
|
+
Send custom actions (undo, rollback, edit) to the agent via `transport.sendAction()`. Actions wake the agent and fire only `hydrateMessages` (if configured) and `onAction` — they're not turns, so `onTurnStart` / `prepareMessages` / `onBeforeTurnComplete` / `onTurnComplete` and `run()` do not fire.
|
|
448
446
|
|
|
449
|
-
|
|
450
|
-
import { useChat } from "@ai-sdk/react";
|
|
451
|
-
import { useChatActions, useTriggerChatTransport } from "@trigger.dev/sdk/chat/react";
|
|
447
|
+
For optimistic UI, mirror the action's effect on the `useChat` state via `setMessages` while the request is in flight:
|
|
452
448
|
|
|
449
|
+
```tsx
|
|
453
450
|
function ChatControls({ chatId }: { chatId: string }) {
|
|
454
451
|
const transport = useTriggerChatTransport({
|
|
455
452
|
task: "my-chat",
|
|
456
453
|
accessToken: ({ chatId }) => mintChatAccessToken(chatId),
|
|
457
|
-
startSession: ({ chatId, clientData }) =>
|
|
454
|
+
startSession: ({ chatId, clientData }) =>
|
|
455
|
+
startChatSession({ chatId, clientData }),
|
|
458
456
|
});
|
|
459
|
-
|
|
460
|
-
const {
|
|
457
|
+
|
|
458
|
+
const { setMessages } = useChat({ transport });
|
|
461
459
|
|
|
462
460
|
return (
|
|
463
461
|
<div>
|
|
464
462
|
<button
|
|
465
463
|
onClick={() => {
|
|
466
|
-
|
|
467
|
-
// push history changes back.
|
|
464
|
+
void transport.sendAction(chatId, { type: "undo" });
|
|
468
465
|
setMessages((prev) => prev.slice(0, -2));
|
|
469
|
-
void sendAction({ type: "undo" });
|
|
470
466
|
}}
|
|
471
467
|
>
|
|
472
468
|
Undo last exchange
|
|
473
469
|
</button>
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
470
|
+
<button
|
|
471
|
+
onClick={() => transport.sendAction(chatId, { type: "rollback", targetMessageId: "msg-5" })}
|
|
472
|
+
>
|
|
473
|
+
Rollback to message
|
|
477
474
|
</button>
|
|
478
475
|
</div>
|
|
479
476
|
);
|
|
480
477
|
}
|
|
481
478
|
```
|
|
482
479
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
The action payload is validated against the agent's `actionSchema` on the backend; invalid actions are rejected. See [Actions](/ai-chat/actions) for the backend setup.
|
|
480
|
+
The action payload is validated against the agent's `actionSchema` on the backend — invalid actions are rejected. See [Actions](/ai-chat/actions) for the backend setup.
|
|
486
481
|
|
|
487
482
|
<Note>
|
|
488
|
-
`
|
|
483
|
+
`sendAction` returns a `ReadableStream<UIMessageChunk>`. For side-effect-only actions (where `onAction` returns `void`), the stream completes immediately with `trigger:turn-complete`. For actions where `onAction` returns a `StreamTextResult`, the stream carries the assistant chunks the same way `sendMessages` does — `useChat` consumes them automatically.
|
|
489
484
|
</Note>
|
|
490
485
|
|
|
491
486
|
For server-to-server usage, `AgentChat` has the same method:
|
|
@@ -86,7 +86,7 @@ export const myChat = chat.agent({
|
|
|
86
86
|
|
|
87
87
|
Fires once on a continuation boot when the dead predecessor was mid-stream — a partial assistant survives on `session.out`. The runtime reconstructs context automatically via a smart default; this hook is the override path for policies that need something different.
|
|
88
88
|
|
|
89
|
-
The hook does NOT fire when there's no partial — clean continuations after `chat.endRun()` or `chat.requestUpgrade()`, fresh chats, OOM retries on top of a complete snapshot. Those paths dispatch any in-flight user message as a normal turn on the new run without involving the hook. It
|
|
89
|
+
The hook does NOT fire when there's no partial — clean continuations after `chat.endRun()` or `chat.requestUpgrade()`, fresh chats, OOM retries on top of a complete snapshot. Those paths dispatch any in-flight user message as a normal turn on the new run without involving the hook. It also does NOT fire when [`hydrateMessages`](#hydratemessages) is registered (the customer owns persistence).
|
|
90
90
|
|
|
91
91
|
```ts
|
|
92
92
|
export const myChat = chat.agent({
|
|
@@ -260,10 +260,6 @@ export const myChat = chat.agent({
|
|
|
260
260
|
|
|
261
261
|
## hydrateMessages
|
|
262
262
|
|
|
263
|
-
<Warning>
|
|
264
|
-
`hydrateMessages` is deprecated. Give the agent a [transcript storage](/ai-chat/transcript-storage) instead: `loadContext` on the storage decides the model's context, and `save` persists every change, so crash recovery and durable compaction cover it. Existing agents keep working with a one-time warning. Setting `hydrateMessages` together with `storage` is an error.
|
|
265
|
-
</Warning>
|
|
266
|
-
|
|
267
263
|
Load the full message history from your backend on every turn, replacing the built-in linear accumulator. When set, the hook's return value becomes the accumulated state; the normal accumulation logic (append for submit, replace for regenerate) is skipped entirely.
|
|
268
264
|
|
|
269
265
|
Use this when the backend should be the source of truth for message history: abuse prevention, branching conversations (DAGs), or rollback/undo support.
|
|
@@ -319,7 +315,7 @@ After the hook returns, the runtime overlays the wire's tool-state advances (`ou
|
|
|
319
315
|
</Note>
|
|
320
316
|
|
|
321
317
|
<Tip>
|
|
322
|
-
Registering `hydrateMessages`
|
|
318
|
+
Registering `hydrateMessages` short-circuits the runtime's [snapshot + replay](/ai-chat/patterns/persistence-and-replay) reconstruction at run boot — your hook is the single source of truth for history, so the runtime skips reading or writing the snapshot entirely. No object storage traffic, no replay cost. The trade-off is that you own persistence end-to-end.
|
|
323
319
|
</Tip>
|
|
324
320
|
|
|
325
321
|
<Note>
|