@trigger.dev/sdk 4.5.0-rc.6 → 4.5.0-rc.7

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 (191) hide show
  1. package/dist/commonjs/v3/ai.d.ts +171 -5
  2. package/dist/commonjs/v3/ai.js +309 -22
  3. package/dist/commonjs/v3/ai.js.map +1 -1
  4. package/dist/commonjs/v3/chat-server.d.ts +8 -0
  5. package/dist/commonjs/v3/chat-server.js +32 -10
  6. package/dist/commonjs/v3/chat-server.js.map +1 -1
  7. package/dist/commonjs/v3/chat-server.test.js +51 -0
  8. package/dist/commonjs/v3/chat-server.test.js.map +1 -1
  9. package/dist/commonjs/v3/createStartSessionAction.test.js +30 -0
  10. package/dist/commonjs/v3/createStartSessionAction.test.js.map +1 -1
  11. package/dist/commonjs/v3/sessions.d.ts +3 -2
  12. package/dist/commonjs/v3/sessions.js +3 -2
  13. package/dist/commonjs/v3/sessions.js.map +1 -1
  14. package/dist/commonjs/version.js +1 -1
  15. package/dist/esm/v3/ai.d.ts +171 -5
  16. package/dist/esm/v3/ai.js +309 -22
  17. package/dist/esm/v3/ai.js.map +1 -1
  18. package/dist/esm/v3/chat-server.d.ts +8 -0
  19. package/dist/esm/v3/chat-server.js +32 -10
  20. package/dist/esm/v3/chat-server.js.map +1 -1
  21. package/dist/esm/v3/chat-server.test.js +51 -0
  22. package/dist/esm/v3/chat-server.test.js.map +1 -1
  23. package/dist/esm/v3/createStartSessionAction.test.js +30 -0
  24. package/dist/esm/v3/createStartSessionAction.test.js.map +1 -1
  25. package/dist/esm/v3/sessions.d.ts +3 -2
  26. package/dist/esm/v3/sessions.js +3 -2
  27. package/dist/esm/v3/sessions.js.map +1 -1
  28. package/dist/esm/version.js +1 -1
  29. package/docs/ai/prompts.mdx +430 -0
  30. package/docs/ai-chat/actions.mdx +115 -0
  31. package/docs/ai-chat/anatomy.mdx +71 -0
  32. package/docs/ai-chat/backend.mdx +817 -0
  33. package/docs/ai-chat/background-injection.mdx +221 -0
  34. package/docs/ai-chat/changelog.mdx +850 -0
  35. package/docs/ai-chat/chat-local.mdx +174 -0
  36. package/docs/ai-chat/client-protocol.mdx +1081 -0
  37. package/docs/ai-chat/compaction.mdx +411 -0
  38. package/docs/ai-chat/custom-agents.mdx +364 -0
  39. package/docs/ai-chat/error-handling.mdx +415 -0
  40. package/docs/ai-chat/fast-starts.mdx +672 -0
  41. package/docs/ai-chat/frontend.mdx +580 -0
  42. package/docs/ai-chat/how-it-works.mdx +230 -0
  43. package/docs/ai-chat/lifecycle-hooks.mdx +530 -0
  44. package/docs/ai-chat/mcp.mdx +101 -0
  45. package/docs/ai-chat/overview.mdx +90 -0
  46. package/docs/ai-chat/patterns/branching-conversations.mdx +284 -0
  47. package/docs/ai-chat/patterns/code-sandbox.mdx +126 -0
  48. package/docs/ai-chat/patterns/database-persistence.mdx +414 -0
  49. package/docs/ai-chat/patterns/human-in-the-loop.mdx +275 -0
  50. package/docs/ai-chat/patterns/large-payloads.mdx +169 -0
  51. package/docs/ai-chat/patterns/oom-resilience.mdx +120 -0
  52. package/docs/ai-chat/patterns/persistence-and-replay.mdx +211 -0
  53. package/docs/ai-chat/patterns/recovery-boot.mdx +230 -0
  54. package/docs/ai-chat/patterns/skills.mdx +221 -0
  55. package/docs/ai-chat/patterns/sub-agents.mdx +383 -0
  56. package/docs/ai-chat/patterns/tool-result-auditing.mdx +148 -0
  57. package/docs/ai-chat/patterns/trusted-edge-signals.mdx +337 -0
  58. package/docs/ai-chat/patterns/version-upgrades.mdx +172 -0
  59. package/docs/ai-chat/pending-messages.mdx +343 -0
  60. package/docs/ai-chat/prompt-caching.mdx +206 -0
  61. package/docs/ai-chat/quick-start.mdx +161 -0
  62. package/docs/ai-chat/reference.mdx +909 -0
  63. package/docs/ai-chat/server-chat.mdx +263 -0
  64. package/docs/ai-chat/sessions.mdx +333 -0
  65. package/docs/ai-chat/testing.mdx +682 -0
  66. package/docs/ai-chat/tools.mdx +191 -0
  67. package/docs/ai-chat/types.mdx +242 -0
  68. package/docs/ai-chat/upgrade-guide.mdx +515 -0
  69. package/docs/apikeys.mdx +54 -0
  70. package/docs/building-with-ai.mdx +261 -0
  71. package/docs/bulk-actions.mdx +49 -0
  72. package/docs/changelog.mdx +6 -0
  73. package/docs/cli-deploy-commands.mdx +9 -0
  74. package/docs/cli-dev-commands.mdx +9 -0
  75. package/docs/cli-dev.mdx +8 -0
  76. package/docs/cli-init-commands.mdx +58 -0
  77. package/docs/cli-introduction.mdx +25 -0
  78. package/docs/cli-list-profiles-commands.mdx +42 -0
  79. package/docs/cli-login-commands.mdx +33 -0
  80. package/docs/cli-logout-commands.mdx +33 -0
  81. package/docs/cli-preview-archive.mdx +59 -0
  82. package/docs/cli-promote-commands.mdx +9 -0
  83. package/docs/cli-switch.mdx +43 -0
  84. package/docs/cli-update-commands.mdx +42 -0
  85. package/docs/cli-whoami-commands.mdx +33 -0
  86. package/docs/community.mdx +6 -0
  87. package/docs/config/config-file.mdx +602 -0
  88. package/docs/config/extensions/additionalFiles.mdx +38 -0
  89. package/docs/config/extensions/additionalPackages.mdx +40 -0
  90. package/docs/config/extensions/aptGet.mdx +34 -0
  91. package/docs/config/extensions/audioWaveform.mdx +20 -0
  92. package/docs/config/extensions/custom.mdx +380 -0
  93. package/docs/config/extensions/emitDecoratorMetadata.mdx +29 -0
  94. package/docs/config/extensions/esbuildPlugin.mdx +31 -0
  95. package/docs/config/extensions/ffmpeg.mdx +45 -0
  96. package/docs/config/extensions/lightpanda.mdx +56 -0
  97. package/docs/config/extensions/overview.mdx +67 -0
  98. package/docs/config/extensions/playwright.mdx +195 -0
  99. package/docs/config/extensions/prismaExtension.mdx +1014 -0
  100. package/docs/config/extensions/puppeteer.mdx +30 -0
  101. package/docs/config/extensions/pythonExtension.mdx +182 -0
  102. package/docs/config/extensions/syncEnvVars.mdx +291 -0
  103. package/docs/context.mdx +235 -0
  104. package/docs/database-connections.mdx +213 -0
  105. package/docs/deploy-environment-variables.mdx +435 -0
  106. package/docs/deployment/atomic-deployment.mdx +172 -0
  107. package/docs/deployment/overview.mdx +257 -0
  108. package/docs/deployment/preview-branches.mdx +224 -0
  109. package/docs/errors-retrying.mdx +379 -0
  110. package/docs/github-actions.mdx +222 -0
  111. package/docs/github-integration.mdx +136 -0
  112. package/docs/github-repo.mdx +8 -0
  113. package/docs/help-email.mdx +6 -0
  114. package/docs/help-slack.mdx +11 -0
  115. package/docs/hidden-tasks.mdx +56 -0
  116. package/docs/how-it-works.mdx +454 -0
  117. package/docs/how-to-reduce-your-spend.mdx +217 -0
  118. package/docs/idempotency.mdx +504 -0
  119. package/docs/introduction.mdx +223 -0
  120. package/docs/limits.mdx +241 -0
  121. package/docs/logging.mdx +195 -0
  122. package/docs/machines.mdx +952 -0
  123. package/docs/manual-setup.mdx +632 -0
  124. package/docs/mcp-agent-rules.mdx +41 -0
  125. package/docs/mcp-introduction.mdx +385 -0
  126. package/docs/mcp-tools.mdx +273 -0
  127. package/docs/migrating-from-v3.mdx +334 -0
  128. package/docs/observability/dashboards.mdx +102 -0
  129. package/docs/observability/query.mdx +585 -0
  130. package/docs/open-source-contributing.mdx +16 -0
  131. package/docs/open-source-self-hosting.mdx +541 -0
  132. package/docs/private-networking/aws-console-setup.mdx +304 -0
  133. package/docs/private-networking/overview.mdx +144 -0
  134. package/docs/private-networking/troubleshooting.mdx +78 -0
  135. package/docs/queue-concurrency.mdx +354 -0
  136. package/docs/quick-start.mdx +97 -0
  137. package/docs/realtime/auth.mdx +208 -0
  138. package/docs/realtime/backend/overview.mdx +45 -0
  139. package/docs/realtime/backend/streams.mdx +418 -0
  140. package/docs/realtime/backend/subscribe.mdx +225 -0
  141. package/docs/realtime/how-it-works.mdx +94 -0
  142. package/docs/realtime/overview.mdx +63 -0
  143. package/docs/realtime/react-hooks/overview.mdx +73 -0
  144. package/docs/realtime/react-hooks/streams.mdx +449 -0
  145. package/docs/realtime/react-hooks/subscribe.mdx +674 -0
  146. package/docs/realtime/react-hooks/swr.mdx +87 -0
  147. package/docs/realtime/react-hooks/triggering.mdx +194 -0
  148. package/docs/realtime/react-hooks/use-wait-token.mdx +34 -0
  149. package/docs/realtime/run-object.mdx +174 -0
  150. package/docs/replaying.mdx +72 -0
  151. package/docs/request-feature.mdx +6 -0
  152. package/docs/roadmap.mdx +6 -0
  153. package/docs/run-tests.mdx +20 -0
  154. package/docs/run-usage.mdx +113 -0
  155. package/docs/runs/heartbeats.mdx +38 -0
  156. package/docs/runs/max-duration.mdx +139 -0
  157. package/docs/runs/metadata.mdx +734 -0
  158. package/docs/runs/priority.mdx +31 -0
  159. package/docs/runs.mdx +396 -0
  160. package/docs/self-hosting/docker.mdx +458 -0
  161. package/docs/self-hosting/env/supervisor.mdx +74 -0
  162. package/docs/self-hosting/env/webapp.mdx +276 -0
  163. package/docs/self-hosting/kubernetes.mdx +601 -0
  164. package/docs/self-hosting/overview.mdx +108 -0
  165. package/docs/skills.mdx +85 -0
  166. package/docs/tags.mdx +120 -0
  167. package/docs/tasks/overview.mdx +697 -0
  168. package/docs/tasks/scheduled.mdx +382 -0
  169. package/docs/tasks/schemaTask.mdx +413 -0
  170. package/docs/tasks/streams.mdx +884 -0
  171. package/docs/triggering.mdx +1320 -0
  172. package/docs/troubleshooting-alerts.mdx +385 -0
  173. package/docs/troubleshooting-debugging-in-vscode.mdx +8 -0
  174. package/docs/troubleshooting-github-issues.mdx +6 -0
  175. package/docs/troubleshooting-uptime-status.mdx +6 -0
  176. package/docs/troubleshooting.mdx +398 -0
  177. package/docs/upgrading-packages.mdx +80 -0
  178. package/docs/vercel-integration.mdx +207 -0
  179. package/docs/versioning.mdx +56 -0
  180. package/docs/video-walkthrough.mdx +23 -0
  181. package/docs/wait-for-token.mdx +540 -0
  182. package/docs/wait-for.mdx +42 -0
  183. package/docs/wait-until.mdx +53 -0
  184. package/docs/wait.mdx +18 -0
  185. package/docs/writing-tasks-introduction.mdx +33 -0
  186. package/package.json +8 -5
  187. package/skills/trigger-authoring-chat-agent/SKILL.md +296 -0
  188. package/skills/trigger-authoring-tasks/SKILL.md +254 -0
  189. package/skills/trigger-chat-agent-advanced/SKILL.md +368 -0
  190. package/skills/trigger-cost-savings/SKILL.md +116 -0
  191. package/skills/trigger-realtime-and-frontend/SKILL.md +276 -0
@@ -0,0 +1,90 @@
1
+ ---
2
+ title: "AI Agents"
3
+ sidebarTitle: "Overview"
4
+ description: "Durable multi-turn AI chats — one Trigger.dev task per conversation, surviving refreshes, deploys, and crashes."
5
+ ---
6
+
7
+ import RcBanner from "/snippets/ai-chat-rc-banner.mdx";
8
+
9
+ <RcBanner />
10
+
11
+ An AI chat isn't a request — it's a session. `chat.agent` runs every conversation as a single long-lived Trigger.dev task: you write the loop, it wakes up when a message arrives, freezes when none do, and the same in-memory state and on-disk workspace survive across page refreshes, deploys, idle gaps, and crashes. The substrate handles the parts most teams stitch together by hand — turn lifecycle, mid-stream resume, recovery from cancel/crash/OOM, HITL approvals, deploy upgrades — so your code is the loop you'd write anyway: messages in, `streamText` out.
12
+
13
+ ## A minimal example
14
+
15
+ A `chat.agent` task takes `messages`, calls `streamText`, and returns the result. The frontend wires the [Vercel AI SDK's `useChat`](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat) to a `TriggerChatTransport`. No API routes.
16
+
17
+ ```ts trigger/chat.ts
18
+ import { chat } from "@trigger.dev/sdk/ai";
19
+ import { streamText, stepCountIs } from "ai";
20
+ import { anthropic } from "@ai-sdk/anthropic";
21
+
22
+ export const myChat = chat.agent({
23
+ id: "my-chat",
24
+ run: async ({ messages, signal }) =>
25
+ streamText({
26
+ model: anthropic("claude-sonnet-4-5"),
27
+ messages,
28
+ abortSignal: signal,
29
+ stopWhen: stepCountIs(15),
30
+ }),
31
+ });
32
+ ```
33
+
34
+ ```tsx app/components/Chat.tsx
35
+ import { useChat } from "@ai-sdk/react";
36
+ import { useTriggerChatTransport } from "@trigger.dev/sdk/chat/react";
37
+
38
+ export function Chat() {
39
+ const transport = useTriggerChatTransport<typeof myChat>({
40
+ task: "my-chat",
41
+ accessToken: ({ chatId }) => mintChatAccessToken(chatId),
42
+ startSession: ({ chatId, clientData }) =>
43
+ startChatSession({ chatId, clientData }),
44
+ });
45
+ const { messages, sendMessage } = useChat({ transport });
46
+ // ... render UI
47
+ }
48
+ ```
49
+
50
+ See [Quick Start](/ai-chat/quick-start) for the matching server actions and a runnable project.
51
+
52
+ ## Why use AI Agents on Trigger.dev
53
+
54
+ - **Resume across refreshes, deploys, and crashes.** A chat in progress when you redeploy keeps streaming on the new version. Mid-stream refreshes pick up where they left off.
55
+ - **Native AI SDK support.** Text, tool calls, reasoning, and custom `data-*` parts all flow through `useChat` over a custom `ChatTransport`. No custom protocol to maintain.
56
+ - **Multi-turn for free.** Each turn is a step inside the same durable task; conversation history accumulates server-side, so clients only ship the new message.
57
+ - **Fast cold starts.** Opt-in [Head Start](/ai-chat/fast-starts#head-start) runs the first `streamText` step in your warm Next.js / Hono / SvelteKit server while the agent boots in parallel — cuts time-to-first-chunk roughly in half.
58
+ - **Production primitives ship in the box.** Stop generation, steering, edits, branching, sub-agents, HITL tool approvals, version upgrades, recovery from cancel/crash/OOM — all first-class.
59
+ - **Observable.** Every turn is a span in the Trigger.dev dashboard. Sessions are queryable via `sessions.list` for inbox-style UIs.
60
+
61
+ ## How it fits together
62
+
63
+ Three primitives, related but distinct:
64
+
65
+ - **Chat agents** — the SDK surface you define with [`chat.agent()`](/ai-chat/backend#chat-agent). Owns the turn loop, lifecycle hooks, and the response stream.
66
+ - **Sessions** — the durable, bi-directional channel keyed on `chatId` that holds the conversation across run boundaries. A chat agent runs *on top of* a [Session](/ai-chat/sessions).
67
+ - **Sub-agents** — Delegate work from one agent to another via [`AgentChat`](/ai-chat/patterns/sub-agents). The sub-agent runs as its own durable agent on its own session; its response streams back through the parent as preliminary tool results, so the frontend sees the sub-agent working inside the parent's tool card.
68
+
69
+ ## Next steps
70
+
71
+ <CardGroup cols={2}>
72
+ <Card title="Quick Start" icon="rocket" href="/ai-chat/quick-start">
73
+ Get a working chat in three steps — agent, token, frontend.
74
+ </Card>
75
+ <Card title="How it works" icon="diagram-project" href="/ai-chat/how-it-works">
76
+ Sessions, the turn loop, durable streams, and what survives a refresh.
77
+ </Card>
78
+ <Card title="Backend" icon="server" href="/ai-chat/backend">
79
+ `chat.agent` options, lifecycle hooks, and the raw-task primitives.
80
+ </Card>
81
+ <Card title="Tools" icon="wrench" href="/ai-chat/tools">
82
+ Declare tools so `toModelOutput` survives across turns, typed in `run()`.
83
+ </Card>
84
+ <Card title="Patterns" icon="puzzle-piece" href="/ai-chat/patterns/sub-agents">
85
+ HITL approvals, branching, sub-agents, OOM/crash recovery.
86
+ </Card>
87
+ <Card title="Database connections" icon="database" href="/database-connections">
88
+ Size and release connection pools so agents don't exhaust your database.
89
+ </Card>
90
+ </CardGroup>
@@ -0,0 +1,284 @@
1
+ ---
2
+ title: "Branching conversations"
3
+ sidebarTitle: "Branching conversations"
4
+ description: "Build ChatGPT-style conversation trees with edit, regenerate, undo, and branch switching using hydrateMessages, chat.history, and actions."
5
+ ---
6
+
7
+ import RcBanner from "/snippets/ai-chat-rc-banner.mdx";
8
+
9
+ <RcBanner />
10
+
11
+ Most chat UIs treat conversations as linear sequences. But real conversations branch — users edit previous messages, regenerate responses, undo exchanges, and explore alternative paths. This pattern shows how to build a branching conversation system using `hydrateMessages`, `chat.history`, and custom actions.
12
+
13
+ ## Data model
14
+
15
+ The standard approach (used by ChatGPT, Open WebUI, LibreChat, and others) stores messages as a tree with parent pointers:
16
+
17
+ ```ts
18
+ // Each message is a node in the tree
19
+ type ChatNode = {
20
+ id: string;
21
+ chatId: string;
22
+ parentId: string | null; // null for root
23
+ role: "user" | "assistant";
24
+ message: UIMessage; // the full AI SDK message
25
+ createdAt: Date;
26
+ };
27
+ ```
28
+
29
+ A conversation is a tree of nodes. The **active branch** is resolved by walking from a leaf node up through `parentId` pointers to the root, then reversing:
30
+
31
+ ```
32
+ root
33
+ ├── user: "Hello"
34
+ │ └── assistant: "Hi there!"
35
+ │ ├── user: "What's the weather?" ← branch A
36
+ │ │ └── assistant: "It's sunny!"
37
+ │ └── user: "Tell me a joke" ← branch B (active)
38
+ │ └── assistant: "Why did the..."
39
+ ```
40
+
41
+ Switching branches means changing which leaf is "active" — the same tree, different path.
42
+
43
+ ## Backend setup
44
+
45
+ ### Store: tree operations
46
+
47
+ Define helpers that read and write the node tree. Adapt to your database:
48
+
49
+ ```ts
50
+ // Resolve the active path: walk from leaf to root, reverse
51
+ async function getActiveBranch(chatId: string): Promise<UIMessage[]> {
52
+ const nodes = await db.chatNode.findMany({ where: { chatId } });
53
+ const byId = new Map(nodes.map((n) => [n.id, n]));
54
+
55
+ // Find active leaf (most recently created leaf node)
56
+ const childIds = new Set(nodes.map((n) => n.parentId).filter(Boolean));
57
+ const leaves = nodes.filter((n) => !childIds.has(n.id));
58
+ const activeLeaf = leaves.sort((a, b) => b.createdAt - a.createdAt)[0];
59
+ if (!activeLeaf) return [];
60
+
61
+ // Walk to root
62
+ const path: UIMessage[] = [];
63
+ let current: ChatNode | undefined = activeLeaf;
64
+ while (current) {
65
+ path.unshift(current.message);
66
+ current = current.parentId ? byId.get(current.parentId) : undefined;
67
+ }
68
+ return path;
69
+ }
70
+
71
+ // Append a message as a child of the current leaf
72
+ async function appendMessage(chatId: string, message: UIMessage): Promise<void> {
73
+ const branch = await getActiveBranch(chatId);
74
+ const parentId = branch.length > 0 ? branch[branch.length - 1]!.id : null;
75
+
76
+ await db.chatNode.create({
77
+ data: { id: message.id, chatId, parentId, role: message.role, message, createdAt: new Date() },
78
+ });
79
+ }
80
+ ```
81
+
82
+ ### Agent: hydration + actions
83
+
84
+ ```ts
85
+ import { chat } from "@trigger.dev/sdk/ai";
86
+ import { streamText, stepCountIs } from "ai";
87
+ import { anthropic } from "@ai-sdk/anthropic";
88
+ import { z } from "zod";
89
+
90
+ export const myChat = chat.agent({
91
+ id: "branching-chat",
92
+
93
+ // Load the active branch from the DB on every turn.
94
+ // The frontend's message array is ignored — the tree is the source of truth.
95
+ hydrateMessages: async ({ chatId, trigger, incomingMessages }) => {
96
+ if (trigger === "submit-message" && incomingMessages.length > 0) {
97
+ await appendMessage(chatId, incomingMessages[incomingMessages.length - 1]!);
98
+ }
99
+ return getActiveBranch(chatId);
100
+ },
101
+
102
+ actionSchema: z.discriminatedUnion("type", [
103
+ // Edit a previous user message — creates a sibling node in the tree
104
+ z.object({ type: z.literal("edit"), messageId: z.string(), text: z.string() }),
105
+ // Switch to a different branch by selecting a leaf node
106
+ z.object({ type: z.literal("switch-branch"), leafId: z.string() }),
107
+ // Undo the last user + assistant exchange
108
+ z.object({ type: z.literal("undo") }),
109
+ ]),
110
+
111
+ onAction: async ({ action, chatId }) => {
112
+ switch (action.type) {
113
+ case "edit": {
114
+ // Find the original message's parent, create a sibling with new content
115
+ const original = await db.chatNode.findUnique({ where: { id: action.messageId } });
116
+ if (!original) break;
117
+
118
+ const newId = generateId();
119
+ await db.chatNode.create({
120
+ data: {
121
+ id: newId,
122
+ chatId,
123
+ parentId: original.parentId, // same parent = sibling
124
+ role: "user",
125
+ message: { id: newId, role: "user", parts: [{ type: "text", text: action.text }] },
126
+ createdAt: new Date(),
127
+ },
128
+ });
129
+ // Active branch now resolves through the new sibling (most recent leaf)
130
+ break;
131
+ }
132
+
133
+ case "switch-branch": {
134
+ // Mark this leaf as the most recently accessed so getActiveBranch picks it
135
+ await db.chatNode.update({
136
+ where: { id: action.leafId },
137
+ data: { createdAt: new Date() },
138
+ });
139
+ break;
140
+ }
141
+
142
+ case "undo": {
143
+ // Remove the last two nodes (user + assistant) from the active branch
144
+ const branch = await getActiveBranch(chatId);
145
+ if (branch.length >= 2) {
146
+ const lastTwo = branch.slice(-2);
147
+ await db.chatNode.deleteMany({
148
+ where: { id: { in: lastTwo.map((m) => m.id) } },
149
+ });
150
+ }
151
+ break;
152
+ }
153
+ }
154
+
155
+ // Reload the (now modified) active branch into the accumulator
156
+ const updated = await getActiveBranch(chatId);
157
+ chat.history.set(updated);
158
+ },
159
+
160
+ onTurnComplete: async ({ chatId, responseMessage }) => {
161
+ // Persist the assistant's response as a new node
162
+ if (responseMessage) {
163
+ await appendMessage(chatId, responseMessage);
164
+ }
165
+ },
166
+
167
+ run: async ({ messages, signal }) => {
168
+ return streamText({
169
+ model: anthropic("claude-sonnet-4-5"),
170
+ messages,
171
+ abortSignal: signal,
172
+ stopWhen: stepCountIs(15),
173
+ });
174
+ },
175
+ });
176
+ ```
177
+
178
+ ## Frontend
179
+
180
+ ### Sending actions
181
+
182
+ Wire up edit, undo, and branch switching to the transport:
183
+
184
+ ```tsx
185
+ function MessageActions({ message, chatId }: { message: UIMessage; chatId: string }) {
186
+ const transport = useTransport();
187
+ const [editing, setEditing] = useState(false);
188
+ const [editText, setEditText] = useState("");
189
+
190
+ if (message.role !== "user") return null;
191
+
192
+ return (
193
+ <div>
194
+ {editing ? (
195
+ <form onSubmit={() => {
196
+ transport.sendAction(chatId, { type: "edit", messageId: message.id, text: editText });
197
+ setEditing(false);
198
+ }}>
199
+ <input value={editText} onChange={(e) => setEditText(e.target.value)} />
200
+ <button type="submit">Save</button>
201
+ </form>
202
+ ) : (
203
+ <button onClick={() => { setEditText(getMessageText(message)); setEditing(true); }}>
204
+ Edit
205
+ </button>
206
+ )}
207
+ </div>
208
+ );
209
+ }
210
+ ```
211
+
212
+ ### Branch navigation
213
+
214
+ To show the `< 2/3 >` sibling switcher, query the tree for siblings at each fork point. This is a frontend concern — the backend exposes the data, the UI navigates it.
215
+
216
+ ```tsx
217
+ function BranchSwitcher({ message, chatId, siblings }: {
218
+ message: UIMessage;
219
+ chatId: string;
220
+ siblings: { id: string; createdAt: string }[];
221
+ }) {
222
+ const transport = useTransport();
223
+ if (siblings.length <= 1) return null;
224
+
225
+ const currentIndex = siblings.findIndex((s) => s.id === message.id);
226
+
227
+ return (
228
+ <div>
229
+ <button
230
+ disabled={currentIndex === 0}
231
+ onClick={() => {
232
+ // Find the leaf of the previous sibling's subtree
233
+ transport.sendAction(chatId, {
234
+ type: "switch-branch",
235
+ leafId: siblings[currentIndex - 1]!.id,
236
+ });
237
+ }}
238
+ >
239
+ &lt;
240
+ </button>
241
+ <span>{currentIndex + 1}/{siblings.length}</span>
242
+ <button
243
+ disabled={currentIndex === siblings.length - 1}
244
+ onClick={() => {
245
+ transport.sendAction(chatId, {
246
+ type: "switch-branch",
247
+ leafId: siblings[currentIndex + 1]!.id,
248
+ });
249
+ }}
250
+ >
251
+ &gt;
252
+ </button>
253
+ </div>
254
+ );
255
+ }
256
+ ```
257
+
258
+ <Note>
259
+ The sibling data (which messages share the same parent) needs to come from your database — query it when loading the chat or include it as client data. The agent only returns the active branch via `hydrateMessages`.
260
+ </Note>
261
+
262
+ ## How it works
263
+
264
+ | Operation | What happens |
265
+ |-----------|-------------|
266
+ | **Send message** | `hydrateMessages` appends the new message as a child of the current leaf, returns the active path |
267
+ | **Edit message** | `onAction` creates a sibling node with the same parent. The new node becomes the latest leaf, so `hydrateMessages` resolves through it. LLM responds to the edited history |
268
+ | **Regenerate** | Same as edit — create a new assistant sibling. The AI SDK's `regenerate()` handles this via `trigger: "regenerate-message"` |
269
+ | **Undo** | `onAction` removes the last two nodes. `chat.history.set()` updates the accumulator. LLM responds to the earlier state |
270
+ | **Switch branch** | `onAction` updates which leaf is "active". `hydrateMessages` loads the new path. LLM responds to the switched context |
271
+
272
+ ## Design notes
273
+
274
+ - **Messages are immutable** — edits create siblings, not mutations. This preserves full history for analytics and auditing.
275
+ - **The tree lives in your database** — the agent loads a linear path from it via `hydrateMessages`. The agent itself doesn't know about the tree structure.
276
+ - **`hydrateMessages` + `onAction` + `chat.history`** are the three primitives. Hydration loads the active path, actions modify the tree, and `chat.history.set()` syncs the accumulator after tree modifications.
277
+ - **Frontend owns navigation** — the `< 2/3 >` UI, sibling queries, and branch switching triggers are client-side concerns. The backend just processes actions and returns responses.
278
+
279
+ ## See also
280
+
281
+ - [`hydrateMessages`](/ai-chat/lifecycle-hooks#hydratemessages) — backend-controlled message history
282
+ - [Actions](/ai-chat/actions) — custom actions with `actionSchema` and `onAction`
283
+ - [`chat.history`](/ai-chat/backend#chat-history) — imperative history mutations
284
+ - [Database persistence](/ai-chat/patterns/database-persistence) — basic persistence pattern (linear)
@@ -0,0 +1,126 @@
1
+ ---
2
+ title: "Code execution sandbox"
3
+ sidebarTitle: "Code sandbox"
4
+ description: "Warm an isolated sandbox on each chat turn, run an AI SDK executeCode tool, and tear down right before the run suspends — using chat.agent hooks and chat.local."
5
+ ---
6
+
7
+ import RcBanner from "/snippets/ai-chat-rc-banner.mdx";
8
+
9
+ <RcBanner />
10
+
11
+ Use a **hosted code sandbox** (for example [E2B](https://e2b.dev)) when the model should run short scripts to analyze tool output (PostHog queries, CSV-like data, math) without executing arbitrary code on the Trigger worker host.
12
+
13
+ This page describes a **durable chat** pattern that fits `chat.agent()`:
14
+
15
+ - **Warm** the sandbox at the start of each turn (**non-blocking**).
16
+ - **Reuse** it for every `executeCode` tool call during that turn (and across turns in the same run if you keep the handle).
17
+ - **Dispose** it **right before the run suspends** waiting for the next user message — using the **`onChatSuspend`** hook, not `onTurnComplete`.
18
+
19
+
20
+ ## Why not tear down in `onTurnComplete`?
21
+
22
+ After a turn finishes, the chat runtime still goes through an **idle** window and only then suspends. During that window the run is still executing — useful for `chat.defer()` work — and the run hasn't suspended yet.
23
+
24
+ The boundary you want for “turn done, about to sleep” is **`onChatSuspend`**, which fires right before the run transitions from idle to suspended. It provides the `phase` (`”preload”` or `”turn”`) and full chat context. See [onChatSuspend / onChatResume](/ai-chat/lifecycle-hooks#onchatsuspend--onchatresume).
25
+
26
+ ```mermaid
27
+ sequenceDiagram
28
+ participant TurnStart as onTurnStart
29
+ participant Run as run / streamText
30
+ participant TurnDone as onTurnComplete
31
+ participant Idle as Idle window
32
+ participant Suspend as onChatSuspend
33
+ participant Sleep as suspended
34
+
35
+ TurnStart->>Run: warm sandbox (async)
36
+ Run->>TurnDone: persist / inject / etc.
37
+ TurnDone->>Idle: still running
38
+ Idle->>Suspend: dispose sandbox
39
+ Suspend->>Sleep: waiting for next message
40
+ ```
41
+
42
+ ## Recommended provider: E2B
43
+
44
+ - **API key** auth — works from any Trigger.dev worker; no Vercel-only OIDC.
45
+ - **Code Interpreter** SDK (`@e2b/code-interpreter`): long-lived sandbox, `runCode()`, `kill()`.
46
+
47
+ Alternatives (Modal, Daytona, raw Docker) are fine but more DIY. Vercel’s sandbox + AI SDK helpers are a better fit when execution stays **on Vercel**, not on the Trigger worker.
48
+
49
+ ## Implementation sketch
50
+
51
+ ### 1. Run-scoped sandbox map
52
+
53
+ Keep a `Map<runId, Promise<Sandbox>>` (or similar) in a **task-only module** so your Next.js app never imports it.
54
+
55
+ ### 2. `onTurnStart` — warm without blocking
56
+
57
+ ```ts
58
+ onTurnStart: async ({ runId, ctx, ...rest }) => {
59
+ warmCodeSandbox(runId); // fire-and-forget Sandbox.create()
60
+ // ...persist messages, writer, etc.
61
+ },
62
+ ```
63
+
64
+ ### 3. `chat.local` — run id for tools
65
+
66
+ Tool `execute` functions do not receive hook payloads. Use [`chat.local()`](/ai-chat/chat-local) to store the current run id for the sandbox key, **initialized from `onTurnStart`** (same `runId` as the map):
67
+
68
+ ```ts
69
+ // In the same task module as your tools
70
+ import { chat } from "@trigger.dev/sdk/ai";
71
+
72
+ export const codeSandboxRun = chat.local<{ runId: string }>({ id: "codeSandboxRun" });
73
+
74
+ export function warmCodeSandbox(runId: string) {
75
+ codeSandboxRun.init({ runId });
76
+ // ...start Sandbox.create(), store promise in Map by runId
77
+ }
78
+ ```
79
+
80
+ The **`executeCode`** tool reads `codeSandboxRun.runId` and awaits the sandbox promise before `runCode`.
81
+
82
+ ### 4. `onChatSuspend` / `onComplete` — teardown
83
+
84
+ Use **`onChatSuspend`** to dispose the sandbox right before the run suspends, and **`onComplete`** as a safety net when the run ends entirely.
85
+
86
+ ```ts
87
+ export const aiChat = chat.agent({
88
+ id: "ai-chat",
89
+ // ...
90
+ onChatSuspend: async ({ phase, ctx }) => {
91
+ await disposeCodeSandboxForRun(ctx.run.id);
92
+ },
93
+ onComplete: async ({ ctx }) => {
94
+ await disposeCodeSandboxForRun(ctx.run.id);
95
+ },
96
+ });
97
+ ```
98
+
99
+ Unlike `onWait` (which fires for all wait types), `onChatSuspend` only fires at chat suspension points — no need to filter on `wait.type`. The `phase` discriminator tells you if this is a preload or post-turn suspension.
100
+
101
+ Optional **`onChatResume`**: log or reset flags; a fresh sandbox can be warmed again on the next **`onTurnStart`**.
102
+
103
+ ### 5. AI SDK tool
104
+
105
+ Wrap the provider in a normal AI SDK `tool({ inputSchema, execute })` (same pattern as `webFetch`). Keep tool definitions in **task code**, not in the Next.js server bundle.
106
+
107
+ ### 6. Environment
108
+
109
+ Set **`E2B_API_KEY`** (or your provider’s secret) on the **Trigger environment** for the worker — not in public client env.
110
+
111
+ ## Typing `ctx`
112
+
113
+ Every `chat.agent` lifecycle event and the `run` payload include **`ctx`**: the same **[`TaskRunContext`](/ai-chat/reference#task-context-ctx)** shape as `task({ run: (payload, { ctx }) => ... })`.
114
+
115
+ ```ts
116
+ import type { TaskRunContext } from "@trigger.dev/sdk";
117
+ ```
118
+
119
+ The alias **`Context`** is also exported from `@trigger.dev/sdk` and is the same type.
120
+
121
+ ## See also
122
+
123
+ - [Database persistence for chat](/ai-chat/patterns/database-persistence) — conversation + session rows, hooks, token renewal
124
+ - [Lifecycle hooks](/ai-chat/lifecycle-hooks)
125
+ - [API Reference — `ctx` on events](/ai-chat/reference#task-context-ctx)
126
+ - [Per-run data with `chat.local`](/ai-chat/chat-local)