@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.
- package/dist/commonjs/v3/ai-shared.d.ts +6 -0
- package/dist/commonjs/v3/ai-shared.js +7 -1
- package/dist/commonjs/v3/ai-shared.js.map +1 -1
- package/dist/commonjs/v3/ai.d.ts +45 -0
- package/dist/commonjs/v3/ai.js +110 -69
- package/dist/commonjs/v3/ai.js.map +1 -1
- package/dist/commonjs/v3/auth.d.ts +2 -1
- package/dist/commonjs/v3/auth.js +18 -12
- package/dist/commonjs/v3/auth.js.map +1 -1
- package/dist/commonjs/v3/chat-client.d.ts +11 -2
- package/dist/commonjs/v3/chat-client.js +28 -10
- package/dist/commonjs/v3/chat-client.js.map +1 -1
- package/dist/commonjs/v3/chat-react.d.ts +21 -0
- package/dist/commonjs/v3/chat-react.js +86 -30
- package/dist/commonjs/v3/chat-react.js.map +1 -1
- package/dist/commonjs/v3/chat.d.ts +1 -0
- package/dist/commonjs/v3/chat.js +4 -1
- package/dist/commonjs/v3/chat.js.map +1 -1
- package/dist/commonjs/v3/managedChatResponse.d.ts +13 -5
- package/dist/commonjs/v3/managedChatResponse.js +36 -18
- package/dist/commonjs/v3/managedChatResponse.js.map +1 -1
- package/dist/commonjs/v3/steeringContext.d.ts +11 -0
- package/dist/commonjs/v3/steeringContext.js +44 -3
- package/dist/commonjs/v3/steeringContext.js.map +1 -1
- package/dist/commonjs/v3/subAgentStream.d.ts +50 -0
- package/dist/commonjs/v3/subAgentStream.js +154 -0
- package/dist/commonjs/v3/subAgentStream.js.map +1 -0
- package/dist/commonjs/v3/transcriptStorage.js +3 -0
- package/dist/commonjs/v3/transcriptStorage.js.map +1 -1
- package/dist/commonjs/v3/uiMessageChunks.js +5 -1
- package/dist/commonjs/v3/uiMessageChunks.js.map +1 -1
- package/dist/commonjs/version.js +1 -1
- package/dist/esm/v3/ai-shared.d.ts +6 -0
- package/dist/esm/v3/ai-shared.js +6 -0
- package/dist/esm/v3/ai-shared.js.map +1 -1
- package/dist/esm/v3/ai.d.ts +45 -0
- package/dist/esm/v3/ai.js +113 -72
- package/dist/esm/v3/ai.js.map +1 -1
- package/dist/esm/v3/auth.d.ts +2 -1
- package/dist/esm/v3/auth.js +19 -13
- package/dist/esm/v3/auth.js.map +1 -1
- package/dist/esm/v3/chat-client.d.ts +11 -2
- package/dist/esm/v3/chat-client.js +29 -11
- package/dist/esm/v3/chat-client.js.map +1 -1
- package/dist/esm/v3/chat-react.d.ts +21 -0
- package/dist/esm/v3/chat-react.js +81 -30
- package/dist/esm/v3/chat-react.js.map +1 -1
- package/dist/esm/v3/chat.d.ts +1 -0
- package/dist/esm/v3/chat.js +1 -0
- package/dist/esm/v3/chat.js.map +1 -1
- package/dist/esm/v3/managedChatResponse.d.ts +13 -5
- package/dist/esm/v3/managedChatResponse.js +36 -18
- package/dist/esm/v3/managedChatResponse.js.map +1 -1
- package/dist/esm/v3/steeringContext.d.ts +11 -0
- package/dist/esm/v3/steeringContext.js +44 -4
- package/dist/esm/v3/steeringContext.js.map +1 -1
- package/dist/esm/v3/subAgentStream.d.ts +50 -0
- package/dist/esm/v3/subAgentStream.js +149 -0
- package/dist/esm/v3/subAgentStream.js.map +1 -0
- package/dist/esm/v3/transcriptStorage.js +3 -0
- package/dist/esm/v3/transcriptStorage.js.map +1 -1
- package/dist/esm/v3/uiMessageChunks.js +5 -1
- package/dist/esm/v3/uiMessageChunks.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/docs/ai/prompts.mdx +9 -6
- package/docs/ai-chat/background-injection.mdx +2 -0
- package/docs/ai-chat/chat-local.mdx +4 -4
- package/docs/ai-chat/client-protocol.mdx +1 -0
- package/docs/ai-chat/fast-starts.mdx +1 -1
- package/docs/ai-chat/frontend.mdx +15 -0
- package/docs/ai-chat/overview.mdx +5 -4
- package/docs/ai-chat/patterns/database-persistence.mdx +3 -3
- package/docs/ai-chat/patterns/native-compaction.mdx +6 -6
- package/docs/ai-chat/patterns/sub-agents.mdx +82 -56
- package/docs/ai-chat/patterns/version-upgrades.mdx +3 -3
- package/docs/ai-chat/pending-messages.mdx +2 -0
- package/docs/ai-chat/quick-start.mdx +1 -1
- package/docs/ai-chat/reference.mdx +22 -0
- package/docs/ai-chat/server-chat.mdx +11 -7
- package/docs/ai-chat/side-channels.mdx +2 -2
- package/docs/ai-chat/types.mdx +5 -5
- package/docs/config/config-file.mdx +2 -2
- package/docs/database-connections.mdx +2 -2
- package/docs/how-to-reduce-your-spend.mdx +4 -0
- package/docs/introduction.mdx +104 -24
- package/docs/limits.mdx +41 -0
- package/docs/mcp-tools.mdx +5 -0
- package/docs/observability/query.mdx +29 -0
- package/docs/self-hosting/docker.mdx +62 -0
- package/docs/self-hosting/env/supervisor.mdx +4 -0
- package/docs/self-hosting/env/webapp.mdx +38 -0
- package/docs/self-hosting/kubernetes.mdx +2 -0
- package/docs/self-hosting/overview.mdx +2 -0
- package/docs/troubleshooting-alerts.mdx +39 -0
- package/docs/webhooks/deliveries.mdx +4 -0
- package/docs/webhooks/overview.mdx +8 -0
- package/docs/webhooks/sources.mdx +3 -1
- package/docs/webhooks/waiters.mdx +3 -3
- package/package.json +2 -2
- package/skills/trigger-chat-agent-advanced/SKILL.md +17 -7
- 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
|
|
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.
|
|
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
|
-
|
|
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 (
|
|
14
|
-
2. The tool's `execute`
|
|
15
|
-
3.
|
|
16
|
-
4.
|
|
17
|
-
5.
|
|
18
|
-
6. `toModelOutput` compresses
|
|
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
|
|
25
|
+
├─ calls research tool
|
|
24
26
|
│ │
|
|
25
27
|
│ ├─ AgentChat triggers sub-agent run
|
|
26
|
-
│ ├─ sub-agent streams
|
|
27
|
-
│ ├─
|
|
28
|
-
│
|
|
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
|
|
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
|
|
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
|
|
50
|
-
const
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
import { streamText } from "ai";
|
|
77
|
-
import { anthropic } from "@ai-sdk/anthropic";
|
|
80
|
+
## Render sub-agents in the frontend
|
|
78
81
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
268
|
+
## Snapshots with `stream.messages()`
|
|
242
269
|
|
|
243
|
-
|
|
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
|
-
|
|
246
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
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)
|
|
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.
|
|
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
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
|
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
|
-
|
|
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
|
package/docs/ai-chat/types.mdx
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|