@trigger.dev/sdk 0.0.0-prerelease-20260908122921 → 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.d.ts +16 -199
- package/dist/commonjs/v3/ai.js +102 -983
- package/dist/commonjs/v3/ai.js.map +1 -1
- package/dist/commonjs/v3/chat-client.d.ts +2 -3
- package/dist/commonjs/v3/chat-client.js +5 -31
- package/dist/commonjs/v3/chat-client.js.map +1 -1
- package/dist/commonjs/v3/chat-react.d.ts +0 -34
- package/dist/commonjs/v3/chat-react.js +1 -47
- 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 -81
- package/dist/commonjs/v3/chat.js +46 -292
- 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/mock-chat-agent.d.ts +0 -43
- package/dist/commonjs/v3/test/mock-chat-agent.js +0 -90
- 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.d.ts +16 -199
- package/dist/esm/v3/ai.js +103 -984
- package/dist/esm/v3/ai.js.map +1 -1
- package/dist/esm/v3/chat-client.d.ts +2 -3
- package/dist/esm/v3/chat-client.js +5 -31
- package/dist/esm/v3/chat-client.js.map +1 -1
- package/dist/esm/v3/chat-react.d.ts +0 -34
- package/dist/esm/v3/chat-react.js +1 -46
- 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 -81
- package/dist/esm/v3/chat.js +47 -293
- 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/mock-chat-agent.d.ts +0 -43
- package/dist/esm/v3/test/mock-chat-agent.js +2 -92
- 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 -67
- package/docs/ai-chat/client-protocol.mdx +4 -5
- package/docs/ai-chat/compaction.mdx +7 -11
- 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 -17
- package/docs/ai-chat/migrating-from-a-route-handler.mdx +14 -16
- 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 +5 -12
- 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/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 +27 -38
- package/skills/trigger-chat-agent-advanced/SKILL.md +12 -31
- 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/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/docs/ai-chat/patterns/native-compaction.mdx +0 -310
- package/docs/reports.mdx +0 -157
- package/docs/troubleshooting-zod.mdx +0 -158
package/docs/ai-chat/actions.mdx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "Actions"
|
|
3
3
|
sidebarTitle: "Actions"
|
|
4
|
-
description: "Custom commands sent from the frontend that mutate chat state without consuming a turn
|
|
4
|
+
description: "Custom commands sent from the frontend that mutate chat state without consuming a turn — undo, rollback, edit, regenerate."
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## Overview
|
|
@@ -44,7 +44,7 @@ export const myChat = chat.agent({
|
|
|
44
44
|
// returning void → side-effect-only, no model call
|
|
45
45
|
},
|
|
46
46
|
|
|
47
|
-
run: async ({ messages, signal
|
|
47
|
+
run: async ({ messages, signal }) => {
|
|
48
48
|
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
49
49
|
},
|
|
50
50
|
});
|
|
@@ -52,68 +52,36 @@ export const myChat = chat.agent({
|
|
|
52
52
|
|
|
53
53
|
**Lifecycle flow:** Wake → parse action against `actionSchema` → `hydrateMessages` (if set) → **`onAction`** → apply `chat.history` mutations → emit `trigger:turn-complete` → wait for next message.
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
## Returning a model response from an action
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
An action is a state edit. To answer after the edit, return `chat.turn()`: the edit is applied and snapshotted, then a turn runs on the edited history exactly as a message turn does. `onTurnStart`, `run()`, `onBeforeTurnComplete` and `onTurnComplete` fire, the turn counter advances, and the answer gets everything a turn has: the agent's system prompt and tools, steering, compaction, injected instructions and persistence.
|
|
57
|
+
`onAction` can return a `StreamTextResult`, `string`, or `UIMessage` to produce a response. The returned stream is auto-piped to the frontend just like a normal turn, but the rest of the turn machinery (`onTurnStart`, `onTurnComplete`, etc.) still does not fire.
|
|
60
58
|
|
|
61
59
|
```ts
|
|
62
|
-
onAction: async ({ action }) => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return chat.turn(); // answer the edited history
|
|
71
|
-
|
|
72
|
-
case "retry-formal":
|
|
73
|
-
chat.history.slice(0, -1);
|
|
74
|
-
chat.inject([{ role: "system", content: "Answer formally this time." }]);
|
|
75
|
-
return chat.turn(); // with a one-shot instruction
|
|
60
|
+
onAction: async ({ action, messages }) => {
|
|
61
|
+
if (action.type === "regenerate") {
|
|
62
|
+
chat.history.slice(0, -1); // drop the last assistant
|
|
63
|
+
return streamText({
|
|
64
|
+
model: anthropic("claude-sonnet-4-5"),
|
|
65
|
+
messages,
|
|
66
|
+
stopWhen: stepCountIs(15),
|
|
67
|
+
});
|
|
76
68
|
}
|
|
69
|
+
// other actions return void → side-effect only
|
|
77
70
|
}
|
|
78
71
|
```
|
|
79
72
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
### Actions and persistence
|
|
83
|
-
|
|
84
|
-
An action that returns nothing does not fire `onTurnComplete`, and that is where an app that owns its own transcript normally writes. What that means depends on which persistence model you use.
|
|
85
|
-
|
|
86
|
-
**Platform-managed** (no `hydrateMessages`): nothing to do. After an action that changed the conversation, the runtime writes the snapshot, so the edit survives the run ending. An action that returns `chat.turn()` is followed by a turn, which persists its answer the way every turn does.
|
|
87
|
-
|
|
88
|
-
**Your own store** (`hydrateMessages` registered): the runtime deliberately does not write, because your store is the source of truth. A history edit lives only in the running worker until you persist it, and a continuation rehydrates from your store, not from what the worker had in memory. Mirror each edit in your store, not only additions: a regenerate is a delete *and* an insert. The answer that follows `chat.turn()` reaches your store through `onTurnComplete`, like any turn's answer.
|
|
89
|
-
|
|
90
|
-
```ts
|
|
91
|
-
onAction: async ({ action, chatId }) => {
|
|
92
|
-
if (action.type === "undo") {
|
|
93
|
-
chat.history.slice(0, -2);
|
|
94
|
-
await db.deleteLastExchange(chatId); // the rollback is yours to persist
|
|
95
|
-
}
|
|
96
|
-
if (action.type === "regenerate") {
|
|
97
|
-
chat.history.slice(0, -1);
|
|
98
|
-
await db.deleteLastAssistant(chatId); // the delete half
|
|
99
|
-
return chat.turn(); // the insert half arrives in onTurnComplete
|
|
100
|
-
}
|
|
101
|
-
},
|
|
102
|
-
onTurnComplete: async ({ chatId, newUIMessages }) => {
|
|
103
|
-
await db.saveMessages(chatId, newUIMessages);
|
|
104
|
-
},
|
|
105
|
-
```
|
|
73
|
+
This is useful for actions that both mutate state and want a fresh model response (regenerate-from-here, retry-with-different-style). Persistence is your responsibility inside `onAction` itself; you have access to the streamed response object.
|
|
106
74
|
|
|
107
75
|
## Gating actions on HITL state
|
|
108
76
|
|
|
109
77
|
If you have a [human-in-the-loop](/ai-chat/patterns/human-in-the-loop) tool waiting on `addToolOutput`, you usually want to refuse competing actions like `regenerate` until the answer arrives. [`chat.history.getPendingToolCalls()`](/ai-chat/backend#chat-history) gives you exactly that signal:
|
|
110
78
|
|
|
111
79
|
```ts
|
|
112
|
-
onAction: async ({ action }) => {
|
|
80
|
+
onAction: async ({ action, messages, signal }) => {
|
|
113
81
|
if (action.type === "regenerate") {
|
|
114
82
|
if (chat.history.getPendingToolCalls().length > 0) return; // gated
|
|
115
83
|
chat.history.slice(0, -1);
|
|
116
|
-
return
|
|
84
|
+
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
117
85
|
}
|
|
118
86
|
},
|
|
119
87
|
```
|
|
@@ -121,10 +89,10 @@ onAction: async ({ action }) => {
|
|
|
121
89
|
## Sending actions from the frontend
|
|
122
90
|
|
|
123
91
|
```ts
|
|
124
|
-
// Browser
|
|
92
|
+
// Browser — TriggerChatTransport
|
|
125
93
|
const stream = await transport.sendAction(chatId, { type: "undo" });
|
|
126
94
|
|
|
127
|
-
// Server
|
|
95
|
+
// Server — AgentChat
|
|
128
96
|
const stream = await agentChat.sendAction({ type: "rollback", targetMessageId: "msg-3" });
|
|
129
97
|
```
|
|
130
98
|
|
|
@@ -136,8 +104,8 @@ The action payload is validated against `actionSchema` on the backend; invalid a
|
|
|
136
104
|
|
|
137
105
|
## See also
|
|
138
106
|
|
|
139
|
-
- [`chat.history`](/ai-chat/backend#chat-history)
|
|
140
|
-
- [Sending actions from the frontend](/ai-chat/frontend#sending-actions)
|
|
141
|
-
- [`hydrateMessages`](/ai-chat/lifecycle-hooks#hydratemessages)
|
|
142
|
-
- [Branching conversations](/ai-chat/patterns/branching-conversations)
|
|
143
|
-
- [Human-in-the-loop](/ai-chat/patterns/human-in-the-loop)
|
|
107
|
+
- [`chat.history`](/ai-chat/backend#chat-history) — the imperative API actions use to mutate state
|
|
108
|
+
- [Sending actions from the frontend](/ai-chat/frontend#sending-actions) — `transport.sendAction` ergonomics
|
|
109
|
+
- [`hydrateMessages`](/ai-chat/lifecycle-hooks#hydratemessages) — fires before `onAction` when set
|
|
110
|
+
- [Branching conversations](/ai-chat/patterns/branching-conversations) — pairs action handlers with backend-controlled history
|
|
111
|
+
- [Human-in-the-loop](/ai-chat/patterns/human-in-the-loop) — gating fresh actions while a tool is waiting
|
package/docs/ai-chat/anatomy.mdx
CHANGED
|
@@ -18,7 +18,7 @@ Everything below maps onto one annotated agent:
|
|
|
18
18
|
|
|
19
19
|
```ts trigger/my-agent.ts
|
|
20
20
|
import { chat } from "@trigger.dev/sdk/ai";
|
|
21
|
-
import { stepCountIs } from "ai";
|
|
21
|
+
import { streamText, stepCountIs } from "ai";
|
|
22
22
|
import { anthropic } from "@ai-sdk/anthropic";
|
|
23
23
|
|
|
24
24
|
export const myAgent = chat.agent({
|
|
@@ -36,9 +36,9 @@ export const myAgent = chat.agent({
|
|
|
36
36
|
|
|
37
37
|
// The turn loop. Messages arrive accumulated; you stream back.
|
|
38
38
|
// Options, levels, and alternatives — see Backend.
|
|
39
|
-
run: async ({ messages, tools, signal
|
|
39
|
+
run: async ({ messages, tools, signal }) =>
|
|
40
40
|
streamText({
|
|
41
|
-
tools,
|
|
41
|
+
...chat.toStreamTextOptions({ tools }),
|
|
42
42
|
model: anthropic("claude-sonnet-4-5"),
|
|
43
43
|
messages,
|
|
44
44
|
abortSignal: signal,
|
package/docs/ai-chat/backend.mdx
CHANGED
|
@@ -30,13 +30,14 @@ 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 { stepCountIs } from "ai";
|
|
33
|
+
import { streamText, 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 }) => {
|
|
39
39
|
return streamText({
|
|
40
|
+
...chat.toStreamTextOptions(), // prepareStep, system, telemetry (see note below)
|
|
40
41
|
model: anthropic("claude-sonnet-4-5"),
|
|
41
42
|
system: "You are a helpful assistant.",
|
|
42
43
|
messages,
|
|
@@ -47,64 +48,29 @@ export const simpleChat = chat.agent({
|
|
|
47
48
|
});
|
|
48
49
|
```
|
|
49
50
|
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
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 })`.
|
|
51
|
+
<Warning>
|
|
52
|
+
**Always spread `chat.toStreamTextOptions()` first** (as above) so your explicit overrides win. It wires up the `prepareStep` callback behind [compaction](/ai-chat/compaction), [steering](/ai-chat/pending-messages), and [background injection](/ai-chat/background-injection), all of which silently no-op without it, and injects the system prompt from `chat.prompt()`, the resolved model (when you pass a `registry`), and telemetry metadata. Examples below keep the spread implicit for brevity, so include it in real code.
|
|
53
|
+
</Warning>
|
|
87
54
|
|
|
88
55
|
### Using chat.pipe() for complex flows
|
|
89
56
|
|
|
90
57
|
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.
|
|
91
58
|
|
|
92
59
|
```ts trigger/agent-chat.ts
|
|
93
|
-
import { chat
|
|
60
|
+
import { chat } from "@trigger.dev/sdk/ai";
|
|
61
|
+
import { streamText } from "ai";
|
|
94
62
|
import { anthropic } from "@ai-sdk/anthropic";
|
|
95
|
-
import {
|
|
63
|
+
import type { ModelMessage } from "ai";
|
|
96
64
|
|
|
97
65
|
export const agentChat = chat.agent({
|
|
98
66
|
id: "agent-chat",
|
|
99
|
-
run: async ({ messages
|
|
100
|
-
// Don't return anything
|
|
101
|
-
await runAgentLoop(messages
|
|
67
|
+
run: async ({ messages }) => {
|
|
68
|
+
// Don't return anything — chat.pipe is called inside
|
|
69
|
+
await runAgentLoop(messages);
|
|
102
70
|
},
|
|
103
71
|
});
|
|
104
72
|
|
|
105
|
-
|
|
106
|
-
// managed options. `ChatStreamText` types the parameter.
|
|
107
|
-
async function runAgentLoop(messages: ModelMessage[], streamText: ChatStreamText) {
|
|
73
|
+
async function runAgentLoop(messages: ModelMessage[]) {
|
|
108
74
|
// ... agent logic, tool calls, etc.
|
|
109
75
|
|
|
110
76
|
const result = streamText({
|
|
@@ -136,7 +102,7 @@ export const myChat = chat.agent({
|
|
|
136
102
|
// responseMessage.parts includes the data-metadata part
|
|
137
103
|
await db.messages.save(responseMessage);
|
|
138
104
|
},
|
|
139
|
-
run: async ({ messages, signal
|
|
105
|
+
run: async ({ messages, signal }) => {
|
|
140
106
|
// Also works from run() via chat.response
|
|
141
107
|
chat.response.write({
|
|
142
108
|
type: "data-context",
|
|
@@ -211,9 +177,9 @@ const tools = { searchDocs };
|
|
|
211
177
|
export const myChat = chat.agent({
|
|
212
178
|
id: "my-chat",
|
|
213
179
|
tools,
|
|
214
|
-
run: async ({ messages, tools, signal
|
|
180
|
+
run: async ({ messages, tools, signal }) =>
|
|
215
181
|
streamText({
|
|
216
|
-
tools,
|
|
182
|
+
...chat.toStreamTextOptions({ tools }),
|
|
217
183
|
model: anthropic("claude-sonnet-4-5"),
|
|
218
184
|
messages,
|
|
219
185
|
abortSignal: signal,
|
|
@@ -234,12 +200,12 @@ See [Tools](/ai-chat/tools) for `toModelOutput` across turns, per-turn dynamic t
|
|
|
234
200
|
|
|
235
201
|
### Using prompts
|
|
236
202
|
|
|
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()
|
|
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()`, then spread `chat.toStreamTextOptions()` into `streamText` — it includes the system prompt, model, config, and telemetry automatically.
|
|
238
204
|
|
|
239
205
|
```ts
|
|
240
206
|
import { chat } from "@trigger.dev/sdk/ai";
|
|
241
207
|
import { prompts } from "@trigger.dev/sdk";
|
|
242
|
-
import { createProviderRegistry } from "ai";
|
|
208
|
+
import { streamText, createProviderRegistry } from "ai";
|
|
243
209
|
import { anthropic } from "@ai-sdk/anthropic";
|
|
244
210
|
import { z } from "zod";
|
|
245
211
|
|
|
@@ -255,15 +221,15 @@ const systemPrompt = prompts.define({
|
|
|
255
221
|
|
|
256
222
|
export const myChat = chat.agent({
|
|
257
223
|
id: "my-chat",
|
|
258
|
-
registry,
|
|
259
224
|
clientDataSchema: z.object({ userId: z.string() }),
|
|
260
225
|
onChatStart: async ({ clientData }) => {
|
|
261
226
|
const user = await db.user.findUnique({ where: { id: clientData.userId } });
|
|
262
227
|
const resolved = await systemPrompt.resolve({ name: user.name });
|
|
263
228
|
chat.prompt.set(resolved);
|
|
264
229
|
},
|
|
265
|
-
run: async ({ messages, signal
|
|
230
|
+
run: async ({ messages, signal }) => {
|
|
266
231
|
return streamText({
|
|
232
|
+
...chat.toStreamTextOptions({ registry }), // system, model, config, telemetry
|
|
267
233
|
messages,
|
|
268
234
|
abortSignal: signal,
|
|
269
235
|
stopWhen: stepCountIs(15),
|
|
@@ -272,9 +238,16 @@ export const myChat = chat.agent({
|
|
|
272
238
|
});
|
|
273
239
|
```
|
|
274
240
|
|
|
275
|
-
|
|
241
|
+
`chat.toStreamTextOptions()` returns an object with `system`, `model` (resolved via the registry), `temperature`, and `experimental_telemetry` — all from the stored prompt. Properties you set after the spread (like a client-selected model) take precedence.
|
|
242
|
+
|
|
243
|
+
**Which form to call:**
|
|
276
244
|
|
|
277
|
-
|
|
245
|
+
| Form | Use when |
|
|
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. |
|
|
278
251
|
|
|
279
252
|
<Tip>
|
|
280
253
|
See [Prompts](/ai/prompts) for the full guide — defining templates, variable schemas, dashboard
|
|
@@ -300,7 +273,7 @@ The `run` function receives three abort signals:
|
|
|
300
273
|
```ts
|
|
301
274
|
export const myChat = chat.agent({
|
|
302
275
|
id: "my-chat",
|
|
303
|
-
run: async ({ messages, signal, stopSignal, cancelSignal
|
|
276
|
+
run: async ({ messages, signal, stopSignal, cancelSignal }) => {
|
|
304
277
|
return streamText({
|
|
305
278
|
model: anthropic("claude-sonnet-4-5"),
|
|
306
279
|
messages,
|
|
@@ -329,7 +302,7 @@ export const myChat = chat.agent({
|
|
|
329
302
|
data: { messages: uiMessages, lastStoppedAt: stopped ? new Date() : undefined },
|
|
330
303
|
});
|
|
331
304
|
},
|
|
332
|
-
run: async ({ messages, signal
|
|
305
|
+
run: async ({ messages, signal }) => {
|
|
333
306
|
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
334
307
|
},
|
|
335
308
|
});
|
|
@@ -339,10 +312,11 @@ You can also check stop status from **anywhere** during a turn using `chat.isSto
|
|
|
339
312
|
|
|
340
313
|
```ts
|
|
341
314
|
import { chat } from "@trigger.dev/sdk/ai";
|
|
315
|
+
import { streamText } from "ai";
|
|
342
316
|
|
|
343
317
|
export const myChat = chat.agent({
|
|
344
318
|
id: "my-chat",
|
|
345
|
-
run: async ({ messages, signal
|
|
319
|
+
run: async ({ messages, signal }) => {
|
|
346
320
|
return streamText({
|
|
347
321
|
model: anthropic("claude-sonnet-4-5"),
|
|
348
322
|
messages,
|
|
@@ -395,7 +369,7 @@ const sendEmail = tool({
|
|
|
395
369
|
|
|
396
370
|
export const myChat = chat.agent({
|
|
397
371
|
id: "my-chat",
|
|
398
|
-
run: async ({ messages, signal
|
|
372
|
+
run: async ({ messages, signal }) => {
|
|
399
373
|
return streamText({
|
|
400
374
|
model: anthropic("claude-sonnet-4-5"),
|
|
401
375
|
messages,
|
|
@@ -431,12 +405,12 @@ Users can send messages while the agent is executing tool calls. With `pendingMe
|
|
|
431
405
|
```ts
|
|
432
406
|
export const myChat = chat.agent({
|
|
433
407
|
id: "my-chat",
|
|
434
|
-
registry,
|
|
435
408
|
pendingMessages: {
|
|
436
409
|
shouldInject: ({ steps }) => steps.length > 0,
|
|
437
410
|
},
|
|
438
|
-
run: async ({ messages, signal
|
|
411
|
+
run: async ({ messages, signal }) => {
|
|
439
412
|
return streamText({
|
|
413
|
+
...chat.toStreamTextOptions({ registry }),
|
|
440
414
|
messages,
|
|
441
415
|
tools: {
|
|
442
416
|
/* ... */
|
|
@@ -462,7 +436,6 @@ Inject context from background work into the conversation using `chat.inject()`.
|
|
|
462
436
|
```ts
|
|
463
437
|
export const myChat = chat.agent({
|
|
464
438
|
id: "my-chat",
|
|
465
|
-
registry,
|
|
466
439
|
onTurnComplete: async ({ messages }) => {
|
|
467
440
|
chat.defer(
|
|
468
441
|
(async () => {
|
|
@@ -480,8 +453,8 @@ export const myChat = chat.agent({
|
|
|
480
453
|
})()
|
|
481
454
|
);
|
|
482
455
|
},
|
|
483
|
-
run: async ({ messages, signal
|
|
484
|
-
return streamText({ messages, abortSignal: signal });
|
|
456
|
+
run: async ({ messages, signal }) => {
|
|
457
|
+
return streamText({ ...chat.toStreamTextOptions({ registry }), messages, abortSignal: signal });
|
|
485
458
|
},
|
|
486
459
|
});
|
|
487
460
|
```
|
|
@@ -592,7 +565,7 @@ export const myChat = chat.agent({
|
|
|
592
565
|
},
|
|
593
566
|
];
|
|
594
567
|
},
|
|
595
|
-
run: async ({ messages, signal
|
|
568
|
+
run: async ({ messages, signal }) => {
|
|
596
569
|
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
597
570
|
},
|
|
598
571
|
});
|
|
@@ -617,7 +590,7 @@ By default, a chat agent stays idle after each turn waiting for the next user me
|
|
|
617
590
|
```ts
|
|
618
591
|
chat.agent({
|
|
619
592
|
id: "one-shot",
|
|
620
|
-
run: async ({ messages, signal
|
|
593
|
+
run: async ({ messages, signal }) => {
|
|
621
594
|
// Single-response agent — exit after this turn.
|
|
622
595
|
chat.endRun();
|
|
623
596
|
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
@@ -633,57 +606,6 @@ Use this when the agent knows its work is done (budget exhausted, goal achieved,
|
|
|
633
606
|
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.
|
|
634
607
|
</Warning>
|
|
635
608
|
|
|
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
|
-
|
|
687
609
|
### Runtime configuration
|
|
688
610
|
|
|
689
611
|
#### chat.setTurnTimeout()
|
|
@@ -691,7 +613,7 @@ A session can also be closed from outside the run: `sessions.close(chatId)` from
|
|
|
691
613
|
Override how long the run stays suspended waiting for the next message. Call from inside `run()`:
|
|
692
614
|
|
|
693
615
|
```ts
|
|
694
|
-
run: async ({ messages, signal
|
|
616
|
+
run: async ({ messages, signal }) => {
|
|
695
617
|
chat.setTurnTimeout("2h"); // Wait longer for this conversation
|
|
696
618
|
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
697
619
|
},
|
|
@@ -702,7 +624,7 @@ run: async ({ messages, signal, streamText }) => {
|
|
|
702
624
|
Override how long the run stays idle (active, using compute) after each turn:
|
|
703
625
|
|
|
704
626
|
```ts
|
|
705
|
-
run: async ({ messages, signal
|
|
627
|
+
run: async ({ messages, signal }) => {
|
|
706
628
|
chat.setIdleTimeoutInSeconds(60); // Stay idle for 1 minute
|
|
707
629
|
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
708
630
|
},
|
|
@@ -737,7 +659,7 @@ export const myChat = chat.agent({
|
|
|
737
659
|
return "Something went wrong. Please try again.";
|
|
738
660
|
},
|
|
739
661
|
},
|
|
740
|
-
run: async ({ messages, signal
|
|
662
|
+
run: async ({ messages, signal }) => {
|
|
741
663
|
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
742
664
|
},
|
|
743
665
|
});
|
|
@@ -768,7 +690,7 @@ export const myChat = chat.agent({
|
|
|
768
690
|
sendReasoning: true, // Forward model reasoning (default: true)
|
|
769
691
|
sendSources: true, // Forward source citations (default: false)
|
|
770
692
|
},
|
|
771
|
-
run: async ({ messages, signal
|
|
693
|
+
run: async ({ messages, signal }) => {
|
|
772
694
|
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
773
695
|
},
|
|
774
696
|
});
|
|
@@ -786,7 +708,7 @@ export const myChat = chat.agent({
|
|
|
786
708
|
uiMessageStreamOptions: {
|
|
787
709
|
generateMessageId: () => uuidv7(),
|
|
788
710
|
},
|
|
789
|
-
run: async ({ messages, signal
|
|
711
|
+
run: async ({ messages, signal }) => {
|
|
790
712
|
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
791
713
|
},
|
|
792
714
|
});
|
|
@@ -806,7 +728,7 @@ export const myChat = chat
|
|
|
806
728
|
})
|
|
807
729
|
.agent({
|
|
808
730
|
id: "my-chat",
|
|
809
|
-
run: async ({ messages, signal
|
|
731
|
+
run: async ({ messages, signal }) => {
|
|
810
732
|
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
811
733
|
},
|
|
812
734
|
});
|
|
@@ -824,7 +746,7 @@ export const myChat = chat
|
|
|
824
746
|
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.
|
|
825
747
|
|
|
826
748
|
```ts
|
|
827
|
-
run: async ({ messages, clientData, signal
|
|
749
|
+
run: async ({ messages, clientData, signal }) => {
|
|
828
750
|
// Enable reasoning only for certain models
|
|
829
751
|
if (clientData.model?.includes("claude")) {
|
|
830
752
|
chat.setUIMessageStreamOptions({ sendReasoning: true });
|
|
@@ -850,6 +772,7 @@ If you need full control over task options, use the standard `task()` with `Chat
|
|
|
850
772
|
```ts
|
|
851
773
|
import { task } from "@trigger.dev/sdk";
|
|
852
774
|
import { chat, type ChatTaskPayload } from "@trigger.dev/sdk/ai";
|
|
775
|
+
import { streamText } from "ai";
|
|
853
776
|
import { anthropic } from "@ai-sdk/anthropic";
|
|
854
777
|
|
|
855
778
|
export const manualChat = task({
|