@trigger.dev/sdk 4.8.0 → 4.8.2

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 (101) hide show
  1. package/dist/commonjs/v3/ai-shared.d.ts +6 -0
  2. package/dist/commonjs/v3/ai-shared.js +7 -1
  3. package/dist/commonjs/v3/ai-shared.js.map +1 -1
  4. package/dist/commonjs/v3/ai.d.ts +45 -0
  5. package/dist/commonjs/v3/ai.js +110 -69
  6. package/dist/commonjs/v3/ai.js.map +1 -1
  7. package/dist/commonjs/v3/auth.d.ts +2 -1
  8. package/dist/commonjs/v3/auth.js +18 -12
  9. package/dist/commonjs/v3/auth.js.map +1 -1
  10. package/dist/commonjs/v3/chat-client.d.ts +11 -2
  11. package/dist/commonjs/v3/chat-client.js +28 -10
  12. package/dist/commonjs/v3/chat-client.js.map +1 -1
  13. package/dist/commonjs/v3/chat-react.d.ts +21 -0
  14. package/dist/commonjs/v3/chat-react.js +86 -30
  15. package/dist/commonjs/v3/chat-react.js.map +1 -1
  16. package/dist/commonjs/v3/chat.d.ts +1 -0
  17. package/dist/commonjs/v3/chat.js +4 -1
  18. package/dist/commonjs/v3/chat.js.map +1 -1
  19. package/dist/commonjs/v3/managedChatResponse.d.ts +13 -5
  20. package/dist/commonjs/v3/managedChatResponse.js +36 -18
  21. package/dist/commonjs/v3/managedChatResponse.js.map +1 -1
  22. package/dist/commonjs/v3/steeringContext.d.ts +11 -0
  23. package/dist/commonjs/v3/steeringContext.js +44 -3
  24. package/dist/commonjs/v3/steeringContext.js.map +1 -1
  25. package/dist/commonjs/v3/subAgentStream.d.ts +50 -0
  26. package/dist/commonjs/v3/subAgentStream.js +154 -0
  27. package/dist/commonjs/v3/subAgentStream.js.map +1 -0
  28. package/dist/commonjs/v3/transcriptStorage.js +3 -0
  29. package/dist/commonjs/v3/transcriptStorage.js.map +1 -1
  30. package/dist/commonjs/v3/uiMessageChunks.js +5 -1
  31. package/dist/commonjs/v3/uiMessageChunks.js.map +1 -1
  32. package/dist/commonjs/version.js +1 -1
  33. package/dist/esm/v3/ai-shared.d.ts +6 -0
  34. package/dist/esm/v3/ai-shared.js +6 -0
  35. package/dist/esm/v3/ai-shared.js.map +1 -1
  36. package/dist/esm/v3/ai.d.ts +45 -0
  37. package/dist/esm/v3/ai.js +113 -72
  38. package/dist/esm/v3/ai.js.map +1 -1
  39. package/dist/esm/v3/auth.d.ts +2 -1
  40. package/dist/esm/v3/auth.js +19 -13
  41. package/dist/esm/v3/auth.js.map +1 -1
  42. package/dist/esm/v3/chat-client.d.ts +11 -2
  43. package/dist/esm/v3/chat-client.js +29 -11
  44. package/dist/esm/v3/chat-client.js.map +1 -1
  45. package/dist/esm/v3/chat-react.d.ts +21 -0
  46. package/dist/esm/v3/chat-react.js +81 -30
  47. package/dist/esm/v3/chat-react.js.map +1 -1
  48. package/dist/esm/v3/chat.d.ts +1 -0
  49. package/dist/esm/v3/chat.js +1 -0
  50. package/dist/esm/v3/chat.js.map +1 -1
  51. package/dist/esm/v3/managedChatResponse.d.ts +13 -5
  52. package/dist/esm/v3/managedChatResponse.js +36 -18
  53. package/dist/esm/v3/managedChatResponse.js.map +1 -1
  54. package/dist/esm/v3/steeringContext.d.ts +11 -0
  55. package/dist/esm/v3/steeringContext.js +44 -4
  56. package/dist/esm/v3/steeringContext.js.map +1 -1
  57. package/dist/esm/v3/subAgentStream.d.ts +50 -0
  58. package/dist/esm/v3/subAgentStream.js +149 -0
  59. package/dist/esm/v3/subAgentStream.js.map +1 -0
  60. package/dist/esm/v3/transcriptStorage.js +3 -0
  61. package/dist/esm/v3/transcriptStorage.js.map +1 -1
  62. package/dist/esm/v3/uiMessageChunks.js +5 -1
  63. package/dist/esm/v3/uiMessageChunks.js.map +1 -1
  64. package/dist/esm/version.js +1 -1
  65. package/docs/ai/prompts.mdx +9 -6
  66. package/docs/ai-chat/background-injection.mdx +2 -0
  67. package/docs/ai-chat/chat-local.mdx +4 -4
  68. package/docs/ai-chat/client-protocol.mdx +1 -0
  69. package/docs/ai-chat/fast-starts.mdx +1 -1
  70. package/docs/ai-chat/frontend.mdx +15 -0
  71. package/docs/ai-chat/overview.mdx +5 -4
  72. package/docs/ai-chat/patterns/database-persistence.mdx +3 -3
  73. package/docs/ai-chat/patterns/native-compaction.mdx +6 -6
  74. package/docs/ai-chat/patterns/sub-agents.mdx +82 -56
  75. package/docs/ai-chat/patterns/version-upgrades.mdx +3 -3
  76. package/docs/ai-chat/pending-messages.mdx +2 -0
  77. package/docs/ai-chat/quick-start.mdx +1 -1
  78. package/docs/ai-chat/reference.mdx +22 -0
  79. package/docs/ai-chat/server-chat.mdx +11 -7
  80. package/docs/ai-chat/side-channels.mdx +2 -2
  81. package/docs/ai-chat/types.mdx +5 -5
  82. package/docs/config/config-file.mdx +2 -2
  83. package/docs/database-connections.mdx +2 -2
  84. package/docs/how-to-reduce-your-spend.mdx +4 -0
  85. package/docs/introduction.mdx +104 -24
  86. package/docs/limits.mdx +41 -0
  87. package/docs/mcp-tools.mdx +5 -0
  88. package/docs/observability/query.mdx +29 -0
  89. package/docs/self-hosting/docker.mdx +62 -0
  90. package/docs/self-hosting/env/supervisor.mdx +4 -0
  91. package/docs/self-hosting/env/webapp.mdx +38 -0
  92. package/docs/self-hosting/kubernetes.mdx +2 -0
  93. package/docs/self-hosting/overview.mdx +2 -0
  94. package/docs/troubleshooting-alerts.mdx +39 -0
  95. package/docs/webhooks/deliveries.mdx +4 -0
  96. package/docs/webhooks/overview.mdx +8 -0
  97. package/docs/webhooks/sources.mdx +3 -1
  98. package/docs/webhooks/waiters.mdx +3 -3
  99. package/package.json +2 -2
  100. package/skills/trigger-chat-agent-advanced/SKILL.md +17 -7
  101. package/skills/trigger-cost-savings/SKILL.md +1 -0
@@ -1,33 +1,36 @@
1
1
  ---
2
2
  title: "Sub-Agents"
3
3
  sidebarTitle: "Sub-Agents"
4
- description: "Delegate work to durable sub-agents from within a parent agent's tool calls, with streaming preliminary results."
4
+ description: "Delegate work to durable sub-agents from a parent agent's tool calls, and stream each sub-agent's work to the frontend as it happens."
5
5
  ---
6
6
 
7
- Sub-agents let a parent agent delegate work to other agents running as durable Trigger.dev tasks. The sub-agent's response streams back through the parent as preliminary tool results, so the frontend sees the sub-agent working inside the parent's tool call card.
7
+ **Sub-agents let a parent agent delegate work to other agents running as durable Trigger.dev tasks, and `chat.forwardSubAgent` streams each sub-agent's reply to the frontend inside the parent's tool call.**
8
8
 
9
- This builds on the AI SDK's [async generator tool pattern](https://ai-sdk.dev/docs/agents/subagents) and Trigger.dev's [AgentChat](/ai-chat/server-chat) for server-side agent interaction.
9
+ Every chunk the sub-agent streams is written to the parent chat once, so the cost of streaming a sub-agent grows with the length of its reply. When the sub-agent finishes, its final message becomes the tool's output and persists with the parent's history.
10
+
11
+ This builds on Trigger.dev's [AgentChat](/ai-chat/server-chat) for server-side agent interaction.
10
12
 
11
13
  ## How it works
12
14
 
13
- 1. The parent LLM calls a tool (e.g., `researchAgent`)
14
- 2. The tool's `execute` is an `async function*` (async generator)
15
- 3. Inside, it creates an `AgentChat` and sends a message to the sub-agent
16
- 4. `yield* stream.messages()` streams each accumulated `UIMessage` snapshot as a preliminary tool result
17
- 5. The frontend renders the sub-agent's response building up inside the parent's tool card
18
- 6. `toModelOutput` compresses the full output into a summary for the parent LLM
15
+ 1. The parent LLM calls a tool (for example `research`).
16
+ 2. The tool's `execute` creates an `AgentChat` and sends a message to the sub-agent.
17
+ 3. `chat.forwardSubAgent(stream, { toolCallId })` writes each sub-agent chunk to the parent chat as a transient `data-subagent` part.
18
+ 4. The frontend rebuilds the sub-agent's message from those chunks with `useSubAgentMessages` and renders it inside the parent's tool card.
19
+ 5. When the sub-agent finishes, `forwardSubAgent` resolves with its final `UIMessage`, which becomes the tool's output.
20
+ 6. `toModelOutput` compresses that output into a summary for the parent LLM.
19
21
 
20
22
  ```
21
23
  Parent LLM
22
24
  │
23
- ├─ calls researchAgent tool
25
+ ├─ calls research tool
24
26
  │ │
25
27
  │ ├─ AgentChat triggers sub-agent run
26
- │ ├─ sub-agent streams response (text, tool calls, etc.)
27
- │ ├─ yield* sends UIMessage snapshots as preliminary results
28
- │ └─ toModelOutput compresses for parent LLM
28
+ │ ├─ sub-agent streams its reply (text, tool calls, etc.)
29
+ │ ├─ forwardSubAgent writes each chunk once as a transient data-subagent part
30
+ │ ├─ the tool returns the sub-agent's final UIMessage
31
+ │ └─ toModelOutput compresses it for the parent LLM
29
32
  │
30
- └─ parent LLM reads compressed summary, continues reasoning
33
+ └─ parent LLM reads the summary and continues
31
34
  ```
32
35
 
33
36
  ## Single-turn sub-agent
@@ -35,7 +38,8 @@ Parent LLM
35
38
  The simplest pattern: one tool call, one sub-agent turn, conversation closes.
36
39
 
37
40
  ```ts
38
- import { tool, stepCountIs } from "ai";
41
+ import { tool } from "ai";
42
+ import { chat } from "@trigger.dev/sdk/ai";
39
43
  import { AgentChat } from "@trigger.dev/sdk/chat";
40
44
  import { z } from "zod";
41
45
  import type { prReviewAgent } from "./trigger/pr-review";
@@ -46,19 +50,20 @@ const prReviewTool = tool({
46
50
  prNumber: z.number().describe("The PR number to review"),
47
51
  repo: z.string().describe("The GitHub repo URL"),
48
52
  }),
49
- execute: async function* ({ prNumber, repo }, { abortSignal }) {
50
- const chat = new AgentChat<typeof prReviewAgent>({
53
+ execute: async ({ prNumber, repo }, { toolCallId, abortSignal }) => {
54
+ const reviewer = new AgentChat<typeof prReviewAgent>({
51
55
  agent: "pr-review",
52
56
  id: `review-${prNumber}`,
53
57
  clientData: { userId: "parent-agent", githubUrl: repo },
54
58
  });
55
59
 
56
- const stream = await chat.sendMessage(`Review PR #${prNumber}`, { abortSignal });
57
-
58
- // Each yield sends a UIMessage snapshot to the frontend
59
- yield* stream.messages();
60
-
61
- await chat.close();
60
+ try {
61
+ const stream = await reviewer.sendMessage(`Review PR #${prNumber}`, { abortSignal });
62
+ // Streams the review to the frontend and resolves with the final message
63
+ return await chat.forwardSubAgent(stream, { toolCallId });
64
+ } finally {
65
+ await reviewer.close();
66
+ }
62
67
  },
63
68
  // The parent LLM only sees this compressed summary
64
69
  toModelOutput: ({ output: message }) => {
@@ -70,26 +75,42 @@ const prReviewTool = tool({
70
75
  });
71
76
  ```
72
77
 
73
- Use this tool in a parent agent's `streamText` call:
78
+ `forwardSubAgent` needs the parent chat to write to, so call it from a tool that runs inside a `chat.agent` or `chat.customAgent` run. [Combining with chat.agent()](#combining-with-chat-agent) below shows the agent side.
74
79
 
75
- ```ts
76
- import { streamText } from "ai";
77
- import { anthropic } from "@ai-sdk/anthropic";
80
+ ## Render sub-agents in the frontend
78
81
 
79
- const result = streamText({
80
- model: anthropic("claude-sonnet-4-6"),
81
- tools: { prReview: prReviewTool },
82
- prompt: "Review PR #42 on triggerdotdev/trigger.dev",
83
- stopWhen: stepCountIs(15),
84
- });
82
+ While the sub-agent runs, the parent's tool part has no output yet. `useSubAgentMessages` collects the forwarded chunks and gives you the sub-agent's message so far for each tool call:
83
+
84
+ ```tsx app/components/chat.tsx
85
+ import { useChat } from "@ai-sdk/react";
86
+ import type { TriggerChatTransport } from "@trigger.dev/sdk/chat";
87
+ import { useSubAgentMessages } from "@trigger.dev/sdk/chat/react";
88
+ import { ReviewCard } from "./review-card";
89
+
90
+ export function Chat({ transport }: { transport: TriggerChatTransport }) {
91
+ const subAgents = useSubAgentMessages();
92
+ const { messages } = useChat({ transport, onData: subAgents.onData });
93
+
94
+ return messages.map((message) =>
95
+ message.parts.map((part) => {
96
+ if (part.type !== "tool-prReview") return null;
97
+ // Live while the sub-agent runs, the tool output once it finishes
98
+ const review = subAgents.get(part.toolCallId) ?? part.output;
99
+ return <ReviewCard key={part.toolCallId} message={review} />;
100
+ })
101
+ );
102
+ }
85
103
  ```
86
104
 
105
+ The hook rebuilds each message at most once per `throttleMs` (100ms by default), so a long reply costs a bounded number of rebuilds. After a reload mid-turn, the transport replays the current turn and the hook rebuilds the sub-agent's message from it; batches it has already seen are skipped. Outside React, `createSubAgentMessages()` from `@trigger.dev/sdk/chat` does the same job.
106
+
87
107
  ## Multi-turn sub-agent (LLM-driven)
88
108
 
89
109
  The parent LLM drives a persistent conversation with a sub-agent across multiple tool calls. Each call with the same `conversationId` hits the same durable agent run.
90
110
 
91
111
  ```ts
92
112
  import { tool } from "ai";
113
+ import { chat } from "@trigger.dev/sdk/ai";
93
114
  import { AgentChat } from "@trigger.dev/sdk/chat";
94
115
  import { z } from "zod";
95
116
 
@@ -106,7 +127,7 @@ const researchTool = tool({
106
127
  .describe("Unique ID for this research thread. Reuse to continue."),
107
128
  message: z.string().describe("Your message to the research agent"),
108
129
  }),
109
- execute: async function* ({ conversationId, message }, { abortSignal }) {
130
+ execute: async ({ conversationId, message }, { toolCallId, abortSignal }) => {
110
131
  let agent = subAgents.get(conversationId);
111
132
  if (!agent) {
112
133
  agent = new AgentChat({
@@ -117,7 +138,7 @@ const researchTool = tool({
117
138
  }
118
139
 
119
140
  const stream = await agent.sendMessage(message, { abortSignal });
120
- yield* stream.messages();
141
+ return await chat.forwardSubAgent(stream, { toolCallId });
121
142
  },
122
143
  toModelOutput: ({ output: message }) => {
123
144
  const lastText = message?.parts?.findLast(
@@ -209,13 +230,19 @@ For the multi-turn pattern, the parent should clean up sub-agents when it exits
209
230
 
210
231
  ## What the frontend sees
211
232
 
212
- Each `yield` from `stream.messages()` sends a complete `UIMessage` containing all the sub-agent's parts accumulated so far. The AI SDK delivers these as `tool-output-available` chunks with `preliminary: true`.
233
+ While the sub-agent runs, the parent's tool part stays in `state: "input-available"`, and the parent stream carries transient `data-subagent` parts. Each one holds a batch of the sub-agent's chunks, tagged with the parent's `toolCallId` and a sequence number:
213
234
 
214
- The frontend renders the tool part with:
215
- - `state: "output-available"` and `preliminary: true` while streaming
216
- - `state: "output-available"` and `preliminary: false` (or absent) when done
235
+ ```ts
236
+ {
237
+ type: "data-subagent",
238
+ transient: true,
239
+ data: { toolCallId: "toolu_01...", seq: 3, chunks: [/* UIMessageChunk[] */] },
240
+ }
241
+ ```
242
+
243
+ Transient parts reach `useChat`'s `onData` and are not added to `message.parts`, so they never persist. When the sub-agent finishes, the tool part moves to `state: "output-available"` with the full `UIMessage` as its output: text, the sub-agent's own tool calls and results, reasoning, and so on. That output is what reloaded history renders.
217
244
 
218
- The tool output contains the full `UIMessage` with nested parts — text, the sub-agent's own tool calls and results, reasoning, etc.
245
+ If the user stops the parent turn while a sub-agent is running, the tool call ends without an output, the same as any stopped tool. The sub-agent's work up to that point was only ever in the transient parts, so neither the parent's history nor the next turn's model context includes it.
219
246
 
220
247
  ### Controlling what the parent LLM sees
221
248
 
@@ -238,30 +265,27 @@ This is important for token efficiency: the sub-agent might use 100K tokens expl
238
265
  `toModelOutput` only runs when the SDK has your tools at conversion time. On a multi-turn parent, the SDK re-converts the persisted history at the start of each turn, so you must declare the sub-agent tool on the agent config (`chat.agent({ tools })`) for the compression to survive. Without it, the summary holds on turn 1 but turn 2 onward re-ingests the full sub-agent output. In a `chat.customAgent` loop you own the conversion, so pass the tools to `convertToModelMessages(uiMessages, { tools })` yourself. See [Tools: toModelOutput across turns](/ai-chat/tools#tomodeloutput-across-turns).
239
266
  </Warning>
240
267
 
241
- ## ChatStream.messages()
268
+ ## Snapshots with `stream.messages()`
242
269
 
243
- The `messages()` method on `ChatStream` wraps the AI SDK's `readUIMessageStream`. It reads the raw `UIMessageChunk` stream and yields complete `UIMessage` snapshots — each containing all parts received so far.
270
+ `ChatStream.messages()` wraps the AI SDK's `readUIMessageStream` and yields the whole accumulated `UIMessage` after almost every chunk. Yielding those snapshots from an `async function*` tool (`yield* stream.messages()`) turns each one into a preliminary tool result, which streams the sub-agent too, but every snapshot repeats the whole message so far. The bytes written grow with the square of the reply length:
244
271
 
245
- ```ts
246
- const stream = await chat.sendMessage("Research this topic");
272
+ | Sub-agent reply | `yield* stream.messages()` | `chat.forwardSubAgent` |
273
+ | --- | --- | --- |
274
+ | 500 text deltas, no tools | 1.1 MB | 0.07 MB |
275
+ | 5 tool results of 20 KB, then 2,000 deltas | 218 MB | 0.47 MB |
276
+ | 10 tool results of 50 KB, then 4,000 deltas | 2.1 GB | 1.5 MB |
277
+
278
+ Every one of those bytes goes to the chat's stream storage and to each browser watching the chat. Use `forwardSubAgent` for sub-agents, and keep `stream.messages()` for reading a short reply in server code:
247
279
 
248
- // Each yield is a complete UIMessage with all accumulated parts
280
+ ```ts
281
+ const stream = await agent.sendMessage("Summarize this ticket in one sentence");
249
282
  for await (const message of stream.messages()) {
250
283
  console.log(message.parts.length, "parts so far");
251
284
  }
252
285
  ```
253
286
 
254
- For the sub-agent pattern, use `yield*` to delegate all yields to the parent tool's generator:
255
-
256
- ```ts
257
- execute: async function* ({ topic }, { abortSignal }) {
258
- const stream = await chat.sendMessage(topic, { abortSignal });
259
- yield* stream.messages();
260
- },
261
- ```
262
-
263
287
  <Tip>
264
- `stream.messages()` consumes the stream. You can't also call `stream.text()` or iterate over chunks on the same stream. Pick one consumption mode.
288
+ A `ChatStream` can be consumed once: by `forwardSubAgent`, `stream.messages()`, `stream.text()`, or by iterating its chunks. Pick one.
265
289
  </Tip>
266
290
 
267
291
  ## Combining with chat.agent()
@@ -275,7 +299,7 @@ const tools = { research: researchTool };
275
299
  export const myAgent = chat.agent({
276
300
  id: "orchestrator",
277
301
  tools, // declare here so toModelOutput survives across turns
278
- run: async ({ messages, tools, stopSignal }) => {
302
+ run: async ({ messages, tools, stopSignal, streamText }) => {
279
303
  return streamText({
280
304
  model: anthropic("claude-sonnet-4-6"),
281
305
  messages,
@@ -345,6 +369,8 @@ const research = tool({
345
369
  });
346
370
  ```
347
371
 
372
+ Keep a part you update this way small. The frontend replaces the part on each write, but the chat stream stores every version, so writing a growing payload under one `id` (a full message history, say) stores every copy. Mark progress updates that don't need to persist with `transient: true`, and use `chat.forwardSubAgent` when the thing you are streaming is a sub-agent's reply.
373
+
348
374
  On the frontend, render the custom data part:
349
375
 
350
376
  ```tsx
@@ -100,7 +100,7 @@ On the agent side, declare which versions the current code supports:
100
100
 
101
101
  ```ts
102
102
  import { chat } from "@trigger.dev/sdk/ai";
103
- import { streamText } from "ai";
103
+
104
104
  import { anthropic } from "@ai-sdk/anthropic";
105
105
 
106
106
  // The set of frontend protocol versions this agent code supports.
@@ -121,7 +121,7 @@ export const myChat = chat
121
121
  chat.requestUpgrade();
122
122
  }
123
123
  },
124
- run: async ({ messages, signal }) => {
124
+ run: async ({ messages, signal, streamText }) => {
125
125
  return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
126
126
  },
127
127
  });
@@ -183,7 +183,7 @@ export const myChat = chat
183
183
  chat.requestUpgrade();
184
184
  }
185
185
  },
186
- run: async ({ messages, signal }) => {
186
+ run: async ({ messages, signal, streamText }) => {
187
187
  return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
188
188
  },
189
189
  });
@@ -132,6 +132,8 @@ When messages are injected, the SDK automatically writes a `data-pending-message
132
132
  - Remove them from the pending overlay
133
133
  - Render them inline at the injection point in the assistant response
134
134
 
135
+ Background injections appear as a different data part (`data-background-injected`) that `usePendingMessages` ignores. It is not a user message.
136
+
135
137
  A "pending message injected" span also appears in the run trace.
136
138
 
137
139
  ## Backend: chat.createSession
@@ -159,5 +159,5 @@ Run your frontend and `pnpm exec trigger dev`, then send a message. You should s
159
159
  - [Frontend](/ai-chat/frontend) — Session management, client data, reconnection
160
160
  - [Types](/ai-chat/types) — `chat.withUIMessage`, `InferChatUIMessage`, and related typing
161
161
  - [`chat.local`](/ai-chat/chat-local) — Per-run typed state across hooks, run, tools, subtasks
162
- - [Sub-agents pattern](/ai-chat/patterns/sub-agents) — Subtask-as-tool, `target: "root"` streaming, `ai.toolExecute` helpers
162
+ - [Sub-agents pattern](/ai-chat/patterns/sub-agents): `chat.forwardSubAgent`, subtask-as-tool, `target: "root"` streaming, `ai.toolExecute` helpers
163
163
  - [Background injection](/ai-chat/background-injection) — `chat.inject()` and `chat.defer()` for between-turn work
@@ -565,6 +565,7 @@ All methods available on the `chat` object from `@trigger.dev/sdk/ai`.
565
565
  | `chat.isStopped()` | Check if the current turn was stopped by the user |
566
566
  | `chat.cleanupAbortedParts(message)` | Remove incomplete parts from a stopped response message |
567
567
  | `chat.response.write(chunk)` | Write a data part that streams to the frontend AND persists in `onTurnComplete`'s `responseMessage` |
568
+ | `chat.forwardSubAgent(stream, { toolCallId, flushIntervalMs? })` | Stream a sub-agent's reply through a parent tool call as transient `data-subagent` parts, and resolve with its final `UIMessage`. Call it inside a tool's `execute`. See [Sub-agents](/ai-chat/patterns/sub-agents) |
568
569
  | `chat.stream` | Raw chat output stream — use `.writer()`, `.pipe()`, `.append()`, `.read()`. Chunks are NOT accumulated into the response. |
569
570
  | `chat.history.all()` | Read the current accumulated UI messages (returns a copy). See [chat.history](/ai-chat/backend#chat-history) |
570
571
  | `chat.history.set(messages)` | Replace all accumulated messages (same as `chat.setMessages()`) |
@@ -837,6 +838,27 @@ const transport = useTriggerChatTransport({
837
838
 
838
839
  No-op when `BroadcastChannel` is unavailable (SSR, Node.js). See [Multi-tab coordination](/ai-chat/frontend#multi-tab-coordination).
839
840
 
841
+ ## useSubAgentMessages
842
+
843
+ React hook that rebuilds sub-agents streamed with [`chat.forwardSubAgent`](/ai-chat/patterns/sub-agents) while they run. Import from `@trigger.dev/sdk/chat/react`.
844
+
845
+ ```tsx
846
+ import { useSubAgentMessages } from "@trigger.dev/sdk/chat/react";
847
+
848
+ const subAgents = useSubAgentMessages();
849
+ const { messages } = useChat({ transport, onData: subAgents.onData });
850
+
851
+ const message = subAgents.get(part.toolCallId) ?? part.output;
852
+ ```
853
+
854
+ | Option | Type | Default | Description |
855
+ | ------------ | -------- | ------- | ------------------------------------------------------------------------ |
856
+ | `throttleMs` | `number` | `100` | Minimum time between rebuilds of a sub-agent's message while chunks stream in. |
857
+
858
+ **Returns:** `{ onData, get }`. Pass `onData` to `useChat`. `get(toolCallId)` returns the sub-agent's `UIMessage` so far, or `undefined` before its first chunk. Batches a resumed stream replays are skipped, so a reload mid-turn does not duplicate content.
859
+
860
+ `createSubAgentMessages({ throttleMs?, onChange? })` from `@trigger.dev/sdk/chat` is the same store without React: call its `onData` with every data part you receive, read with `get(toolCallId)`, and `dispose()` when you are done.
861
+
840
862
  ### Trigger configuration
841
863
 
842
864
  Trigger config (machine, queue, tags, maxAttempts, idleTimeoutInSeconds) lives server-side in `chat.createStartSessionAction(taskId, options?)`. The transport doesn't accept these options directly — pass them when wrapping the action:
@@ -161,21 +161,25 @@ export async function POST(req: Request) {
161
161
 
162
162
  ## Sub-agent tool pattern
163
163
 
164
- `AgentChat` can be used inside an AI SDK tool to delegate work to a durable sub-agent. The sub-agent's response streams as preliminary tool results:
164
+ `AgentChat` can be used inside an AI SDK tool to delegate work to a durable sub-agent. `chat.forwardSubAgent` streams the sub-agent's reply to the frontend inside the tool call and resolves with its final message:
165
165
 
166
166
  ```ts
167
167
  import { tool } from "ai";
168
+ import { chat } from "@trigger.dev/sdk/ai";
168
169
  import { AgentChat } from "@trigger.dev/sdk/chat";
169
170
  import { z } from "zod";
170
171
 
171
172
  const researchTool = tool({
172
173
  description: "Delegate research to a specialist agent.",
173
174
  inputSchema: z.object({ topic: z.string() }),
174
- execute: async function* ({ topic }, { abortSignal }) {
175
- const chat = new AgentChat({ agent: "research-agent" });
176
- const stream = await chat.sendMessage(topic, { abortSignal });
177
- yield* stream.messages();
178
- await chat.close();
175
+ execute: async ({ topic }, { toolCallId, abortSignal }) => {
176
+ const researcher = new AgentChat({ agent: "research-agent" });
177
+ try {
178
+ const stream = await researcher.sendMessage(topic, { abortSignal });
179
+ return await chat.forwardSubAgent(stream, { toolCallId });
180
+ } finally {
181
+ await researcher.close();
182
+ }
179
183
  },
180
184
  toModelOutput: ({ output: message }) => {
181
185
  const lastText = message?.parts?.findLast(
@@ -254,6 +258,6 @@ const stream = await chat.reconnect();
254
258
  |---|---|---|
255
259
  | `text()` | `Promise<string>` | Consume stream, return accumulated text |
256
260
  | `result()` | `Promise<ChatStreamResult>` | Consume stream, return `{ text, toolCalls, toolResults }` |
257
- | `messages()` | `AsyncGenerator<UIMessage>` | Yield accumulated UIMessage snapshots (sub-agent pattern) |
261
+ | `messages()` | `AsyncGenerator<UIMessage>` | Yield the accumulated UIMessage after each chunk. For sub-agents, pass the stream to `chat.forwardSubAgent` instead |
258
262
  | `[Symbol.asyncIterator]` | `UIMessageChunk` | Iterate over typed stream chunks |
259
263
  | `.stream` | `ReadableStream<UIMessageChunk>` | Raw stream for AI SDK utilities |
@@ -38,12 +38,12 @@ Inside a `chat.agent` run, `chat.channel(...)` opens a channel on the current ru
38
38
 
39
39
  ```ts /trigger/browser-agent.ts
40
40
  import { chat } from "@trigger.dev/sdk/ai";
41
- import { streamText } from "ai";
41
+
42
42
  import { screenshots } from "./channels";
43
43
 
44
44
  export const browserAgent = chat.agent({
45
45
  id: "browser-agent",
46
- run: async ({ messages, signal }) => {
46
+ run: async ({ messages, signal, streamText }) => {
47
47
  const frames = chat.channel(screenshots);
48
48
 
49
49
  frames.in.on((control) => setPaused(control.paused)); // control: ViewportControl
@@ -56,7 +56,7 @@ Call `withUIMessage` **once**, then chain `.agent({ ... })` instead of `chat.age
56
56
 
57
57
  ```ts
58
58
  import { chat } from "@trigger.dev/sdk/ai";
59
- import { streamText, tool } from "ai";
59
+ import { tool } from "ai";
60
60
  import { anthropic } from "@ai-sdk/anthropic";
61
61
  import { z } from "zod";
62
62
  import type { MyChatUIMessage } from "./my-chat-types";
@@ -90,7 +90,7 @@ export const myChat = chat
90
90
  data: { status: "preparing" },
91
91
  });
92
92
  },
93
- run: async ({ messages, tools, signal }) => {
93
+ run: async ({ messages, tools, signal, streamText }) => {
94
94
  // `tools` is myTools, typed, handed back on the payload
95
95
  return streamText({
96
96
  model: anthropic("claude-sonnet-4-5"),
@@ -156,7 +156,7 @@ export const myChat = chat
156
156
  // clientData is typed as { userId: string; model?: string }
157
157
  await initUser(clientData.userId);
158
158
  },
159
- run: async ({ messages, clientData, signal }) => {
159
+ run: async ({ messages, clientData, signal, streamText }) => {
160
160
  return streamText({
161
161
  model: getModel(clientData.model),
162
162
  messages,
@@ -193,7 +193,7 @@ export const myChat = chat
193
193
  })
194
194
  .agent({
195
195
  id: "my-chat",
196
- run: async ({ messages, signal }) => {
196
+ run: async ({ messages, signal, streamText }) => {
197
197
  return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
198
198
  },
199
199
  });
@@ -218,7 +218,7 @@ chat
218
218
  // Runs second — task-specific logic
219
219
  await createChatRecord(event.chatId);
220
220
  },
221
- run: async ({ messages, signal }) => {
221
+ run: async ({ messages, signal, streamText }) => {
222
222
  return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
223
223
  },
224
224
  });
@@ -329,7 +329,7 @@ After each call we restore the few attributes we use to link a span to its run,
329
329
 
330
330
  If you don't set a `runtime`, your tasks run on the current Node.js LTS. Setting `runtime: "node"` does the same thing — see the Node.js versions section below to pin a specific version instead.
331
331
 
332
- We currently only officially support the `node` runtime, but you can try our experimental `bun` runtime by setting the `runtime` option in your config file:
332
+ We currently only officially support the `node` runtime, but you can try our experimental Bun runtimes by setting the `runtime` option in your config file:
333
333
 
334
334
  ```ts trigger.config.ts
335
335
  import { defineConfig } from "@trigger.dev/sdk";
@@ -341,7 +341,7 @@ export default defineConfig({
341
341
  });
342
342
  ```
343
343
 
344
- See our [Bun guide](/guides/frameworks/bun) for more information.
344
+ `bun` runs on Bun 1.3 with a Node.js binary alongside it. `bun-1.4` runs on a Bun-only image with no Node.js binary. See our [Bun guide](/guides/frameworks/bun) for the differences.
345
345
 
346
346
  ### Node.js versions
347
347
 
@@ -176,13 +176,13 @@ A chat agent runs one long-lived worker per conversation and suspends between me
176
176
 
177
177
  ```ts /trigger/chat.ts
178
178
  import { chat } from "@trigger.dev/sdk/ai";
179
- import { streamText } from "ai";
179
+
180
180
  import { openai } from "@ai-sdk/openai";
181
181
  import { prisma } from "@/lib/db";
182
182
 
183
183
  export const myChat = chat.agent({
184
184
  id: "my-chat",
185
- run: async ({ messages, clientData, signal }) => {
185
+ run: async ({ messages, clientData, signal, streamText }) => {
186
186
  const user = await prisma.user.findUnique({ where: { id: clientData.userId } });
187
187
  // The connection is back in the pool before the model stream starts.
188
188
  return streamText({
@@ -190,6 +190,10 @@ export const waitpointTask = task({
190
190
 
191
191
  Waiting saves you compute, but it doesn't always free up concurrency — see [Wait](/wait).
192
192
 
193
+ ## Filter webhooks before they start runs
194
+
195
+ Give each [webhook](/webhooks/overview) subscriber a [filter](/webhooks/filters) so it only runs for the events it handles. A delivery that no subscriber's filter matches starts no run for any subscriber, so you aren't charged for it. Filters only apply to subscribers: a run [waiting](/webhooks/waiters) for a matching webhook still resumes and uses compute. Receiving webhooks has no separate charge.
196
+
193
197
  ## Use debounce to consolidate multiple triggers
194
198
 
195
199
  When a task might be triggered multiple times in quick succession, use debounce to consolidate them into a single run. This is useful for document indexing, webhook aggregation, cache invalidation, and real-time sync scenarios.