@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
@@ -63,6 +63,10 @@ const chatTurnContextKey = locals_js_1.locals.create("chat.turnContext");
63
63
  * @internal
64
64
  */
65
65
  const chatSessionHandleKey = locals_js_1.locals.create("chat.sessionHandle");
66
+ // The external `chatId` from the boot payload — the value `ToolCallExecutionOptions.chatId`
67
+ // is documented to carry. Custom-agent loops never set per-turn context, so subtask tool
68
+ // metadata reads this directly rather than the Session handle id.
69
+ const chatExternalIdKey = locals_js_1.locals.create("chat.externalId");
66
70
  /**
67
71
  * S2 seq_num of the most recent `turn-complete` control record written by
68
72
  * this worker. Read by `writeTurnCompleteChunk` to know what to trim back
@@ -117,6 +121,43 @@ async function findLatestSessionInCursor(chatId) {
117
121
  async function __findLatestSessionInCursorForTests(chatId) {
118
122
  return findLatestSessionInCursor(chatId);
119
123
  }
124
+ /**
125
+ * Seed the `.in` resume cursor for custom-agent loops (`chat.customAgent`
126
+ * raw loops and `chat.createSession`) the way `chat.agent`'s boot does.
127
+ *
128
+ * MUST run before anything attaches a `.in` listener (`createStopSignal`,
129
+ * `chat.messages.on`, the first wait): attaching opens the SSE tail with
130
+ * `Last-Event-ID` from the seeded cursor, so attach-then-seed replays
131
+ * every record from seq 0 — already-answered user messages get delivered
132
+ * into the new run's first wait and the loop re-answers them.
133
+ *
134
+ * Seeds both cursors: `setLastSeqNum` controls the SSE `Last-Event-ID`,
135
+ * `setLastDispatchedSeqNum` gates waiter dispatch — seeding only the
136
+ * former still re-delivers records the manager buffered before the seed.
137
+ *
138
+ * No-ops on fresh boots and when a cursor is already seeded (e.g. the
139
+ * `chatCustomAgent` wrapper ran before a nested `createChatSession`).
140
+ * @internal
141
+ */
142
+ async function seedSessionInResumeCursorForCustomLoop(payload) {
143
+ if (v3_1.sessionStreams.lastSeqNum(payload.chatId, "in") !== undefined)
144
+ return;
145
+ // No continuation/attempt gate: the wire may omit `continuation` on a
146
+ // run that still has prior turns (chat.agent covers that case via its
147
+ // snapshot). The scan doubles as the prior-state probe — a fresh
148
+ // session has no turn-complete on `.out`, returns no cursor, and
149
+ // seeds nothing. Cost on fresh boots is one non-blocking records read.
150
+ try {
151
+ const cursor = await findLatestSessionInCursor(payload.chatId);
152
+ if (cursor !== undefined) {
153
+ v3_1.sessionStreams.setLastSeqNum(payload.chatId, "in", cursor);
154
+ v3_1.sessionStreams.setLastDispatchedSeqNum(payload.chatId, "in", cursor);
155
+ }
156
+ }
157
+ catch (error) {
158
+ v3_1.logger.warn("chat session: session.in resume cursor lookup failed; old messages may replay", { error: error instanceof Error ? error.message : String(error) });
159
+ }
160
+ }
120
161
  let readChatSnapshotImpl;
121
162
  function __setReadChatSnapshotImplForTests(impl) {
122
163
  readChatSnapshotImpl = impl;
@@ -668,6 +709,16 @@ function createTaskToolExecuteHandler(task) {
668
709
  toolMeta.continuation = chatCtx.continuation;
669
710
  toolMeta.clientData = chatCtx.clientData;
670
711
  }
712
+ else {
713
+ // Hand-rolled chat.customAgent loops never set per-turn context, but
714
+ // the wrapper records the boot payload's external chatId at run boot
715
+ // — thread it so subtask chat helpers (`chat.stream.writer` with
716
+ // target "root") can open the parent's session.
717
+ const chatExternalId = locals_js_1.locals.get(chatExternalIdKey);
718
+ if (chatExternalId) {
719
+ toolMeta.chatId = chatExternalId;
720
+ }
721
+ }
671
722
  const chatLocals = {};
672
723
  for (const entry of chatLocalRegistry) {
673
724
  const value = locals_js_1.locals.get(entry.key);
@@ -1200,6 +1251,36 @@ const handoverInput = {
1200
1251
  }
1201
1252
  },
1202
1253
  };
1254
+ /**
1255
+ * Wait for a `chat.headStart` handover signal inside a custom-agent loop or
1256
+ * `chat.createSession`. Returns:
1257
+ * - `null` — this run is not a `handover-prepare` boot, or the wait idled out /
1258
+ * the warm handler crashed before signaling. Treat as "no handover".
1259
+ * - `{ kind: "handover-skip" }` — the warm handler aborted; exit without a turn.
1260
+ * - `{ kind: "handover", partialAssistantMessage, messageId?, isFinal }` — splice
1261
+ * the partial (`chat.MessageAccumulator.applyHandover`) and, when `isFinal` is
1262
+ * false, fall through to `streamText` to run the handed-over tool round.
1263
+ *
1264
+ * For the common case prefer `accumulator.consumeHandover()`, which also seeds
1265
+ * `payload.headStartMessages` and applies the partial for you.
1266
+ *
1267
+ * Must be called at turn 0 before any `chat.messages.waitWithIdleTimeout` —
1268
+ * that facade consumes and discards non-message chunks, which would swallow the
1269
+ * handover signal.
1270
+ */
1271
+ async function waitForHandover(options) {
1272
+ if (options.payload.trigger !== "handover-prepare")
1273
+ return null;
1274
+ const result = await handoverInput.waitWithIdleTimeout({
1275
+ idleTimeoutInSeconds: options.idleTimeoutInSeconds ?? options.payload.idleTimeoutInSeconds ?? 60,
1276
+ timeout: options.timeout,
1277
+ spanName: options.spanName ?? "waiting for handover signal",
1278
+ });
1279
+ // Non-ok = idle timeout or the warm handler crashed without signaling.
1280
+ if (!result.ok)
1281
+ return null;
1282
+ return result.output;
1283
+ }
1203
1284
  /**
1204
1285
  * Per-turn deferred promises. Registered via `chat.defer()`, awaited
1205
1286
  * before `onTurnComplete` fires. Reset each turn.
@@ -1296,6 +1377,27 @@ function synthesizeHandoverUIMessage(partial, messageId) {
1296
1377
  parts,
1297
1378
  };
1298
1379
  }
1380
+ /**
1381
+ * Splice a head-start handover partial into an accumulating message pair
1382
+ * (model + UI). Dedups by `messageId` against the UI chain (so a hydrated
1383
+ * history that already persisted the partial isn't doubled), then pushes the
1384
+ * partial into `modelMessages` and the synthesized UIMessage into `uiMessages`.
1385
+ * Shared by the `chat.agent` turn-0 splice and `ChatMessageAccumulator.applyHandover`.
1386
+ * @internal
1387
+ */
1388
+ function spliceHandoverPartial(modelMessages, uiMessages, signal) {
1389
+ if (!signal.partialAssistantMessage || signal.partialAssistantMessage.length === 0) {
1390
+ return;
1391
+ }
1392
+ // Skip if the hydrated chain already persisted the partial under this id.
1393
+ const alreadyInChain = signal.messageId !== undefined && uiMessages.some((m) => m.id === signal.messageId);
1394
+ if (alreadyInChain)
1395
+ return;
1396
+ modelMessages.push(...signal.partialAssistantMessage);
1397
+ const partialUI = synthesizeHandoverUIMessage(signal.partialAssistantMessage, signal.messageId);
1398
+ if (partialUI)
1399
+ uiMessages.push(partialUI);
1400
+ }
1299
1401
  /**
1300
1402
  * Per-turn background context queue. Messages added via `chat.backgroundWork.inject()`
1301
1403
  * are drained at the next `prepareStep` boundary and appended to the model messages.
@@ -2250,11 +2352,18 @@ function isCompactionSafe(messages) {
2250
2352
  }
2251
2353
  /** @internal */
2252
2354
  const chatPromptKey = locals_js_1.locals.create("chat.prompt");
2355
+ /**
2356
+ * @internal Provider options attached to the system message that
2357
+ * `toStreamTextOptions()` builds from the stored prompt — lets a provider cache
2358
+ * the system block. Stored separately so it works for both the `ResolvedPrompt`
2359
+ * and plain-string forms without mutating the prompt object.
2360
+ */
2361
+ const chatPromptProviderOptionsKey = locals_js_1.locals.create("chat.prompt.providerOptions");
2253
2362
  /**
2254
2363
  * Store a resolved prompt (or plain string) for the current run.
2255
2364
  * Call from any hook (`onPreload`, `onChatStart`, `onTurnStart`) or `run()`.
2256
2365
  */
2257
- function setChatPrompt(resolved) {
2366
+ function setChatPrompt(resolved, options) {
2258
2367
  if (typeof resolved === "string") {
2259
2368
  locals_js_1.locals.set(chatPromptKey, {
2260
2369
  text: resolved,
@@ -2271,6 +2380,9 @@ function setChatPrompt(resolved) {
2271
2380
  else {
2272
2381
  locals_js_1.locals.set(chatPromptKey, resolved);
2273
2382
  }
2383
+ // Always overwrite the slot (even with undefined) so a later prompt.set with
2384
+ // no options clears a previous prompt's cache opt-in rather than leaking it.
2385
+ locals_js_1.locals.set(chatPromptProviderOptionsKey, options?.providerOptions);
2274
2386
  }
2275
2387
  /**
2276
2388
  * Read the stored prompt. Throws if `chat.prompt.set()` has not been called.
@@ -2436,7 +2548,21 @@ function toStreamTextOptions(options) {
2436
2548
  const promptText = prompt?.text ?? "";
2437
2549
  const skillsText = skills && skills.length > 0 ? buildSkillsSystemPrompt(skills) : "";
2438
2550
  if (promptText || skillsText) {
2439
- result.system = [promptText, skillsText].filter(Boolean).join("\n\n");
2551
+ const systemText = [promptText, skillsText].filter(Boolean).join("\n\n");
2552
+ // Resolve system-prompt provider options for caching. Precedence (most
2553
+ // specific wins, no deep merge): explicit `systemProviderOptions` →
2554
+ // `cacheControl` sugar → `providerOptions` stored on `chat.prompt.set()`.
2555
+ const systemProviderOptions = options?.systemProviderOptions ??
2556
+ (options?.cacheControl
2557
+ ? { anthropic: { cacheControl: options.cacheControl } }
2558
+ : undefined) ??
2559
+ locals_js_1.locals.get(chatPromptProviderOptionsKey);
2560
+ // A bare string stays a bare string (the unchanged default). With provider
2561
+ // options, emit a structured `SystemModelMessage` so the provider can cache
2562
+ // the system block — `streamText`'s `system` accepts string | message.
2563
+ result.system = systemProviderOptions
2564
+ ? { role: "system", content: systemText, providerOptions: systemProviderOptions }
2565
+ : systemText;
2440
2566
  }
2441
2567
  // Prompt-related options (only if chat.prompt.set() was called)
2442
2568
  if (prompt) {
@@ -2618,6 +2744,7 @@ function chatCustomAgent(options) {
2618
2744
  // `chat.createStartSessionAction`) before this run is triggered.
2619
2745
  // No client-side upsert needed.
2620
2746
  locals_js_1.locals.set(chatSessionHandleKey, sessions_js_1.sessions.open(payload.chatId));
2747
+ locals_js_1.locals.set(chatExternalIdKey, payload.chatId);
2621
2748
  locals_js_1.locals.set(chatAgentRunContextKey, runOptions.ctx);
2622
2749
  // Initialize the turn-complete trim slot so `chat.writeTurnComplete`
2623
2750
  // trims `session.out` back to the previous turn boundary. Without
@@ -2627,6 +2754,10 @@ function chatCustomAgent(options) {
2627
2754
  (0, streams_js_1.markChatAgentRunForStreamsWarning)();
2628
2755
  v3_1.taskContext.setConversationId(payload.chatId);
2629
2756
  stampConversationIdOnActiveSpan(payload.chatId);
2757
+ // Seed the `.in` resume cursor before user code attaches any `.in`
2758
+ // listener — otherwise a continuation boot replays already-answered
2759
+ // messages into the loop's first wait.
2760
+ await seedSessionInResumeCursorForCustomLoop(payload);
2630
2761
  return userRun(payload, runOptions);
2631
2762
  },
2632
2763
  });
@@ -2673,6 +2804,7 @@ function chatAgent(options) {
2673
2804
  // `chat.createStartSessionAction` or browser-direct) before this
2674
2805
  // run is triggered — no client-side upsert needed here.
2675
2806
  locals_js_1.locals.set(chatSessionHandleKey, sessions_js_1.sessions.open(payload.chatId));
2807
+ locals_js_1.locals.set(chatExternalIdKey, payload.chatId);
2676
2808
  // Mutable holder; advances in `writeTurnCompleteChunk` after each turn
2677
2809
  // and is the trim target for the NEXT turn's trim record.
2678
2810
  locals_js_1.locals.set(lastTurnCompleteSeqNumKey, { value: undefined });
@@ -3882,18 +4014,10 @@ function chatAgent(options) {
3882
4014
  // `UIMessageStreamError: No tool invocation found`.
3883
4015
  const pendingHandoverPartial = locals_js_1.locals.get(chatHandoverPartialKey);
3884
4016
  if (pendingHandoverPartial && pendingHandoverPartial.length > 0) {
3885
- const handoverMessageId = locals_js_1.locals.get(chatHandoverMessageIdKey);
3886
- // Skip if the hydrated chain already persisted the
3887
- // partial under the handover messageId.
3888
- const alreadyInChain = handoverMessageId !== undefined &&
3889
- accumulatedUIMessages.some((m) => m.id === handoverMessageId);
3890
- if (!alreadyInChain) {
3891
- accumulatedMessages.push(...pendingHandoverPartial);
3892
- const partialUI = synthesizeHandoverUIMessage(pendingHandoverPartial, handoverMessageId);
3893
- if (partialUI) {
3894
- accumulatedUIMessages.push(partialUI);
3895
- }
3896
- }
4017
+ spliceHandoverPartial(accumulatedMessages, accumulatedUIMessages, {
4018
+ partialAssistantMessage: pendingHandoverPartial,
4019
+ messageId: locals_js_1.locals.get(chatHandoverMessageIdKey),
4020
+ });
3897
4021
  locals_js_1.locals.set(chatHandoverPartialKey, []); // consume once
3898
4022
  }
3899
4023
  }
@@ -5453,8 +5577,19 @@ async function pipeChatAndCapture(source, options) {
5453
5577
  const onFinishPromise = new Promise((r) => {
5454
5578
  resolveOnFinish = r;
5455
5579
  });
5580
+ const resolvedOptions = resolveUIMessageStreamOptions();
5456
5581
  const uiStream = source.toUIMessageStream({
5457
- ...resolveUIMessageStreamOptions(),
5582
+ ...resolvedOptions,
5583
+ // Thread the prior chain (incl. a spliced handover partial) so a resumed
5584
+ // tool round's tool-output chunks merge into the originating tool-call
5585
+ // instead of throwing "No tool invocation found".
5586
+ ...(options?.originalMessages ? { originalMessages: options.originalMessages } : {}),
5587
+ // Stamp a server-generated id on the start chunk, same as chat.agent's
5588
+ // pipe. Without it the AI SDK regenerates the assistant id when a
5589
+ // prepareStep injection (steering) starts a new step mid-stream, and
5590
+ // the frontend replaces the partial message — wiping the
5591
+ // pre-injection text from the UI and the captured response.
5592
+ generateMessageId: resolvedOptions.generateMessageId ?? ai_runtime_js_1.generateId,
5458
5593
  onFinish: ({ responseMessage }) => {
5459
5594
  captured = responseMessage;
5460
5595
  resolveOnFinish();
@@ -5524,10 +5659,65 @@ class ChatMessageAccumulator {
5524
5659
  this.uiMessages = [...uiMessages];
5525
5660
  this.modelMessages = await toModelMessages(uiMessages);
5526
5661
  }
5662
+ /**
5663
+ * Splice a `chat.headStart` handover partial into the accumulator (the warm
5664
+ * step-1 response). Dedups by `messageId` so a seeded/hydrated history that
5665
+ * already carries the partial isn't doubled. Seed any prior history first
5666
+ * (e.g. `setMessages(payload.headStartMessages)`). Low-level — see
5667
+ * `consumeHandover` for the wait+seed+apply convenience.
5668
+ */
5669
+ applyHandover(signal) {
5670
+ spliceHandoverPartial(this.modelMessages, this.uiMessages, signal);
5671
+ }
5672
+ /**
5673
+ * One-call `chat.headStart` handover for a custom-agent loop: waits for the
5674
+ * handover signal, seeds prior history from `payload.headStartMessages`,
5675
+ * applies the warm step-1 partial, and reports what to do next.
5676
+ *
5677
+ * Returns `{ isFinal, skipped }`:
5678
+ * - `skipped: true` — not a `handover-prepare` run, the wait idled out, or the
5679
+ * warm handler aborted. Exit the run without a turn.
5680
+ * - `isFinal: true` — step 1 IS the response (pure text). Write turn-complete
5681
+ * and continue; do not call `streamText`.
5682
+ * - `isFinal: false` — fall through to `streamText`, which runs the pending
5683
+ * tool round handed over from step 1.
5684
+ */
5685
+ async consumeHandover(options) {
5686
+ const signal = await waitForHandover({
5687
+ payload: options.payload,
5688
+ idleTimeoutInSeconds: options.idleTimeoutInSeconds,
5689
+ timeout: options.timeout,
5690
+ });
5691
+ if (!signal || signal.kind === "handover-skip") {
5692
+ return { isFinal: false, skipped: true };
5693
+ }
5694
+ if (options.payload.headStartMessages && options.payload.headStartMessages.length > 0) {
5695
+ await this.setMessages(options.payload.headStartMessages);
5696
+ }
5697
+ this.applyHandover(signal);
5698
+ return { isFinal: signal.isFinal, skipped: false };
5699
+ }
5527
5700
  async addResponse(response) {
5528
5701
  if (!response.id) {
5529
5702
  response = { ...response, id: (0, ai_runtime_js_1.generateId)() };
5530
5703
  }
5704
+ // Tool-approval and handover-resume continuations reuse the trailing
5705
+ // assistant's ID (via originalMessages on the pipe), so the captured
5706
+ // response can carry the same ID as a message already in the chain
5707
+ // (e.g. a spliced handover partial). Replace in place instead of pushing
5708
+ // a duplicate, mirroring the chat.agent accumulator.
5709
+ const existingIdx = this.uiMessages.findIndex((m) => m.id === response.id);
5710
+ if (existingIdx !== -1) {
5711
+ this.uiMessages[existingIdx] = response;
5712
+ try {
5713
+ // Reconvert all model messages since we replaced rather than appended.
5714
+ this.modelMessages = await toModelMessages(this.uiMessages.map((m) => stripProviderMetadata(m)));
5715
+ }
5716
+ catch {
5717
+ // Conversion failed — leave the existing model messages in place
5718
+ }
5719
+ return;
5720
+ }
5531
5721
  this.uiMessages.push(response);
5532
5722
  try {
5533
5723
  const msgs = await toModelMessages([stripProviderMetadata(response)]);
@@ -5660,14 +5850,18 @@ class ChatMessageAccumulator {
5660
5850
  * signaling, and idle/suspend between turns. You control: initialization,
5661
5851
  * model/tool selection, persistence, and any custom per-turn logic.
5662
5852
  *
5853
+ * Call from inside a `chat.customAgent()` run — the wrapper binds the
5854
+ * backing Session that the iterator's stop signal and message channels
5855
+ * resolve to. (A plain `task()` does not bind it, so `createSession`
5856
+ * would throw "session handle is not initialized".)
5857
+ *
5663
5858
  * @example
5664
5859
  * ```ts
5665
- * import { task } from "@trigger.dev/sdk";
5666
5860
  * import { chat, type ChatTaskWirePayload } from "@trigger.dev/sdk/ai";
5667
5861
  * import { streamText } from "ai";
5668
5862
  * import { openai } from "@ai-sdk/openai";
5669
5863
  *
5670
- * export const myChat = task({
5864
+ * export const myChat = chat.customAgent({
5671
5865
  * id: "my-chat",
5672
5866
  * run: async (payload: ChatTaskWirePayload, { signal }) => {
5673
5867
  * const session = chat.createSession(payload, { signal });
@@ -5691,13 +5885,46 @@ function createChatSession(payload, options) {
5691
5885
  [Symbol.asyncIterator]() {
5692
5886
  let currentPayload = payload;
5693
5887
  let turn = -1;
5694
- const stop = createStopSignal();
5888
+ // Created on the first next() call, AFTER the resume-cursor seed —
5889
+ // createStopSignal attaches the `.in` SSE tail, and attaching
5890
+ // before the seed replays every record from seq 0 (the seed is a
5891
+ // no-op when the chatCustomAgent wrapper already ran it).
5892
+ let stop;
5893
+ let booted = false;
5695
5894
  const accumulator = new ChatMessageAccumulator();
5696
5895
  let previousTurnUsage;
5697
5896
  let cumulativeUsage = emptyUsage();
5698
5897
  return {
5699
5898
  async next() {
5899
+ if (!booted) {
5900
+ booted = true;
5901
+ await seedSessionInResumeCursorForCustomLoop(currentPayload);
5902
+ stop = createStopSignal();
5903
+ }
5700
5904
  turn++;
5905
+ // Head-start handover: the server triggered this run with
5906
+ // `trigger: "handover-prepare"` and signals the warm step-1 partial on
5907
+ // `session.in`. Wait for it BEFORE any `messagesInput.waitWithIdleTimeout`
5908
+ // (that facade consumes-and-discards non-message chunks and would swallow
5909
+ // the signal). Turn-0 only — continuation boots never carry this trigger.
5910
+ let handoverThisTurn = null;
5911
+ let pendingHandoverSignal = null;
5912
+ if (turn === 0 && currentPayload.trigger === "handover-prepare") {
5913
+ const signal = await waitForHandover({
5914
+ payload: currentPayload,
5915
+ idleTimeoutInSeconds: sessionIdleTimeoutOpt ?? currentPayload.idleTimeoutInSeconds ?? idleTimeoutInSeconds,
5916
+ timeout,
5917
+ });
5918
+ if (!signal || signal.kind === "handover-skip" || runSignal.aborted) {
5919
+ stop.cleanup();
5920
+ return { done: true, value: undefined };
5921
+ }
5922
+ pendingHandoverSignal = signal;
5923
+ handoverThisTurn = { isFinal: signal.isFinal };
5924
+ // Rewrite to a normal first-turn message turn so the rest of the loop
5925
+ // (steering setup, addIncoming, turnObj) runs unchanged.
5926
+ currentPayload = { ...currentPayload, trigger: "submit-message", message: undefined };
5927
+ }
5701
5928
  // First turn: wait when the boot payload carries no message.
5702
5929
  // Preload boots wait for the first real message; continuation
5703
5930
  // boots (fresh run via `ensureRunForSession` / end-and-continue)
@@ -5804,6 +6031,16 @@ function createChatSession(payload, options) {
5804
6031
  ? [currentPayload.message]
5805
6032
  : [];
5806
6033
  const messages = await accumulator.addIncoming(incomingForAccumulator, currentPayload.trigger, turn);
6034
+ // Apply the head-start handover AFTER addIncoming — turn-0 addIncoming
6035
+ // replaces accumulator state, which would wipe a pre-applied splice.
6036
+ // Seed prior history first, then splice the warm step-1 partial.
6037
+ if (pendingHandoverSignal) {
6038
+ const priorHistory = currentPayload.headStartMessages;
6039
+ if (priorHistory && priorHistory.length > 0) {
6040
+ await accumulator.setMessages(priorHistory);
6041
+ }
6042
+ accumulator.applyHandover(pendingHandoverSignal);
6043
+ }
5807
6044
  // chat.requestUpgrade() called before this turn — signal transport and exit
5808
6045
  if (locals_js_1.locals.get(chatUpgradeRequestedKey)) {
5809
6046
  await writeUpgradeRequiredChunk();
@@ -5830,13 +6067,38 @@ function createChatSession(payload, options) {
5830
6067
  continuation: currentPayload.continuation ?? false,
5831
6068
  previousTurnUsage,
5832
6069
  totalUsage: cumulativeUsage,
6070
+ handover: handoverThisTurn,
5833
6071
  async setMessages(uiMessages) {
5834
6072
  await accumulator.setMessages(uiMessages);
5835
6073
  },
5836
6074
  async complete(source) {
6075
+ // Head-start final turn: the warm step-1 partial is already spliced
6076
+ // into the accumulator and IS the response — nothing to pipe. Only
6077
+ // valid on a final handover; a missing source on any other turn is a
6078
+ // mistake (it would silently finalize without an assistant response).
6079
+ if (!source) {
6080
+ if (!handoverThisTurn?.isFinal) {
6081
+ throw new Error("turn.complete() requires a stream source unless turn.handover.isFinal is true");
6082
+ }
6083
+ const response = accumulator.uiMessages.at(-1);
6084
+ if (!response || response.role !== "assistant") {
6085
+ throw new Error("turn.complete() could not find the spliced handover response");
6086
+ }
6087
+ sessionMsgSub.off();
6088
+ await chatWriteTurnComplete();
6089
+ return response;
6090
+ }
5837
6091
  let response;
5838
6092
  try {
5839
- response = await pipeChatAndCapture(source, { signal: combinedSignal });
6093
+ response = await pipeChatAndCapture(source, {
6094
+ signal: combinedSignal,
6095
+ // On a non-final handover turn, thread the spliced partial so a
6096
+ // resumed tool round's tool-output chunks merge into the
6097
+ // handed-over tool-call. Gated on the handover turn only — a
6098
+ // normal turn must not pass originalMessages (it would merge the
6099
+ // fresh response into the prior assistant message).
6100
+ ...(handoverThisTurn ? { originalMessages: accumulator.uiMessages } : {}),
6101
+ });
5840
6102
  }
5841
6103
  catch (error) {
5842
6104
  if (error instanceof Error && error.name === "AbortError") {
@@ -5991,7 +6253,8 @@ function createChatSession(payload, options) {
5991
6253
  return { done: false, value: turnObj };
5992
6254
  },
5993
6255
  async return() {
5994
- stop.cleanup();
6256
+ // `stop` only exists once next() has booted the iterator.
6257
+ stop?.cleanup();
5995
6258
  return { done: true, value: undefined };
5996
6259
  },
5997
6260
  };
@@ -6234,8 +6497,8 @@ function createChatStartSessionAction(taskId, options) {
6234
6497
  // run-list filter by chat works without the customer having to wire it
6235
6498
  // up. Mirrors the browser-mediated `TriggerChatTransport.doStart` path.
6236
6499
  const userTags = params.triggerConfig?.tags ?? options?.triggerConfig?.tags ?? [];
6237
- // Platform cap is 10 tags per run; the auto chat tag takes one slot.
6238
- const tags = [`chat:${params.chatId}`, ...userTags].slice(0, 10);
6500
+ // SessionTriggerConfig.tags allows at most 5; the auto chat tag takes one slot.
6501
+ const tags = [`chat:${params.chatId}`, ...userTags].slice(0, 5);
6239
6502
  const clientDataMetadata = params.clientData !== undefined ? { metadata: params.clientData } : {};
6240
6503
  const triggerConfig = {
6241
6504
  basePayload: {
@@ -6259,6 +6522,20 @@ function createChatStartSessionAction(taskId, options) {
6259
6522
  maxAttempts: params.triggerConfig?.maxAttempts ?? options?.triggerConfig?.maxAttempts,
6260
6523
  }
6261
6524
  : {}),
6525
+ ...(options?.triggerConfig?.maxDuration !== undefined ||
6526
+ params.triggerConfig?.maxDuration !== undefined
6527
+ ? {
6528
+ maxDuration: params.triggerConfig?.maxDuration ?? options?.triggerConfig?.maxDuration,
6529
+ }
6530
+ : {}),
6531
+ ...(options?.triggerConfig?.region || params.triggerConfig?.region
6532
+ ? { region: params.triggerConfig?.region ?? options?.triggerConfig?.region }
6533
+ : {}),
6534
+ ...(options?.triggerConfig?.lockToVersion || params.triggerConfig?.lockToVersion
6535
+ ? {
6536
+ lockToVersion: params.triggerConfig?.lockToVersion ?? options?.triggerConfig?.lockToVersion,
6537
+ }
6538
+ : {}),
6262
6539
  ...(options?.triggerConfig?.idleTimeoutInSeconds !== undefined ||
6263
6540
  params.triggerConfig?.idleTimeoutInSeconds !== undefined
6264
6541
  ? {
@@ -6437,10 +6714,20 @@ exports.chat = {
6437
6714
  MessageAccumulator: ChatMessageAccumulator,
6438
6715
  /** Create a chat session (async iterator). See {@link createChatSession}. */
6439
6716
  createSession: createChatSession,
6717
+ /**
6718
+ * Wait for a `chat.headStart` handover signal inside a `chat.customAgent`
6719
+ * loop (turn 0). See {@link waitForHandover}. For most loops prefer the
6720
+ * `chat.MessageAccumulator.consumeHandover()` convenience, which also seeds
6721
+ * `payload.headStartMessages` and applies the partial.
6722
+ */
6723
+ waitForHandover,
6440
6724
  /**
6441
6725
  * Store and retrieve a resolved prompt for the current run.
6442
6726
  *
6443
6727
  * - `chat.prompt.set(resolved)` — store a `ResolvedPrompt` or plain string
6728
+ * - `chat.prompt.set(resolved, { providerOptions })` — also attach provider
6729
+ * options to the system block so a provider can cache it (e.g. Anthropic
6730
+ * prompt caching). See the prompt-caching guide.
6444
6731
  * - `chat.prompt()` — read the stored prompt (throws if not set)
6445
6732
  */
6446
6733
  prompt: Object.assign(getChatPrompt, { set: setChatPrompt }),