@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.
Files changed (106) hide show
  1. package/dist/commonjs/imports/ai-runtime-cjs.cjs.map +1 -1
  2. package/dist/commonjs/imports/ai-runtime.js +0 -2
  3. package/dist/commonjs/v3/ai.d.ts +16 -199
  4. package/dist/commonjs/v3/ai.js +102 -983
  5. package/dist/commonjs/v3/ai.js.map +1 -1
  6. package/dist/commonjs/v3/chat-client.d.ts +2 -3
  7. package/dist/commonjs/v3/chat-client.js +5 -31
  8. package/dist/commonjs/v3/chat-client.js.map +1 -1
  9. package/dist/commonjs/v3/chat-react.d.ts +0 -34
  10. package/dist/commonjs/v3/chat-react.js +1 -47
  11. package/dist/commonjs/v3/chat-react.js.map +1 -1
  12. package/dist/commonjs/v3/chat-server.d.ts +6 -42
  13. package/dist/commonjs/v3/chat-server.js +7 -52
  14. package/dist/commonjs/v3/chat-server.js.map +1 -1
  15. package/dist/commonjs/v3/chat.d.ts +10 -81
  16. package/dist/commonjs/v3/chat.js +46 -292
  17. package/dist/commonjs/v3/chat.js.map +1 -1
  18. package/dist/commonjs/v3/sessions.d.ts +2 -15
  19. package/dist/commonjs/v3/sessions.js +1 -12
  20. package/dist/commonjs/v3/sessions.js.map +1 -1
  21. package/dist/commonjs/v3/shared.js +36 -30
  22. package/dist/commonjs/v3/shared.js.map +1 -1
  23. package/dist/commonjs/v3/test/mock-chat-agent.d.ts +0 -43
  24. package/dist/commonjs/v3/test/mock-chat-agent.js +0 -90
  25. package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
  26. package/dist/commonjs/v3/test/test-session-handle.js +0 -6
  27. package/dist/commonjs/v3/test/test-session-handle.js.map +1 -1
  28. package/dist/commonjs/version.js +1 -1
  29. package/dist/esm/imports/ai-runtime.d.ts +2 -2
  30. package/dist/esm/imports/ai-runtime.js +2 -2
  31. package/dist/esm/imports/ai-runtime.js.map +1 -1
  32. package/dist/esm/v3/ai.d.ts +16 -199
  33. package/dist/esm/v3/ai.js +103 -984
  34. package/dist/esm/v3/ai.js.map +1 -1
  35. package/dist/esm/v3/chat-client.d.ts +2 -3
  36. package/dist/esm/v3/chat-client.js +5 -31
  37. package/dist/esm/v3/chat-client.js.map +1 -1
  38. package/dist/esm/v3/chat-react.d.ts +0 -34
  39. package/dist/esm/v3/chat-react.js +1 -46
  40. package/dist/esm/v3/chat-react.js.map +1 -1
  41. package/dist/esm/v3/chat-server.d.ts +6 -42
  42. package/dist/esm/v3/chat-server.js +8 -53
  43. package/dist/esm/v3/chat-server.js.map +1 -1
  44. package/dist/esm/v3/chat.d.ts +10 -81
  45. package/dist/esm/v3/chat.js +47 -293
  46. package/dist/esm/v3/chat.js.map +1 -1
  47. package/dist/esm/v3/sessions.d.ts +2 -15
  48. package/dist/esm/v3/sessions.js +1 -11
  49. package/dist/esm/v3/sessions.js.map +1 -1
  50. package/dist/esm/v3/shared.js +23 -17
  51. package/dist/esm/v3/shared.js.map +1 -1
  52. package/dist/esm/v3/test/mock-chat-agent.d.ts +0 -43
  53. package/dist/esm/v3/test/mock-chat-agent.js +2 -92
  54. package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
  55. package/dist/esm/v3/test/test-session-handle.js +0 -6
  56. package/dist/esm/v3/test/test-session-handle.js.map +1 -1
  57. package/dist/esm/version.js +1 -1
  58. package/docs/ai-chat/actions.mdx +23 -55
  59. package/docs/ai-chat/anatomy.mdx +3 -3
  60. package/docs/ai-chat/backend.mdx +48 -125
  61. package/docs/ai-chat/background-injection.mdx +19 -67
  62. package/docs/ai-chat/client-protocol.mdx +4 -5
  63. package/docs/ai-chat/compaction.mdx +7 -11
  64. package/docs/ai-chat/custom-agents.mdx +0 -23
  65. package/docs/ai-chat/fast-starts.mdx +20 -27
  66. package/docs/ai-chat/frontend.mdx +14 -17
  67. package/docs/ai-chat/migrating-from-a-route-handler.mdx +14 -16
  68. package/docs/ai-chat/patterns/skills.mdx +10 -7
  69. package/docs/ai-chat/patterns/version-upgrades.mdx +6 -79
  70. package/docs/ai-chat/pending-messages.mdx +3 -3
  71. package/docs/ai-chat/prompt-caching.mdx +25 -23
  72. package/docs/ai-chat/quick-start.mdx +11 -11
  73. package/docs/ai-chat/reference.mdx +5 -12
  74. package/docs/ai-chat/sessions.mdx +1 -6
  75. package/docs/ai-chat/testing.mdx +1 -2
  76. package/docs/ai-chat/tools.mdx +13 -18
  77. package/docs/ai-chat/upgrade-guide.mdx +2 -2
  78. package/docs/apikeys.mdx +45 -27
  79. package/docs/deployment/overview.mdx +8 -4
  80. package/docs/deployment/preview-branches.mdx +4 -4
  81. package/docs/deployment/version-skew-protection.mdx +0 -62
  82. package/docs/manual-setup.mdx +7 -7
  83. package/docs/mcp-tools.mdx +0 -9
  84. package/docs/quick-start.mdx +3 -3
  85. package/docs/realtime/auth.mdx +1 -1
  86. package/docs/self-hosting/security.mdx +0 -5
  87. package/docs/tasks/scheduled.mdx +0 -24
  88. package/docs/triggering.mdx +1 -1
  89. package/package.json +4 -4
  90. package/skills/trigger-authoring-chat-agent/SKILL.md +27 -38
  91. package/skills/trigger-chat-agent-advanced/SKILL.md +12 -31
  92. package/dist/commonjs/v3/chatVersionSkew.d.ts +0 -12
  93. package/dist/commonjs/v3/chatVersionSkew.js +0 -30
  94. package/dist/commonjs/v3/chatVersionSkew.js.map +0 -1
  95. package/dist/commonjs/v3/externalDeploymentId.d.ts +0 -23
  96. package/dist/commonjs/v3/externalDeploymentId.js +0 -43
  97. package/dist/commonjs/v3/externalDeploymentId.js.map +0 -1
  98. package/dist/esm/v3/chatVersionSkew.d.ts +0 -12
  99. package/dist/esm/v3/chatVersionSkew.js +0 -27
  100. package/dist/esm/v3/chatVersionSkew.js.map +0 -1
  101. package/dist/esm/v3/externalDeploymentId.d.ts +0 -23
  102. package/dist/esm/v3/externalDeploymentId.js +0 -38
  103. package/dist/esm/v3/externalDeploymentId.js.map +0 -1
  104. package/docs/ai-chat/patterns/native-compaction.mdx +0 -310
  105. package/docs/reports.mdx +0 -157
  106. package/docs/troubleshooting-zod.mdx +0 -158
@@ -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: undo, rollback, edit, regenerate."
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, streamText }) => {
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
- When `onAction` returns `chat.turn()`, the flow continues instead of emitting `trigger:turn-complete`: the edit is snapshotted, then a turn runs on the edited history with `trigger: "action-turn"`, so `onTurnStart`, `run()`, `onBeforeTurnComplete` and `onTurnComplete` all fire and the answer is persisted like any turn's. See [Answering after an action](#answering-after-an-action).
55
+ ## Returning a model response from an action
56
56
 
57
- ## Answering after an action
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
- switch (action.type) {
64
- case "undo":
65
- chat.history.slice(0, -2);
66
- return; // edit only, no turn
67
-
68
- case "regenerate":
69
- chat.history.slice(0, -1);
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
- `run()` receives the edited history with no incoming user message, the same shape as a `regenerate-message` turn, and its `trigger` is `"action-turn"`, so a `run()` that returns early on `"action"` (the pre-May behaviour, when actions invoked `run()` directly) still answers. Returning anything other than `chat.turn()` or nothing is an error; a response can no longer be returned from `onAction` directly.
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 chat.turn();
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: TriggerChatTransport
92
+ // Browser — TriggerChatTransport
125
93
  const stream = await transport.sendAction(chatId, { type: "undo" });
126
94
 
127
- // Server: AgentChat
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): the imperative API actions use to mutate state
140
- - [Sending actions from the frontend](/ai-chat/frontend#sending-actions): sending actions through `useChat` so a turn that follows one renders like any turn
141
- - [`hydrateMessages`](/ai-chat/lifecycle-hooks#hydratemessages): fires before `onAction` when set
142
- - [Branching conversations](/ai-chat/patterns/branching-conversations): pairs action handlers with backend-controlled history
143
- - [Human-in-the-loop](/ai-chat/patterns/human-in-the-loop): gating fresh actions while a tool is waiting
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
@@ -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, streamText }) =>
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,
@@ -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, streamText }) => {
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
- <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 })`.
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, type ChatStreamText } from "@trigger.dev/sdk/ai";
60
+ import { chat } from "@trigger.dev/sdk/ai";
61
+ import { streamText } from "ai";
94
62
  import { anthropic } from "@ai-sdk/anthropic";
95
- import { stepCountIs, type ModelMessage } from "ai";
63
+ import type { ModelMessage } from "ai";
96
64
 
97
65
  export const agentChat = chat.agent({
98
66
  id: "agent-chat",
99
- run: async ({ messages, streamText }) => {
100
- // Don't return anything, chat.pipe is called inside
101
- await runAgentLoop(messages, streamText);
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
- // 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) {
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, streamText }) => {
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, streamText }) =>
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()`. The `streamText` from `run`'s argument picks it up: system prompt, model, config and telemetry.
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, streamText }) => {
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
- 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.
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
- `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`.
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, streamText }) => {
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, streamText }) => {
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, streamText }) => {
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, streamText }) => {
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, streamText }) => {
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, streamText }) => {
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, streamText }) => {
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, streamText }) => {
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, streamText }) => {
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, streamText }) => {
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, streamText }) => {
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, streamText }) => {
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, streamText }) => {
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, streamText }) => {
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, streamText }) => {
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({