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