@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
package/docs/ai-chat/backend.mdx
CHANGED
|
@@ -30,14 +30,13 @@ Return the `streamText` result from `run` and it's automatically piped to the fr
|
|
|
30
30
|
|
|
31
31
|
```ts
|
|
32
32
|
import { chat } from "@trigger.dev/sdk/ai";
|
|
33
|
-
import {
|
|
33
|
+
import { stepCountIs } from "ai";
|
|
34
34
|
import { anthropic } from "@ai-sdk/anthropic";
|
|
35
35
|
|
|
36
36
|
export const simpleChat = chat.agent({
|
|
37
37
|
id: "simple-chat",
|
|
38
|
-
run: async ({ messages, signal }) => {
|
|
38
|
+
run: async ({ messages, signal, streamText }) => {
|
|
39
39
|
return streamText({
|
|
40
|
-
...chat.toStreamTextOptions(), // prepareStep, system, telemetry (see note below)
|
|
41
40
|
model: anthropic("claude-sonnet-4-5"),
|
|
42
41
|
system: "You are a helpful assistant.",
|
|
43
42
|
messages,
|
|
@@ -48,29 +47,64 @@ export const simpleChat = chat.agent({
|
|
|
48
47
|
});
|
|
49
48
|
```
|
|
50
49
|
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
<Note>
|
|
51
|
+
The `streamText` destructured from `run`'s argument is the SDK's, not the one
|
|
52
|
+
imported from `ai`. It carries the agent's managed options, so nothing has to be
|
|
53
|
+
spread in. [The managed streamText](#the-managed-streamtext) covers what those
|
|
54
|
+
options are and what happens when yours collide with them.
|
|
55
|
+
</Note>
|
|
56
|
+
|
|
57
|
+
### The managed streamText
|
|
58
|
+
|
|
59
|
+
`run()` is handed a `streamText` that already carries everything the spread provides, so the managed state cannot be lost by leaving the spread out:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
export const simpleChat = chat.agent({
|
|
63
|
+
id: "simple-chat",
|
|
64
|
+
run: async ({ messages, signal, streamText }) =>
|
|
65
|
+
streamText({
|
|
66
|
+
model: anthropic("claude-sonnet-4-5"),
|
|
67
|
+
messages,
|
|
68
|
+
abortSignal: signal,
|
|
69
|
+
stopWhen: stepCountIs(15),
|
|
70
|
+
}),
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Note the destructured `streamText`: it shadows the one imported from `ai` inside `run`, so the managed options apply without a spread. Spreading `chat.toStreamTextOptions()` into the imported `streamText` is still supported and equivalent.
|
|
75
|
+
|
|
76
|
+
It differs from the spread in three ways, all of them about what happens when your options collide with the managed ones:
|
|
77
|
+
|
|
78
|
+
| Option | Spread | Managed `streamText` |
|
|
79
|
+
| --- | --- | --- |
|
|
80
|
+
| `tools` | Passing `tools` after the spread replaces the skill tools | Merged, so skill tools survive |
|
|
81
|
+
| `prepareStep` | Passing your own after the spread replaces the managed one, silently disabling steering, compaction and injection | Composed, yours runs after the managed one |
|
|
82
|
+
| `system` | Yours replaces the managed prompt and any injected instructions | Throws |
|
|
83
|
+
|
|
84
|
+
`system` throws rather than merging because there is no shape that combines two system values on every supported AI SDK version: v5 rejects an array of blocks, and a structured block carries the provider options that make [prompt caching](/ai-chat/prompt-caching) work, so concatenating discards the cache entry. Set a static prompt with [`chat.prompt.set()`](#using-prompts) and add per-turn context with [`chat.inject()`](/ai-chat/background-injection).
|
|
85
|
+
|
|
86
|
+
If the managed prompt names a model, pass a registry on the agent so the runtime can resolve it: `chat.agent({ registry, run })`.
|
|
54
87
|
|
|
55
88
|
### Using chat.pipe() for complex flows
|
|
56
89
|
|
|
57
90
|
For complex agent flows where `streamText` is called deep inside your code, use `chat.pipe()`. It works from **anywhere inside a task** — even nested function calls.
|
|
58
91
|
|
|
59
92
|
```ts trigger/agent-chat.ts
|
|
60
|
-
import { chat } from "@trigger.dev/sdk/ai";
|
|
61
|
-
import { streamText } from "ai";
|
|
93
|
+
import { chat, type ChatStreamText } from "@trigger.dev/sdk/ai";
|
|
62
94
|
import { anthropic } from "@ai-sdk/anthropic";
|
|
63
|
-
import type
|
|
95
|
+
import { stepCountIs, type ModelMessage } from "ai";
|
|
64
96
|
|
|
65
97
|
export const agentChat = chat.agent({
|
|
66
98
|
id: "agent-chat",
|
|
67
|
-
run: async ({ messages }) => {
|
|
68
|
-
// Don't return anything
|
|
69
|
-
await runAgentLoop(messages);
|
|
99
|
+
run: async ({ messages, streamText }) => {
|
|
100
|
+
// Don't return anything, chat.pipe is called inside
|
|
101
|
+
await runAgentLoop(messages, streamText);
|
|
70
102
|
},
|
|
71
103
|
});
|
|
72
104
|
|
|
73
|
-
|
|
105
|
+
// A loop factored out of `run` takes `streamText` as an argument so it keeps the
|
|
106
|
+
// managed options. `ChatStreamText` types the parameter.
|
|
107
|
+
async function runAgentLoop(messages: ModelMessage[], streamText: ChatStreamText) {
|
|
74
108
|
// ... agent logic, tool calls, etc.
|
|
75
109
|
|
|
76
110
|
const result = streamText({
|
|
@@ -102,7 +136,7 @@ export const myChat = chat.agent({
|
|
|
102
136
|
// responseMessage.parts includes the data-metadata part
|
|
103
137
|
await db.messages.save(responseMessage);
|
|
104
138
|
},
|
|
105
|
-
run: async ({ messages, signal }) => {
|
|
139
|
+
run: async ({ messages, signal, streamText }) => {
|
|
106
140
|
// Also works from run() via chat.response
|
|
107
141
|
chat.response.write({
|
|
108
142
|
type: "data-context",
|
|
@@ -177,9 +211,9 @@ const tools = { searchDocs };
|
|
|
177
211
|
export const myChat = chat.agent({
|
|
178
212
|
id: "my-chat",
|
|
179
213
|
tools,
|
|
180
|
-
run: async ({ messages, tools, signal }) =>
|
|
214
|
+
run: async ({ messages, tools, signal, streamText }) =>
|
|
181
215
|
streamText({
|
|
182
|
-
|
|
216
|
+
tools,
|
|
183
217
|
model: anthropic("claude-sonnet-4-5"),
|
|
184
218
|
messages,
|
|
185
219
|
abortSignal: signal,
|
|
@@ -200,12 +234,12 @@ See [Tools](/ai-chat/tools) for `toModelOutput` across turns, per-turn dynamic t
|
|
|
200
234
|
|
|
201
235
|
### Using prompts
|
|
202
236
|
|
|
203
|
-
Use [AI Prompts](/ai/prompts) to manage your system prompt as versioned, overridable config. Store the resolved prompt in a lifecycle hook with `chat.prompt.set()
|
|
237
|
+
Use [AI Prompts](/ai/prompts) to manage your system prompt as versioned, overridable config. Store the resolved prompt in a lifecycle hook with `chat.prompt.set()`. The `streamText` from `run`'s argument picks it up: system prompt, model, config and telemetry.
|
|
204
238
|
|
|
205
239
|
```ts
|
|
206
240
|
import { chat } from "@trigger.dev/sdk/ai";
|
|
207
241
|
import { prompts } from "@trigger.dev/sdk";
|
|
208
|
-
import {
|
|
242
|
+
import { createProviderRegistry } from "ai";
|
|
209
243
|
import { anthropic } from "@ai-sdk/anthropic";
|
|
210
244
|
import { z } from "zod";
|
|
211
245
|
|
|
@@ -221,15 +255,15 @@ const systemPrompt = prompts.define({
|
|
|
221
255
|
|
|
222
256
|
export const myChat = chat.agent({
|
|
223
257
|
id: "my-chat",
|
|
258
|
+
registry,
|
|
224
259
|
clientDataSchema: z.object({ userId: z.string() }),
|
|
225
260
|
onChatStart: async ({ clientData }) => {
|
|
226
261
|
const user = await db.user.findUnique({ where: { id: clientData.userId } });
|
|
227
262
|
const resolved = await systemPrompt.resolve({ name: user.name });
|
|
228
263
|
chat.prompt.set(resolved);
|
|
229
264
|
},
|
|
230
|
-
run: async ({ messages, signal }) => {
|
|
265
|
+
run: async ({ messages, signal, streamText }) => {
|
|
231
266
|
return streamText({
|
|
232
|
-
...chat.toStreamTextOptions({ registry }), // system, model, config, telemetry
|
|
233
267
|
messages,
|
|
234
268
|
abortSignal: signal,
|
|
235
269
|
stopWhen: stepCountIs(15),
|
|
@@ -238,16 +272,9 @@ export const myChat = chat.agent({
|
|
|
238
272
|
});
|
|
239
273
|
```
|
|
240
274
|
|
|
241
|
-
`
|
|
242
|
-
|
|
243
|
-
**Which form to call:**
|
|
275
|
+
The managed `streamText` carries the stored prompt's `system`, `model` (resolved through the agent's `registry`), sampling config, and `experimental_telemetry`. Options you pass at the call site win, apart from `system`, which throws when the prompt already set one.
|
|
244
276
|
|
|
245
|
-
|
|
246
|
-
|---|---|
|
|
247
|
-
| `chat.toStreamTextOptions()` | Default. Wires up `prepareStep` (compaction, steering, background injection), the stored prompt's `system` / `model` / `config`, and telemetry metadata. |
|
|
248
|
-
| `chat.toStreamTextOptions({ registry })` | You're using [Prompts](/ai/prompts) with a provider-prefixed model string (e.g. `"anthropic:claude-sonnet-4-5"`). The registry resolves the prefix to a real model instance via `createProviderRegistry({ anthropic, openai, ... })`. |
|
|
249
|
-
| `chat.toStreamTextOptions({ tools })` | You want HITL tool approvals — pass the same `tools` object you give to `streamText`. The SDK then knows which tool calls need to pause on `needsApproval: true`. |
|
|
250
|
-
| `chat.toStreamTextOptions({ registry, tools })` | Both of the above. |
|
|
277
|
+
`chat.toStreamTextOptions()` remains available for the same job, and is the only option in a [custom agent](#custom-agents), which has no `run` argument to take a bound `streamText` from. A `chat.headStart` route does get one, and there it also owns `messages`, `stopWhen` and `abortSignal`, since the handover depends on them. Pass `{ registry }` when a prompt names a provider-prefixed model, and `{ tools }` when you want HITL tool approvals, so the SDK knows which calls pause on `needsApproval`.
|
|
251
278
|
|
|
252
279
|
<Tip>
|
|
253
280
|
See [Prompts](/ai/prompts) for the full guide — defining templates, variable schemas, dashboard
|
|
@@ -273,7 +300,7 @@ The `run` function receives three abort signals:
|
|
|
273
300
|
```ts
|
|
274
301
|
export const myChat = chat.agent({
|
|
275
302
|
id: "my-chat",
|
|
276
|
-
run: async ({ messages, signal, stopSignal, cancelSignal }) => {
|
|
303
|
+
run: async ({ messages, signal, stopSignal, cancelSignal, streamText }) => {
|
|
277
304
|
return streamText({
|
|
278
305
|
model: anthropic("claude-sonnet-4-5"),
|
|
279
306
|
messages,
|
|
@@ -302,7 +329,7 @@ export const myChat = chat.agent({
|
|
|
302
329
|
data: { messages: uiMessages, lastStoppedAt: stopped ? new Date() : undefined },
|
|
303
330
|
});
|
|
304
331
|
},
|
|
305
|
-
run: async ({ messages, signal }) => {
|
|
332
|
+
run: async ({ messages, signal, streamText }) => {
|
|
306
333
|
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
307
334
|
},
|
|
308
335
|
});
|
|
@@ -312,11 +339,10 @@ You can also check stop status from **anywhere** during a turn using `chat.isSto
|
|
|
312
339
|
|
|
313
340
|
```ts
|
|
314
341
|
import { chat } from "@trigger.dev/sdk/ai";
|
|
315
|
-
import { streamText } from "ai";
|
|
316
342
|
|
|
317
343
|
export const myChat = chat.agent({
|
|
318
344
|
id: "my-chat",
|
|
319
|
-
run: async ({ messages, signal }) => {
|
|
345
|
+
run: async ({ messages, signal, streamText }) => {
|
|
320
346
|
return streamText({
|
|
321
347
|
model: anthropic("claude-sonnet-4-5"),
|
|
322
348
|
messages,
|
|
@@ -369,7 +395,7 @@ const sendEmail = tool({
|
|
|
369
395
|
|
|
370
396
|
export const myChat = chat.agent({
|
|
371
397
|
id: "my-chat",
|
|
372
|
-
run: async ({ messages, signal }) => {
|
|
398
|
+
run: async ({ messages, signal, streamText }) => {
|
|
373
399
|
return streamText({
|
|
374
400
|
model: anthropic("claude-sonnet-4-5"),
|
|
375
401
|
messages,
|
|
@@ -405,12 +431,12 @@ Users can send messages while the agent is executing tool calls. With `pendingMe
|
|
|
405
431
|
```ts
|
|
406
432
|
export const myChat = chat.agent({
|
|
407
433
|
id: "my-chat",
|
|
434
|
+
registry,
|
|
408
435
|
pendingMessages: {
|
|
409
436
|
shouldInject: ({ steps }) => steps.length > 0,
|
|
410
437
|
},
|
|
411
|
-
run: async ({ messages, signal }) => {
|
|
438
|
+
run: async ({ messages, signal, streamText }) => {
|
|
412
439
|
return streamText({
|
|
413
|
-
...chat.toStreamTextOptions({ registry }),
|
|
414
440
|
messages,
|
|
415
441
|
tools: {
|
|
416
442
|
/* ... */
|
|
@@ -436,6 +462,7 @@ Inject context from background work into the conversation using `chat.inject()`.
|
|
|
436
462
|
```ts
|
|
437
463
|
export const myChat = chat.agent({
|
|
438
464
|
id: "my-chat",
|
|
465
|
+
registry,
|
|
439
466
|
onTurnComplete: async ({ messages }) => {
|
|
440
467
|
chat.defer(
|
|
441
468
|
(async () => {
|
|
@@ -453,8 +480,8 @@ export const myChat = chat.agent({
|
|
|
453
480
|
})()
|
|
454
481
|
);
|
|
455
482
|
},
|
|
456
|
-
run: async ({ messages, signal }) => {
|
|
457
|
-
return streamText({
|
|
483
|
+
run: async ({ messages, signal, streamText }) => {
|
|
484
|
+
return streamText({ messages, abortSignal: signal });
|
|
458
485
|
},
|
|
459
486
|
});
|
|
460
487
|
```
|
|
@@ -565,7 +592,7 @@ export const myChat = chat.agent({
|
|
|
565
592
|
},
|
|
566
593
|
];
|
|
567
594
|
},
|
|
568
|
-
run: async ({ messages, signal }) => {
|
|
595
|
+
run: async ({ messages, signal, streamText }) => {
|
|
569
596
|
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
570
597
|
},
|
|
571
598
|
});
|
|
@@ -590,7 +617,7 @@ By default, a chat agent stays idle after each turn waiting for the next user me
|
|
|
590
617
|
```ts
|
|
591
618
|
chat.agent({
|
|
592
619
|
id: "one-shot",
|
|
593
|
-
run: async ({ messages, signal }) => {
|
|
620
|
+
run: async ({ messages, signal, streamText }) => {
|
|
594
621
|
// Single-response agent — exit after this turn.
|
|
595
622
|
chat.endRun();
|
|
596
623
|
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
@@ -606,6 +633,57 @@ Use this when the agent knows its work is done (budget exhausted, goal achieved,
|
|
|
606
633
|
If you persist `lastEventId` to your own storage for cross-page-load resume, **don't clear it on `chat.endRun()`**. The cursor is sessionId-keyed and stays valid across Run boundaries — clearing it forces the next `sendMessages` to subscribe from `seq_num=0`, where it may hit the prior turn's stale `turn-complete` record and close the stream empty before the new Run's chunks arrive.
|
|
607
634
|
</Warning>
|
|
608
635
|
|
|
636
|
+
### Ending the conversation
|
|
637
|
+
|
|
638
|
+
`chat.close()` ends the whole conversation. The session row is marked closed, further appends are refused with HTTP 409, and no continuation run is scheduled. Call it from `run()`, `prepareStep`, or `onBeforeTurnComplete`.
|
|
639
|
+
|
|
640
|
+
```ts
|
|
641
|
+
chat.agent({
|
|
642
|
+
id: "budgeted-agent",
|
|
643
|
+
run: async ({ messages, signal }) =>
|
|
644
|
+
streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal }),
|
|
645
|
+
// onBeforeTurnComplete carries the same fields as onTurnComplete, including
|
|
646
|
+
// `usage`, and still runs while the response stream is open.
|
|
647
|
+
onBeforeTurnComplete: async ({ chatId }) => {
|
|
648
|
+
if (await overBudget(chatId)) {
|
|
649
|
+
chat.close({ reason: "Monthly budget reached" });
|
|
650
|
+
}
|
|
651
|
+
},
|
|
652
|
+
});
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
The current turn finishes and streams through normally. Called mid-step, `chat.close()` aborts the in-flight `streamText` the same way the stop signal does, so the partial response is still captured and delivered. The transport then flips to a closed state, carrying `reason` so you can render why the conversation ended.
|
|
656
|
+
|
|
657
|
+
<Warning>
|
|
658
|
+
**Decide before the turn ends, not in `onTurnComplete`.** The closed state reaches the browser on the turn's final `turn-complete` record. `onTurnComplete` runs after that record is written and after the response stream has closed, so a close decided there does not reach a reader that is already done with the turn: the user sees a normal answer and learns the conversation ended only when their next message is refused. Use `onBeforeTurnComplete` for the same information one step earlier, and the user sees the closed state as soon as the answer finishes.
|
|
659
|
+
</Warning>
|
|
660
|
+
|
|
661
|
+
Closing is one-way. A closed session cannot be reopened, and its transcript stays readable. Start a new conversation under a different `chatId` to continue.
|
|
662
|
+
|
|
663
|
+
On the client, read the closed state off the transport:
|
|
664
|
+
|
|
665
|
+
```tsx app/components/Chat.tsx
|
|
666
|
+
if (transport.sessionStatus(chatId) === "closed") {
|
|
667
|
+
return <p>This conversation has ended: {transport.sessionClosedReason(chatId)}</p>;
|
|
668
|
+
}
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
### Three levels of stopping
|
|
672
|
+
|
|
673
|
+
`chat.agent` stops at three levels, each with a different "what happens next".
|
|
674
|
+
|
|
675
|
+
| Level | Primitive | What stops | What happens next |
|
|
676
|
+
| --- | --- | --- | --- |
|
|
677
|
+
| Turn | `stopWhen`, or the [stop signal](/ai-chat/backend#stop-generation) aborting `signal` | The current `streamText` | The turn completes, the agent idles for the next message |
|
|
678
|
+
| Run | `chat.endRun()` | The current worker | The next message on the same `chatId` starts a fresh continuation run |
|
|
679
|
+
| Session | `chat.close()` | The conversation, permanently | Nothing. Appends are refused with 409 and no run is triggered |
|
|
680
|
+
|
|
681
|
+
Reach for the narrowest level that does the job: a turn budget is a `stopWhen`, a finished one-shot answer is `chat.endRun()`, and an exhausted account or a signed-out user is `chat.close()`.
|
|
682
|
+
|
|
683
|
+
`chat.close()` is available to [custom agents](/ai-chat/custom-agents#ending-the-conversation) too, where the close is performed when your `run()` returns.
|
|
684
|
+
|
|
685
|
+
A session can also be closed from outside the run: `sessions.close(chatId)` from your backend, or the Close action in the dashboard. A live run is told either way: the close lands on the session's input channel, so an idle or suspended agent leaves its loop on the next wake instead of waiting out its idle timeout.
|
|
686
|
+
|
|
609
687
|
### Runtime configuration
|
|
610
688
|
|
|
611
689
|
#### chat.setTurnTimeout()
|
|
@@ -613,7 +691,7 @@ Use this when the agent knows its work is done (budget exhausted, goal achieved,
|
|
|
613
691
|
Override how long the run stays suspended waiting for the next message. Call from inside `run()`:
|
|
614
692
|
|
|
615
693
|
```ts
|
|
616
|
-
run: async ({ messages, signal }) => {
|
|
694
|
+
run: async ({ messages, signal, streamText }) => {
|
|
617
695
|
chat.setTurnTimeout("2h"); // Wait longer for this conversation
|
|
618
696
|
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
619
697
|
},
|
|
@@ -624,7 +702,7 @@ run: async ({ messages, signal }) => {
|
|
|
624
702
|
Override how long the run stays idle (active, using compute) after each turn:
|
|
625
703
|
|
|
626
704
|
```ts
|
|
627
|
-
run: async ({ messages, signal }) => {
|
|
705
|
+
run: async ({ messages, signal, streamText }) => {
|
|
628
706
|
chat.setIdleTimeoutInSeconds(60); // Stay idle for 1 minute
|
|
629
707
|
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
630
708
|
},
|
|
@@ -659,7 +737,7 @@ export const myChat = chat.agent({
|
|
|
659
737
|
return "Something went wrong. Please try again.";
|
|
660
738
|
},
|
|
661
739
|
},
|
|
662
|
-
run: async ({ messages, signal }) => {
|
|
740
|
+
run: async ({ messages, signal, streamText }) => {
|
|
663
741
|
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
664
742
|
},
|
|
665
743
|
});
|
|
@@ -690,7 +768,7 @@ export const myChat = chat.agent({
|
|
|
690
768
|
sendReasoning: true, // Forward model reasoning (default: true)
|
|
691
769
|
sendSources: true, // Forward source citations (default: false)
|
|
692
770
|
},
|
|
693
|
-
run: async ({ messages, signal }) => {
|
|
771
|
+
run: async ({ messages, signal, streamText }) => {
|
|
694
772
|
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
695
773
|
},
|
|
696
774
|
});
|
|
@@ -708,7 +786,7 @@ export const myChat = chat.agent({
|
|
|
708
786
|
uiMessageStreamOptions: {
|
|
709
787
|
generateMessageId: () => uuidv7(),
|
|
710
788
|
},
|
|
711
|
-
run: async ({ messages, signal }) => {
|
|
789
|
+
run: async ({ messages, signal, streamText }) => {
|
|
712
790
|
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
713
791
|
},
|
|
714
792
|
});
|
|
@@ -728,7 +806,7 @@ export const myChat = chat
|
|
|
728
806
|
})
|
|
729
807
|
.agent({
|
|
730
808
|
id: "my-chat",
|
|
731
|
-
run: async ({ messages, signal }) => {
|
|
809
|
+
run: async ({ messages, signal, streamText }) => {
|
|
732
810
|
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
733
811
|
},
|
|
734
812
|
});
|
|
@@ -746,7 +824,7 @@ export const myChat = chat
|
|
|
746
824
|
Override per-turn with `chat.setUIMessageStreamOptions()` — per-turn values merge with the static config (per-turn wins on conflicts). The override is cleared automatically after each turn.
|
|
747
825
|
|
|
748
826
|
```ts
|
|
749
|
-
run: async ({ messages, clientData, signal }) => {
|
|
827
|
+
run: async ({ messages, clientData, signal, streamText }) => {
|
|
750
828
|
// Enable reasoning only for certain models
|
|
751
829
|
if (clientData.model?.includes("claude")) {
|
|
752
830
|
chat.setUIMessageStreamOptions({ sendReasoning: true });
|
|
@@ -772,7 +850,6 @@ If you need full control over task options, use the standard `task()` with `Chat
|
|
|
772
850
|
```ts
|
|
773
851
|
import { task } from "@trigger.dev/sdk";
|
|
774
852
|
import { chat, type ChatTaskPayload } from "@trigger.dev/sdk/ai";
|
|
775
|
-
import { streamText } from "ai";
|
|
776
853
|
import { anthropic } from "@ai-sdk/anthropic";
|
|
777
854
|
|
|
778
855
|
export const manualChat = task({
|
|
@@ -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,8 +33,9 @@ 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,
|
|
36
37
|
onTurnComplete: async ({ messages }) => {
|
|
37
|
-
// Kick off background analysis
|
|
38
|
+
// Kick off background analysis, doesn't block the turn
|
|
38
39
|
chat.defer(
|
|
39
40
|
(async () => {
|
|
40
41
|
const analysis = await analyzeConversation(messages);
|
|
@@ -47,9 +48,8 @@ export const myChat = chat.agent({
|
|
|
47
48
|
})()
|
|
48
49
|
);
|
|
49
50
|
},
|
|
50
|
-
run: async ({ messages, signal }) => {
|
|
51
|
+
run: async ({ messages, signal, streamText }) => {
|
|
51
52
|
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 {
|
|
80
|
+
import { generateObject, createProviderRegistry, stepCountIs } from "ai";
|
|
81
81
|
import { anthropic } from "@ai-sdk/anthropic";
|
|
82
82
|
import { z } from "zod";
|
|
83
83
|
|
|
@@ -98,6 +98,7 @@ Be concise. Only flag issues worth fixing.`,
|
|
|
98
98
|
|
|
99
99
|
export const myChat = chat.agent({
|
|
100
100
|
id: "my-chat",
|
|
101
|
+
registry,
|
|
101
102
|
onTurnComplete: async ({ messages }) => {
|
|
102
103
|
chat.defer(
|
|
103
104
|
(async () => {
|
|
@@ -139,9 +140,8 @@ export const myChat = chat.agent({
|
|
|
139
140
|
})()
|
|
140
141
|
);
|
|
141
142
|
},
|
|
142
|
-
run: async ({ messages, signal }) => {
|
|
143
|
+
run: async ({ messages, signal, streamText }) => {
|
|
143
144
|
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, because `chat.inject()` persists across the idle wait.
|
|
154
154
|
|
|
155
155
|
## Other use cases
|
|
156
156
|
|
|
@@ -161,27 +161,80 @@ 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, streamText }) => {
|
|
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`.**
|
|
182
|
+
**Don't use `chat.defer()` for the message-history write in `onTurnStart`.** `chat.defer` only promises the work finished before `onTurnComplete`, which is long after the answer started streaming. A page refresh in between reads `[]` from your DB and loses the user's message from the rendered conversation. Use [`chat.deferBeforeOutput()`](#chat-deferbeforeoutput) for that write instead, and reserve `chat.defer` for writes whose timing has no resume implication.
|
|
183
183
|
</Warning>
|
|
184
184
|
|
|
185
|
+
## `chat.deferBeforeOutput`
|
|
186
|
+
|
|
187
|
+
`chat.deferBeforeOutput()` is `chat.defer()` for work the next page load has to see. The work starts immediately and the hook does not await it, so it runs alongside the model and costs no time to first token. The difference is that the output stream waits for it: no part of the answer is written to the session until it settles.
|
|
188
|
+
|
|
189
|
+
That ordering is the point. A reader that can see the answer can also see whatever the work persisted, so a refresh mid-answer never renders a reply to a question that is missing.
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
export const myChat = chat.agent({
|
|
193
|
+
id: "my-chat",
|
|
194
|
+
onTurnStart: async ({ chatId, uiMessages }) => {
|
|
195
|
+
// The next page load has to see this, so the answer waits for it.
|
|
196
|
+
chat.deferBeforeOutput(
|
|
197
|
+
db.chat.update({ where: { id: chatId }, data: { messages: uiMessages } })
|
|
198
|
+
);
|
|
199
|
+
},
|
|
200
|
+
run: async ({ messages, signal, streamText }) => {
|
|
201
|
+
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Use it for the conversation write, a message insert, or anything else the frontend reads back on reload. Keep analytics, audit logs and search-index updates on `chat.defer`.
|
|
207
|
+
|
|
208
|
+
<Warning>
|
|
209
|
+
**This is not a consistency barrier for the turn.** It orders the write against what the frontend can see, and nothing else. The work is still in flight while the model runs, so a tool, a `prepareStep`, or another service reading the same row during the turn can still see the state as it was before the write.
|
|
210
|
+
|
|
211
|
+
If the turn's own code reads the write back, `await` it instead:
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
onTurnStart: async ({ chatId, uiMessages }) => {
|
|
215
|
+
// A tool later in this turn reads this row, so the turn has to wait for it.
|
|
216
|
+
await db.chat.update({ where: { id: chatId }, data: { messages: uiMessages } });
|
|
217
|
+
},
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
That costs time to first token, which is the price of the stronger guarantee. `chat.deferBeforeOutput` is for writes whose only reader is the next page load.
|
|
221
|
+
</Warning>
|
|
222
|
+
|
|
223
|
+
A registered promise that rejects, or that runs longer than the internal timeout, lets the answer through rather than stalling the conversation. It is a best-effort ordering guarantee, not a lock.
|
|
224
|
+
|
|
225
|
+
| | `chat.defer()` | `chat.deferBeforeOutput()` |
|
|
226
|
+
|---|---|---|
|
|
227
|
+
| **Awaited by the hook** | No | No |
|
|
228
|
+
| **Costs time to first token** | No | No |
|
|
229
|
+
| **Answer waits for it** | No | Yes |
|
|
230
|
+
| **Settled by** | Before `onTurnComplete` | Before the first chunk reaches the session |
|
|
231
|
+
| **Read back by tools in the same turn** | No | No |
|
|
232
|
+
| **Use for** | Analytics, audit logs, index writes | Conversation and message writes the frontend reads back |
|
|
233
|
+
|
|
234
|
+
<Note>
|
|
235
|
+
If your agent persists through a [transcript storage](/ai-chat/transcript-storage), the runtime already does this for the incoming message on your behalf. `chat.deferBeforeOutput` is for writes your app owns on top of that.
|
|
236
|
+
</Note>
|
|
237
|
+
|
|
185
238
|
## How it differs from pending messages
|
|
186
239
|
|
|
187
240
|
| | `chat.inject()` | [Pending messages](/ai-chat/pending-messages) |
|
|
@@ -189,9 +242,57 @@ export const myChat = chat.agent({
|
|
|
189
242
|
| **Source** | Backend task code | Frontend user input |
|
|
190
243
|
| **Triggered by** | Your code (e.g. `onTurnComplete` + `chat.defer()`) | User sending a message during streaming |
|
|
191
244
|
| **Injection point** | Start of next turn, or next `prepareStep` boundary | Next `prepareStep` boundary only |
|
|
192
|
-
| **Message role** | Any
|
|
245
|
+
| **Message role** | Any. `system` becomes an instruction, others join the conversation (see below) | Typically `user` |
|
|
193
246
|
| **Frontend visibility** | Not visible unless you write custom `data-*` chunks | Visible via `usePendingMessages` hook |
|
|
194
247
|
|
|
248
|
+
## Two lanes: trusted and untrusted
|
|
249
|
+
|
|
250
|
+
The role you inject with decides more than position. It decides whether the model
|
|
251
|
+
treats the content as trustworthy.
|
|
252
|
+
|
|
253
|
+
**`role: "system"` goes to the instructions lane.** The block is appended to the
|
|
254
|
+
system instructions for subsequent inference calls, so it carries the same standing
|
|
255
|
+
as your system prompt. This is the lane for context the agent should believe:
|
|
256
|
+
entitlements, plan changes, operational notices.
|
|
257
|
+
|
|
258
|
+
It has to work this way. On AI SDK 7 a system message inside `messages` is rejected
|
|
259
|
+
for every provider. `standardizePrompt` throws before any provider is called, and
|
|
260
|
+
its own advice is to use the instructions option, so the injected block goes there
|
|
261
|
+
rather than into the transcript.
|
|
262
|
+
|
|
263
|
+
<Warning>
|
|
264
|
+
The instructions lane is delivered by `chat.toStreamTextOptions()`, because that
|
|
265
|
+
is the only place the SDK can set `streamText`'s instructions for you. If your
|
|
266
|
+
`run()` calls `streamText({ model, messages, abortSignal })` without spreading
|
|
267
|
+
`chat.toStreamTextOptions()`, a `role: "system"` injection never reaches the
|
|
268
|
+
model. The conversational lane has no such requirement: it arrives through
|
|
269
|
+
`messages` either way.
|
|
270
|
+
</Warning>
|
|
271
|
+
|
|
272
|
+
- An injection applies to the next turn only. A block injected in `onTurnComplete`
|
|
273
|
+
shapes the following turn and is cleared after it, so it is not repeated on every
|
|
274
|
+
turn from then on. Within that turn it is consumed once rather than once per read,
|
|
275
|
+
so a `run()` that builds options more than once sees the same instructions in
|
|
276
|
+
every build.
|
|
277
|
+
- The injected text is merged into a single instruction rather than added as a
|
|
278
|
+
second block, because AI SDK 5 rejects an array of system blocks while accepting
|
|
279
|
+
one structured block. Merging changes the cached prefix, so a cached system prompt
|
|
280
|
+
gets no cache hit for as long as an injection is live. If you rely on prompt
|
|
281
|
+
caching, inject sparingly and prefer facts that go stale, so the injection clears.
|
|
282
|
+
|
|
283
|
+
**Any other role joins the conversation, and is untrusted by construction.** A
|
|
284
|
+
message injected as `user` is indistinguishable from something the user typed, and a
|
|
285
|
+
well-aligned model treats it accordingly, and may say so and re-derive the answer
|
|
286
|
+
from tools instead of taking it at face value:
|
|
287
|
+
|
|
288
|
+
> "that text arrived embedded in your message, not from a tool I called, so I
|
|
289
|
+
> verified it myself rather than trusting it"
|
|
290
|
+
|
|
291
|
+
That is correct behaviour, not a bug. So inject **checkable facts** in the
|
|
292
|
+
conversational lane and put **directives** in the instructions lane. A conclusion
|
|
293
|
+
injected as a user message is the worst of both: the model neither trusts it nor
|
|
294
|
+
ignores it, and may contradict it in front of the user.
|
|
295
|
+
|
|
195
296
|
## API reference
|
|
196
297
|
|
|
197
298
|
### chat.inject()
|
|
@@ -200,7 +301,9 @@ export const myChat = chat.agent({
|
|
|
200
301
|
chat.inject(messages: ModelMessage[]): void
|
|
201
302
|
```
|
|
202
303
|
|
|
203
|
-
Queue model messages for injection at the next opportunity. Messages persist across the idle wait between turns
|
|
304
|
+
Queue model messages for injection at the next opportunity. Messages persist across the idle wait between turns, and are not reset when a new turn starts.
|
|
305
|
+
|
|
306
|
+
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.
|
|
204
307
|
|
|
205
308
|
**Parameters:**
|
|
206
309
|
|
|
@@ -209,9 +312,9 @@ Queue model messages for injection at the next opportunity. Messages persist acr
|
|
|
209
312
|
| `messages` | `ModelMessage[]` | Model messages to inject (from the `ai` package) |
|
|
210
313
|
|
|
211
314
|
Messages are drained (consumed) when:
|
|
212
|
-
1. A new turn starts
|
|
213
|
-
2. A `prepareStep` boundary is reached
|
|
315
|
+
1. A new turn starts, before `run()` executes
|
|
316
|
+
2. A `prepareStep` boundary is reached, between tool-call steps during streaming
|
|
214
317
|
|
|
215
318
|
<Note>
|
|
216
|
-
`chat.inject()` writes to an in-memory queue in the current process. It works from any code running in the same task
|
|
319
|
+
`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.
|
|
217
320
|
</Note>
|