@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
package/dist/esm/v3/ai.js CHANGED
@@ -49,6 +49,10 @@ const chatTurnContextKey = locals.create("chat.turnContext");
49
49
  * @internal
50
50
  */
51
51
  const chatSessionHandleKey = locals.create("chat.sessionHandle");
52
+ // The external `chatId` from the boot payload — the value `ToolCallExecutionOptions.chatId`
53
+ // is documented to carry. Custom-agent loops never set per-turn context, so subtask tool
54
+ // metadata reads this directly rather than the Session handle id.
55
+ const chatExternalIdKey = locals.create("chat.externalId");
52
56
  /**
53
57
  * S2 seq_num of the most recent `turn-complete` control record written by
54
58
  * this worker. Read by `writeTurnCompleteChunk` to know what to trim back
@@ -103,6 +107,43 @@ async function findLatestSessionInCursor(chatId) {
103
107
  export async function __findLatestSessionInCursorForTests(chatId) {
104
108
  return findLatestSessionInCursor(chatId);
105
109
  }
110
+ /**
111
+ * Seed the `.in` resume cursor for custom-agent loops (`chat.customAgent`
112
+ * raw loops and `chat.createSession`) the way `chat.agent`'s boot does.
113
+ *
114
+ * MUST run before anything attaches a `.in` listener (`createStopSignal`,
115
+ * `chat.messages.on`, the first wait): attaching opens the SSE tail with
116
+ * `Last-Event-ID` from the seeded cursor, so attach-then-seed replays
117
+ * every record from seq 0 — already-answered user messages get delivered
118
+ * into the new run's first wait and the loop re-answers them.
119
+ *
120
+ * Seeds both cursors: `setLastSeqNum` controls the SSE `Last-Event-ID`,
121
+ * `setLastDispatchedSeqNum` gates waiter dispatch — seeding only the
122
+ * former still re-delivers records the manager buffered before the seed.
123
+ *
124
+ * No-ops on fresh boots and when a cursor is already seeded (e.g. the
125
+ * `chatCustomAgent` wrapper ran before a nested `createChatSession`).
126
+ * @internal
127
+ */
128
+ async function seedSessionInResumeCursorForCustomLoop(payload) {
129
+ if (sessionStreams.lastSeqNum(payload.chatId, "in") !== undefined)
130
+ return;
131
+ // No continuation/attempt gate: the wire may omit `continuation` on a
132
+ // run that still has prior turns (chat.agent covers that case via its
133
+ // snapshot). The scan doubles as the prior-state probe — a fresh
134
+ // session has no turn-complete on `.out`, returns no cursor, and
135
+ // seeds nothing. Cost on fresh boots is one non-blocking records read.
136
+ try {
137
+ const cursor = await findLatestSessionInCursor(payload.chatId);
138
+ if (cursor !== undefined) {
139
+ sessionStreams.setLastSeqNum(payload.chatId, "in", cursor);
140
+ sessionStreams.setLastDispatchedSeqNum(payload.chatId, "in", cursor);
141
+ }
142
+ }
143
+ catch (error) {
144
+ logger.warn("chat session: session.in resume cursor lookup failed; old messages may replay", { error: error instanceof Error ? error.message : String(error) });
145
+ }
146
+ }
106
147
  let readChatSnapshotImpl;
107
148
  export function __setReadChatSnapshotImplForTests(impl) {
108
149
  readChatSnapshotImpl = impl;
@@ -654,6 +695,16 @@ function createTaskToolExecuteHandler(task) {
654
695
  toolMeta.continuation = chatCtx.continuation;
655
696
  toolMeta.clientData = chatCtx.clientData;
656
697
  }
698
+ else {
699
+ // Hand-rolled chat.customAgent loops never set per-turn context, but
700
+ // the wrapper records the boot payload's external chatId at run boot
701
+ // — thread it so subtask chat helpers (`chat.stream.writer` with
702
+ // target "root") can open the parent's session.
703
+ const chatExternalId = locals.get(chatExternalIdKey);
704
+ if (chatExternalId) {
705
+ toolMeta.chatId = chatExternalId;
706
+ }
707
+ }
657
708
  const chatLocals = {};
658
709
  for (const entry of chatLocalRegistry) {
659
710
  const value = locals.get(entry.key);
@@ -1186,6 +1237,36 @@ const handoverInput = {
1186
1237
  }
1187
1238
  },
1188
1239
  };
1240
+ /**
1241
+ * Wait for a `chat.headStart` handover signal inside a custom-agent loop or
1242
+ * `chat.createSession`. Returns:
1243
+ * - `null` — this run is not a `handover-prepare` boot, or the wait idled out /
1244
+ * the warm handler crashed before signaling. Treat as "no handover".
1245
+ * - `{ kind: "handover-skip" }` — the warm handler aborted; exit without a turn.
1246
+ * - `{ kind: "handover", partialAssistantMessage, messageId?, isFinal }` — splice
1247
+ * the partial (`chat.MessageAccumulator.applyHandover`) and, when `isFinal` is
1248
+ * false, fall through to `streamText` to run the handed-over tool round.
1249
+ *
1250
+ * For the common case prefer `accumulator.consumeHandover()`, which also seeds
1251
+ * `payload.headStartMessages` and applies the partial for you.
1252
+ *
1253
+ * Must be called at turn 0 before any `chat.messages.waitWithIdleTimeout` —
1254
+ * that facade consumes and discards non-message chunks, which would swallow the
1255
+ * handover signal.
1256
+ */
1257
+ async function waitForHandover(options) {
1258
+ if (options.payload.trigger !== "handover-prepare")
1259
+ return null;
1260
+ const result = await handoverInput.waitWithIdleTimeout({
1261
+ idleTimeoutInSeconds: options.idleTimeoutInSeconds ?? options.payload.idleTimeoutInSeconds ?? 60,
1262
+ timeout: options.timeout,
1263
+ spanName: options.spanName ?? "waiting for handover signal",
1264
+ });
1265
+ // Non-ok = idle timeout or the warm handler crashed without signaling.
1266
+ if (!result.ok)
1267
+ return null;
1268
+ return result.output;
1269
+ }
1189
1270
  /**
1190
1271
  * Per-turn deferred promises. Registered via `chat.defer()`, awaited
1191
1272
  * before `onTurnComplete` fires. Reset each turn.
@@ -1282,6 +1363,27 @@ function synthesizeHandoverUIMessage(partial, messageId) {
1282
1363
  parts,
1283
1364
  };
1284
1365
  }
1366
+ /**
1367
+ * Splice a head-start handover partial into an accumulating message pair
1368
+ * (model + UI). Dedups by `messageId` against the UI chain (so a hydrated
1369
+ * history that already persisted the partial isn't doubled), then pushes the
1370
+ * partial into `modelMessages` and the synthesized UIMessage into `uiMessages`.
1371
+ * Shared by the `chat.agent` turn-0 splice and `ChatMessageAccumulator.applyHandover`.
1372
+ * @internal
1373
+ */
1374
+ function spliceHandoverPartial(modelMessages, uiMessages, signal) {
1375
+ if (!signal.partialAssistantMessage || signal.partialAssistantMessage.length === 0) {
1376
+ return;
1377
+ }
1378
+ // Skip if the hydrated chain already persisted the partial under this id.
1379
+ const alreadyInChain = signal.messageId !== undefined && uiMessages.some((m) => m.id === signal.messageId);
1380
+ if (alreadyInChain)
1381
+ return;
1382
+ modelMessages.push(...signal.partialAssistantMessage);
1383
+ const partialUI = synthesizeHandoverUIMessage(signal.partialAssistantMessage, signal.messageId);
1384
+ if (partialUI)
1385
+ uiMessages.push(partialUI);
1386
+ }
1285
1387
  /**
1286
1388
  * Per-turn background context queue. Messages added via `chat.backgroundWork.inject()`
1287
1389
  * are drained at the next `prepareStep` boundary and appended to the model messages.
@@ -2234,11 +2336,18 @@ function isCompactionSafe(messages) {
2234
2336
  }
2235
2337
  /** @internal */
2236
2338
  const chatPromptKey = locals.create("chat.prompt");
2339
+ /**
2340
+ * @internal Provider options attached to the system message that
2341
+ * `toStreamTextOptions()` builds from the stored prompt — lets a provider cache
2342
+ * the system block. Stored separately so it works for both the `ResolvedPrompt`
2343
+ * and plain-string forms without mutating the prompt object.
2344
+ */
2345
+ const chatPromptProviderOptionsKey = locals.create("chat.prompt.providerOptions");
2237
2346
  /**
2238
2347
  * Store a resolved prompt (or plain string) for the current run.
2239
2348
  * Call from any hook (`onPreload`, `onChatStart`, `onTurnStart`) or `run()`.
2240
2349
  */
2241
- function setChatPrompt(resolved) {
2350
+ function setChatPrompt(resolved, options) {
2242
2351
  if (typeof resolved === "string") {
2243
2352
  locals.set(chatPromptKey, {
2244
2353
  text: resolved,
@@ -2255,6 +2364,9 @@ function setChatPrompt(resolved) {
2255
2364
  else {
2256
2365
  locals.set(chatPromptKey, resolved);
2257
2366
  }
2367
+ // Always overwrite the slot (even with undefined) so a later prompt.set with
2368
+ // no options clears a previous prompt's cache opt-in rather than leaking it.
2369
+ locals.set(chatPromptProviderOptionsKey, options?.providerOptions);
2258
2370
  }
2259
2371
  /**
2260
2372
  * Read the stored prompt. Throws if `chat.prompt.set()` has not been called.
@@ -2420,7 +2532,21 @@ function toStreamTextOptions(options) {
2420
2532
  const promptText = prompt?.text ?? "";
2421
2533
  const skillsText = skills && skills.length > 0 ? buildSkillsSystemPrompt(skills) : "";
2422
2534
  if (promptText || skillsText) {
2423
- result.system = [promptText, skillsText].filter(Boolean).join("\n\n");
2535
+ const systemText = [promptText, skillsText].filter(Boolean).join("\n\n");
2536
+ // Resolve system-prompt provider options for caching. Precedence (most
2537
+ // specific wins, no deep merge): explicit `systemProviderOptions` →
2538
+ // `cacheControl` sugar → `providerOptions` stored on `chat.prompt.set()`.
2539
+ const systemProviderOptions = options?.systemProviderOptions ??
2540
+ (options?.cacheControl
2541
+ ? { anthropic: { cacheControl: options.cacheControl } }
2542
+ : undefined) ??
2543
+ locals.get(chatPromptProviderOptionsKey);
2544
+ // A bare string stays a bare string (the unchanged default). With provider
2545
+ // options, emit a structured `SystemModelMessage` so the provider can cache
2546
+ // the system block — `streamText`'s `system` accepts string | message.
2547
+ result.system = systemProviderOptions
2548
+ ? { role: "system", content: systemText, providerOptions: systemProviderOptions }
2549
+ : systemText;
2424
2550
  }
2425
2551
  // Prompt-related options (only if chat.prompt.set() was called)
2426
2552
  if (prompt) {
@@ -2602,6 +2728,7 @@ function chatCustomAgent(options) {
2602
2728
  // `chat.createStartSessionAction`) before this run is triggered.
2603
2729
  // No client-side upsert needed.
2604
2730
  locals.set(chatSessionHandleKey, sessions.open(payload.chatId));
2731
+ locals.set(chatExternalIdKey, payload.chatId);
2605
2732
  locals.set(chatAgentRunContextKey, runOptions.ctx);
2606
2733
  // Initialize the turn-complete trim slot so `chat.writeTurnComplete`
2607
2734
  // trims `session.out` back to the previous turn boundary. Without
@@ -2611,6 +2738,10 @@ function chatCustomAgent(options) {
2611
2738
  markChatAgentRunForStreamsWarning();
2612
2739
  taskContext.setConversationId(payload.chatId);
2613
2740
  stampConversationIdOnActiveSpan(payload.chatId);
2741
+ // Seed the `.in` resume cursor before user code attaches any `.in`
2742
+ // listener — otherwise a continuation boot replays already-answered
2743
+ // messages into the loop's first wait.
2744
+ await seedSessionInResumeCursorForCustomLoop(payload);
2614
2745
  return userRun(payload, runOptions);
2615
2746
  },
2616
2747
  });
@@ -2657,6 +2788,7 @@ function chatAgent(options) {
2657
2788
  // `chat.createStartSessionAction` or browser-direct) before this
2658
2789
  // run is triggered — no client-side upsert needed here.
2659
2790
  locals.set(chatSessionHandleKey, sessions.open(payload.chatId));
2791
+ locals.set(chatExternalIdKey, payload.chatId);
2660
2792
  // Mutable holder; advances in `writeTurnCompleteChunk` after each turn
2661
2793
  // and is the trim target for the NEXT turn's trim record.
2662
2794
  locals.set(lastTurnCompleteSeqNumKey, { value: undefined });
@@ -3866,18 +3998,10 @@ function chatAgent(options) {
3866
3998
  // `UIMessageStreamError: No tool invocation found`.
3867
3999
  const pendingHandoverPartial = locals.get(chatHandoverPartialKey);
3868
4000
  if (pendingHandoverPartial && pendingHandoverPartial.length > 0) {
3869
- const handoverMessageId = locals.get(chatHandoverMessageIdKey);
3870
- // Skip if the hydrated chain already persisted the
3871
- // partial under the handover messageId.
3872
- const alreadyInChain = handoverMessageId !== undefined &&
3873
- accumulatedUIMessages.some((m) => m.id === handoverMessageId);
3874
- if (!alreadyInChain) {
3875
- accumulatedMessages.push(...pendingHandoverPartial);
3876
- const partialUI = synthesizeHandoverUIMessage(pendingHandoverPartial, handoverMessageId);
3877
- if (partialUI) {
3878
- accumulatedUIMessages.push(partialUI);
3879
- }
3880
- }
4001
+ spliceHandoverPartial(accumulatedMessages, accumulatedUIMessages, {
4002
+ partialAssistantMessage: pendingHandoverPartial,
4003
+ messageId: locals.get(chatHandoverMessageIdKey),
4004
+ });
3881
4005
  locals.set(chatHandoverPartialKey, []); // consume once
3882
4006
  }
3883
4007
  }
@@ -5437,8 +5561,19 @@ async function pipeChatAndCapture(source, options) {
5437
5561
  const onFinishPromise = new Promise((r) => {
5438
5562
  resolveOnFinish = r;
5439
5563
  });
5564
+ const resolvedOptions = resolveUIMessageStreamOptions();
5440
5565
  const uiStream = source.toUIMessageStream({
5441
- ...resolveUIMessageStreamOptions(),
5566
+ ...resolvedOptions,
5567
+ // Thread the prior chain (incl. a spliced handover partial) so a resumed
5568
+ // tool round's tool-output chunks merge into the originating tool-call
5569
+ // instead of throwing "No tool invocation found".
5570
+ ...(options?.originalMessages ? { originalMessages: options.originalMessages } : {}),
5571
+ // Stamp a server-generated id on the start chunk, same as chat.agent's
5572
+ // pipe. Without it the AI SDK regenerates the assistant id when a
5573
+ // prepareStep injection (steering) starts a new step mid-stream, and
5574
+ // the frontend replaces the partial message — wiping the
5575
+ // pre-injection text from the UI and the captured response.
5576
+ generateMessageId: resolvedOptions.generateMessageId ?? generateMessageId,
5442
5577
  onFinish: ({ responseMessage }) => {
5443
5578
  captured = responseMessage;
5444
5579
  resolveOnFinish();
@@ -5508,10 +5643,65 @@ class ChatMessageAccumulator {
5508
5643
  this.uiMessages = [...uiMessages];
5509
5644
  this.modelMessages = await toModelMessages(uiMessages);
5510
5645
  }
5646
+ /**
5647
+ * Splice a `chat.headStart` handover partial into the accumulator (the warm
5648
+ * step-1 response). Dedups by `messageId` so a seeded/hydrated history that
5649
+ * already carries the partial isn't doubled. Seed any prior history first
5650
+ * (e.g. `setMessages(payload.headStartMessages)`). Low-level — see
5651
+ * `consumeHandover` for the wait+seed+apply convenience.
5652
+ */
5653
+ applyHandover(signal) {
5654
+ spliceHandoverPartial(this.modelMessages, this.uiMessages, signal);
5655
+ }
5656
+ /**
5657
+ * One-call `chat.headStart` handover for a custom-agent loop: waits for the
5658
+ * handover signal, seeds prior history from `payload.headStartMessages`,
5659
+ * applies the warm step-1 partial, and reports what to do next.
5660
+ *
5661
+ * Returns `{ isFinal, skipped }`:
5662
+ * - `skipped: true` — not a `handover-prepare` run, the wait idled out, or the
5663
+ * warm handler aborted. Exit the run without a turn.
5664
+ * - `isFinal: true` — step 1 IS the response (pure text). Write turn-complete
5665
+ * and continue; do not call `streamText`.
5666
+ * - `isFinal: false` — fall through to `streamText`, which runs the pending
5667
+ * tool round handed over from step 1.
5668
+ */
5669
+ async consumeHandover(options) {
5670
+ const signal = await waitForHandover({
5671
+ payload: options.payload,
5672
+ idleTimeoutInSeconds: options.idleTimeoutInSeconds,
5673
+ timeout: options.timeout,
5674
+ });
5675
+ if (!signal || signal.kind === "handover-skip") {
5676
+ return { isFinal: false, skipped: true };
5677
+ }
5678
+ if (options.payload.headStartMessages && options.payload.headStartMessages.length > 0) {
5679
+ await this.setMessages(options.payload.headStartMessages);
5680
+ }
5681
+ this.applyHandover(signal);
5682
+ return { isFinal: signal.isFinal, skipped: false };
5683
+ }
5511
5684
  async addResponse(response) {
5512
5685
  if (!response.id) {
5513
5686
  response = { ...response, id: generateMessageId() };
5514
5687
  }
5688
+ // Tool-approval and handover-resume continuations reuse the trailing
5689
+ // assistant's ID (via originalMessages on the pipe), so the captured
5690
+ // response can carry the same ID as a message already in the chain
5691
+ // (e.g. a spliced handover partial). Replace in place instead of pushing
5692
+ // a duplicate, mirroring the chat.agent accumulator.
5693
+ const existingIdx = this.uiMessages.findIndex((m) => m.id === response.id);
5694
+ if (existingIdx !== -1) {
5695
+ this.uiMessages[existingIdx] = response;
5696
+ try {
5697
+ // Reconvert all model messages since we replaced rather than appended.
5698
+ this.modelMessages = await toModelMessages(this.uiMessages.map((m) => stripProviderMetadata(m)));
5699
+ }
5700
+ catch {
5701
+ // Conversion failed — leave the existing model messages in place
5702
+ }
5703
+ return;
5704
+ }
5515
5705
  this.uiMessages.push(response);
5516
5706
  try {
5517
5707
  const msgs = await toModelMessages([stripProviderMetadata(response)]);
@@ -5644,14 +5834,18 @@ class ChatMessageAccumulator {
5644
5834
  * signaling, and idle/suspend between turns. You control: initialization,
5645
5835
  * model/tool selection, persistence, and any custom per-turn logic.
5646
5836
  *
5837
+ * Call from inside a `chat.customAgent()` run — the wrapper binds the
5838
+ * backing Session that the iterator's stop signal and message channels
5839
+ * resolve to. (A plain `task()` does not bind it, so `createSession`
5840
+ * would throw "session handle is not initialized".)
5841
+ *
5647
5842
  * @example
5648
5843
  * ```ts
5649
- * import { task } from "@trigger.dev/sdk";
5650
5844
  * import { chat, type ChatTaskWirePayload } from "@trigger.dev/sdk/ai";
5651
5845
  * import { streamText } from "ai";
5652
5846
  * import { openai } from "@ai-sdk/openai";
5653
5847
  *
5654
- * export const myChat = task({
5848
+ * export const myChat = chat.customAgent({
5655
5849
  * id: "my-chat",
5656
5850
  * run: async (payload: ChatTaskWirePayload, { signal }) => {
5657
5851
  * const session = chat.createSession(payload, { signal });
@@ -5675,13 +5869,46 @@ function createChatSession(payload, options) {
5675
5869
  [Symbol.asyncIterator]() {
5676
5870
  let currentPayload = payload;
5677
5871
  let turn = -1;
5678
- const stop = createStopSignal();
5872
+ // Created on the first next() call, AFTER the resume-cursor seed —
5873
+ // createStopSignal attaches the `.in` SSE tail, and attaching
5874
+ // before the seed replays every record from seq 0 (the seed is a
5875
+ // no-op when the chatCustomAgent wrapper already ran it).
5876
+ let stop;
5877
+ let booted = false;
5679
5878
  const accumulator = new ChatMessageAccumulator();
5680
5879
  let previousTurnUsage;
5681
5880
  let cumulativeUsage = emptyUsage();
5682
5881
  return {
5683
5882
  async next() {
5883
+ if (!booted) {
5884
+ booted = true;
5885
+ await seedSessionInResumeCursorForCustomLoop(currentPayload);
5886
+ stop = createStopSignal();
5887
+ }
5684
5888
  turn++;
5889
+ // Head-start handover: the server triggered this run with
5890
+ // `trigger: "handover-prepare"` and signals the warm step-1 partial on
5891
+ // `session.in`. Wait for it BEFORE any `messagesInput.waitWithIdleTimeout`
5892
+ // (that facade consumes-and-discards non-message chunks and would swallow
5893
+ // the signal). Turn-0 only — continuation boots never carry this trigger.
5894
+ let handoverThisTurn = null;
5895
+ let pendingHandoverSignal = null;
5896
+ if (turn === 0 && currentPayload.trigger === "handover-prepare") {
5897
+ const signal = await waitForHandover({
5898
+ payload: currentPayload,
5899
+ idleTimeoutInSeconds: sessionIdleTimeoutOpt ?? currentPayload.idleTimeoutInSeconds ?? idleTimeoutInSeconds,
5900
+ timeout,
5901
+ });
5902
+ if (!signal || signal.kind === "handover-skip" || runSignal.aborted) {
5903
+ stop.cleanup();
5904
+ return { done: true, value: undefined };
5905
+ }
5906
+ pendingHandoverSignal = signal;
5907
+ handoverThisTurn = { isFinal: signal.isFinal };
5908
+ // Rewrite to a normal first-turn message turn so the rest of the loop
5909
+ // (steering setup, addIncoming, turnObj) runs unchanged.
5910
+ currentPayload = { ...currentPayload, trigger: "submit-message", message: undefined };
5911
+ }
5685
5912
  // First turn: wait when the boot payload carries no message.
5686
5913
  // Preload boots wait for the first real message; continuation
5687
5914
  // boots (fresh run via `ensureRunForSession` / end-and-continue)
@@ -5788,6 +6015,16 @@ function createChatSession(payload, options) {
5788
6015
  ? [currentPayload.message]
5789
6016
  : [];
5790
6017
  const messages = await accumulator.addIncoming(incomingForAccumulator, currentPayload.trigger, turn);
6018
+ // Apply the head-start handover AFTER addIncoming — turn-0 addIncoming
6019
+ // replaces accumulator state, which would wipe a pre-applied splice.
6020
+ // Seed prior history first, then splice the warm step-1 partial.
6021
+ if (pendingHandoverSignal) {
6022
+ const priorHistory = currentPayload.headStartMessages;
6023
+ if (priorHistory && priorHistory.length > 0) {
6024
+ await accumulator.setMessages(priorHistory);
6025
+ }
6026
+ accumulator.applyHandover(pendingHandoverSignal);
6027
+ }
5791
6028
  // chat.requestUpgrade() called before this turn — signal transport and exit
5792
6029
  if (locals.get(chatUpgradeRequestedKey)) {
5793
6030
  await writeUpgradeRequiredChunk();
@@ -5814,13 +6051,38 @@ function createChatSession(payload, options) {
5814
6051
  continuation: currentPayload.continuation ?? false,
5815
6052
  previousTurnUsage,
5816
6053
  totalUsage: cumulativeUsage,
6054
+ handover: handoverThisTurn,
5817
6055
  async setMessages(uiMessages) {
5818
6056
  await accumulator.setMessages(uiMessages);
5819
6057
  },
5820
6058
  async complete(source) {
6059
+ // Head-start final turn: the warm step-1 partial is already spliced
6060
+ // into the accumulator and IS the response — nothing to pipe. Only
6061
+ // valid on a final handover; a missing source on any other turn is a
6062
+ // mistake (it would silently finalize without an assistant response).
6063
+ if (!source) {
6064
+ if (!handoverThisTurn?.isFinal) {
6065
+ throw new Error("turn.complete() requires a stream source unless turn.handover.isFinal is true");
6066
+ }
6067
+ const response = accumulator.uiMessages.at(-1);
6068
+ if (!response || response.role !== "assistant") {
6069
+ throw new Error("turn.complete() could not find the spliced handover response");
6070
+ }
6071
+ sessionMsgSub.off();
6072
+ await chatWriteTurnComplete();
6073
+ return response;
6074
+ }
5821
6075
  let response;
5822
6076
  try {
5823
- response = await pipeChatAndCapture(source, { signal: combinedSignal });
6077
+ response = await pipeChatAndCapture(source, {
6078
+ signal: combinedSignal,
6079
+ // On a non-final handover turn, thread the spliced partial so a
6080
+ // resumed tool round's tool-output chunks merge into the
6081
+ // handed-over tool-call. Gated on the handover turn only — a
6082
+ // normal turn must not pass originalMessages (it would merge the
6083
+ // fresh response into the prior assistant message).
6084
+ ...(handoverThisTurn ? { originalMessages: accumulator.uiMessages } : {}),
6085
+ });
5824
6086
  }
5825
6087
  catch (error) {
5826
6088
  if (error instanceof Error && error.name === "AbortError") {
@@ -5975,7 +6237,8 @@ function createChatSession(payload, options) {
5975
6237
  return { done: false, value: turnObj };
5976
6238
  },
5977
6239
  async return() {
5978
- stop.cleanup();
6240
+ // `stop` only exists once next() has booted the iterator.
6241
+ stop?.cleanup();
5979
6242
  return { done: true, value: undefined };
5980
6243
  },
5981
6244
  };
@@ -6218,8 +6481,8 @@ function createChatStartSessionAction(taskId, options) {
6218
6481
  // run-list filter by chat works without the customer having to wire it
6219
6482
  // up. Mirrors the browser-mediated `TriggerChatTransport.doStart` path.
6220
6483
  const userTags = params.triggerConfig?.tags ?? options?.triggerConfig?.tags ?? [];
6221
- // Platform cap is 10 tags per run; the auto chat tag takes one slot.
6222
- const tags = [`chat:${params.chatId}`, ...userTags].slice(0, 10);
6484
+ // SessionTriggerConfig.tags allows at most 5; the auto chat tag takes one slot.
6485
+ const tags = [`chat:${params.chatId}`, ...userTags].slice(0, 5);
6223
6486
  const clientDataMetadata = params.clientData !== undefined ? { metadata: params.clientData } : {};
6224
6487
  const triggerConfig = {
6225
6488
  basePayload: {
@@ -6243,6 +6506,20 @@ function createChatStartSessionAction(taskId, options) {
6243
6506
  maxAttempts: params.triggerConfig?.maxAttempts ?? options?.triggerConfig?.maxAttempts,
6244
6507
  }
6245
6508
  : {}),
6509
+ ...(options?.triggerConfig?.maxDuration !== undefined ||
6510
+ params.triggerConfig?.maxDuration !== undefined
6511
+ ? {
6512
+ maxDuration: params.triggerConfig?.maxDuration ?? options?.triggerConfig?.maxDuration,
6513
+ }
6514
+ : {}),
6515
+ ...(options?.triggerConfig?.region || params.triggerConfig?.region
6516
+ ? { region: params.triggerConfig?.region ?? options?.triggerConfig?.region }
6517
+ : {}),
6518
+ ...(options?.triggerConfig?.lockToVersion || params.triggerConfig?.lockToVersion
6519
+ ? {
6520
+ lockToVersion: params.triggerConfig?.lockToVersion ?? options?.triggerConfig?.lockToVersion,
6521
+ }
6522
+ : {}),
6246
6523
  ...(options?.triggerConfig?.idleTimeoutInSeconds !== undefined ||
6247
6524
  params.triggerConfig?.idleTimeoutInSeconds !== undefined
6248
6525
  ? {
@@ -6421,10 +6698,20 @@ export const chat = {
6421
6698
  MessageAccumulator: ChatMessageAccumulator,
6422
6699
  /** Create a chat session (async iterator). See {@link createChatSession}. */
6423
6700
  createSession: createChatSession,
6701
+ /**
6702
+ * Wait for a `chat.headStart` handover signal inside a `chat.customAgent`
6703
+ * loop (turn 0). See {@link waitForHandover}. For most loops prefer the
6704
+ * `chat.MessageAccumulator.consumeHandover()` convenience, which also seeds
6705
+ * `payload.headStartMessages` and applies the partial.
6706
+ */
6707
+ waitForHandover,
6424
6708
  /**
6425
6709
  * Store and retrieve a resolved prompt for the current run.
6426
6710
  *
6427
6711
  * - `chat.prompt.set(resolved)` — store a `ResolvedPrompt` or plain string
6712
+ * - `chat.prompt.set(resolved, { providerOptions })` — also attach provider
6713
+ * options to the system block so a provider can cache it (e.g. Anthropic
6714
+ * prompt caching). See the prompt-caching guide.
6428
6715
  * - `chat.prompt()` — read the stored prompt (throws if not set)
6429
6716
  */
6430
6717
  prompt: Object.assign(getChatPrompt, { set: setChatPrompt }),