@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.
Files changed (156) hide show
  1. package/dist/commonjs/imports/ai-runtime-cjs.cjs.map +1 -1
  2. package/dist/commonjs/imports/ai-runtime.js +2 -0
  3. package/dist/commonjs/v3/ai-shared.d.ts +15 -0
  4. package/dist/commonjs/v3/ai-shared.js +35 -1
  5. package/dist/commonjs/v3/ai-shared.js.map +1 -1
  6. package/dist/commonjs/v3/ai.d.ts +311 -25
  7. package/dist/commonjs/v3/ai.js +1633 -380
  8. package/dist/commonjs/v3/ai.js.map +1 -1
  9. package/dist/commonjs/v3/auth.d.ts +10 -4
  10. package/dist/commonjs/v3/auth.js.map +1 -1
  11. package/dist/commonjs/v3/chat-client.d.ts +11 -2
  12. package/dist/commonjs/v3/chat-client.js +227 -111
  13. package/dist/commonjs/v3/chat-client.js.map +1 -1
  14. package/dist/commonjs/v3/chat-react.d.ts +88 -0
  15. package/dist/commonjs/v3/chat-react.js +130 -1
  16. package/dist/commonjs/v3/chat-react.js.map +1 -1
  17. package/dist/commonjs/v3/chat-server.d.ts +42 -6
  18. package/dist/commonjs/v3/chat-server.js +52 -7
  19. package/dist/commonjs/v3/chat-server.js.map +1 -1
  20. package/dist/commonjs/v3/chat.d.ts +92 -10
  21. package/dist/commonjs/v3/chat.js +328 -73
  22. package/dist/commonjs/v3/chat.js.map +1 -1
  23. package/dist/commonjs/v3/chatSnapshotIo.d.ts +2 -0
  24. package/dist/commonjs/v3/chatSnapshotIo.js +173 -0
  25. package/dist/commonjs/v3/chatSnapshotIo.js.map +1 -0
  26. package/dist/commonjs/v3/chatVersionSkew.d.ts +12 -0
  27. package/dist/commonjs/v3/chatVersionSkew.js +30 -0
  28. package/dist/commonjs/v3/chatVersionSkew.js.map +1 -0
  29. package/dist/commonjs/v3/externalDeploymentId.d.ts +23 -0
  30. package/dist/commonjs/v3/externalDeploymentId.js +43 -0
  31. package/dist/commonjs/v3/externalDeploymentId.js.map +1 -0
  32. package/dist/commonjs/v3/retry.d.ts +4 -0
  33. package/dist/commonjs/v3/retry.js +22 -13
  34. package/dist/commonjs/v3/retry.js.map +1 -1
  35. package/dist/commonjs/v3/sessions.d.ts +15 -2
  36. package/dist/commonjs/v3/sessions.js +12 -1
  37. package/dist/commonjs/v3/sessions.js.map +1 -1
  38. package/dist/commonjs/v3/shared.js +30 -36
  39. package/dist/commonjs/v3/shared.js.map +1 -1
  40. package/dist/commonjs/v3/test/index.d.ts +1 -0
  41. package/dist/commonjs/v3/test/index.js +3 -1
  42. package/dist/commonjs/v3/test/index.js.map +1 -1
  43. package/dist/commonjs/v3/test/mock-chat-agent.d.ts +47 -4
  44. package/dist/commonjs/v3/test/mock-chat-agent.js +116 -10
  45. package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
  46. package/dist/commonjs/v3/test/test-session-handle.js +6 -0
  47. package/dist/commonjs/v3/test/test-session-handle.js.map +1 -1
  48. package/dist/commonjs/v3/test/transcript-storage-tests.d.ts +40 -0
  49. package/dist/commonjs/v3/test/transcript-storage-tests.js +258 -0
  50. package/dist/commonjs/v3/test/transcript-storage-tests.js.map +1 -0
  51. package/dist/commonjs/v3/transcriptStorage.d.ts +261 -0
  52. package/dist/commonjs/v3/transcriptStorage.js +374 -0
  53. package/dist/commonjs/v3/transcriptStorage.js.map +1 -0
  54. package/dist/commonjs/version.js +1 -1
  55. package/dist/esm/imports/ai-runtime.d.ts +2 -2
  56. package/dist/esm/imports/ai-runtime.js +2 -2
  57. package/dist/esm/imports/ai-runtime.js.map +1 -1
  58. package/dist/esm/v3/ai-shared.d.ts +15 -0
  59. package/dist/esm/v3/ai-shared.js +33 -0
  60. package/dist/esm/v3/ai-shared.js.map +1 -1
  61. package/dist/esm/v3/ai.d.ts +311 -25
  62. package/dist/esm/v3/ai.js +1625 -377
  63. package/dist/esm/v3/ai.js.map +1 -1
  64. package/dist/esm/v3/auth.d.ts +10 -4
  65. package/dist/esm/v3/auth.js.map +1 -1
  66. package/dist/esm/v3/chat-client.d.ts +11 -2
  67. package/dist/esm/v3/chat-client.js +228 -112
  68. package/dist/esm/v3/chat-client.js.map +1 -1
  69. package/dist/esm/v3/chat-react.d.ts +88 -0
  70. package/dist/esm/v3/chat-react.js +127 -1
  71. package/dist/esm/v3/chat-react.js.map +1 -1
  72. package/dist/esm/v3/chat-server.d.ts +42 -6
  73. package/dist/esm/v3/chat-server.js +53 -8
  74. package/dist/esm/v3/chat-server.js.map +1 -1
  75. package/dist/esm/v3/chat.d.ts +92 -10
  76. package/dist/esm/v3/chat.js +329 -74
  77. package/dist/esm/v3/chat.js.map +1 -1
  78. package/dist/esm/v3/chatSnapshotIo.d.ts +2 -0
  79. package/dist/esm/v3/chatSnapshotIo.js +165 -0
  80. package/dist/esm/v3/chatSnapshotIo.js.map +1 -0
  81. package/dist/esm/v3/chatVersionSkew.d.ts +12 -0
  82. package/dist/esm/v3/chatVersionSkew.js +27 -0
  83. package/dist/esm/v3/chatVersionSkew.js.map +1 -0
  84. package/dist/esm/v3/externalDeploymentId.d.ts +23 -0
  85. package/dist/esm/v3/externalDeploymentId.js +38 -0
  86. package/dist/esm/v3/externalDeploymentId.js.map +1 -0
  87. package/dist/esm/v3/retry.d.ts +4 -0
  88. package/dist/esm/v3/retry.js +14 -8
  89. package/dist/esm/v3/retry.js.map +1 -1
  90. package/dist/esm/v3/sessions.d.ts +15 -2
  91. package/dist/esm/v3/sessions.js +11 -1
  92. package/dist/esm/v3/sessions.js.map +1 -1
  93. package/dist/esm/v3/shared.js +17 -23
  94. package/dist/esm/v3/shared.js.map +1 -1
  95. package/dist/esm/v3/test/index.d.ts +1 -0
  96. package/dist/esm/v3/test/index.js +1 -0
  97. package/dist/esm/v3/test/index.js.map +1 -1
  98. package/dist/esm/v3/test/mock-chat-agent.d.ts +47 -4
  99. package/dist/esm/v3/test/mock-chat-agent.js +118 -12
  100. package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
  101. package/dist/esm/v3/test/test-session-handle.js +6 -0
  102. package/dist/esm/v3/test/test-session-handle.js.map +1 -1
  103. package/dist/esm/v3/test/transcript-storage-tests.d.ts +40 -0
  104. package/dist/esm/v3/test/transcript-storage-tests.js +255 -0
  105. package/dist/esm/v3/test/transcript-storage-tests.js.map +1 -0
  106. package/dist/esm/v3/transcriptStorage.d.ts +261 -0
  107. package/dist/esm/v3/transcriptStorage.js +360 -0
  108. package/dist/esm/v3/transcriptStorage.js.map +1 -0
  109. package/dist/esm/version.js +1 -1
  110. package/docs/ai-chat/actions.mdx +55 -23
  111. package/docs/ai-chat/anatomy.mdx +47 -27
  112. package/docs/ai-chat/backend.mdx +125 -48
  113. package/docs/ai-chat/background-injection.mdx +122 -19
  114. package/docs/ai-chat/client-protocol.mdx +5 -4
  115. package/docs/ai-chat/compaction.mdx +13 -7
  116. package/docs/ai-chat/custom-agents.mdx +23 -0
  117. package/docs/ai-chat/fast-starts.mdx +27 -20
  118. package/docs/ai-chat/frontend.mdx +19 -14
  119. package/docs/ai-chat/how-it-works.mdx +1 -1
  120. package/docs/ai-chat/lifecycle-hooks.mdx +6 -2
  121. package/docs/ai-chat/migrating-from-a-route-handler.mdx +16 -14
  122. package/docs/ai-chat/migrating-from-hydrate-messages.mdx +299 -0
  123. package/docs/ai-chat/patterns/database-persistence.mdx +24 -9
  124. package/docs/ai-chat/patterns/native-compaction.mdx +310 -0
  125. package/docs/ai-chat/patterns/persistence-and-replay.mdx +34 -23
  126. package/docs/ai-chat/patterns/skills.mdx +7 -10
  127. package/docs/ai-chat/patterns/version-upgrades.mdx +79 -6
  128. package/docs/ai-chat/pending-messages.mdx +3 -3
  129. package/docs/ai-chat/prompt-caching.mdx +23 -25
  130. package/docs/ai-chat/quick-start.mdx +11 -11
  131. package/docs/ai-chat/reference.mdx +69 -10
  132. package/docs/ai-chat/sessions.mdx +6 -1
  133. package/docs/ai-chat/side-channels.mdx +1 -1
  134. package/docs/ai-chat/testing.mdx +2 -1
  135. package/docs/ai-chat/tools.mdx +18 -13
  136. package/docs/ai-chat/transcript-storage.mdx +247 -0
  137. package/docs/ai-chat/upgrade-guide.mdx +2 -2
  138. package/docs/apikeys.mdx +27 -45
  139. package/docs/config/extensions/syncEnvVars.mdx +6 -0
  140. package/docs/deploy-environment-variables.mdx +23 -2
  141. package/docs/deployment/overview.mdx +4 -8
  142. package/docs/deployment/preview-branches.mdx +4 -4
  143. package/docs/deployment/version-skew-protection.mdx +62 -0
  144. package/docs/introduction.mdx +58 -152
  145. package/docs/manual-setup.mdx +7 -7
  146. package/docs/mcp-tools.mdx +9 -0
  147. package/docs/quick-start.mdx +3 -3
  148. package/docs/realtime/auth.mdx +1 -1
  149. package/docs/reports.mdx +157 -0
  150. package/docs/self-hosting/security.mdx +17 -0
  151. package/docs/tasks/scheduled.mdx +24 -0
  152. package/docs/triggering.mdx +1 -1
  153. package/docs/troubleshooting-zod.mdx +158 -0
  154. package/package.json +4 -4
  155. package/skills/trigger-authoring-chat-agent/SKILL.md +41 -29
  156. package/skills/trigger-chat-agent-advanced/SKILL.md +54 -19
@@ -30,18 +30,18 @@ Add `pendingMessages` to your `chat.agent` configuration:
30
30
 
31
31
  ```ts
32
32
  import { chat } from "@trigger.dev/sdk/ai";
33
- import { streamText, stepCountIs } from "ai";
33
+ import { stepCountIs } from "ai";
34
34
  import { anthropic } from "@ai-sdk/anthropic";
35
35
 
36
36
  export const myChat = chat.agent({
37
37
  id: "my-chat",
38
+ registry,
38
39
  pendingMessages: {
39
40
  // Only inject when there are completed steps (tool calls happened)
40
41
  shouldInject: ({ steps }) => steps.length > 0,
41
42
  },
42
- run: async ({ messages, signal }) => {
43
+ run: async ({ messages, signal, streamText }) => {
43
44
  return streamText({
44
- ...chat.toStreamTextOptions({ registry }),
45
45
  messages,
46
46
  tools: { /* ... */ },
47
47
  abortSignal: signal,
@@ -16,7 +16,7 @@ A request renders as `tools` → `system` → `messages`. There are three prefix
16
16
 
17
17
  | Region | How to cache it | Stability |
18
18
  | --- | --- | --- |
19
- | System prompt (+ tools) | `cacheControl` / `systemProviderOptions` on `chat.toStreamTextOptions()`, or `providerOptions` on `chat.prompt.set()` | Set once, never changes — the highest-value target |
19
+ | System prompt (+ tools) | `cacheControl` / `systemProviderOptions` on `chat.agent()`, or `providerOptions` on `chat.prompt.set()` | Set once, never changes — the highest-value target |
20
20
  | Conversation history | `prepareMessages` adds a breakpoint to the last message | Grows append-only across turns |
21
21
  | Tool definitions | Stable as long as your tool set doesn't change between turns | Render at position 0 — changing them invalidates everything |
22
22
 
@@ -32,23 +32,22 @@ The system prompt (your `chat.prompt` text plus any skills preamble) is usually
32
32
 
33
33
  Three ways to opt in, depending on where you'd rather express it.
34
34
 
35
- **`cacheControl` at the `streamText` call site** — the Anthropic-flavored one-liner:
35
+ **`cacheControl` on the agent** — the Anthropic-flavored one-liner:
36
36
 
37
37
  ```ts /trigger/chat.ts
38
38
  import { chat } from "@trigger.dev/sdk/ai";
39
- import { streamText } from "ai";
40
39
  import { anthropic } from "@ai-sdk/anthropic";
41
40
 
42
41
  export const myChat = chat.agent({
43
42
  id: "my-chat",
43
+ cacheControl: { type: "ephemeral" },
44
44
  onChatStart: async () => {
45
45
  chat.prompt.set(SYSTEM_PROMPT); // a large, stable instruction block
46
46
  },
47
- run: async ({ messages, signal }) => {
47
+ run: async ({ messages, signal, streamText }) => {
48
48
  return streamText({
49
49
  model: anthropic("claude-sonnet-4-6"),
50
50
  // Caches the system block with a 5-minute breakpoint.
51
- ...chat.toStreamTextOptions({ cacheControl: { type: "ephemeral" } }),
52
51
  messages,
53
52
  abortSignal: signal,
54
53
  });
@@ -59,17 +58,19 @@ export const myChat = chat.agent({
59
58
  **`systemProviderOptions`** is the provider-agnostic form — pass the raw `providerOptions` so it composes with any provider:
60
59
 
61
60
  ```ts /trigger/chat.ts
62
- return streamText({
63
- model: anthropic("claude-sonnet-4-6"),
64
- ...chat.toStreamTextOptions({
65
- systemProviderOptions: { anthropic: { cacheControl: { type: "ephemeral" } } },
66
- }),
67
- messages,
68
- abortSignal: signal,
61
+ export const myChat = chat.agent({
62
+ id: "my-chat",
63
+ systemProviderOptions: { anthropic: { cacheControl: { type: "ephemeral" } } },
64
+ run: async ({ messages, signal, streamText }) =>
65
+ streamText({
66
+ model: anthropic("claude-sonnet-4-6"),
67
+ messages,
68
+ abortSignal: signal,
69
+ }),
69
70
  });
70
71
  ```
71
72
 
72
- **`providerOptions` on `chat.prompt.set()`** co-locates the intent with where the prompt is defined. It carries through to `toStreamTextOptions()` with no call-site change:
73
+ **`providerOptions` on `chat.prompt.set()`** co-locates the intent with where the prompt is defined. It carries through to the managed `streamText` with no call-site change:
73
74
 
74
75
  ```ts /trigger/chat.ts
75
76
  onChatStart: async () => {
@@ -77,17 +78,16 @@ onChatStart: async () => {
77
78
  providerOptions: { anthropic: { cacheControl: { type: "ephemeral" } } },
78
79
  });
79
80
  },
80
- run: async ({ messages, signal }) => {
81
+ run: async ({ messages, signal, streamText }) => {
81
82
  return streamText({
82
83
  model: anthropic("claude-sonnet-4-6"),
83
- ...chat.toStreamTextOptions(), // already cached
84
84
  messages,
85
85
  abortSignal: signal,
86
86
  });
87
87
  },
88
88
  ```
89
89
 
90
- If more than one is set, the call-site option wins: `systemProviderOptions` overrides `cacheControl`, and both override `chat.prompt.set`'s `providerOptions`. There's no deep merge — the most specific option replaces the rest.
90
+ If more than one is set, the most specific wins: `systemProviderOptions` overrides `cacheControl`, and both override `chat.prompt.set`'s `providerOptions`. There's no deep merge — the most specific option replaces the rest.
91
91
 
92
92
  <Note>
93
93
  Use the 1-hour cache for prefixes that sit idle longer than 5 minutes between turns: `cacheControl: { type: "ephemeral", ttl: "1h" }`. Writes cost more (2× vs 1.25×), so it pays off only when reads span the longer window.
@@ -100,6 +100,7 @@ Place a breakpoint on the last message and the entire conversation prefix up to
100
100
  ```ts /trigger/chat.ts
101
101
  export const myChat = chat.agent({
102
102
  id: "my-chat",
103
+ cacheControl: { type: "ephemeral" },
103
104
  prepareMessages: async ({ messages }) => {
104
105
  if (messages.length === 0) return messages;
105
106
  const last = messages[messages.length - 1];
@@ -114,10 +115,9 @@ export const myChat = chat.agent({
114
115
  },
115
116
  ];
116
117
  },
117
- run: async ({ messages, signal }) => {
118
+ run: async ({ messages, signal, streamText }) => {
118
119
  return streamText({
119
120
  model: anthropic("claude-sonnet-4-6"),
120
- ...chat.toStreamTextOptions({ cacheControl: { type: "ephemeral" } }),
121
121
  messages,
122
122
  abortSignal: signal,
123
123
  });
@@ -149,11 +149,10 @@ Caching is provider-specific, and most providers don't use per-block breakpoints
149
149
 
150
150
  ```ts /trigger/chat.ts
151
151
  // Amazon Bedrock
152
- return streamText({
153
- ...chat.toStreamTextOptions({
154
- systemProviderOptions: { bedrock: { cachePoint: { type: "default" } } },
155
- }),
156
- messages,
152
+ export const myChat = chat.agent({
153
+ id: "my-chat",
154
+ systemProviderOptions: { bedrock: { cachePoint: { type: "default" } } },
155
+ run: async ({ messages, streamText }) => streamText({ messages }),
157
156
  });
158
157
  ```
159
158
 
@@ -166,14 +165,13 @@ Usage reporting is normalized. Each provider reports cache tokens under its own
166
165
  The turn's usage carries cache token counts. `chat.agent` accumulates them across turns and hands them to `run` as `previousTurnUsage` (last turn) and `totalUsage` (whole chat), both `LanguageModelUsage`:
167
166
 
168
167
  ```ts /trigger/chat.ts
169
- run: async ({ messages, signal, previousTurnUsage }) => {
168
+ run: async ({ messages, signal, previousTurnUsage, streamText }) => {
170
169
  // After turn 1, cacheReadTokens should be > 0 on a stable prefix.
171
170
  console.log("cache read", previousTurnUsage?.inputTokenDetails?.cacheReadTokens);
172
171
  console.log("cache write", previousTurnUsage?.inputTokenDetails?.cacheWriteTokens);
173
172
 
174
173
  return streamText({
175
174
  model: anthropic("claude-sonnet-4-6"),
176
- ...chat.toStreamTextOptions({ cacheControl: { type: "ephemeral" } }),
177
175
  messages,
178
176
  abortSignal: signal,
179
177
  });
@@ -16,19 +16,16 @@ The chat surface works with Vercel AI SDK **v5, v6, or v7**; install whichever m
16
16
 
17
17
  ```ts trigger/chat.ts
18
18
  import { chat } from "@trigger.dev/sdk/ai";
19
- import { streamText, stepCountIs } from "ai";
19
+ import { stepCountIs } from "ai";
20
20
  import { anthropic } from "@ai-sdk/anthropic";
21
21
 
22
22
  export const myChat = chat.agent({
23
23
  id: "my-chat",
24
- run: async ({ messages, signal }) => {
24
+ // `streamText` here is the SDK's, not the one from `ai`: it carries
25
+ // compaction, steering, background injection, the system prompt and
26
+ // telemetry, so none of them have to be wired up by hand.
27
+ run: async ({ messages, signal, streamText }) => {
25
28
  return streamText({
26
- // Spread chat.toStreamTextOptions() FIRST — it wires up
27
- // prepareStep (compaction, steering, background injection),
28
- // the system prompt set via chat.prompt(), and telemetry.
29
- // Skipping this is the single most common cause of subtle
30
- // bugs (silent broken compaction, missing steering, etc.).
31
- ...chat.toStreamTextOptions(),
32
29
  model: anthropic("claude-sonnet-4-5"),
33
30
  messages,
34
31
  abortSignal: signal,
@@ -38,9 +35,12 @@ The chat surface works with Vercel AI SDK **v5, v6, or v7**; install whichever m
38
35
  });
39
36
  ```
40
37
 
41
- <Warning>
42
- **Always spread `chat.toStreamTextOptions()` into your `streamText` call.** It wires up the `prepareStep` callback that drives compaction, mid-turn steering, and background injection — features that silently no-op if the spread is missing. Spread it **first** so any explicit overrides (e.g. a custom `prepareStep`) win.
43
- </Warning>
38
+ <Note>
39
+ Take `streamText` from `run`'s argument rather than importing it from `ai`. The
40
+ imported one drives no `prepareStep`, so compaction, mid-turn steering and
41
+ background injection never run, and nothing reports it. Spreading
42
+ `chat.toStreamTextOptions()` into the imported one does the same job by hand.
43
+ </Note>
44
44
 
45
45
  <Tip>
46
46
  For a **custom** [`UIMessage`](https://sdk.vercel.ai/docs/reference/ai-sdk-core/ui-message) subtype (typed `data-*` parts, tool map, etc.), define the agent with [`chat.withUIMessage<...>().agent({...})`](/ai-chat/types) instead of `chat.agent`.
@@ -44,14 +44,19 @@ Options for `chat.agent()`.
44
44
  | `onPreload` | `(event: PreloadEvent) => Promise<void> \| void` | — | Fires on preloaded runs before the first message |
45
45
  | `onChatStart` | `(event: ChatStartEvent) => Promise<void> \| void` | — | Fires once per chat, on the very first user message. Does NOT fire on continuation runs or OOM-retries — see [onChatStart](/ai-chat/lifecycle-hooks#onchatstart). |
46
46
  | `onValidateMessages` | `(event: ValidateMessagesEvent) => UIMessage[] \| Promise<UIMessage[]>` | — | Validate/transform UIMessages before model conversion. See [onValidateMessages](/ai-chat/lifecycle-hooks#onvalidatemessages) |
47
- | `hydrateMessages` | `(event: HydrateMessagesEvent) => UIMessage[] \| Promise<UIMessage[]>` | — | Load message history from backend, replacing the linear accumulator. See [hydrateMessages](/ai-chat/lifecycle-hooks#hydratemessages) |
47
+ | `storage` | `TranscriptStorage` | `defaultStorage` | Where the conversation is persisted and read back. The platform snapshot by default; bring your own to write each change to your database. See [Transcript storage](/ai-chat/transcript-storage) |
48
+ | `hydrateMessages` | `(event: HydrateMessagesEvent) => UIMessage[] \| Promise<UIMessage[]>` | — | **Deprecated.** Load message history from backend, replacing the linear accumulator. Use `loadContext` on a `storage` instead; cannot be combined with `storage`. See [hydrateMessages](/ai-chat/lifecycle-hooks#hydratemessages) |
48
49
  | `actionSchema` | `TaskSchema` | — | Schema for validating custom actions sent via `transport.sendAction()`. See [Actions](/ai-chat/actions) |
49
- | `onAction` | `(event: ActionEvent) => Promise<unknown> \| unknown` | — | Handle custom actions. Actions are not turns only `hydrateMessages` + `onAction` fire. Return a `StreamTextResult` (or `string` / `UIMessage`) for a model response; return `void` for side-effect-only. See [Actions](/ai-chat/actions) |
50
+ | `onAction` | `(event: ActionEvent) => Promise<void \| ActionTurn> \| void \| ActionTurn` | — | Handle custom actions. Actions are state edits: only `hydrateMessages` (or a storage's `loadContext`) + `onAction` fire. Return `chat.turn()` to run a turn on the edited history, or nothing for an edit only. See [Actions](/ai-chat/actions) |
50
51
  | `onTurnStart` | `(event: TurnStartEvent) => Promise<void> \| void` | — | Fires every turn before `run()` |
51
52
  | `onBeforeTurnComplete` | `(event: BeforeTurnCompleteEvent) => Promise<void> \| void` | — | Fires after response but before stream closes. Includes `writer`. |
52
53
  | `onTurnComplete` | `(event: TurnCompleteEvent) => Promise<void> \| void` | — | Fires after each turn completes (stream closed) |
53
54
  | `onCompacted` | `(event: CompactedEvent) => Promise<void> \| void` | — | Fires when compaction occurs. Includes `writer`. See [Compaction](/ai-chat/compaction) |
54
55
  | `compaction` | `ChatAgentCompactionOptions` | — | Automatic context compaction. See [Compaction](/ai-chat/compaction) |
56
+ | `registry` | `{ languageModel(id: string): unknown }` | — | A provider registry, so the managed `streamText` can resolve a model set through `chat.prompt.set()` |
57
+ | `system` | `string \| SystemModelMessage` | — | The agent's system prompt. Injected instructions append to it. Set it here, at the `streamText` call site, or through `chat.prompt.set()`, but only in one of them |
58
+ | `cacheControl` | `SystemCacheControl` | — | Mark the system prompt for provider-side caching. See [Prompt caching](/ai-chat/prompt-caching) |
59
+ | `systemProviderOptions` | `ProviderMetadata` | — | Raw provider options for the system block. Takes precedence over `cacheControl` |
55
60
  | `pendingMessages` | `PendingMessagesOptions` | — | Mid-execution message injection. See [Pending Messages](/ai-chat/pending-messages) |
56
61
  | `prepareMessages` | `(event: PrepareMessagesEvent) => ModelMessage[]` | — | Transform model messages before use (cache breaks, context injection, etc.) |
57
62
  | `tools` | `ToolSet \| ((event: ResolveToolsEvent) => ToolSet \| Promise<ToolSet>)` | — | Tools for this agent. Threads each tool's `toModelOutput` through cross-turn history re-conversion, and hands the resolved set back on the run payload. Static set or per-turn function. See [Tools](/ai-chat/tools). |
@@ -98,9 +103,10 @@ The payload passed to the `run` function.
98
103
  | `ctx` | `TaskRunContext` | Full task run context — same as `task` `run`’s `{ ctx }` |
99
104
  | `messages` | `ModelMessage[]` | Model-ready messages — pass directly to `streamText` |
100
105
  | `tools` | `ToolSet` | Resolved tools declared on the agent config (empty object when none). Pass straight to `streamText`. See [Tools](/ai-chat/tools). |
106
+ | `streamText` | `typeof streamText` | The AI SDK's `streamText` with this agent's managed options already applied: the prompt, skill tools, telemetry, and the `prepareStep` that delivers steering, compaction and injected context. Prefer it over importing `streamText` from `ai`. See [The managed streamText](/ai-chat/backend#the-managed-streamtext). |
101
107
  | `chatId` | `string` | Your conversation ID (the session's `externalId`) |
102
108
  | `sessionId` | `string` | Friendly ID of the backing Session (`session_*`). Use with `sessions.open()` for advanced cases. Always set — every chat.agent run is bound to a Session. |
103
- | `trigger` | `"submit-message" \| "regenerate-message"` | What triggered the request |
109
+ | `trigger` | `"submit-message" \| "regenerate-message" \| "action-turn"` | What triggered the request; `"action-turn"` is a turn requested by `chat.turn()` |
104
110
  | `messageId` | `string \| undefined` | Message ID (for regenerate) |
105
111
  | `clientData` | Typed by `clientDataSchema` | Custom data from the frontend (typed when schema is provided) |
106
112
  | `continuation` | `boolean` | Whether this run is continuing an existing chat (previous run ended) |
@@ -213,9 +219,38 @@ Passed to the `tools` function form on `chat.agent`, once per turn, to resolve t
213
219
  | `continuation` | `boolean` | Whether this run is continuing an existing chat |
214
220
  | `clientData` | Typed by `clientDataSchema` | Custom data from the frontend |
215
221
 
222
+ ## TranscriptStorage
223
+
224
+ The persistence adapter set through `chat.agent({ storage })`. See [Transcript storage](/ai-chat/transcript-storage). All types below are exported from `@trigger.dev/sdk/ai`.
225
+
226
+ | Member | Signature | Description |
227
+ | --- | --- | --- |
228
+ | `load` | `(scope: TranscriptScope, opts?: TranscriptLoadOptions) => Promise<TranscriptLoadResult>` | The conversation, in order. Called once at a continuation boot and by `chat.createLoadTranscriptAction` for rendering. |
229
+ | `save` | `(ctx: TranscriptStorageContext, changeset: TranscriptChangeset) => Promise<void>` | Apply the changes since the last save. Called after every turn, failed turn and history-changing action. |
230
+ | `loadContext?` | `(scope: TranscriptScope, event: LoadContextEvent) => Promise<UIMessage[]>` | Optional. When present, the storage owns the model's context: called on every turn and action in place of the runtime's transcript. Same event shape as `HydrateMessagesEvent`. |
231
+
232
+ | Type | Shape |
233
+ | --- | --- |
234
+ | `TranscriptScope<TClientData>` | `{ chatId: string; clientData: TClientData }` |
235
+ | `TranscriptStorageContext<TClientData>` | `TranscriptScope` plus `{ turn: number; trigger: "submit-message" \| "regenerate-message" \| "action"; runId: string; ctx: TaskRunContext }` |
236
+ | `TranscriptChange` | `{ op: "put"; message: UIMessage; final?: boolean }` \| `{ op: "remove"; id: string }` \| `{ op: "truncateAfter"; afterId: string }` \| `{ op: "state"; value: unknown \| null }` |
237
+ | `TranscriptChangeset` | `{ reason: "turn-complete" \| "turn-error" \| "action" \| "compaction" \| "recovery"; changes: TranscriptChange[]; transcript: TranscriptState; cursors?: TranscriptCursors }` |
238
+ | `TranscriptState` | `{ entries: Array<{ id: string; final: boolean; message: UIMessage }>; state: unknown \| null }`: the whole conversation after the changeset's changes, for stores that write one document |
239
+ | `TranscriptCursors` | `{ lastOutEventId?: string; lastInEventId?: string }` |
240
+ | `TranscriptLoadOptions` | `{ limit?: number; before?: string }` |
241
+ | `TranscriptLoadResult` | `{ messages: UIMessage[]; state: unknown \| null; cursors?: TranscriptCursors; nextCursor?: string }` |
242
+
243
+ | Export | Description |
244
+ | --- | --- |
245
+ | `defaultStorage` | The platform snapshot storage the agent uses when `storage` is not set. Equal to `snapshotTranscriptStorage()`. |
246
+ | `snapshotTranscriptStorage()` | Factory for the platform snapshot storage. |
247
+ | `memoryTranscriptStorage()` | An in-process storage that also records every changeset it receives. The reference implementation. |
248
+ | `reduceTranscriptChanges(state, changes)` | Pure reducer that applies changes to `{ entries, state }`. Useful for building a storage over a document store. |
249
+ | `runTranscriptStorageTests(makeStorage, options?)` | From `@trigger.dev/sdk/ai/test`. The conformance suite for a storage implementation. Pass `{ api: { describe, it, expect } }` when test globals are off, and `clientData` when your storage scopes by it. |
250
+
216
251
  ## HydrateMessagesEvent
217
252
 
218
- Passed to the `hydrateMessages` callback. See [hydrateMessages](/ai-chat/lifecycle-hooks#hydratemessages).
253
+ Passed to the `hydrateMessages` callback. See [hydrateMessages](/ai-chat/lifecycle-hooks#hydratemessages). `hydrateMessages` is deprecated; `LoadContextEvent`, passed to a storage's `loadContext`, has the same shape.
219
254
 
220
255
  | Field | Type | Description |
221
256
  | ------------------ | ----------------------------------------------------- | --------------------------------------------------------- |
@@ -364,8 +399,8 @@ Passed to `compactUIMessages` and `compactModelMessages` callbacks.
364
399
  | Field | Type | Description |
365
400
  | --------------- | -------------------- | ---------------------------------------------------- |
366
401
  | `summary` | `string` | The generated summary text |
367
- | `uiMessages` | `UIMessage[]` | Current UI messages (full conversation) |
368
- | `modelMessages` | `ModelMessage[]` | Current model messages (full conversation) |
402
+ | `uiMessages` | `UIMessage[]` | Current UI messages (the transcript) |
403
+ | `modelMessages` | `ModelMessage[]` | Current model messages (the lane the model is sent; after a compaction this is the summary plus what followed it, not the whole transcript) |
369
404
  | `chatId` | `string` | Chat session ID |
370
405
  | `turn` | `number` | Current turn (0-indexed) |
371
406
  | `clientData` | `unknown` | Custom data from the frontend |
@@ -490,9 +525,9 @@ Options for [`chat.headStart()`](/ai-chat/fast-starts#head-start), the warm-serv
490
525
  | `agentId` | `string` | required | The `chat.agent` / `chat.customAgent` id to hand off to |
491
526
  | `run` | `(args: HeadStartRunArgs) => Promise<StreamTextResult>` | required | First-turn callback. Call `streamText` and spread `chat.toStreamTextOptions({ tools })` |
492
527
  | `idleTimeoutInSeconds` | `number` | `60` | How long the agent waits for the handover signal |
493
- | `triggerConfig` | `Partial<SessionTriggerConfig>` | `undefined` | Run options (tags, queue, machine, maxAttempts, maxDuration, region, lockToVersion) for the auto-triggered handover-prepare run. The `chat:{chatId}` tag is prepended automatically and counts toward the 10-tag limit |
528
+ | `triggerConfig` | `Partial<SessionTriggerConfig>` | `undefined` | Run options (tags, queue, machine, maxAttempts, maxDuration, region, lockToVersion, externalDeploymentId) for the auto-triggered handover-prepare run. The `chat:{chatId}` tag is prepended automatically and counts toward the 10-tag limit |
494
529
 
495
- `chat.headStart(options)` returns the handler `(req: Request) => Promise<Response>`. The `run` callback receives `HeadStartRunArgs`: `{ messages: UIMessage[], signal: AbortSignal, chat: HeadStartChatHelper }`, where the helper exposes `chat.toStreamTextOptions({ tools })` and a `chat.session` escape hatch. See [Head Start](/ai-chat/fast-starts#head-start) for the full guide.
530
+ `chat.headStart(options)` returns the handler `(req: Request) => Promise<Response>`. The `run` callback receives `HeadStartRunArgs`: `{ messages: UIMessage[], signal: AbortSignal, chat: HeadStartChatHelper }`, where the helper exposes `chat.toStreamTextOptions({ tools })` and a `chat.session` escape hatch (whose `pendingVersion` says whether the agent run is parked waiting for its deployment). See [Head Start](/ai-chat/fast-starts#head-start) for the full guide.
496
531
 
497
532
  ## chat namespace
498
533
 
@@ -509,8 +544,10 @@ All methods available on the `chat` object from `@trigger.dev/sdk/ai`.
509
544
  | `chat.messages` | Incoming message mailbox; supports non-consuming `.peek()` / `.hasPending()`, single-record `.next()`, `.on()`, and suspend-aware `.waitWithIdleTimeout()` |
510
545
  | `chat.local<T>({ id })` | Create a per-run typed local (see [`chat.local`](/ai-chat/chat-local)) |
511
546
  | `chat.createStartSessionAction(taskId, options?)` | Returns a server action that creates a chat Session + triggers the first run + returns a session-scoped PAT. Idempotent on `(env, externalId)`. |
547
+ | `chat.createLoadTranscriptAction(storage, options?)` | Returns a server action that reads a conversation from a transcript storage (`{ chatId, clientData?, limit?, before? }` → `TranscriptLoadResult`). Pair with `useLoadTranscript`. Performs no application authorization: authorize `chatId` for the signed-in user in your own code. See [Transcript storage](/ai-chat/transcript-storage#reading-the-transcript) |
512
548
  | `chat.waitForHandover(options)` | Wait for a [`chat.headStart`](/ai-chat/fast-starts#handover-with-custom-agents) handover signal in a custom loop. Returns the signal or `null`. `chat.MessageAccumulator` wraps this as `consumeHandover()` / `applyHandover()` |
513
549
  | `chat.requestUpgrade()` | End the current run after this turn so the next message starts on the latest agent version. Server-orchestrated handoff. |
550
+ | `chat.close({ reason })` | End the conversation permanently: close the session row, write a terminal `session-closed` record, and exit without a continuation. Decide it before the turn ends (`onBeforeTurnComplete`, not `onTurnComplete`) so the client sees the closed state on that turn. |
514
551
  | `chat.endAndContinue()` | In a hand-rolled custom agent, hand off the Session to a fresh continuation run. Call between turns after detaching input listeners, then return immediately. The promise rejects if the handoff fails. |
515
552
  | `chat.setTurnTimeout(duration)` | Override turn timeout at runtime (e.g. `"2h"`) |
516
553
  | `chat.setTurnTimeoutInSeconds(seconds)` | Override turn timeout at runtime (in seconds) |
@@ -659,6 +696,7 @@ The `onEvent` callback receives a `ChatTransportEvent` (exported from `@trigger.
659
696
  | --- | --- | --- |
660
697
  | `message-sent` | `messageId?`, `source`, `durationMs`, `partId?`, `bodyBytes?` | A send was durably acknowledged — a 2xx from the session input stream append (or the `headStart` POST), after any internal token-refresh retries. This means the message is durably written to the stream the agent consumes from, not merely "request accepted". `partId` is the append's idempotency key, also stored on the server-side record. |
661
698
  | `message-send-failed` | `messageId?`, `source`, `error`, `status?`, `durationMs`, `partId?`, `bodyBytes?` | A send definitively failed after internal retries. Fires in addition to `useChat`'s `onError`. |
699
+ | `run-pending-version` | `source` | The chat's run is parked waiting for the deployment carrying its external deployment id ([version skew protection](/deployment/version-skew-protection#chat-sessions)). Everything already sent is durable and answered once the deployment lands. `source` is `"start"` (learned while starting the session), `"send"` (from a message append, re-emitted on every send while parked) `"head-start"` (from the `headStart` POST, where step 1 still streams from your server and only step 2 waits) or `"upgrade"` (an automatic version handover whose successor is parked on a deployment that has not landed). |
662
700
  | `stream-connected` | `resumed`, `lastEventId?`, `messageId?` | The SSE subscription to the session's output stream started delivering. `resumed: true` when reconnecting from a stored cursor (page reload) rather than following a fresh send. `lastEventId` is the cursor it connected from. |
663
701
  | `first-chunk` | `chunkType?`, `lastEventId?`, `messageId?`, `sinceSendMs?` | The first response chunk of a turn arrived. `sinceSendMs` is the delta from the last turn-producing send — time to first token without any bookkeeping. |
664
702
  | `turn-completed` | `lastEventId?`, `sessionInEventId?`, `messageId?`, `sinceSendMs?` | The agent's turn-complete control record arrived — the "finished answering" signal. `sinceSendMs` is the full turn latency; `sessionInEventId` is the cursor the agent can safely resume its input stream from. Treat it as a lower bound: it is held back behind any message still waiting to be handled, so it can be below the sequence of the record this turn answered. Do not use it to decide whether a turn boundary belongs to your own send. |
@@ -794,7 +832,7 @@ See [Stop generation](/ai-chat/frontend#stop-generation) for full details.
794
832
 
795
833
  ### transport.sendAction()
796
834
 
797
- Send a custom action to the agent. Actions wake the agent from suspension and fire `onAction`. They are not turns `run()` and turn lifecycle hooks do not fire. If `onAction` returns a `StreamTextResult`, the response is auto-piped to the frontend.
835
+ Send a custom action to the agent, outside `useChat`. Actions wake the agent from suspension and fire `onAction`. An action that returns `chat.turn()` is followed by a turn; its answer arrives on the returned stream, which the caller must read. From a `useChat` app, send actions as requests instead (`sendMessage(undefined, { body: { action } })` or the `useChatActions` hook) so `useChat` renders the answer.
798
836
 
799
837
  ```ts
800
838
  transport.sendAction(chatId: string, action: unknown): Promise<ReadableStream<UIMessageChunk>>
@@ -869,6 +907,26 @@ Second argument to `chat.createStartSessionAction(taskId, options?)`. Controls h
869
907
  | `baseURL` | `string \| (ctx: { endpoint: "sessions" \| "auth"; chatId: string }) => string` | `apiClientManager.baseURL` | API base URL. `endpoint` is `"sessions"` for `POST /api/v1/sessions` or `"auth"` for `POST /api/v1/auth/jwt/claims` (only fires when `tokenTTL` is set). |
870
908
  | `fetch` | `(url: string, init: RequestInit, ctx: { endpoint: "sessions" \| "auth"; chatId: string }) => Promise<Response>` | — | Per-request fetch override. Use to route session-create through a trusted edge proxy so `basePayload.metadata` is rewritten before reaching `api.trigger.dev`. |
871
909
 
910
+ ## createLoadTranscriptAction options
911
+
912
+ Second argument to `chat.createLoadTranscriptAction(storage, options?)`.
913
+
914
+ | Option | Type | Default | Description |
915
+ | ----------- | ------------------------ | -------------------------- | --------------------------------------------------------------------------- |
916
+ | `limit` | `number` | — | Page size when the caller passes none. Returns the most recent messages and a `nextCursor`. |
917
+ | `apiClient` | `ApiClientConfiguration` | `apiClientManager` config | Scope the read to a specific API client (secret key, base URL). The default storage reads through it. |
918
+
919
+ ## useLoadTranscript
920
+
921
+ `useLoadTranscript(chatId, load, options?)` from `@trigger.dev/sdk/chat/react`. Loads a conversation through a `chat.createLoadTranscriptAction` action, for rendering before the chat connects. Re-runs when `chatId` changes.
922
+
923
+ | Option | Type | Description |
924
+ | ----------- | ---------------------- | -------------------------------------------------------------------------------------------- |
925
+ | `transport` | `TriggerChatTransport` | Seed the transport's resume cursor for this chat from the transcript, once it knows the session. |
926
+ | `limit` | `number` | Page size passed to the action. |
927
+
928
+ Returns `{ messages, isLoading, error, nextCursor }`. `nextCursor` is the id to pass as `before` to the action for the page before this one.
929
+
872
930
  ## useMultiTabChat
873
931
 
874
932
  React hook for multi-tab message coordination. Import from `@trigger.dev/sdk/chat/react`.
@@ -929,9 +987,10 @@ Tokens minted for `TriggerChatTransport` and `AgentChat` are session-scoped —
929
987
  | Scope | Grants |
930
988
  | --- | --- |
931
989
  | `read:sessions:<chatId>` | Subscribe to `.out`, HEAD probe the stream, retrieve the session row |
990
+ | `read:sessions:<chatId>:out` | Subscribe to `.out` and HEAD probe the stream only. Use this when the browser needs nothing but the response stream. |
932
991
  | `write:sessions:<chatId>` | Append to `.in`, close the session, end-and-continue, update metadata |
933
992
 
934
- Tokens are produced by `auth.createPublicToken({ scopes: { read: { sessions: chatId }, write: { sessions: chatId } } })` (used by the customer's `accessToken` server action) or returned automatically from `chat.createStartSessionAction` / `POST /api/v1/sessions`. Either form authorizes both URL forms (`/sessions/{chatId}/...` and `/sessions/session_*/...`) on every read and write route.
993
+ Reading `.in` requires a secret key, so no browser token can replay what clients sent. Tokens are produced by `auth.createPublicToken({ scopes: { read: { sessions: chatId }, write: { sessions: chatId } } })` (used by the customer's `accessToken` server action) or returned automatically from `chat.createStartSessionAction` / `POST /api/v1/sessions`. Either form authorizes both URL forms (`/sessions/{chatId}/...` and `/sessions/session_*/...`) on every read and write route.
935
994
 
936
995
  ## Related
937
996
 
@@ -111,7 +111,7 @@ const { id, runId, publicAccessToken, isCached } = await sessions.start({
111
111
  | `type` | `string` | Free-form discriminator. `chat.agent` uses `"chat.agent"`. |
112
112
  | `externalId` | `string?` | Your stable identity. Cannot start with `session_` (reserved). |
113
113
  | `taskIdentifier` | `string` | Task this session triggers runs against. |
114
- | `triggerConfig` | `SessionTriggerConfig` | Trigger options applied to every run: `tags` (up to 10, same as [run tags](/tags); the chat helpers such as `chat.createStartSessionAction` and `AgentChat` add a `chat:{chatId}` tag themselves, which uses one slot. Direct `sessions.start` callers get all 10 and must add any chat tag themselves), `queue`, `machine`, `maxAttempts`, `idleTimeoutInSeconds`, `basePayload`. |
114
+ | `triggerConfig` | `SessionTriggerConfig` | Trigger options applied to every run: `tags` (up to 10, same as [run tags](/tags); the chat helpers such as `chat.createStartSessionAction` and `AgentChat` add a `chat:{chatId}` tag themselves, which uses one slot. Direct `sessions.start` callers get all 10 and must add any chat tag themselves), `queue`, `machine`, `maxAttempts`, `maxDuration`, `region`, `idleTimeoutInSeconds`, `basePayload`, and the version pins `lockToVersion` / [`externalDeploymentId`](/deployment/version-skew-protection#chat-sessions). |
115
115
  | `tags` | `string[]?` | Up to 10 tags on the Session row (separate from `triggerConfig.tags`). |
116
116
  | `metadata` | `Record<string, unknown>?` | Arbitrary JSON. |
117
117
  | `expiresAt` | `Date?` | Hard retention deadline. |
@@ -146,6 +146,11 @@ Mark a Session as closed. Terminal and idempotent. The optional `reason` is stor
146
146
  await sessions.close(chatId, { reason: "user signed out" });
147
147
  ```
148
148
 
149
+ Closing tells a live run too: the close lands on the session's input channel, so an idle or suspended agent exits its loop on the next wake rather than waiting out its idle timeout. After it lands, appends to `.in` are refused with HTTP 409 and `code: "session_closed"`.
150
+
151
+ To close from inside the agent instead, call [`chat.close()`](/ai-chat/backend#ending-the-conversation). It writes a terminal record to the response stream so the browser learns the reason, then closes the row.
152
+
153
+
149
154
  ### `sessions.list(options?, requestOptions?)`
150
155
 
151
156
  Cursor-paginated list of Sessions in the current environment. Returns a `CursorPagePromise` you can iterate with `for await`.
@@ -138,7 +138,7 @@ A side channel's streams are bounded by the same retention as the rest of your r
138
138
 
139
139
  ## Auth
140
140
 
141
- A side channel is covered by the session's public access token: a token scoped to `read:sessions:{id}` / `write:sessions:{id}` grants every channel of that session. Mint a narrower token scoped to a single channel with `read:sessions:{id}:channels:{name}`. Writing a channel's `.out` requires secret-key auth (only the agent run), so a browser cannot forge frames; `.in` is writable with the session token. See [Realtime auth](/realtime/auth).
141
+ A side channel is covered by the session's public access token: a token scoped to `read:sessions:{id}` / `write:sessions:{id}` grants every channel of that session. Mint a narrower token scoped to a single channel with `read:sessions:{id}:channels:{name}`, or to one direction of it with `read:sessions:{id}:channels:{name}:out`. Writing a channel's `.out` requires secret-key auth (only the agent run), so a browser cannot forge frames; `.in` is writable with the session token. See [Realtime auth](/realtime/auth).
142
142
 
143
143
  ### Scope tokens to the channel, not the whole session
144
144
 
@@ -203,7 +203,7 @@ Equivalent to the frontend's `useChat().regenerate()` — replays a turn with th
203
203
 
204
204
  ### sendAction
205
205
 
206
- Routes a payload through `actionSchema` + `onAction`. Actions are not turns: only `hydrateMessages` and `onAction` fire on the agent side — no turn lifecycle hooks, no `run()`. The returned `turn.rawChunks` contains whatever `onAction` produced (a streamed model response if it returned a `StreamTextResult`, otherwise just `trigger:turn-complete`):
206
+ Routes a payload through `actionSchema` + `onAction`. An action is a state edit: only `hydrateMessages` and `onAction` fire, unless `onAction` returns `chat.turn()`, in which case a turn runs on the edited history and the returned `turn.rawChunks` carries that turn's answer.
207
207
 
208
208
  ```ts
209
209
  const turn = await harness.sendAction({ type: "undo" });
@@ -634,6 +634,7 @@ The harness's initial wire payload depends on `mode`:
634
634
  | `sendHandover({ partialAssistantMessage, isFinal?, messageId? })` | Dispatch a `handover` signal — only meaningful when started with `mode: "handover-prepare"`. The agent picks up partial assistant messages and continues the turn. |
635
635
  | `sendHandoverSkip()` | Dispatch a `handover-skip` signal — only meaningful when started with `mode: "handover-prepare"`. The agent exits cleanly without firing turn hooks. |
636
636
  | `sendAction(action)` | Route a custom action through `actionSchema` + `onAction`. |
637
+ | `sendPendingMessage(message)` | Append a user message mid-turn without waiting for a turn to complete, so it reaches the running turn as a steering message. Resolves once the record has landed on `session.in`. |
637
638
  | `sendStop(message?)` | Fire a stop signal. Does not wait for the turn — the run's `signal.aborted` becomes `true`. |
638
639
  | `seedSnapshot(snapshot)` | Pre-seed the snapshot read for the next boot. Effective on the next run boot only. |
639
640
  | `seedSessionOutTail(chunks?)` | Pre-seed `session.out` chunks for the next boot's replay. Reduces to settled assistant turns. |
@@ -8,7 +8,7 @@ description: "Declare tools on chat.agent so toModelOutput survives across turns
8
8
 
9
9
  ```ts
10
10
  import { chat } from "@trigger.dev/sdk/ai";
11
- import { streamText, stepCountIs, tool } from "ai";
11
+ import { stepCountIs, tool } from "ai";
12
12
  import { anthropic } from "@ai-sdk/anthropic";
13
13
  import { z } from "zod";
14
14
 
@@ -23,9 +23,9 @@ const tools = {
23
23
  export const myChat = chat.agent({
24
24
  id: "my-chat",
25
25
  tools, // ← declare here
26
- run: async ({ messages, tools, signal }) =>
26
+ run: async ({ messages, tools, signal, streamText }) =>
27
27
  streamText({
28
- ...chat.toStreamTextOptions({ tools }), // ← the same set, handed back on the payload
28
+ tools,
29
29
  model: anthropic("claude-sonnet-4-5"),
30
30
  messages,
31
31
  abortSignal: signal,
@@ -46,10 +46,15 @@ There are three places a tool set shows up. Declare once, reuse:
46
46
  | Surface | What it's for |
47
47
  | --- | --- |
48
48
  | `chat.agent({ tools })` | Re-applies `toModelOutput` on prior-turn history; hands the set back typed on the `run()` payload. |
49
- | `chat.toStreamTextOptions({ tools })` | Detects which tool calls need [HITL approval](/ai-chat/patterns/human-in-the-loop) (`needsApproval`) and merges any auto-injected [skill](/ai-chat/patterns/skills) tools. |
50
- | `streamText({ tools })` | What the model actually calls. `chat.toStreamTextOptions({ tools })` already sets this, so spread it instead of passing `tools` twice. |
49
+ | `streamText({ tools })` on the `run` argument's `streamText` | What the model actually calls. Detects which calls need [HITL approval](/ai-chat/patterns/human-in-the-loop) (`needsApproval`) and merges the auto-injected [skill](/ai-chat/patterns/skills) tools on top. Naming `tools` replaces the config set for that call, so you can narrow it; omitting `tools` falls back to the config set. |
50
+ | `chat.toStreamTextOptions({ tools })` | The same job by hand, for a [custom agent](#manual-turn-loops-chatcustomagent), which has no `run` argument. |
51
51
 
52
- The canonical pattern: declare `tools` on the config, read them back from the `run()` payload, and pass that to `chat.toStreamTextOptions({ tools })`. One declaration flows everywhere.
52
+ The canonical pattern: declare `tools` on the config, read them back from the `run()` payload, and pass that set to the `streamText` the payload also carries.
53
+
54
+ ```ts
55
+ run: async ({ messages, tools, signal, streamText }) =>
56
+ streamText({ model, messages, tools, abortSignal: signal }),
57
+ ```
53
58
 
54
59
  <Tip>
55
60
  Conversion only reads each tool's `inputSchema` and `toModelOutput`, never `execute`. If you keep heavy `execute` dependencies out of a module (for bundle reasons), you can declare a lightweight schema-only tool map on the config and add the executes where you call `streamText`.
@@ -80,9 +85,9 @@ const tools = {
80
85
  export const chartChat = chat.agent({
81
86
  id: "chart-chat",
82
87
  tools, // ← without this, the image is "remembered" on turn 1 and gone from turn 2
83
- run: async ({ messages, tools, signal }) =>
88
+ run: async ({ messages, tools, signal, streamText }) =>
84
89
  streamText({
85
- ...chat.toStreamTextOptions({ tools }),
90
+ tools,
86
91
  model: anthropic("claude-sonnet-4-5"),
87
92
  messages,
88
93
  abortSignal: signal,
@@ -104,9 +109,9 @@ export const myChat = chat
104
109
  searchDocs,
105
110
  ...(clientData?.plan === "pro" ? { deepResearch } : {}),
106
111
  }),
107
- run: async ({ messages, tools, signal }) =>
112
+ run: async ({ messages, tools, signal, streamText }) =>
108
113
  streamText({
109
- ...chat.toStreamTextOptions({ tools }),
114
+ tools,
110
115
  model: anthropic("claude-sonnet-4-5"),
111
116
  messages,
112
117
  abortSignal: signal,
@@ -131,10 +136,10 @@ The resolved set is what lands on the `run()` payload's `tools`.
131
136
  The `run()` payload's `tools` is typed to whatever you declared, so you can pass it straight through without re-importing the map:
132
137
 
133
138
  ```ts
134
- run: async ({ messages, tools, signal }) => {
139
+ run: async ({ messages, tools, signal, streamText }) => {
135
140
  // `tools` is typed as your tool set, not a broad `ToolSet`
136
141
  return streamText({
137
- ...chat.toStreamTextOptions({ tools }),
142
+ tools,
138
143
  model: anthropic("claude-sonnet-4-5"),
139
144
  messages,
140
145
  abortSignal: signal,
@@ -160,7 +165,7 @@ This is shorthand for `UIMessage<unknown, UIDataTypes, InferUITools<typeof tools
160
165
 
161
166
  ## Skills
162
167
 
163
- [Agent skills](/ai-chat/patterns/skills) are auto-injected as tools (`loadSkill`, `readFile`, `bash`) by `chat.toStreamTextOptions()`. They're separate from your config `tools`: declare your own tools on the config (so their `toModelOutput` survives across turns), and let `toStreamTextOptions` merge the skill tools on top at call time. Skill tools don't define `toModelOutput`, so they don't need to be on the config.
168
+ [Agent skills](/ai-chat/patterns/skills) are auto-injected as tools (`loadSkill`, `readFile`, `bash`) by the managed `streamText`, or by `chat.toStreamTextOptions()` if you build the options yourself. They're separate from your config `tools`: declare your own tools on the config (so their `toModelOutput` survives across turns), and the merge happens at call time. Skill tools don't define `toModelOutput`, so they don't need to be on the config.
164
169
 
165
170
  ## Manual turn loops (`chat.customAgent`)
166
171