@trigger.dev/sdk 4.5.16 → 4.6.0
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 +2 -0
- package/dist/commonjs/v3/ai-shared.d.ts +15 -0
- package/dist/commonjs/v3/ai-shared.js +35 -1
- package/dist/commonjs/v3/ai-shared.js.map +1 -1
- package/dist/commonjs/v3/ai.d.ts +311 -25
- package/dist/commonjs/v3/ai.js +1633 -380
- package/dist/commonjs/v3/ai.js.map +1 -1
- package/dist/commonjs/v3/auth.d.ts +10 -4
- package/dist/commonjs/v3/auth.js.map +1 -1
- package/dist/commonjs/v3/chat-client.d.ts +11 -2
- package/dist/commonjs/v3/chat-client.js +227 -111
- package/dist/commonjs/v3/chat-client.js.map +1 -1
- package/dist/commonjs/v3/chat-react.d.ts +88 -0
- package/dist/commonjs/v3/chat-react.js +130 -1
- package/dist/commonjs/v3/chat-react.js.map +1 -1
- package/dist/commonjs/v3/chat-server.d.ts +42 -6
- package/dist/commonjs/v3/chat-server.js +52 -7
- package/dist/commonjs/v3/chat-server.js.map +1 -1
- package/dist/commonjs/v3/chat.d.ts +92 -10
- package/dist/commonjs/v3/chat.js +328 -73
- package/dist/commonjs/v3/chat.js.map +1 -1
- package/dist/commonjs/v3/chatSnapshotIo.d.ts +2 -0
- package/dist/commonjs/v3/chatSnapshotIo.js +173 -0
- package/dist/commonjs/v3/chatSnapshotIo.js.map +1 -0
- package/dist/commonjs/v3/chatVersionSkew.d.ts +12 -0
- package/dist/commonjs/v3/chatVersionSkew.js +30 -0
- package/dist/commonjs/v3/chatVersionSkew.js.map +1 -0
- package/dist/commonjs/v3/externalDeploymentId.d.ts +23 -0
- package/dist/commonjs/v3/externalDeploymentId.js +43 -0
- package/dist/commonjs/v3/externalDeploymentId.js.map +1 -0
- package/dist/commonjs/v3/retry.d.ts +4 -0
- package/dist/commonjs/v3/retry.js +22 -13
- package/dist/commonjs/v3/retry.js.map +1 -1
- package/dist/commonjs/v3/sessions.d.ts +15 -2
- package/dist/commonjs/v3/sessions.js +12 -1
- package/dist/commonjs/v3/sessions.js.map +1 -1
- package/dist/commonjs/v3/shared.js +30 -36
- package/dist/commonjs/v3/shared.js.map +1 -1
- package/dist/commonjs/v3/test/index.d.ts +1 -0
- package/dist/commonjs/v3/test/index.js +3 -1
- package/dist/commonjs/v3/test/index.js.map +1 -1
- package/dist/commonjs/v3/test/mock-chat-agent.d.ts +47 -4
- package/dist/commonjs/v3/test/mock-chat-agent.js +116 -10
- package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
- package/dist/commonjs/v3/test/test-session-handle.js +6 -0
- package/dist/commonjs/v3/test/test-session-handle.js.map +1 -1
- package/dist/commonjs/v3/test/transcript-storage-tests.d.ts +40 -0
- package/dist/commonjs/v3/test/transcript-storage-tests.js +258 -0
- package/dist/commonjs/v3/test/transcript-storage-tests.js.map +1 -0
- package/dist/commonjs/v3/transcriptStorage.d.ts +261 -0
- package/dist/commonjs/v3/transcriptStorage.js +374 -0
- package/dist/commonjs/v3/transcriptStorage.js.map +1 -0
- 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 +15 -0
- package/dist/esm/v3/ai-shared.js +33 -0
- package/dist/esm/v3/ai-shared.js.map +1 -1
- package/dist/esm/v3/ai.d.ts +311 -25
- package/dist/esm/v3/ai.js +1625 -377
- package/dist/esm/v3/ai.js.map +1 -1
- package/dist/esm/v3/auth.d.ts +10 -4
- package/dist/esm/v3/auth.js.map +1 -1
- package/dist/esm/v3/chat-client.d.ts +11 -2
- package/dist/esm/v3/chat-client.js +228 -112
- package/dist/esm/v3/chat-client.js.map +1 -1
- package/dist/esm/v3/chat-react.d.ts +88 -0
- package/dist/esm/v3/chat-react.js +127 -1
- package/dist/esm/v3/chat-react.js.map +1 -1
- package/dist/esm/v3/chat-server.d.ts +42 -6
- package/dist/esm/v3/chat-server.js +53 -8
- package/dist/esm/v3/chat-server.js.map +1 -1
- package/dist/esm/v3/chat.d.ts +92 -10
- package/dist/esm/v3/chat.js +329 -74
- package/dist/esm/v3/chat.js.map +1 -1
- package/dist/esm/v3/chatSnapshotIo.d.ts +2 -0
- package/dist/esm/v3/chatSnapshotIo.js +165 -0
- package/dist/esm/v3/chatSnapshotIo.js.map +1 -0
- package/dist/esm/v3/chatVersionSkew.d.ts +12 -0
- package/dist/esm/v3/chatVersionSkew.js +27 -0
- package/dist/esm/v3/chatVersionSkew.js.map +1 -0
- package/dist/esm/v3/externalDeploymentId.d.ts +23 -0
- package/dist/esm/v3/externalDeploymentId.js +38 -0
- package/dist/esm/v3/externalDeploymentId.js.map +1 -0
- package/dist/esm/v3/retry.d.ts +4 -0
- package/dist/esm/v3/retry.js +14 -8
- package/dist/esm/v3/retry.js.map +1 -1
- package/dist/esm/v3/sessions.d.ts +15 -2
- package/dist/esm/v3/sessions.js +11 -1
- package/dist/esm/v3/sessions.js.map +1 -1
- package/dist/esm/v3/shared.js +17 -23
- package/dist/esm/v3/shared.js.map +1 -1
- package/dist/esm/v3/test/index.d.ts +1 -0
- package/dist/esm/v3/test/index.js +1 -0
- package/dist/esm/v3/test/index.js.map +1 -1
- package/dist/esm/v3/test/mock-chat-agent.d.ts +47 -4
- package/dist/esm/v3/test/mock-chat-agent.js +118 -12
- package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
- package/dist/esm/v3/test/test-session-handle.js +6 -0
- package/dist/esm/v3/test/test-session-handle.js.map +1 -1
- package/dist/esm/v3/test/transcript-storage-tests.d.ts +40 -0
- package/dist/esm/v3/test/transcript-storage-tests.js +255 -0
- package/dist/esm/v3/test/transcript-storage-tests.js.map +1 -0
- package/dist/esm/v3/transcriptStorage.d.ts +261 -0
- package/dist/esm/v3/transcriptStorage.js +360 -0
- package/dist/esm/v3/transcriptStorage.js.map +1 -0
- package/dist/esm/version.js +1 -1
- package/docs/ai-chat/actions.mdx +55 -23
- package/docs/ai-chat/anatomy.mdx +47 -27
- package/docs/ai-chat/backend.mdx +125 -48
- package/docs/ai-chat/background-injection.mdx +122 -19
- package/docs/ai-chat/client-protocol.mdx +5 -4
- package/docs/ai-chat/compaction.mdx +13 -7
- package/docs/ai-chat/custom-agents.mdx +23 -0
- package/docs/ai-chat/fast-starts.mdx +27 -20
- package/docs/ai-chat/frontend.mdx +19 -14
- package/docs/ai-chat/how-it-works.mdx +1 -1
- package/docs/ai-chat/lifecycle-hooks.mdx +6 -2
- package/docs/ai-chat/migrating-from-a-route-handler.mdx +16 -14
- package/docs/ai-chat/migrating-from-hydrate-messages.mdx +299 -0
- package/docs/ai-chat/patterns/database-persistence.mdx +24 -9
- package/docs/ai-chat/patterns/native-compaction.mdx +310 -0
- package/docs/ai-chat/patterns/persistence-and-replay.mdx +34 -23
- package/docs/ai-chat/patterns/skills.mdx +7 -10
- package/docs/ai-chat/patterns/version-upgrades.mdx +79 -6
- package/docs/ai-chat/pending-messages.mdx +3 -3
- package/docs/ai-chat/prompt-caching.mdx +23 -25
- package/docs/ai-chat/quick-start.mdx +11 -11
- package/docs/ai-chat/reference.mdx +69 -10
- package/docs/ai-chat/sessions.mdx +6 -1
- package/docs/ai-chat/side-channels.mdx +1 -1
- package/docs/ai-chat/testing.mdx +2 -1
- package/docs/ai-chat/tools.mdx +18 -13
- package/docs/ai-chat/transcript-storage.mdx +247 -0
- package/docs/ai-chat/upgrade-guide.mdx +2 -2
- package/docs/apikeys.mdx +27 -45
- package/docs/config/extensions/syncEnvVars.mdx +6 -0
- package/docs/deploy-environment-variables.mdx +23 -2
- package/docs/deployment/overview.mdx +4 -8
- package/docs/deployment/preview-branches.mdx +4 -4
- package/docs/deployment/version-skew-protection.mdx +62 -0
- package/docs/introduction.mdx +58 -152
- package/docs/manual-setup.mdx +7 -7
- package/docs/mcp-tools.mdx +9 -0
- package/docs/quick-start.mdx +3 -3
- package/docs/realtime/auth.mdx +1 -1
- package/docs/reports.mdx +157 -0
- package/docs/self-hosting/security.mdx +17 -0
- package/docs/tasks/scheduled.mdx +24 -0
- package/docs/triggering.mdx +1 -1
- package/docs/troubleshooting-zod.mdx +158 -0
- package/package.json +4 -4
- package/skills/trigger-authoring-chat-agent/SKILL.md +41 -29
- package/skills/trigger-chat-agent-advanced/SKILL.md +54 -19
|
@@ -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_sk_..." # 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,6 +210,7 @@ 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. |
|
|
213
214
|
| `triggerConfig.region` | `string` | Region preference. |
|
|
214
215
|
| `triggerConfig.idleTimeoutInSeconds` | `number` | Surfaced to the agent through the wire payload (1–3600). |
|
|
215
216
|
|
|
@@ -266,7 +267,7 @@ x-trigger-jwt-claims: {"sub":"...","scopes":["read:runs:run_abc123","write:input
|
|
|
266
267
|
Re-calling `POST /api/v1/sessions` with the same `(taskIdentifier, externalId)` pair is **idempotent for the lifetime of the session**:
|
|
267
268
|
|
|
268
269
|
- 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).)
|
|
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.
|
|
270
|
+
- If the session has been closed (`POST /api/v1/sessions/{id}/close`, or `chat.close()` from inside the agent): returns **HTTP 409**. Closed is one-way; reuse a different `externalId` to start a new conversation.
|
|
270
271
|
- 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.
|
|
271
272
|
|
|
272
273
|
<Warning>
|
|
@@ -693,7 +694,7 @@ The body is a JSON-serialized [`ChatInputChunk`](#chatinputchunk), a tagged unio
|
|
|
693
694
|
| --- | --- |
|
|
694
695
|
| `401` | Missing or invalid `Authorization` header. |
|
|
695
696
|
| `403` | Token doesn't carry `write:sessions:{externalId}`. |
|
|
696
|
-
| `409` | The session is closed
|
|
697
|
+
| `409` | The session is closed: `{ "ok": false, "error": "Cannot append to a closed session", "code": "session_closed", "closedReason": "<reason or null>" }`. Key on `code`, not the message. Terminal: do not retry, and stop reconnecting to `.out`. |
|
|
697
698
|
| `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. |
|
|
698
699
|
| `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). |
|
|
699
700
|
|
|
@@ -832,7 +833,7 @@ Custom actions (undo, rollback, edit) ride on the same `.in` channel using `kind
|
|
|
832
833
|
}
|
|
833
834
|
```
|
|
834
835
|
|
|
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
|
|
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 `chat.turn()`, a turn runs on the edited history and its chunks follow on `.out` like any turn's.
|
|
836
837
|
|
|
837
838
|
Raw `chat.customAgent()` tasks receive `action` as `unknown` and must validate it in their own loop.
|
|
838
839
|
|
|
@@ -19,11 +19,12 @@ Provide `shouldCompact` to decide when to compact and `summarize` to generate th
|
|
|
19
19
|
|
|
20
20
|
```ts
|
|
21
21
|
import { chat } from "@trigger.dev/sdk/ai";
|
|
22
|
-
import {
|
|
22
|
+
import { generateText, stepCountIs } from "ai";
|
|
23
23
|
import { anthropic } from "@ai-sdk/anthropic";
|
|
24
24
|
|
|
25
25
|
export const myChat = chat.agent({
|
|
26
26
|
id: "my-chat",
|
|
27
|
+
registry,
|
|
27
28
|
compaction: {
|
|
28
29
|
shouldCompact: ({ totalTokens }) => (totalTokens ?? 0) > 80_000,
|
|
29
30
|
summarize: async ({ messages }) => {
|
|
@@ -34,9 +35,8 @@ export const myChat = chat.agent({
|
|
|
34
35
|
return result.text;
|
|
35
36
|
},
|
|
36
37
|
},
|
|
37
|
-
run: async ({ messages, signal }) => {
|
|
38
|
+
run: async ({ messages, signal, streamText }) => {
|
|
38
39
|
return streamText({
|
|
39
|
-
...chat.toStreamTextOptions({ registry }),
|
|
40
40
|
messages,
|
|
41
41
|
abortSignal: signal,
|
|
42
42
|
stopWhen: stepCountIs(15),
|
|
@@ -61,6 +61,12 @@ After each turn completes:
|
|
|
61
61
|
|
|
62
62
|
On the next turn, the LLM receives the compact summary instead of the full history — dramatically reducing token usage while preserving context.
|
|
63
63
|
|
|
64
|
+
The compacted context is durable. The runtime writes it to the [transcript storage](/ai-chat/transcript-storage)'s `state` alongside the messages, so a new run that boots to continue the conversation starts from the summary rather than re-reading the whole transcript and summarising it again. An undo or edit that reaches into the summarised part of the conversation clears the stored summary, and compaction runs again from the edited history when the threshold is next crossed.
|
|
65
|
+
|
|
66
|
+
<Note>
|
|
67
|
+
This is Trigger.dev's provider-agnostic compaction. To persist a **provider's own** compaction across turns instead (Anthropic context editing or OpenAI stored responses), and to fall back between providers without re-sending history, see [Native compaction & provider fallback](/ai-chat/patterns/native-compaction).
|
|
68
|
+
</Note>
|
|
69
|
+
|
|
64
70
|
## Customizing what gets persisted
|
|
65
71
|
|
|
66
72
|
By default, compaction only affects model messages — UI messages stay intact so users see the full conversation after a page refresh. You can customize this with `compactUIMessages`:
|
|
@@ -91,7 +97,7 @@ export const myChat = chat.agent({
|
|
|
91
97
|
...uiMessages.slice(-4), // Keep the last 4 messages
|
|
92
98
|
],
|
|
93
99
|
},
|
|
94
|
-
run: async ({ messages, signal }) => {
|
|
100
|
+
run: async ({ messages, signal, streamText }) => {
|
|
95
101
|
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
96
102
|
},
|
|
97
103
|
});
|
|
@@ -185,7 +191,7 @@ export const myChat = chat.agent({
|
|
|
185
191
|
data: { chatId, summary, totalTokens, messageCount },
|
|
186
192
|
});
|
|
187
193
|
},
|
|
188
|
-
run: async ({ messages, signal }) => {
|
|
194
|
+
run: async ({ messages, signal, streamText }) => {
|
|
189
195
|
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
190
196
|
},
|
|
191
197
|
});
|
|
@@ -201,7 +207,7 @@ Define a `compact` action that reuses your existing `summarize` function:
|
|
|
201
207
|
|
|
202
208
|
```ts
|
|
203
209
|
import { chat } from "@trigger.dev/sdk/ai";
|
|
204
|
-
import {
|
|
210
|
+
import { generateText, generateId, convertToModelMessages } from "ai";
|
|
205
211
|
import { anthropic } from "@ai-sdk/anthropic";
|
|
206
212
|
import { z } from "zod";
|
|
207
213
|
|
|
@@ -243,7 +249,7 @@ export const myChat = chat.agent({
|
|
|
243
249
|
]);
|
|
244
250
|
},
|
|
245
251
|
|
|
246
|
-
run: async ({ messages, signal }) => {
|
|
252
|
+
run: async ({ messages, signal, streamText }) => {
|
|
247
253
|
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
248
254
|
},
|
|
249
255
|
});
|
|
@@ -290,6 +290,29 @@ Read `turn.stopped` to tell a user stop from a full run cancel:
|
|
|
290
290
|
|
|
291
291
|
A hand-rolled loop wires this itself with `chat.createStopSignal()` and `chat.cleanupAbortedParts()`. Two things `createSession` handles for you are easy to get wrong there — see the [hand-rolled loop checklist](#hand-rolled-loop-checklist).
|
|
292
292
|
|
|
293
|
+
### Ending the conversation
|
|
294
|
+
|
|
295
|
+
`chat.close({ reason })` works in a custom agent exactly as it does in [`chat.agent`](/ai-chat/backend#ending-the-conversation): the session row is closed, further sends are refused with HTTP 409, and no continuation run is scheduled. Call it from anywhere in your loop.
|
|
296
|
+
|
|
297
|
+
```ts trigger/my-chat.ts
|
|
298
|
+
for await (const turn of session) {
|
|
299
|
+
const result = streamText({ model, messages: turn.messages, abortSignal: turn.signal });
|
|
300
|
+
|
|
301
|
+
// Close BEFORE turn.complete(): that call writes the turn boundary the
|
|
302
|
+
// browser stops reading at, and it carries the closed state out with it.
|
|
303
|
+
if (await overBudget(turn.chatId)) {
|
|
304
|
+
chat.close({ reason: "Monthly budget reached" });
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
await turn.complete(result);
|
|
308
|
+
if (turn.stopped) break;
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
The close is performed when your `run()` returns, so it lands whether you `break` out of the loop, return early, or keep iterating. A hand-rolled loop with no iterator at all works the same way, as long as you call `chat.close()` before the `chat.writeTurnComplete()` that ends the turn.
|
|
313
|
+
|
|
314
|
+
Only `chat.agent` and `chat.customAgent` bind the run to its Session, so `chat.close()` throws in a plain `task()`.
|
|
315
|
+
|
|
293
316
|
## Hand-rolled loop with primitives
|
|
294
317
|
|
|
295
318
|
For full control, skip `createSession` and compose the primitives directly:
|
|
@@ -239,11 +239,11 @@ This is an **import-chain** problem, not a runtime one. A "we'll strip the execu
|
|
|
239
239
|
|
|
240
240
|
export const myChat = chat.agent({
|
|
241
241
|
id: "my-chat",
|
|
242
|
-
run: async ({ messages, signal }) =>
|
|
242
|
+
run: async ({ messages, signal, streamText }) =>
|
|
243
243
|
streamText({
|
|
244
|
-
...chat.toStreamTextOptions({ tools: chatTools }),
|
|
245
244
|
model: anthropic("claude-sonnet-4-6"),
|
|
246
245
|
messages,
|
|
246
|
+
tools: chatTools,
|
|
247
247
|
stopWhen: stepCountIs(10),
|
|
248
248
|
abortSignal: signal,
|
|
249
249
|
}),
|
|
@@ -251,7 +251,7 @@ This is an **import-chain** problem, not a runtime one. A "we'll strip the execu
|
|
|
251
251
|
```
|
|
252
252
|
</Step>
|
|
253
253
|
<Step title="Build the head-start handler">
|
|
254
|
-
Call `chat.headStart({ agentId, run })`. It returns a standard Web Fetch handler: `(req: Request) => Promise<Response>`.
|
|
254
|
+
Call `chat.headStart({ agentId, run })`. It returns a standard Web Fetch handler: `(req: Request) => Promise<Response>`. The `run` callback receives a `streamText` that already carries the SDK-owned wiring: the converted messages, `stopWhen: stepCountIs(1)` and the abort signal. Pass your schema-only tools to it explicitly, and add your own `model` and `system` on top.
|
|
255
255
|
|
|
256
256
|
```ts lib/chat-handler.ts
|
|
257
257
|
import { chat } from "@trigger.dev/sdk/chat-server";
|
|
@@ -261,18 +261,23 @@ This is an **import-chain** problem, not a runtime one. A "we'll strip the execu
|
|
|
261
261
|
|
|
262
262
|
export const chatHandler = chat.headStart({
|
|
263
263
|
agentId: "my-chat",
|
|
264
|
-
run: async ({
|
|
264
|
+
run: async ({ streamText }) =>
|
|
265
265
|
streamText({
|
|
266
|
-
...helper.toStreamTextOptions({ tools: headStartTools }),
|
|
267
266
|
model: anthropic("claude-sonnet-4-6"),
|
|
268
267
|
system: "You are a helpful assistant.",
|
|
268
|
+
tools: headStartTools,
|
|
269
269
|
}),
|
|
270
270
|
});
|
|
271
271
|
```
|
|
272
272
|
|
|
273
|
-
<
|
|
274
|
-
|
|
275
|
-
|
|
273
|
+
<Note>
|
|
274
|
+
That `streamText` is the SDK's, not the one from `ai`. It pins `messages`,
|
|
275
|
+
`prompt`, `stopWhen: stepCountIs(1)` and `abortSignal`, which the handover depends on:
|
|
276
|
+
running past step 1 would splice a stream the agent is supposed to own. Setting
|
|
277
|
+
any of the four at the call site is a type error, and a throw if you get past
|
|
278
|
+
the types, rather than breaking the handover quietly. `chat.toStreamTextOptions()` is still there if you want to build the
|
|
279
|
+
options yourself.
|
|
280
|
+
</Note>
|
|
276
281
|
|
|
277
282
|
<Tip>
|
|
278
283
|
Use the **same model** on both sides (route handler and `chat.agent`) to avoid a tone or style shift between step 1 and step 2+. Your LLM provider keys stay server-side in your warm process — Trigger.dev never holds them in this design.
|
|
@@ -625,27 +630,27 @@ chat.headStart<TTools>({
|
|
|
625
630
|
export const chatHandler = chat.headStart({
|
|
626
631
|
agentId: "my-chat",
|
|
627
632
|
triggerConfig: { tags: ["org:acme"], queue: "chat", machine: "small-2x" },
|
|
628
|
-
run: async ({
|
|
629
|
-
streamText({ ...helper.toStreamTextOptions({ tools: headStartTools }), model, system }),
|
|
633
|
+
run: async ({ streamText }) => streamText({ model, system, tools: headStartTools }),
|
|
630
634
|
});
|
|
631
635
|
```
|
|
632
636
|
|
|
633
637
|
The `run` callback receives:
|
|
634
638
|
|
|
635
|
-
- `messages: UIMessage[]
|
|
636
|
-
- `signal: AbortSignal
|
|
637
|
-
- `
|
|
639
|
+
- `messages: UIMessage[]`: user messages parsed from the request body.
|
|
640
|
+
- `signal: AbortSignal`: fires when the request closes or the SDK times out the handover.
|
|
641
|
+
- `streamText`: the AI SDK's `streamText` with the keys below already applied. Prefer it.
|
|
642
|
+
- `chat: HeadStartChatHelper<TTools>`: exposes `chat.toStreamTextOptions({ tools })` for building the options by hand, plus a `chat.session` escape hatch for power users.
|
|
638
643
|
|
|
639
|
-
|
|
644
|
+
The SDK owns these keys. Passing one to the callback's `streamText` is a type error, and a throw behind that; re-setting one after a `chat.toStreamTextOptions()` spread breaks the protocol with no error at all:
|
|
640
645
|
|
|
641
646
|
| Key | What the SDK sets | Why |
|
|
642
647
|
| --- | --- | --- |
|
|
643
648
|
| `messages` | `convertToModelMessages(uiMessages)` | First-turn user history |
|
|
644
|
-
| `
|
|
645
|
-
| `stopWhen` | `stepCountIs(1)` | Step 1 only
|
|
649
|
+
| `prompt` | Nothing, and rejects yours | `messages` already carries the history |
|
|
650
|
+
| `stopWhen` | `stepCountIs(1)` | Step 1 only, the agent picks up step 2 onward |
|
|
646
651
|
| `abortSignal` | Combined request + idle timeout | Safe cleanup on disconnect |
|
|
647
652
|
|
|
648
|
-
You bring `model`, `system`, `providerOptions`, `prepareStep`, anything else `streamText` accepts.
|
|
653
|
+
You bring `model`, `system`, `providerOptions`, `prepareStep`, anything else `streamText` accepts. `tools` is yours too: pass your schema-only set to the callback's `streamText` (or to `chat.toStreamTextOptions({ tools })`), since the SDK cannot know it.
|
|
649
654
|
|
|
650
655
|
#### The transport option
|
|
651
656
|
|
|
@@ -684,11 +689,11 @@ export async function POST(req: Request) {
|
|
|
684
689
|
agentId: "my-chat",
|
|
685
690
|
chatId, // session externalId; reuse it on the destination page
|
|
686
691
|
messages, // first-turn user history
|
|
687
|
-
run: async ({
|
|
692
|
+
run: async ({ streamText }) =>
|
|
688
693
|
streamText({
|
|
689
|
-
...helper.toStreamTextOptions({ tools: headStartTools }),
|
|
690
694
|
model: anthropic("claude-sonnet-4-6"),
|
|
691
695
|
system: "You are a helpful assistant.",
|
|
696
|
+
tools: headStartTools,
|
|
692
697
|
}),
|
|
693
698
|
});
|
|
694
699
|
|
|
@@ -735,11 +740,13 @@ chat.startHeadStart<TTools>({
|
|
|
735
740
|
triggerConfig?: Partial<SessionTriggerConfig>, // tags, queue, machine, …
|
|
736
741
|
apiClient?: ApiClientConfiguration, // when the agent lives in another project/env
|
|
737
742
|
metadata?: Record<string, unknown>, // merged into the run payload; never sent to the browser
|
|
738
|
-
}): Promise<{ chatId: string; completion: Promise<void> }>
|
|
743
|
+
}): Promise<{ chatId: string; pendingVersion: boolean; completion: Promise<void> }>
|
|
739
744
|
```
|
|
740
745
|
|
|
741
746
|
`completion` resolves once the head start finishes; `await` it or hand it to `waitUntil`. It rejects if the warm step or the dispatch fails.
|
|
742
747
|
|
|
748
|
+
`pendingVersion` is `true` when the agent run is parked waiting for the deployment carrying the session's [external deployment id](/deployment/version-skew-protection#chat-sessions). Step 1 still runs in your process and still reaches the browser, so pass the flag to the destination page if you want it to say a deploy is in progress rather than appear to stall on step 2.
|
|
749
|
+
|
|
743
750
|
### Limitations
|
|
744
751
|
|
|
745
752
|
- **First turn only.** Step 2+ and turn 2+ run on the trigger side. There's no per-turn "head start every turn" mode — the win comes from amortizing agent boot across the LLM call once.
|
|
@@ -137,6 +137,8 @@ On page load, fetch both the messages and the session state from your database,
|
|
|
137
137
|
|
|
138
138
|
Because the underlying Session row outlives individual runs, a chat you were in yesterday resumes against the same chat — even if the original run has long since exited. The transport hydrates from the persisted state and uses `lastEventId` to resubscribe; if the client tries to send a new message and no run is alive, the server triggers a fresh continuation run on the same session before the message is appended.
|
|
139
139
|
|
|
140
|
+
If you do not keep your own copy of the conversation, load it from the agent's [transcript storage](/ai-chat/transcript-storage#reading-the-transcript) instead: `chat.createLoadTranscriptAction(storage)` on the server and `useLoadTranscript(chatId, action, { transport })` in the browser return the messages and seed the transport's resume cursor, for the default storage and your own alike.
|
|
141
|
+
|
|
140
142
|
```tsx app/chat/[chatId]/ChatPage.tsx
|
|
141
143
|
"use client";
|
|
142
144
|
|
|
@@ -442,45 +444,48 @@ function Chat({ chatId, transport }) {
|
|
|
442
444
|
|
|
443
445
|
## Sending actions
|
|
444
446
|
|
|
445
|
-
Send custom actions (undo, rollback, edit)
|
|
446
|
-
|
|
447
|
-
For optimistic UI, mirror the action's effect on the `useChat` state via `setMessages` while the request is in flight:
|
|
447
|
+
Send custom actions (undo, rollback, edit, regenerate) as `useChat` requests, with the action in the request `body`. The transport recognises `body.action` and sends it as an action rather than a message, and because `useChat` made the request it owns the response: an action that returns `chat.turn()` on the server streams its answer into the message list like any turn, with `status`, `error` and `stop` behaving as for a message. An action that returns nothing completes with no message added.
|
|
448
448
|
|
|
449
449
|
```tsx
|
|
450
|
+
import { useChat } from "@ai-sdk/react";
|
|
451
|
+
import { useChatActions, useTriggerChatTransport } from "@trigger.dev/sdk/chat/react";
|
|
452
|
+
|
|
450
453
|
function ChatControls({ chatId }: { chatId: string }) {
|
|
451
454
|
const transport = useTriggerChatTransport({
|
|
452
455
|
task: "my-chat",
|
|
453
456
|
accessToken: ({ chatId }) => mintChatAccessToken(chatId),
|
|
454
|
-
startSession: ({ chatId, clientData }) =>
|
|
455
|
-
startChatSession({ chatId, clientData }),
|
|
457
|
+
startSession: ({ chatId, clientData }) => startChatSession({ chatId, clientData }),
|
|
456
458
|
});
|
|
457
|
-
|
|
458
|
-
const {
|
|
459
|
+
const { sendMessage, regenerate, setMessages } = useChat({ id: chatId, transport });
|
|
460
|
+
const { sendAction } = useChatActions({ sendMessage });
|
|
459
461
|
|
|
460
462
|
return (
|
|
461
463
|
<div>
|
|
462
464
|
<button
|
|
463
465
|
onClick={() => {
|
|
464
|
-
|
|
466
|
+
// Mirror the server-side edit optimistically; the server does not
|
|
467
|
+
// push history changes back.
|
|
465
468
|
setMessages((prev) => prev.slice(0, -2));
|
|
469
|
+
void sendAction({ type: "undo" });
|
|
466
470
|
}}
|
|
467
471
|
>
|
|
468
472
|
Undo last exchange
|
|
469
473
|
</button>
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
Rollback to message
|
|
474
|
+
{/* regenerate() removes the trailing answer locally; the server does the same before its turn */}
|
|
475
|
+
<button onClick={() => regenerate({ body: { action: { type: "regenerate" } } })}>
|
|
476
|
+
Regenerate
|
|
474
477
|
</button>
|
|
475
478
|
</div>
|
|
476
479
|
);
|
|
477
480
|
}
|
|
478
481
|
```
|
|
479
482
|
|
|
480
|
-
|
|
483
|
+
`useChatActions` is a two-line convenience over `sendMessage(undefined, { body: { action } })`. Any `useChat` request can carry an action the same way, `regenerate({ body })` included.
|
|
484
|
+
|
|
485
|
+
The action payload is validated against the agent's `actionSchema` on the backend; invalid actions are rejected. See [Actions](/ai-chat/actions) for the backend setup.
|
|
481
486
|
|
|
482
487
|
<Note>
|
|
483
|
-
`sendAction`
|
|
488
|
+
`transport.sendAction()` still exists for callers outside `useChat` (server to server, or a custom client). It returns the response as a raw `ReadableStream<UIMessageChunk>` that the caller must read; `useChat` does not consume it.
|
|
484
489
|
</Note>
|
|
485
490
|
|
|
486
491
|
For server-to-server usage, `AgentChat` has the same method:
|
|
@@ -90,7 +90,7 @@ Here is a typical cold turn — user opens the page, types "What's the weather?"
|
|
|
90
90
|
Immediately after writing the new turn-complete marker, the agent issues an S2 trim command targeting the *previous* turn-complete's sequence number. This bounds the stream's storage to roughly one turn of chunks plus the latest control record.
|
|
91
91
|
</Step>
|
|
92
92
|
<Step title="Task: fires onTurnComplete, writes snapshot to S3">
|
|
93
|
-
`onTurnComplete` runs (your hook for persistence). Then the agent writes
|
|
93
|
+
`onTurnComplete` runs (your hook for persistence). Then the agent writes the transcript to S3 at `sessions/:chatId/snapshot.json`: a header line carrying the stream cursors and the runtime's own state, one line per message, and an index of the messages at the end so a page of history can be read without downloading the whole conversation. Once [compaction](/ai-chat/compaction) has run, only the most recent messages are kept, so the file stops growing with the conversation. If you need the full history, keep your own [transcript storage](/ai-chat/transcript-storage). This write is awaited, not fire-and-forget, so the next run is guaranteed to find it.
|
|
94
94
|
</Step>
|
|
95
95
|
<Step title="Task: goes idle, then suspends">
|
|
96
96
|
The agent re-enters the waitpoint on `.in`. After `idleTimeoutInSeconds` of nothing arriving, `onChatSuspend` fires and the engine snapshots the run. Compute is freed.
|
|
@@ -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 fires regardless of whether [`hydrateMessages`](#hydratemessages) or a [transcript storage](/ai-chat/transcript-storage) is registered: crash recovery is runtime-owned for every agent.
|
|
90
90
|
|
|
91
91
|
```ts
|
|
92
92
|
export const myChat = chat.agent({
|
|
@@ -260,6 +260,10 @@ export const myChat = chat.agent({
|
|
|
260
260
|
|
|
261
261
|
## hydrateMessages
|
|
262
262
|
|
|
263
|
+
<Warning>
|
|
264
|
+
`hydrateMessages` is deprecated. Give the agent a [transcript storage](/ai-chat/transcript-storage) instead: `loadContext` on the storage decides the model's context, and `save` persists every change, so crash recovery and durable compaction cover it. Existing agents keep working with a one-time warning. Setting `hydrateMessages` together with `storage` is an error.
|
|
265
|
+
</Warning>
|
|
266
|
+
|
|
263
267
|
Load the full message history from your backend on every turn, replacing the built-in linear accumulator. When set, the hook's return value becomes the accumulated state; the normal accumulation logic (append for submit, replace for regenerate) is skipped entirely.
|
|
264
268
|
|
|
265
269
|
Use this when the backend should be the source of truth for message history: abuse prevention, branching conversations (DAGs), or rollback/undo support.
|
|
@@ -315,7 +319,7 @@ After the hook returns, the runtime overlays the wire's tool-state advances (`ou
|
|
|
315
319
|
</Note>
|
|
316
320
|
|
|
317
321
|
<Tip>
|
|
318
|
-
Registering `hydrateMessages`
|
|
322
|
+
Registering `hydrateMessages` turns off the runtime's transcript reads and writes: your hook is the source of truth for history, and the runtime does not read or write its [snapshot](/ai-chat/patterns/persistence-and-replay). Crash recovery still runs. When a new run boots, the dead run's unfinished answer and unacknowledged messages are replayed from the session streams, `onRecoveryBoot` fires, and the hook receives the recovered tail in `previousMessages`. Persisting that tail is yours to do; a [transcript storage](/ai-chat/transcript-storage) does it for you.
|
|
319
323
|
</Tip>
|
|
320
324
|
|
|
321
325
|
<Note>
|
|
@@ -56,7 +56,7 @@ Then make these changes:
|
|
|
56
56
|
- Create a `chat.agent` task in `trigger/chat.ts`. Move the existing `streamText` call
|
|
57
57
|
into its `run` function UNCHANGED — same model, same `system`, same `temperature`,
|
|
58
58
|
same `stopWhen`, same provider options. Do not rewrite the prompt or swap the model.
|
|
59
|
-
-
|
|
59
|
+
- Take `streamText` from the `run` argument, so the managed options apply to that
|
|
60
60
|
`streamText` call, so the explicit options after it still win.
|
|
61
61
|
- `run` receives `ModelMessage[]` already. Delete the `convertToModelMessages` call.
|
|
62
62
|
- Forward the `signal` from `run` as `abortSignal` so Stop works.
|
|
@@ -145,10 +145,9 @@ import { tools } from "@/lib/tools";
|
|
|
145
145
|
export const myChat = chat.agent({
|
|
146
146
|
id: "my-chat",
|
|
147
147
|
tools,
|
|
148
|
-
run: async ({ messages, tools, signal }) =>
|
|
148
|
+
run: async ({ messages, tools, signal, streamText }) =>
|
|
149
149
|
streamText({
|
|
150
|
-
|
|
151
|
-
...chat.toStreamTextOptions({ tools }),
|
|
150
|
+
tools,
|
|
152
151
|
model: anthropic("claude-sonnet-4-5"),
|
|
153
152
|
system: "You are a helpful assistant.",
|
|
154
153
|
messages,
|
|
@@ -163,10 +162,10 @@ Four things changed inside the `streamText` call, and `tools` moved onto the age
|
|
|
163
162
|
- **`messages` arrives as `ModelMessage[]`.** The runtime converts the frontend's `UIMessage[]` for you, so `convertToModelMessages` is gone.
|
|
164
163
|
- **`abortSignal` comes from `signal` on the payload**, not `req.signal`. It fires on stop and on cancel.
|
|
165
164
|
- **Return the `StreamTextResult`.** It's piped to the frontend automatically — no `toUIMessageStreamResponse`. If `streamText` is buried in a helper, call `await chat.pipe(result)` from anywhere in the task instead and let `run` resolve `void`.
|
|
166
|
-
-
|
|
165
|
+
- **`streamText` comes from the `run` argument, not from `ai`.** It carries the `prepareStep` behind [compaction](/ai-chat/compaction), [mid-turn steering](/ai-chat/pending-messages) and [background injection](/ai-chat/background-injection), plus the system prompt set via [`chat.prompt()`](/ai-chat/backend#using-prompts) and telemetry.
|
|
167
166
|
|
|
168
167
|
<Warning>
|
|
169
|
-
|
|
168
|
+
Importing `streamText` from `ai` instead throws no error: compaction, steering and background injection never run. Spreading `chat.toStreamTextOptions()` into the imported one is the equivalent, and is what a custom agent has to do, since it has no `run` argument. A `chat.headStart` route gets a bound `streamText` of its own.
|
|
170
169
|
</Warning>
|
|
171
170
|
|
|
172
171
|
There's no `maxDuration` equivalent to set. A turn isn't bounded by a function timeout; a run stays alive across turns and suspends when nothing is happening.
|
|
@@ -357,9 +356,9 @@ export const myChat = chat.agent({
|
|
|
357
356
|
}),
|
|
358
357
|
]);
|
|
359
358
|
},
|
|
360
|
-
run: async ({ messages, tools, signal }) =>
|
|
359
|
+
run: async ({ messages, tools, signal, streamText }) =>
|
|
361
360
|
streamText({
|
|
362
|
-
|
|
361
|
+
tools,
|
|
363
362
|
model: anthropic("claude-sonnet-4-5"),
|
|
364
363
|
system: "You are a helpful assistant.",
|
|
365
364
|
messages,
|
|
@@ -468,18 +467,21 @@ Head Start brings the route handler back for exactly that first turn. It runs st
|
|
|
468
467
|
|
|
469
468
|
export const chatHandler = chat.headStart({
|
|
470
469
|
agentId: "my-chat",
|
|
471
|
-
run: async ({
|
|
470
|
+
run: async ({ streamText }) =>
|
|
472
471
|
streamText({
|
|
473
|
-
...helper.toStreamTextOptions({ tools: headStartTools }),
|
|
474
472
|
model: anthropic("claude-sonnet-4-5"),
|
|
475
473
|
system: "You are a helpful assistant.",
|
|
474
|
+
tools: headStartTools,
|
|
476
475
|
}),
|
|
477
476
|
});
|
|
478
477
|
```
|
|
479
478
|
|
|
480
|
-
<
|
|
481
|
-
|
|
482
|
-
|
|
479
|
+
<Note>
|
|
480
|
+
That `streamText` owns `messages`, `prompt`, `abortSignal` and `stopWhen`. Passing one is
|
|
481
|
+
a type error, and a runtime throw if you get past the types. Unlike the agent side, re-setting one breaks the
|
|
482
|
+
handover rather than degrading it: `stopWhen` is pinned to `stepCountIs(1)`
|
|
483
|
+
because the agent, not the handler, runs step 2 onward.
|
|
484
|
+
</Note>
|
|
483
485
|
|
|
484
486
|
Your provider keys never leave your server — the first-turn model call runs in your process, so that environment needs whatever the model requires.
|
|
485
487
|
</Step>
|
|
@@ -558,7 +560,7 @@ The shape is identical outside Next.js. The agent task and the React component d
|
|
|
558
560
|
|
|
559
561
|
**The head-start route dies mid-turn on Vercel.** The handler holds the SSE response open until the agent signals turn-complete, so the function timeout has to cover the whole turn, not just step 1. Set `maxDuration` on that route segment.
|
|
560
562
|
|
|
561
|
-
**Compaction and steering do nothing.**
|
|
563
|
+
**Compaction and steering do nothing.** `run` is calling the `streamText` imported from `ai` rather than the one in its argument. Destructure `streamText` from the `run` argument, or spread `chat.toStreamTextOptions()` into the imported one.
|
|
562
564
|
|
|
563
565
|
**`toModelOutput` works on the first turn, then stops.** Tools are declared only on `streamText`. Declare the same set on `chat.agent({ tools })` too, and read it back off the `run` payload.
|
|
564
566
|
|