@trigger.dev/sdk 4.5.0-rc.6 → 4.5.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 (247) hide show
  1. package/dist/commonjs/v3/ai-shared.js +1 -4
  2. package/dist/commonjs/v3/ai-shared.js.map +1 -1
  3. package/dist/commonjs/v3/ai.d.ts +178 -6
  4. package/dist/commonjs/v3/ai.js +417 -64
  5. package/dist/commonjs/v3/ai.js.map +1 -1
  6. package/dist/commonjs/v3/chat-client.js +18 -14
  7. package/dist/commonjs/v3/chat-client.js.map +1 -1
  8. package/dist/commonjs/v3/chat-react.js +1 -3
  9. package/dist/commonjs/v3/chat-react.js.map +1 -1
  10. package/dist/commonjs/v3/chat-server.d.ts +76 -0
  11. package/dist/commonjs/v3/chat-server.js +194 -24
  12. package/dist/commonjs/v3/chat-server.js.map +1 -1
  13. package/dist/commonjs/v3/chat-server.test.js +201 -6
  14. package/dist/commonjs/v3/chat-server.test.js.map +1 -1
  15. package/dist/commonjs/v3/chat.js +4 -3
  16. package/dist/commonjs/v3/chat.js.map +1 -1
  17. package/dist/commonjs/v3/chat.test.js +6 -18
  18. package/dist/commonjs/v3/chat.test.js.map +1 -1
  19. package/dist/commonjs/v3/createStartSessionAction.test.js +30 -0
  20. package/dist/commonjs/v3/createStartSessionAction.test.js.map +1 -1
  21. package/dist/commonjs/v3/idempotencyKeys.js.map +1 -1
  22. package/dist/commonjs/v3/prompt.js +2 -6
  23. package/dist/commonjs/v3/prompt.js.map +1 -1
  24. package/dist/commonjs/v3/promptManagement.js.map +1 -1
  25. package/dist/commonjs/v3/retry.js.map +1 -1
  26. package/dist/commonjs/v3/runs.d.ts +1 -1
  27. package/dist/commonjs/v3/sessions.d.ts +3 -2
  28. package/dist/commonjs/v3/sessions.js +4 -2
  29. package/dist/commonjs/v3/sessions.js.map +1 -1
  30. package/dist/commonjs/v3/shared.js.map +1 -1
  31. package/dist/commonjs/v3/skill.js.map +1 -1
  32. package/dist/commonjs/v3/streams.js +2 -3
  33. package/dist/commonjs/v3/streams.js.map +1 -1
  34. package/dist/commonjs/v3/streams.test.js.map +1 -1
  35. package/dist/commonjs/v3/test/mock-chat-agent.js +1 -4
  36. package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
  37. package/dist/commonjs/v3/test/test-session-handle.js.map +1 -1
  38. package/dist/commonjs/v3/triggerClient.test.js +1 -1
  39. package/dist/commonjs/v3/triggerClient.test.js.map +1 -1
  40. package/dist/commonjs/v3/triggerClient.types.test.js +11 -11
  41. package/dist/commonjs/v3/triggerClient.types.test.js.map +1 -1
  42. package/dist/commonjs/version.js +1 -1
  43. package/dist/esm/v3/ai-shared.js +1 -4
  44. package/dist/esm/v3/ai-shared.js.map +1 -1
  45. package/dist/esm/v3/ai.d.ts +178 -6
  46. package/dist/esm/v3/ai.js +417 -64
  47. package/dist/esm/v3/ai.js.map +1 -1
  48. package/dist/esm/v3/chat-client.js +18 -14
  49. package/dist/esm/v3/chat-client.js.map +1 -1
  50. package/dist/esm/v3/chat-react.js +1 -3
  51. package/dist/esm/v3/chat-react.js.map +1 -1
  52. package/dist/esm/v3/chat-server.d.ts +76 -0
  53. package/dist/esm/v3/chat-server.js +194 -24
  54. package/dist/esm/v3/chat-server.js.map +1 -1
  55. package/dist/esm/v3/chat-server.test.js +201 -6
  56. package/dist/esm/v3/chat-server.test.js.map +1 -1
  57. package/dist/esm/v3/chat.js +4 -3
  58. package/dist/esm/v3/chat.js.map +1 -1
  59. package/dist/esm/v3/chat.test.js +6 -18
  60. package/dist/esm/v3/chat.test.js.map +1 -1
  61. package/dist/esm/v3/createStartSessionAction.test.js +30 -0
  62. package/dist/esm/v3/createStartSessionAction.test.js.map +1 -1
  63. package/dist/esm/v3/idempotencyKeys.js +1 -1
  64. package/dist/esm/v3/idempotencyKeys.js.map +1 -1
  65. package/dist/esm/v3/prompt.js +2 -6
  66. package/dist/esm/v3/prompt.js.map +1 -1
  67. package/dist/esm/v3/promptManagement.js.map +1 -1
  68. package/dist/esm/v3/retry.js.map +1 -1
  69. package/dist/esm/v3/sessions.d.ts +3 -2
  70. package/dist/esm/v3/sessions.js +4 -2
  71. package/dist/esm/v3/sessions.js.map +1 -1
  72. package/dist/esm/v3/shared.js.map +1 -1
  73. package/dist/esm/v3/skill.js.map +1 -1
  74. package/dist/esm/v3/streams.js +2 -3
  75. package/dist/esm/v3/streams.js.map +1 -1
  76. package/dist/esm/v3/streams.test.js.map +1 -1
  77. package/dist/esm/v3/test/mock-chat-agent.js +4 -7
  78. package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
  79. package/dist/esm/v3/test/test-session-handle.js.map +1 -1
  80. package/dist/esm/v3/triggerClient.test.js +1 -1
  81. package/dist/esm/v3/triggerClient.test.js.map +1 -1
  82. package/dist/esm/v3/triggerClient.types.test.js +11 -11
  83. package/dist/esm/v3/triggerClient.types.test.js.map +1 -1
  84. package/dist/esm/version.js +1 -1
  85. package/docs/ai/prompts.mdx +426 -0
  86. package/docs/ai-chat/actions.mdx +111 -0
  87. package/docs/ai-chat/anatomy.mdx +67 -0
  88. package/docs/ai-chat/backend.mdx +813 -0
  89. package/docs/ai-chat/background-injection.mdx +217 -0
  90. package/docs/ai-chat/changelog.mdx +958 -0
  91. package/docs/ai-chat/chat-local.mdx +170 -0
  92. package/docs/ai-chat/client-protocol.mdx +1077 -0
  93. package/docs/ai-chat/compaction.mdx +407 -0
  94. package/docs/ai-chat/custom-agents.mdx +392 -0
  95. package/docs/ai-chat/error-handling.mdx +411 -0
  96. package/docs/ai-chat/fast-starts.mdx +754 -0
  97. package/docs/ai-chat/frontend.mdx +576 -0
  98. package/docs/ai-chat/how-it-works.mdx +226 -0
  99. package/docs/ai-chat/lifecycle-hooks.mdx +526 -0
  100. package/docs/ai-chat/mcp.mdx +97 -0
  101. package/docs/ai-chat/overview.mdx +86 -0
  102. package/docs/ai-chat/patterns/branching-conversations.mdx +280 -0
  103. package/docs/ai-chat/patterns/code-sandbox.mdx +122 -0
  104. package/docs/ai-chat/patterns/database-persistence.mdx +410 -0
  105. package/docs/ai-chat/patterns/human-in-the-loop.mdx +279 -0
  106. package/docs/ai-chat/patterns/large-payloads.mdx +165 -0
  107. package/docs/ai-chat/patterns/oom-resilience.mdx +116 -0
  108. package/docs/ai-chat/patterns/persistence-and-replay.mdx +207 -0
  109. package/docs/ai-chat/patterns/recovery-boot.mdx +226 -0
  110. package/docs/ai-chat/patterns/skills.mdx +217 -0
  111. package/docs/ai-chat/patterns/sub-agents.mdx +379 -0
  112. package/docs/ai-chat/patterns/tool-result-auditing.mdx +144 -0
  113. package/docs/ai-chat/patterns/trusted-edge-signals.mdx +333 -0
  114. package/docs/ai-chat/patterns/version-upgrades.mdx +168 -0
  115. package/docs/ai-chat/pending-messages.mdx +339 -0
  116. package/docs/ai-chat/prompt-caching.mdx +206 -0
  117. package/docs/ai-chat/quick-start.mdx +157 -0
  118. package/docs/ai-chat/reference.mdx +905 -0
  119. package/docs/ai-chat/server-chat.mdx +259 -0
  120. package/docs/ai-chat/sessions.mdx +329 -0
  121. package/docs/ai-chat/testing.mdx +678 -0
  122. package/docs/ai-chat/tools.mdx +187 -0
  123. package/docs/ai-chat/types.mdx +238 -0
  124. package/docs/ai-chat/upgrade-guide.mdx +511 -0
  125. package/docs/apikeys.mdx +54 -0
  126. package/docs/building-with-ai.mdx +261 -0
  127. package/docs/bulk-actions.mdx +49 -0
  128. package/docs/changelog.mdx +6 -0
  129. package/docs/cli-deploy-commands.mdx +9 -0
  130. package/docs/cli-dev-commands.mdx +9 -0
  131. package/docs/cli-init-commands.mdx +58 -0
  132. package/docs/cli-introduction.mdx +25 -0
  133. package/docs/cli-list-profiles-commands.mdx +42 -0
  134. package/docs/cli-login-commands.mdx +33 -0
  135. package/docs/cli-logout-commands.mdx +33 -0
  136. package/docs/cli-preview-archive.mdx +59 -0
  137. package/docs/cli-promote-commands.mdx +9 -0
  138. package/docs/cli-switch.mdx +43 -0
  139. package/docs/cli-update-commands.mdx +42 -0
  140. package/docs/cli-whoami-commands.mdx +33 -0
  141. package/docs/community.mdx +6 -0
  142. package/docs/config/config-file.mdx +602 -0
  143. package/docs/config/extensions/additionalFiles.mdx +38 -0
  144. package/docs/config/extensions/additionalPackages.mdx +40 -0
  145. package/docs/config/extensions/aptGet.mdx +34 -0
  146. package/docs/config/extensions/audioWaveform.mdx +20 -0
  147. package/docs/config/extensions/custom.mdx +380 -0
  148. package/docs/config/extensions/emitDecoratorMetadata.mdx +29 -0
  149. package/docs/config/extensions/esbuildPlugin.mdx +31 -0
  150. package/docs/config/extensions/ffmpeg.mdx +45 -0
  151. package/docs/config/extensions/lightpanda.mdx +56 -0
  152. package/docs/config/extensions/overview.mdx +67 -0
  153. package/docs/config/extensions/playwright.mdx +195 -0
  154. package/docs/config/extensions/prismaExtension.mdx +1014 -0
  155. package/docs/config/extensions/puppeteer.mdx +30 -0
  156. package/docs/config/extensions/pythonExtension.mdx +182 -0
  157. package/docs/config/extensions/syncEnvVars.mdx +291 -0
  158. package/docs/context.mdx +235 -0
  159. package/docs/database-connections.mdx +213 -0
  160. package/docs/deploy-environment-variables.mdx +435 -0
  161. package/docs/deployment/atomic-deployment.mdx +172 -0
  162. package/docs/deployment/dev-branches.mdx +92 -0
  163. package/docs/deployment/overview.mdx +257 -0
  164. package/docs/deployment/preview-branches.mdx +224 -0
  165. package/docs/errors-retrying.mdx +379 -0
  166. package/docs/github-actions.mdx +222 -0
  167. package/docs/github-integration.mdx +136 -0
  168. package/docs/github-repo.mdx +8 -0
  169. package/docs/help-email.mdx +6 -0
  170. package/docs/help-slack.mdx +12 -0
  171. package/docs/hidden-tasks.mdx +56 -0
  172. package/docs/how-it-works.mdx +454 -0
  173. package/docs/how-to-reduce-your-spend.mdx +224 -0
  174. package/docs/idempotency.mdx +504 -0
  175. package/docs/introduction.mdx +223 -0
  176. package/docs/limits.mdx +241 -0
  177. package/docs/logging.mdx +195 -0
  178. package/docs/machines.mdx +952 -0
  179. package/docs/manual-setup.mdx +632 -0
  180. package/docs/mcp-agent-rules.mdx +41 -0
  181. package/docs/mcp-introduction.mdx +385 -0
  182. package/docs/mcp-tools.mdx +273 -0
  183. package/docs/migrating-from-v3.mdx +334 -0
  184. package/docs/observability/dashboards.mdx +102 -0
  185. package/docs/observability/query.mdx +585 -0
  186. package/docs/open-source-contributing.mdx +16 -0
  187. package/docs/open-source-self-hosting.mdx +541 -0
  188. package/docs/private-networking/aws-console-setup.mdx +304 -0
  189. package/docs/private-networking/overview.mdx +144 -0
  190. package/docs/private-networking/troubleshooting.mdx +78 -0
  191. package/docs/queue-concurrency.mdx +354 -0
  192. package/docs/quick-start.mdx +97 -0
  193. package/docs/realtime/auth.mdx +208 -0
  194. package/docs/realtime/backend/overview.mdx +45 -0
  195. package/docs/realtime/backend/streams.mdx +418 -0
  196. package/docs/realtime/backend/subscribe.mdx +225 -0
  197. package/docs/realtime/how-it-works.mdx +94 -0
  198. package/docs/realtime/overview.mdx +63 -0
  199. package/docs/realtime/react-hooks/overview.mdx +73 -0
  200. package/docs/realtime/react-hooks/streams.mdx +449 -0
  201. package/docs/realtime/react-hooks/subscribe.mdx +674 -0
  202. package/docs/realtime/react-hooks/swr.mdx +87 -0
  203. package/docs/realtime/react-hooks/triggering.mdx +194 -0
  204. package/docs/realtime/react-hooks/use-wait-token.mdx +34 -0
  205. package/docs/realtime/run-object.mdx +174 -0
  206. package/docs/replaying.mdx +72 -0
  207. package/docs/request-feature.mdx +6 -0
  208. package/docs/roadmap.mdx +6 -0
  209. package/docs/run-tests.mdx +20 -0
  210. package/docs/run-usage.mdx +113 -0
  211. package/docs/runs/heartbeats.mdx +38 -0
  212. package/docs/runs/max-duration.mdx +139 -0
  213. package/docs/runs/metadata.mdx +734 -0
  214. package/docs/runs/priority.mdx +31 -0
  215. package/docs/runs.mdx +396 -0
  216. package/docs/self-hosting/docker.mdx +458 -0
  217. package/docs/self-hosting/env/supervisor.mdx +74 -0
  218. package/docs/self-hosting/env/webapp.mdx +276 -0
  219. package/docs/self-hosting/kubernetes.mdx +601 -0
  220. package/docs/self-hosting/overview.mdx +109 -0
  221. package/docs/skills.mdx +85 -0
  222. package/docs/tags.mdx +120 -0
  223. package/docs/tasks/overview.mdx +697 -0
  224. package/docs/tasks/scheduled.mdx +382 -0
  225. package/docs/tasks/schemaTask.mdx +413 -0
  226. package/docs/tasks/streams.mdx +884 -0
  227. package/docs/triggering.mdx +1320 -0
  228. package/docs/troubleshooting-alerts.mdx +385 -0
  229. package/docs/troubleshooting-debugging-in-vscode.mdx +9 -0
  230. package/docs/troubleshooting-github-issues.mdx +6 -0
  231. package/docs/troubleshooting-uptime-status.mdx +7 -0
  232. package/docs/troubleshooting.mdx +398 -0
  233. package/docs/upgrading-packages.mdx +80 -0
  234. package/docs/vercel-integration.mdx +207 -0
  235. package/docs/versioning.mdx +56 -0
  236. package/docs/video-walkthrough.mdx +23 -0
  237. package/docs/wait-for-token.mdx +540 -0
  238. package/docs/wait-for.mdx +42 -0
  239. package/docs/wait-until.mdx +53 -0
  240. package/docs/wait.mdx +18 -0
  241. package/docs/writing-tasks-introduction.mdx +33 -0
  242. package/package.json +8 -5
  243. package/skills/trigger-authoring-chat-agent/SKILL.md +296 -0
  244. package/skills/trigger-authoring-tasks/SKILL.md +254 -0
  245. package/skills/trigger-chat-agent-advanced/SKILL.md +368 -0
  246. package/skills/trigger-cost-savings/SKILL.md +116 -0
  247. package/skills/trigger-realtime-and-frontend/SKILL.md +276 -0
@@ -0,0 +1,379 @@
1
+ ---
2
+ title: "Sub-Agents"
3
+ sidebarTitle: "Sub-Agents"
4
+ description: "Delegate work to durable sub-agents from within a parent agent's tool calls, with streaming preliminary results."
5
+ ---
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.
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.
10
+
11
+ ## How it works
12
+
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
19
+
20
+ ```
21
+ Parent LLM
22
+
23
+ ├─ calls researchAgent tool
24
+ │ │
25
+ │ ├─ 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
29
+
30
+ └─ parent LLM reads compressed summary, continues reasoning
31
+ ```
32
+
33
+ ## Single-turn sub-agent
34
+
35
+ The simplest pattern: one tool call, one sub-agent turn, conversation closes.
36
+
37
+ ```ts
38
+ import { tool, stepCountIs } from "ai";
39
+ import { AgentChat } from "@trigger.dev/sdk/chat";
40
+ import { z } from "zod";
41
+ import type { prReviewAgent } from "./trigger/pr-review";
42
+
43
+ const prReviewTool = tool({
44
+ description: "Delegate a PR review to the PR review agent.",
45
+ inputSchema: z.object({
46
+ prNumber: z.number().describe("The PR number to review"),
47
+ repo: z.string().describe("The GitHub repo URL"),
48
+ }),
49
+ execute: async function* ({ prNumber, repo }, { abortSignal }) {
50
+ const chat = new AgentChat<typeof prReviewAgent>({
51
+ agent: "pr-review",
52
+ id: `review-${prNumber}`,
53
+ clientData: { userId: "parent-agent", githubUrl: repo },
54
+ });
55
+
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();
62
+ },
63
+ // The parent LLM only sees this compressed summary
64
+ toModelOutput: ({ output: message }) => {
65
+ const lastText = message?.parts?.findLast(
66
+ (p: { type: string }) => p.type === "text"
67
+ ) as { text?: string } | undefined;
68
+ return { type: "text", value: lastText?.text ?? "Review complete." };
69
+ },
70
+ });
71
+ ```
72
+
73
+ Use this tool in a parent agent's `streamText` call:
74
+
75
+ ```ts
76
+ import { streamText } from "ai";
77
+ import { anthropic } from "@ai-sdk/anthropic";
78
+
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
+ });
85
+ ```
86
+
87
+ ## Multi-turn sub-agent (LLM-driven)
88
+
89
+ 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
+
91
+ ```ts
92
+ import { tool } from "ai";
93
+ import { AgentChat } from "@trigger.dev/sdk/chat";
94
+ import { z } from "zod";
95
+
96
+ // Track active sub-agent conversations
97
+ const subAgents = new Map<string, AgentChat>();
98
+
99
+ const researchTool = tool({
100
+ description:
101
+ "Talk to a research agent. Use the same conversationId to continue " +
102
+ "an existing conversation — the agent remembers full context.",
103
+ inputSchema: z.object({
104
+ conversationId: z
105
+ .string()
106
+ .describe("Unique ID for this research thread. Reuse to continue."),
107
+ message: z.string().describe("Your message to the research agent"),
108
+ }),
109
+ execute: async function* ({ conversationId, message }, { abortSignal }) {
110
+ let agent = subAgents.get(conversationId);
111
+ if (!agent) {
112
+ agent = new AgentChat({
113
+ agent: "research-agent",
114
+ id: conversationId,
115
+ });
116
+ subAgents.set(conversationId, agent);
117
+ }
118
+
119
+ const stream = await agent.sendMessage(message, { abortSignal });
120
+ yield* stream.messages();
121
+ },
122
+ toModelOutput: ({ output: message }) => {
123
+ const lastText = message?.parts?.findLast(
124
+ (p: { type: string }) => p.type === "text"
125
+ ) as { text?: string } | undefined;
126
+ return { type: "text", value: lastText?.text ?? "Done." };
127
+ },
128
+ });
129
+ ```
130
+
131
+ The parent LLM naturally calls this tool multiple times:
132
+
133
+ 1. `researchAgent({ conversationId: "competitors", message: "Research competitors in AI agents" })` — first call triggers a new sub-agent run
134
+ 2. `researchAgent({ conversationId: "competitors", message: "Go deeper on pricing" })` — same run, sub-agent has full context
135
+ 3. `researchAgent({ conversationId: "new-topic", message: "..." })` — different ID = different sub-agent
136
+
137
+ ### Cross-turn persistence
138
+
139
+ Sub-agent conversations persist across **parent turns** because the `Map` lives in the parent's process heap. When the parent suspends and restores via snapshot, the heap is preserved — the Map still has the conversations, the sessions still have the run IDs.
140
+
141
+ ```ts
142
+ export const orchestrator = chat
143
+ .withClientData({ schema: z.object({ userId: z.string() }) })
144
+ .customAgent({
145
+ id: "orchestrator",
146
+ run: async (payload, { signal: runSignal }) => {
147
+ // These survive across parent turns via snapshot/restore
148
+ const subAgents = new Map<string, AgentChat>();
149
+
150
+ const researchTool = tool({
151
+ // ... closes over subAgents Map
152
+ });
153
+
154
+ // Turn loop — subAgents persist across all turns
155
+ for (let turn = 0; turn < 50; turn++) {
156
+ // ... streamText with researchTool
157
+ }
158
+
159
+ // Cleanup when parent exits
160
+ await Promise.all(
161
+ Array.from(subAgents.values()).map((a) => a.close().catch(() => {}))
162
+ );
163
+ },
164
+ });
165
+ ```
166
+
167
+ ## How sub-agents clean up
168
+
169
+ Sub-agents clean up through three mechanisms:
170
+
171
+ 1. **Explicit close**: Call `chat.close()` or `agent.close()` when done
172
+ 2. **Idle timeout**: The sub-agent's idle timeout expires, it suspends
173
+ 3. **Suspend timeout**: The sub-agent's suspend timeout expires, the run ends
174
+
175
+ For the multi-turn pattern, the parent should clean up sub-agents when it exits (in `onComplete` for managed agents, or at the end of the loop for custom agents). Without explicit cleanup, sub-agents close on their own via timeouts — no leaked resources or cost while suspended.
176
+
177
+ ## What the frontend sees
178
+
179
+ 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`.
180
+
181
+ The frontend renders the tool part with:
182
+ - `state: "output-available"` and `preliminary: true` while streaming
183
+ - `state: "output-available"` and `preliminary: false` (or absent) when done
184
+
185
+ The tool output contains the full `UIMessage` with nested parts — text, the sub-agent's own tool calls and results, reasoning, etc.
186
+
187
+ ### Controlling what the parent LLM sees
188
+
189
+ `toModelOutput` transforms the tool's output before it enters the parent LLM's context. The full UIMessage streams to the frontend, but the model only sees the compressed version:
190
+
191
+ ```ts
192
+ toModelOutput: ({ output: message }) => {
193
+ // Extract just the final text — the model doesn't need
194
+ // to see all the sub-agent's tool calls and intermediate work
195
+ const lastText = message?.parts?.findLast(
196
+ (p: { type: string }) => p.type === "text"
197
+ ) as { text?: string } | undefined;
198
+ return { type: "text", value: lastText?.text ?? "Done." };
199
+ },
200
+ ```
201
+
202
+ This is important for token efficiency: the sub-agent might use 100K tokens exploring and reasoning, but the parent LLM only consumes the summary.
203
+
204
+ <Warning>
205
+ `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).
206
+ </Warning>
207
+
208
+ ## ChatStream.messages()
209
+
210
+ 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.
211
+
212
+ ```ts
213
+ const stream = await chat.sendMessage("Research this topic");
214
+
215
+ // Each yield is a complete UIMessage with all accumulated parts
216
+ for await (const message of stream.messages()) {
217
+ console.log(message.parts.length, "parts so far");
218
+ }
219
+ ```
220
+
221
+ For the sub-agent pattern, use `yield*` to delegate all yields to the parent tool's generator:
222
+
223
+ ```ts
224
+ execute: async function* ({ topic }, { abortSignal }) {
225
+ const stream = await chat.sendMessage(topic, { abortSignal });
226
+ yield* stream.messages();
227
+ },
228
+ ```
229
+
230
+ <Tip>
231
+ `stream.messages()` consumes the stream. You can't also call `stream.text()` or iterate over chunks on the same stream. Pick one consumption mode.
232
+ </Tip>
233
+
234
+ ## Combining with chat.agent()
235
+
236
+ Sub-agent tools work inside both `chat.agent()` (managed) and `chat.customAgent()` (manual lifecycle):
237
+
238
+ ```ts
239
+ // Managed agent with sub-agent tool
240
+ const tools = { research: researchTool };
241
+
242
+ export const myAgent = chat.agent({
243
+ id: "orchestrator",
244
+ tools, // declare here so toModelOutput survives across turns
245
+ run: async ({ messages, tools, stopSignal }) => {
246
+ return streamText({
247
+ model: anthropic("claude-sonnet-4-6"),
248
+ messages,
249
+ tools,
250
+ abortSignal: stopSignal,
251
+ stopWhen: stepCountIs(15),
252
+ });
253
+ },
254
+ });
255
+ ```
256
+
257
+ For `chat.customAgent()`, define the tool and sub-agent Map inside the `run` closure so they survive across turns. Since you own the turn loop there, convert history with your tools in scope so `toModelOutput` is re-applied each turn: `convertToModelMessages(uiMessages, { tools })`. See [Tools: manual turn loops](/ai-chat/tools#manual-turn-loops-chatcustomagent).
258
+
259
+ ## Streaming progress from a subtask to the parent chat
260
+
261
+ When a tool invokes a subtask via `triggerAndWait`, the subtask can stream custom data parts directly to the parent chat using `chat.stream.writer({ target: "root" })`. The frontend receives these as `DataUIPart` objects in `message.parts` on the **parent's** message stream:
262
+
263
+ ```ts
264
+ import { chat, ai } from "@trigger.dev/sdk/ai";
265
+ import { schemaTask } from "@trigger.dev/sdk";
266
+ import { streamText, tool, generateId } from "ai";
267
+ import { anthropic } from "@ai-sdk/anthropic";
268
+ import { z } from "zod";
269
+
270
+ export const researchTask = schemaTask({
271
+ id: "research",
272
+ schema: z.object({ query: z.string() }),
273
+ run: async ({ query }) => {
274
+ const partId = generateId();
275
+
276
+ // Stream a data-* chunk to the root run's chat stream.
277
+ const { waitUntilComplete } = chat.stream.writer({
278
+ target: "root",
279
+ execute: ({ write }) => {
280
+ write({
281
+ type: "data-research-status",
282
+ id: partId,
283
+ data: { query, status: "in-progress" },
284
+ });
285
+ },
286
+ });
287
+ await waitUntilComplete();
288
+
289
+ const result = await doResearch(query);
290
+
291
+ // Update the same part with the final status — same type + id replaces it.
292
+ const { waitUntilComplete: waitDone } = chat.stream.writer({
293
+ target: "root",
294
+ execute: ({ write }) => {
295
+ write({
296
+ type: "data-research-status",
297
+ id: partId,
298
+ data: { query, status: "done", resultCount: result.length },
299
+ });
300
+ },
301
+ });
302
+ await waitDone();
303
+
304
+ return result;
305
+ },
306
+ });
307
+
308
+ const research = tool({
309
+ description: researchTask.description ?? "",
310
+ inputSchema: researchTask.schema!,
311
+ execute: ai.toolExecute(researchTask),
312
+ });
313
+ ```
314
+
315
+ On the frontend, render the custom data part:
316
+
317
+ ```tsx
318
+ {message.parts.map((part, i) => {
319
+ if (part.type === "data-research-status") {
320
+ const { query, status, resultCount } = part.data;
321
+ return (
322
+ <div key={i}>
323
+ {status === "done" ? `Found ${resultCount} results` : `Researching "${query}"...`}
324
+ </div>
325
+ );
326
+ }
327
+ // ...other part types
328
+ })}
329
+ ```
330
+
331
+ The `target` option accepts:
332
+ - `"self"` — current run (default)
333
+ - `"parent"` — parent task's run
334
+ - `"root"` — root task's run (the chat agent)
335
+ - A specific run ID string
336
+
337
+ ## Inside `ai.toolExecute`: accessing tool + chat context
338
+
339
+ When a subtask runs via `execute: ai.toolExecute(task)`, it can read the parent's tool call ID and chat context from inside the subtask body:
340
+
341
+ ```ts
342
+ import { ai, chat } from "@trigger.dev/sdk/ai";
343
+ import type { myChat } from "./chat";
344
+
345
+ export const mySubtask = schemaTask({
346
+ id: "my-subtask",
347
+ schema: z.object({ query: z.string() }),
348
+ run: async ({ query }) => {
349
+ // The AI SDK tool call ID — useful as a stable `data-*` chunk id
350
+ const toolCallId = ai.toolCallId();
351
+
352
+ // Typed chat context — `clientData` is typed off your chat's `clientDataSchema`
353
+ const { chatId, clientData } = ai.chatContextOrThrow<typeof myChat>();
354
+
355
+ const { waitUntilComplete } = chat.stream.writer({
356
+ target: "root",
357
+ execute: ({ write }) => {
358
+ write({
359
+ type: "data-progress",
360
+ id: toolCallId,
361
+ data: { status: "working", query, userId: clientData?.userId },
362
+ });
363
+ },
364
+ });
365
+ await waitUntilComplete();
366
+
367
+ return { result: "done" };
368
+ },
369
+ });
370
+ ```
371
+
372
+ | Helper | Returns | Description |
373
+ |--------|---------|-------------|
374
+ | `ai.toolCallId()` | `string \| undefined` | The AI SDK tool call ID |
375
+ | `ai.chatContext<typeof myChat>()` | `{ chatId, turn, continuation, clientData } \| undefined` | Chat context with typed `clientData`. Returns `undefined` if not in a chat context. |
376
+ | `ai.chatContextOrThrow<typeof myChat>()` | `{ chatId, turn, continuation, clientData }` | Same as above but throws if not in a chat context |
377
+ | `ai.currentToolOptions()` | `ToolCallExecutionOptions \| undefined` | Full tool execution options |
378
+
379
+ The subtask body also has read-only access to any [`chat.local`](/ai-chat/chat-local) values initialized in the parent — auto-hydrated from the parent's metadata on first access.
@@ -0,0 +1,144 @@
1
+ ---
2
+ title: "Tool result auditing"
3
+ sidebarTitle: "Tool result auditing"
4
+ description: "Fire side effects exactly once per resolved tool call — audit logs, billing, notifications — using extractNewToolResults inside hydrateMessages or onTurnComplete."
5
+ ---
6
+
7
+ When a chat agent uses [tools](/ai-chat/tools) (especially [human-in-the-loop](/ai-chat/patterns/human-in-the-loop) tools that wait on `addToolOutput` from the frontend), you often need to fire side effects exactly once per resolved tool call:
8
+
9
+ - **Audit logs** — record every tool result for compliance.
10
+ - **Billing** — charge per tool invocation.
11
+ - **Notifications** — alert downstream systems when a specific tool resolves.
12
+ - **Search-index updates** — reflect tool outputs into a derived store.
13
+
14
+ The naive approach — "log every tool part you see" — over-counts. The same assistant message gets re-shown across re-renders, replays, and retries. You want a function of the form **"is this tool result one I haven't already logged?"** That's exactly what [`chat.history.extractNewToolResults`](/ai-chat/backend#chat-history) returns.
15
+
16
+ ## The pattern
17
+
18
+ ```ts
19
+ import { chat } from "@trigger.dev/sdk/ai";
20
+ import { auditLog } from "@/lib/audit";
21
+
22
+ export const myChat = chat.agent({
23
+ id: "my-chat",
24
+ hydrateMessages: async ({ chatId, incomingMessages }) => {
25
+ for (const msg of incomingMessages) {
26
+ for (const r of chat.history.extractNewToolResults(msg)) {
27
+ await auditLog.record({
28
+ chatId,
29
+ toolCallId: r.toolCallId,
30
+ toolName: r.toolName,
31
+ output: r.output,
32
+ errorText: r.errorText,
33
+ });
34
+ }
35
+ }
36
+ return await db.getMessages(chatId);
37
+ },
38
+ run: async ({ messages, signal }) => {
39
+ return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
40
+ },
41
+ });
42
+ ```
43
+
44
+ The hook fires per turn. `incomingMessages` is the new wire message (0-or-1-length, see [v4.5 wire format change](/ai-chat/upgrade-guide#v45-wire-format-change)). For each new tool result on that message, write one audit row. Then return the canonical chain from your DB.
45
+
46
+ `extractNewToolResults` compares the message against the current `chat.history` chain and returns only tool parts whose `toolCallId` is **not** already resolved. That's what makes the call exactly-once:
47
+
48
+ - A re-emitted message (same id, same toolCallId) returns `[]` — no duplicate log.
49
+ - A genuinely new tool result on a known assistant message returns just the new ones.
50
+ - A first-time tool result returns the full set.
51
+
52
+ ## Why `hydrateMessages` is the right hook
53
+
54
+ The pattern works in any pre-merge callback, but `hydrateMessages` is the canonical spot for two reasons:
55
+
56
+ 1. **It fires before the runtime merges** the incoming message into the accumulator. Once merged, the tool results are already on the chain, and `extractNewToolResults` returns `[]` for them.
57
+ 2. **It always fires per turn** — including HITL turns where the user resolved a tool with `addToolOutput`, which is the highest-volume audit event in most apps.
58
+
59
+ By the time `onTurnComplete` fires, the chain already contains `responseMessage`, so calling `extractNewToolResults(responseMessage)` there returns `[]`. Don't put audit logging there for the resolution path.
60
+
61
+ ## Without `hydrateMessages` — `onTurnComplete` for self-emitted tool calls
62
+
63
+ If you don't use `hydrateMessages`, the runtime's snapshot+replay path handles persistence. You can still audit the agent's **own** tool executions in `onTurnComplete` — but compare against the prior message rather than the just-emitted one:
64
+
65
+ ```ts
66
+ onTurnComplete: async ({ chatId, newUIMessages }) => {
67
+ // The assistant message from this turn is in newUIMessages.
68
+ for (const msg of newUIMessages) {
69
+ if (msg.role !== "assistant") continue;
70
+ for (const part of msg.parts) {
71
+ if (
72
+ typeof part.type === "string" &&
73
+ part.type.startsWith("tool-") &&
74
+ ((part as any).state === "output-available" ||
75
+ (part as any).state === "output-error")
76
+ ) {
77
+ await auditLog.record({
78
+ chatId,
79
+ toolCallId: (part as any).toolCallId,
80
+ toolName: (part as any).type.slice("tool-".length),
81
+ output: (part as any).output,
82
+ errorText: (part as any).errorText,
83
+ });
84
+ }
85
+ }
86
+ }
87
+ },
88
+ ```
89
+
90
+ `newUIMessages` is just the messages this turn produced — no prior-chain noise. Each tool part shows up exactly once.
91
+
92
+ This works for tools the agent itself calls (no HITL pause). For HITL flows where the user resolves a tool with `addToolOutput`, the resolution arrives on the **next** turn's wire message, not in `newUIMessages` of the resolving turn — use `hydrateMessages` for those.
93
+
94
+ ## Idempotency at the storage layer
95
+
96
+ Even with `extractNewToolResults`, transient failures (e.g. an audit-log POST that times out and is retried) can produce duplicates. Make the audit-log writer idempotent on `toolCallId`:
97
+
98
+ ```ts
99
+ await auditLog.upsert({
100
+ where: { toolCallId: r.toolCallId },
101
+ create: { /* ... */ },
102
+ update: { /* timestamp, retry count, etc. */ },
103
+ });
104
+ ```
105
+
106
+ `toolCallId` is unique per tool invocation (assigned by the AI SDK when the model emits the tool call) and stable across retries — perfect for an idempotency key.
107
+
108
+ ## What `extractNewToolResults` returns
109
+
110
+ ```ts
111
+ type ChatNewToolResult = {
112
+ toolCallId: string;
113
+ toolName: string;
114
+ output: unknown; // The tool's return value (carries the resolved value; in output-error state see errorText)
115
+ errorText?: string; // Set iff the part is in output-error state
116
+ };
117
+ ```
118
+
119
+ Tool parts in `input-available` state (the model called the tool but it hasn't resolved yet) are not returned — only **resolved** results count.
120
+
121
+ ## Combining with HITL
122
+
123
+ [Human-in-the-loop](/ai-chat/patterns/human-in-the-loop) tools pause the turn waiting for `addToolOutput` from the frontend. When the user submits, the wire message carries an updated assistant message with the tool now in `output-available` state. `extractNewToolResults` against that message returns the just-resolved tool — exactly one audit row per user resolution:
124
+
125
+ ```ts
126
+ hydrateMessages: async ({ chatId, incomingMessages }) => {
127
+ for (const msg of incomingMessages) {
128
+ for (const r of chat.history.extractNewToolResults(msg)) {
129
+ // Fires once per ask_user / approval / similar resolution
130
+ await auditLog.record({ chatId, /* ... */ });
131
+ }
132
+ }
133
+ return await db.getMessages(chatId);
134
+ }
135
+ ```
136
+
137
+ This is the original motivator for the helper — see the [HITL pattern's net-new-tool-result section](/ai-chat/patterns/human-in-the-loop#acting-once-per-net-new-tool-result).
138
+
139
+ ## See also
140
+
141
+ - [`chat.history`](/ai-chat/backend#chat-history) — full reference for `extractNewToolResults`, `getPendingToolCalls`, `getResolvedToolCalls`
142
+ - [Human-in-the-loop](/ai-chat/patterns/human-in-the-loop) — the pattern this auditing hook complements
143
+ - [`hydrateMessages`](/ai-chat/lifecycle-hooks#hydratemessages) — where pre-merge auditing lives
144
+ - [Persistence and replay](/ai-chat/patterns/persistence-and-replay) — how the runtime rebuilds chains, and why `extractNewToolResults` works against them