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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. package/dist/commonjs/v3/ai-shared.js +1 -4
  2. package/dist/commonjs/v3/ai-shared.js.map +1 -1
  3. package/dist/commonjs/v3/ai.d.ts +178 -6
  4. package/dist/commonjs/v3/ai.js +417 -64
  5. package/dist/commonjs/v3/ai.js.map +1 -1
  6. package/dist/commonjs/v3/chat-client.js +18 -14
  7. package/dist/commonjs/v3/chat-client.js.map +1 -1
  8. package/dist/commonjs/v3/chat-react.js +1 -3
  9. package/dist/commonjs/v3/chat-react.js.map +1 -1
  10. package/dist/commonjs/v3/chat-server.d.ts +76 -0
  11. package/dist/commonjs/v3/chat-server.js +194 -24
  12. package/dist/commonjs/v3/chat-server.js.map +1 -1
  13. package/dist/commonjs/v3/chat-server.test.js +201 -6
  14. package/dist/commonjs/v3/chat-server.test.js.map +1 -1
  15. package/dist/commonjs/v3/chat.js +4 -3
  16. package/dist/commonjs/v3/chat.js.map +1 -1
  17. package/dist/commonjs/v3/chat.test.js +6 -18
  18. package/dist/commonjs/v3/chat.test.js.map +1 -1
  19. package/dist/commonjs/v3/createStartSessionAction.test.js +30 -0
  20. package/dist/commonjs/v3/createStartSessionAction.test.js.map +1 -1
  21. package/dist/commonjs/v3/idempotencyKeys.js.map +1 -1
  22. package/dist/commonjs/v3/prompt.js +2 -6
  23. package/dist/commonjs/v3/prompt.js.map +1 -1
  24. package/dist/commonjs/v3/promptManagement.js.map +1 -1
  25. package/dist/commonjs/v3/retry.js.map +1 -1
  26. package/dist/commonjs/v3/runs.d.ts +1 -1
  27. package/dist/commonjs/v3/sessions.d.ts +3 -2
  28. package/dist/commonjs/v3/sessions.js +4 -2
  29. package/dist/commonjs/v3/sessions.js.map +1 -1
  30. package/dist/commonjs/v3/shared.js.map +1 -1
  31. package/dist/commonjs/v3/skill.js.map +1 -1
  32. package/dist/commonjs/v3/streams.js +2 -3
  33. package/dist/commonjs/v3/streams.js.map +1 -1
  34. package/dist/commonjs/v3/streams.test.js.map +1 -1
  35. package/dist/commonjs/v3/test/mock-chat-agent.js +1 -4
  36. package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
  37. package/dist/commonjs/v3/test/test-session-handle.js.map +1 -1
  38. package/dist/commonjs/v3/triggerClient.test.js +1 -1
  39. package/dist/commonjs/v3/triggerClient.test.js.map +1 -1
  40. package/dist/commonjs/v3/triggerClient.types.test.js +11 -11
  41. package/dist/commonjs/v3/triggerClient.types.test.js.map +1 -1
  42. package/dist/commonjs/version.js +1 -1
  43. package/dist/esm/v3/ai-shared.js +1 -4
  44. package/dist/esm/v3/ai-shared.js.map +1 -1
  45. package/dist/esm/v3/ai.d.ts +178 -6
  46. package/dist/esm/v3/ai.js +417 -64
  47. package/dist/esm/v3/ai.js.map +1 -1
  48. package/dist/esm/v3/chat-client.js +18 -14
  49. package/dist/esm/v3/chat-client.js.map +1 -1
  50. package/dist/esm/v3/chat-react.js +1 -3
  51. package/dist/esm/v3/chat-react.js.map +1 -1
  52. package/dist/esm/v3/chat-server.d.ts +76 -0
  53. package/dist/esm/v3/chat-server.js +194 -24
  54. package/dist/esm/v3/chat-server.js.map +1 -1
  55. package/dist/esm/v3/chat-server.test.js +201 -6
  56. package/dist/esm/v3/chat-server.test.js.map +1 -1
  57. package/dist/esm/v3/chat.js +4 -3
  58. package/dist/esm/v3/chat.js.map +1 -1
  59. package/dist/esm/v3/chat.test.js +6 -18
  60. package/dist/esm/v3/chat.test.js.map +1 -1
  61. package/dist/esm/v3/createStartSessionAction.test.js +30 -0
  62. package/dist/esm/v3/createStartSessionAction.test.js.map +1 -1
  63. package/dist/esm/v3/idempotencyKeys.js +1 -1
  64. package/dist/esm/v3/idempotencyKeys.js.map +1 -1
  65. package/dist/esm/v3/prompt.js +2 -6
  66. package/dist/esm/v3/prompt.js.map +1 -1
  67. package/dist/esm/v3/promptManagement.js.map +1 -1
  68. package/dist/esm/v3/retry.js.map +1 -1
  69. package/dist/esm/v3/sessions.d.ts +3 -2
  70. package/dist/esm/v3/sessions.js +4 -2
  71. package/dist/esm/v3/sessions.js.map +1 -1
  72. package/dist/esm/v3/shared.js.map +1 -1
  73. package/dist/esm/v3/skill.js.map +1 -1
  74. package/dist/esm/v3/streams.js +2 -3
  75. package/dist/esm/v3/streams.js.map +1 -1
  76. package/dist/esm/v3/streams.test.js.map +1 -1
  77. package/dist/esm/v3/test/mock-chat-agent.js +4 -7
  78. package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
  79. package/dist/esm/v3/test/test-session-handle.js.map +1 -1
  80. package/dist/esm/v3/triggerClient.test.js +1 -1
  81. package/dist/esm/v3/triggerClient.test.js.map +1 -1
  82. package/dist/esm/v3/triggerClient.types.test.js +11 -11
  83. package/dist/esm/v3/triggerClient.types.test.js.map +1 -1
  84. package/dist/esm/version.js +1 -1
  85. package/docs/ai/prompts.mdx +426 -0
  86. package/docs/ai-chat/actions.mdx +111 -0
  87. package/docs/ai-chat/anatomy.mdx +67 -0
  88. package/docs/ai-chat/backend.mdx +813 -0
  89. package/docs/ai-chat/background-injection.mdx +217 -0
  90. package/docs/ai-chat/changelog.mdx +958 -0
  91. package/docs/ai-chat/chat-local.mdx +170 -0
  92. package/docs/ai-chat/client-protocol.mdx +1077 -0
  93. package/docs/ai-chat/compaction.mdx +407 -0
  94. package/docs/ai-chat/custom-agents.mdx +392 -0
  95. package/docs/ai-chat/error-handling.mdx +411 -0
  96. package/docs/ai-chat/fast-starts.mdx +754 -0
  97. package/docs/ai-chat/frontend.mdx +576 -0
  98. package/docs/ai-chat/how-it-works.mdx +226 -0
  99. package/docs/ai-chat/lifecycle-hooks.mdx +526 -0
  100. package/docs/ai-chat/mcp.mdx +97 -0
  101. package/docs/ai-chat/overview.mdx +86 -0
  102. package/docs/ai-chat/patterns/branching-conversations.mdx +280 -0
  103. package/docs/ai-chat/patterns/code-sandbox.mdx +122 -0
  104. package/docs/ai-chat/patterns/database-persistence.mdx +410 -0
  105. package/docs/ai-chat/patterns/human-in-the-loop.mdx +279 -0
  106. package/docs/ai-chat/patterns/large-payloads.mdx +165 -0
  107. package/docs/ai-chat/patterns/oom-resilience.mdx +116 -0
  108. package/docs/ai-chat/patterns/persistence-and-replay.mdx +207 -0
  109. package/docs/ai-chat/patterns/recovery-boot.mdx +226 -0
  110. package/docs/ai-chat/patterns/skills.mdx +217 -0
  111. package/docs/ai-chat/patterns/sub-agents.mdx +379 -0
  112. package/docs/ai-chat/patterns/tool-result-auditing.mdx +144 -0
  113. package/docs/ai-chat/patterns/trusted-edge-signals.mdx +333 -0
  114. package/docs/ai-chat/patterns/version-upgrades.mdx +168 -0
  115. package/docs/ai-chat/pending-messages.mdx +339 -0
  116. package/docs/ai-chat/prompt-caching.mdx +206 -0
  117. package/docs/ai-chat/quick-start.mdx +157 -0
  118. package/docs/ai-chat/reference.mdx +905 -0
  119. package/docs/ai-chat/server-chat.mdx +259 -0
  120. package/docs/ai-chat/sessions.mdx +329 -0
  121. package/docs/ai-chat/testing.mdx +678 -0
  122. package/docs/ai-chat/tools.mdx +187 -0
  123. package/docs/ai-chat/types.mdx +238 -0
  124. package/docs/ai-chat/upgrade-guide.mdx +511 -0
  125. package/docs/apikeys.mdx +54 -0
  126. package/docs/building-with-ai.mdx +261 -0
  127. package/docs/bulk-actions.mdx +49 -0
  128. package/docs/changelog.mdx +6 -0
  129. package/docs/cli-deploy-commands.mdx +9 -0
  130. package/docs/cli-dev-commands.mdx +9 -0
  131. package/docs/cli-init-commands.mdx +58 -0
  132. package/docs/cli-introduction.mdx +25 -0
  133. package/docs/cli-list-profiles-commands.mdx +42 -0
  134. package/docs/cli-login-commands.mdx +33 -0
  135. package/docs/cli-logout-commands.mdx +33 -0
  136. package/docs/cli-preview-archive.mdx +59 -0
  137. package/docs/cli-promote-commands.mdx +9 -0
  138. package/docs/cli-switch.mdx +43 -0
  139. package/docs/cli-update-commands.mdx +42 -0
  140. package/docs/cli-whoami-commands.mdx +33 -0
  141. package/docs/community.mdx +6 -0
  142. package/docs/config/config-file.mdx +602 -0
  143. package/docs/config/extensions/additionalFiles.mdx +38 -0
  144. package/docs/config/extensions/additionalPackages.mdx +40 -0
  145. package/docs/config/extensions/aptGet.mdx +34 -0
  146. package/docs/config/extensions/audioWaveform.mdx +20 -0
  147. package/docs/config/extensions/custom.mdx +380 -0
  148. package/docs/config/extensions/emitDecoratorMetadata.mdx +29 -0
  149. package/docs/config/extensions/esbuildPlugin.mdx +31 -0
  150. package/docs/config/extensions/ffmpeg.mdx +45 -0
  151. package/docs/config/extensions/lightpanda.mdx +56 -0
  152. package/docs/config/extensions/overview.mdx +67 -0
  153. package/docs/config/extensions/playwright.mdx +195 -0
  154. package/docs/config/extensions/prismaExtension.mdx +1014 -0
  155. package/docs/config/extensions/puppeteer.mdx +30 -0
  156. package/docs/config/extensions/pythonExtension.mdx +182 -0
  157. package/docs/config/extensions/syncEnvVars.mdx +291 -0
  158. package/docs/context.mdx +235 -0
  159. package/docs/database-connections.mdx +213 -0
  160. package/docs/deploy-environment-variables.mdx +435 -0
  161. package/docs/deployment/atomic-deployment.mdx +172 -0
  162. package/docs/deployment/dev-branches.mdx +92 -0
  163. package/docs/deployment/overview.mdx +257 -0
  164. package/docs/deployment/preview-branches.mdx +224 -0
  165. package/docs/errors-retrying.mdx +379 -0
  166. package/docs/github-actions.mdx +222 -0
  167. package/docs/github-integration.mdx +136 -0
  168. package/docs/github-repo.mdx +8 -0
  169. package/docs/help-email.mdx +6 -0
  170. package/docs/help-slack.mdx +12 -0
  171. package/docs/hidden-tasks.mdx +56 -0
  172. package/docs/how-it-works.mdx +454 -0
  173. package/docs/how-to-reduce-your-spend.mdx +224 -0
  174. package/docs/idempotency.mdx +504 -0
  175. package/docs/introduction.mdx +223 -0
  176. package/docs/limits.mdx +241 -0
  177. package/docs/logging.mdx +195 -0
  178. package/docs/machines.mdx +952 -0
  179. package/docs/manual-setup.mdx +632 -0
  180. package/docs/mcp-agent-rules.mdx +41 -0
  181. package/docs/mcp-introduction.mdx +385 -0
  182. package/docs/mcp-tools.mdx +273 -0
  183. package/docs/migrating-from-v3.mdx +334 -0
  184. package/docs/observability/dashboards.mdx +102 -0
  185. package/docs/observability/query.mdx +585 -0
  186. package/docs/open-source-contributing.mdx +16 -0
  187. package/docs/open-source-self-hosting.mdx +541 -0
  188. package/docs/private-networking/aws-console-setup.mdx +304 -0
  189. package/docs/private-networking/overview.mdx +144 -0
  190. package/docs/private-networking/troubleshooting.mdx +78 -0
  191. package/docs/queue-concurrency.mdx +354 -0
  192. package/docs/quick-start.mdx +97 -0
  193. package/docs/realtime/auth.mdx +208 -0
  194. package/docs/realtime/backend/overview.mdx +45 -0
  195. package/docs/realtime/backend/streams.mdx +418 -0
  196. package/docs/realtime/backend/subscribe.mdx +225 -0
  197. package/docs/realtime/how-it-works.mdx +94 -0
  198. package/docs/realtime/overview.mdx +63 -0
  199. package/docs/realtime/react-hooks/overview.mdx +73 -0
  200. package/docs/realtime/react-hooks/streams.mdx +449 -0
  201. package/docs/realtime/react-hooks/subscribe.mdx +674 -0
  202. package/docs/realtime/react-hooks/swr.mdx +87 -0
  203. package/docs/realtime/react-hooks/triggering.mdx +194 -0
  204. package/docs/realtime/react-hooks/use-wait-token.mdx +34 -0
  205. package/docs/realtime/run-object.mdx +174 -0
  206. package/docs/replaying.mdx +72 -0
  207. package/docs/request-feature.mdx +6 -0
  208. package/docs/roadmap.mdx +6 -0
  209. package/docs/run-tests.mdx +20 -0
  210. package/docs/run-usage.mdx +113 -0
  211. package/docs/runs/heartbeats.mdx +38 -0
  212. package/docs/runs/max-duration.mdx +139 -0
  213. package/docs/runs/metadata.mdx +734 -0
  214. package/docs/runs/priority.mdx +31 -0
  215. package/docs/runs.mdx +396 -0
  216. package/docs/self-hosting/docker.mdx +458 -0
  217. package/docs/self-hosting/env/supervisor.mdx +74 -0
  218. package/docs/self-hosting/env/webapp.mdx +276 -0
  219. package/docs/self-hosting/kubernetes.mdx +601 -0
  220. package/docs/self-hosting/overview.mdx +109 -0
  221. package/docs/skills.mdx +85 -0
  222. package/docs/tags.mdx +120 -0
  223. package/docs/tasks/overview.mdx +697 -0
  224. package/docs/tasks/scheduled.mdx +382 -0
  225. package/docs/tasks/schemaTask.mdx +413 -0
  226. package/docs/tasks/streams.mdx +884 -0
  227. package/docs/triggering.mdx +1320 -0
  228. package/docs/troubleshooting-alerts.mdx +385 -0
  229. package/docs/troubleshooting-debugging-in-vscode.mdx +9 -0
  230. package/docs/troubleshooting-github-issues.mdx +6 -0
  231. package/docs/troubleshooting-uptime-status.mdx +7 -0
  232. package/docs/troubleshooting.mdx +398 -0
  233. package/docs/upgrading-packages.mdx +80 -0
  234. package/docs/vercel-integration.mdx +207 -0
  235. package/docs/versioning.mdx +56 -0
  236. package/docs/video-walkthrough.mdx +23 -0
  237. package/docs/wait-for-token.mdx +540 -0
  238. package/docs/wait-for.mdx +42 -0
  239. package/docs/wait-until.mdx +53 -0
  240. package/docs/wait.mdx +18 -0
  241. package/docs/writing-tasks-introduction.mdx +33 -0
  242. package/package.json +8 -5
  243. package/skills/trigger-authoring-chat-agent/SKILL.md +296 -0
  244. package/skills/trigger-authoring-tasks/SKILL.md +254 -0
  245. package/skills/trigger-chat-agent-advanced/SKILL.md +368 -0
  246. package/skills/trigger-cost-savings/SKILL.md +116 -0
  247. package/skills/trigger-realtime-and-frontend/SKILL.md +276 -0
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,45 @@ 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", {
145
+ error: error instanceof Error ? error.message : String(error),
146
+ });
147
+ }
148
+ }
106
149
  let readChatSnapshotImpl;
107
150
  export function __setReadChatSnapshotImplForTests(impl) {
108
151
  readChatSnapshotImpl = impl;
@@ -654,6 +697,16 @@ function createTaskToolExecuteHandler(task) {
654
697
  toolMeta.continuation = chatCtx.continuation;
655
698
  toolMeta.clientData = chatCtx.clientData;
656
699
  }
700
+ else {
701
+ // Hand-rolled chat.customAgent loops never set per-turn context, but
702
+ // the wrapper records the boot payload's external chatId at run boot
703
+ // — thread it so subtask chat helpers (`chat.stream.writer` with
704
+ // target "root") can open the parent's session.
705
+ const chatExternalId = locals.get(chatExternalIdKey);
706
+ if (chatExternalId) {
707
+ toolMeta.chatId = chatExternalId;
708
+ }
709
+ }
657
710
  const chatLocals = {};
658
711
  for (const entry of chatLocalRegistry) {
659
712
  const value = locals.get(entry.key);
@@ -1176,8 +1229,7 @@ const handoverInput = {
1176
1229
  const result = await getChatSession().in.waitWithIdleTimeout(options);
1177
1230
  if (!result.ok)
1178
1231
  return result;
1179
- if (result.output.kind === "handover" ||
1180
- result.output.kind === "handover-skip") {
1232
+ if (result.output.kind === "handover" || result.output.kind === "handover-skip") {
1181
1233
  return { ok: true, output: result.output };
1182
1234
  }
1183
1235
  // Other kinds (message, stop) are not expected during handover-prepare.
@@ -1186,6 +1238,36 @@ const handoverInput = {
1186
1238
  }
1187
1239
  },
1188
1240
  };
1241
+ /**
1242
+ * Wait for a `chat.headStart` handover signal inside a custom-agent loop or
1243
+ * `chat.createSession`. Returns:
1244
+ * - `null` — this run is not a `handover-prepare` boot, or the wait idled out /
1245
+ * the warm handler crashed before signaling. Treat as "no handover".
1246
+ * - `{ kind: "handover-skip" }` — the warm handler aborted; exit without a turn.
1247
+ * - `{ kind: "handover", partialAssistantMessage, messageId?, isFinal }` — splice
1248
+ * the partial (`chat.MessageAccumulator.applyHandover`) and, when `isFinal` is
1249
+ * false, fall through to `streamText` to run the handed-over tool round.
1250
+ *
1251
+ * For the common case prefer `accumulator.consumeHandover()`, which also seeds
1252
+ * `payload.headStartMessages` and applies the partial for you.
1253
+ *
1254
+ * Must be called at turn 0 before any `chat.messages.waitWithIdleTimeout` —
1255
+ * that facade consumes and discards non-message chunks, which would swallow the
1256
+ * handover signal.
1257
+ */
1258
+ async function waitForHandover(options) {
1259
+ if (options.payload.trigger !== "handover-prepare")
1260
+ return null;
1261
+ const result = await handoverInput.waitWithIdleTimeout({
1262
+ idleTimeoutInSeconds: options.idleTimeoutInSeconds ?? options.payload.idleTimeoutInSeconds ?? 60,
1263
+ timeout: options.timeout,
1264
+ spanName: options.spanName ?? "waiting for handover signal",
1265
+ });
1266
+ // Non-ok = idle timeout or the warm handler crashed without signaling.
1267
+ if (!result.ok)
1268
+ return null;
1269
+ return result.output;
1270
+ }
1189
1271
  /**
1190
1272
  * Per-turn deferred promises. Registered via `chat.defer()`, awaited
1191
1273
  * before `onTurnComplete` fires. Reset each turn.
@@ -1282,6 +1364,27 @@ function synthesizeHandoverUIMessage(partial, messageId) {
1282
1364
  parts,
1283
1365
  };
1284
1366
  }
1367
+ /**
1368
+ * Splice a head-start handover partial into an accumulating message pair
1369
+ * (model + UI). Dedups by `messageId` against the UI chain (so a hydrated
1370
+ * history that already persisted the partial isn't doubled), then pushes the
1371
+ * partial into `modelMessages` and the synthesized UIMessage into `uiMessages`.
1372
+ * Shared by the `chat.agent` turn-0 splice and `ChatMessageAccumulator.applyHandover`.
1373
+ * @internal
1374
+ */
1375
+ function spliceHandoverPartial(modelMessages, uiMessages, signal) {
1376
+ if (!signal.partialAssistantMessage || signal.partialAssistantMessage.length === 0) {
1377
+ return;
1378
+ }
1379
+ // Skip if the hydrated chain already persisted the partial under this id.
1380
+ const alreadyInChain = signal.messageId !== undefined && uiMessages.some((m) => m.id === signal.messageId);
1381
+ if (alreadyInChain)
1382
+ return;
1383
+ modelMessages.push(...signal.partialAssistantMessage);
1384
+ const partialUI = synthesizeHandoverUIMessage(signal.partialAssistantMessage, signal.messageId);
1385
+ if (partialUI)
1386
+ uiMessages.push(partialUI);
1387
+ }
1285
1388
  /**
1286
1389
  * Per-turn background context queue. Messages added via `chat.backgroundWork.inject()`
1287
1390
  * are drained at the next `prepareStep` boundary and appended to the model messages.
@@ -1837,6 +1940,55 @@ function isStepBoundarySafe(step) {
1837
1940
  .map((p) => p.toolCallId));
1838
1941
  return ![...callIds].some((id) => !settledIds.has(id));
1839
1942
  }
1943
+ /**
1944
+ * True when a model message is a `tool` message carrying a
1945
+ * `tool-approval-response` part — the trailing row a head-start handover
1946
+ * reshapes a pending first-turn tool call into. AI SDK's `collectToolApprovals`
1947
+ * only inspects the conversation's last message, so this row must survive to
1948
+ * `streamText` intact for the agent to execute the handed-over call.
1949
+ * @internal
1950
+ */
1951
+ function hasToolApprovalResponse(message) {
1952
+ return (message?.role === "tool" &&
1953
+ Array.isArray(message.content) &&
1954
+ message.content.some((part) => part != null &&
1955
+ typeof part === "object" &&
1956
+ part.type === "tool-approval-response"));
1957
+ }
1958
+ /**
1959
+ * Keep a head-start handover's tool-approval tail intact across `prepareMessages`.
1960
+ *
1961
+ * The handover reshapes the warm step-1's pending tool call into AI SDK's
1962
+ * tool-approval round: a `tool-approval-request` on the assistant plus a
1963
+ * trailing `tool` message with `tool-approval-response { approved: true }`. The
1964
+ * agent's next `streamText` runs `collectToolApprovals`, which ONLY looks at the
1965
+ * last message — so that tool row must stay last and unmodified for the pending
1966
+ * call to execute. A user `prepareMessages` hook that rewrites or drops the last
1967
+ * message (e.g. rolling a provider cache breakpoint onto it) silently breaks the
1968
+ * resume: the agent sends a bare `tool_use` and the turn dies with
1969
+ * "tool_use ids were found without tool_result". If the hook's input ended with
1970
+ * that approval tail, re-assert the original tail as the last message.
1971
+ *
1972
+ * No-op for every normal turn — only fires when the input genuinely ended with a
1973
+ * pending tool-approval response (i.e. a head-start resume).
1974
+ * @internal
1975
+ */
1976
+ function preserveToolApprovalTail(original, prepared) {
1977
+ const originalTail = original[original.length - 1];
1978
+ if (!hasToolApprovalResponse(originalTail))
1979
+ return prepared;
1980
+ // Hook left the exact tail object in place — nothing to do.
1981
+ if (prepared[prepared.length - 1] === originalTail)
1982
+ return prepared;
1983
+ // Otherwise drop only the trailing approval tail the hook produced (the
1984
+ // original moved, or a rewritten copy) and re-append the original so it is
1985
+ // last and intact. Older approval rounds deeper in history must survive.
1986
+ const withoutMovedOriginal = prepared.filter((m) => m !== originalTail);
1987
+ while (hasToolApprovalResponse(withoutMovedOriginal[withoutMovedOriginal.length - 1])) {
1988
+ withoutMovedOriginal.pop();
1989
+ }
1990
+ return [...withoutMovedOriginal, originalTail];
1991
+ }
1840
1992
  /**
1841
1993
  * Apply the prepareMessages hook if one is set in locals.
1842
1994
  * @internal
@@ -1846,7 +1998,7 @@ async function applyPrepareMessages(messages, reason) {
1846
1998
  if (!hook)
1847
1999
  return messages;
1848
2000
  const turnCtx = locals.get(chatTurnContextKey);
1849
- return tracer.startActiveSpan("prepareMessages()", async () => {
2001
+ const prepared = await tracer.startActiveSpan("prepareMessages()", async () => {
1850
2002
  return hook({
1851
2003
  messages,
1852
2004
  reason,
@@ -1862,6 +2014,9 @@ async function applyPrepareMessages(messages, reason) {
1862
2014
  "chat.prepareMessages.messageCount": messages.length,
1863
2015
  },
1864
2016
  });
2017
+ // A user hook must never be able to break the head-start handover resume by
2018
+ // disturbing the trailing tool-approval row (see preserveToolApprovalTail).
2019
+ return preserveToolApprovalTail(messages, prepared);
1865
2020
  }
1866
2021
  /**
1867
2022
  * Resolve the `tools` option into a concrete `ToolSet` and cache it in locals so
@@ -2234,11 +2389,18 @@ function isCompactionSafe(messages) {
2234
2389
  }
2235
2390
  /** @internal */
2236
2391
  const chatPromptKey = locals.create("chat.prompt");
2392
+ /**
2393
+ * @internal Provider options attached to the system message that
2394
+ * `toStreamTextOptions()` builds from the stored prompt — lets a provider cache
2395
+ * the system block. Stored separately so it works for both the `ResolvedPrompt`
2396
+ * and plain-string forms without mutating the prompt object.
2397
+ */
2398
+ const chatPromptProviderOptionsKey = locals.create("chat.prompt.providerOptions");
2237
2399
  /**
2238
2400
  * Store a resolved prompt (or plain string) for the current run.
2239
2401
  * Call from any hook (`onPreload`, `onChatStart`, `onTurnStart`) or `run()`.
2240
2402
  */
2241
- function setChatPrompt(resolved) {
2403
+ function setChatPrompt(resolved, options) {
2242
2404
  if (typeof resolved === "string") {
2243
2405
  locals.set(chatPromptKey, {
2244
2406
  text: resolved,
@@ -2255,6 +2417,9 @@ function setChatPrompt(resolved) {
2255
2417
  else {
2256
2418
  locals.set(chatPromptKey, resolved);
2257
2419
  }
2420
+ // Always overwrite the slot (even with undefined) so a later prompt.set with
2421
+ // no options clears a previous prompt's cache opt-in rather than leaking it.
2422
+ locals.set(chatPromptProviderOptionsKey, options?.providerOptions);
2258
2423
  }
2259
2424
  /**
2260
2425
  * Read the stored prompt. Throws if `chat.prompt.set()` has not been called.
@@ -2420,7 +2585,21 @@ function toStreamTextOptions(options) {
2420
2585
  const promptText = prompt?.text ?? "";
2421
2586
  const skillsText = skills && skills.length > 0 ? buildSkillsSystemPrompt(skills) : "";
2422
2587
  if (promptText || skillsText) {
2423
- result.system = [promptText, skillsText].filter(Boolean).join("\n\n");
2588
+ const systemText = [promptText, skillsText].filter(Boolean).join("\n\n");
2589
+ // Resolve system-prompt provider options for caching. Precedence (most
2590
+ // specific wins, no deep merge): explicit `systemProviderOptions` →
2591
+ // `cacheControl` sugar → `providerOptions` stored on `chat.prompt.set()`.
2592
+ const systemProviderOptions = options?.systemProviderOptions ??
2593
+ (options?.cacheControl
2594
+ ? { anthropic: { cacheControl: options.cacheControl } }
2595
+ : undefined) ??
2596
+ locals.get(chatPromptProviderOptionsKey);
2597
+ // A bare string stays a bare string (the unchanged default). With provider
2598
+ // options, emit a structured `SystemModelMessage` so the provider can cache
2599
+ // the system block — `streamText`'s `system` accepts string | message.
2600
+ result.system = systemProviderOptions
2601
+ ? { role: "system", content: systemText, providerOptions: systemProviderOptions }
2602
+ : systemText;
2424
2603
  }
2425
2604
  // Prompt-related options (only if chat.prompt.set() was called)
2426
2605
  if (prompt) {
@@ -2602,6 +2781,7 @@ function chatCustomAgent(options) {
2602
2781
  // `chat.createStartSessionAction`) before this run is triggered.
2603
2782
  // No client-side upsert needed.
2604
2783
  locals.set(chatSessionHandleKey, sessions.open(payload.chatId));
2784
+ locals.set(chatExternalIdKey, payload.chatId);
2605
2785
  locals.set(chatAgentRunContextKey, runOptions.ctx);
2606
2786
  // Initialize the turn-complete trim slot so `chat.writeTurnComplete`
2607
2787
  // trims `session.out` back to the previous turn boundary. Without
@@ -2611,6 +2791,10 @@ function chatCustomAgent(options) {
2611
2791
  markChatAgentRunForStreamsWarning();
2612
2792
  taskContext.setConversationId(payload.chatId);
2613
2793
  stampConversationIdOnActiveSpan(payload.chatId);
2794
+ // Seed the `.in` resume cursor before user code attaches any `.in`
2795
+ // listener — otherwise a continuation boot replays already-answered
2796
+ // messages into the loop's first wait.
2797
+ await seedSessionInResumeCursorForCustomLoop(payload);
2614
2798
  return userRun(payload, runOptions);
2615
2799
  },
2616
2800
  });
@@ -2657,6 +2841,7 @@ function chatAgent(options) {
2657
2841
  // `chat.createStartSessionAction` or browser-direct) before this
2658
2842
  // run is triggered — no client-side upsert needed here.
2659
2843
  locals.set(chatSessionHandleKey, sessions.open(payload.chatId));
2844
+ locals.set(chatExternalIdKey, payload.chatId);
2660
2845
  // Mutable holder; advances in `writeTurnCompleteChunk` after each turn
2661
2846
  // and is the trim target for the NEXT turn's trim record.
2662
2847
  locals.set(lastTurnCompleteSeqNumKey, { value: undefined });
@@ -2780,7 +2965,9 @@ function chatAgent(options) {
2780
2965
  const replayOutPhase = async () => {
2781
2966
  const replayOutStart = Date.now();
2782
2967
  try {
2783
- const replayResult = await replaySessionOutTail(sessionIdForSnapshot, { lastEventId: bootSnapshot?.lastOutEventId });
2968
+ const replayResult = await replaySessionOutTail(sessionIdForSnapshot, {
2969
+ lastEventId: bootSnapshot?.lastOutEventId,
2970
+ });
2784
2971
  replayedSettled = replayResult.settled;
2785
2972
  replayedPartial = replayResult.partial;
2786
2973
  replayedPartialRaw = replayResult.partialRaw;
@@ -2877,9 +3064,7 @@ function chatAgent(options) {
2877
3064
  // crashed / was canceled / requested upgrade
2878
3065
  // - Snapshot exists at all (catches edge cases where the wire
2879
3066
  // didn't set `continuation` but a snapshot indicates prior turns)
2880
- const needsResumeCursor = ctx.attempt.number > 1 ||
2881
- payload.continuation === true ||
2882
- bootSnapshot !== undefined;
3067
+ const needsResumeCursor = ctx.attempt.number > 1 || payload.continuation === true || bootSnapshot !== undefined;
2883
3068
  if (needsResumeCursor) {
2884
3069
  try {
2885
3070
  // Reuse the cursor the boot block already resolved (snapshot
@@ -3226,9 +3411,7 @@ function chatAgent(options) {
3226
3411
  // Wait for the first real message — task-level idle settings win over
3227
3412
  // `transport.preload(..., { idleTimeoutInSeconds })` / wire payload so
3228
3413
  // `chat.agent({ idleTimeoutInSeconds, preloadIdleTimeoutInSeconds })` is authoritative.
3229
- const effectivePreloadIdleTimeout = preloadIdleTimeoutInSeconds ??
3230
- idleTimeoutInSeconds ??
3231
- payload.idleTimeoutInSeconds;
3414
+ const effectivePreloadIdleTimeout = preloadIdleTimeoutInSeconds ?? idleTimeoutInSeconds ?? payload.idleTimeoutInSeconds;
3232
3415
  const effectivePreloadTimeout = metadata.get(TURN_TIMEOUT_METADATA_KEY) ??
3233
3416
  preloadTimeout ??
3234
3417
  turnTimeout;
@@ -3473,7 +3656,9 @@ function chatAgent(options) {
3473
3656
  if (lastUserMessage) {
3474
3657
  turnAttributes["chat.user_message"] = lastUserMessage;
3475
3658
  // Show a truncated preview of the user message as an accessory
3476
- const preview = lastUserMessage.length > 80 ? lastUserMessage.slice(0, 80) + "..." : lastUserMessage;
3659
+ const preview = lastUserMessage.length > 80
3660
+ ? lastUserMessage.slice(0, 80) + "..."
3661
+ : lastUserMessage;
3477
3662
  Object.assign(turnAttributes, accessoryAttributes({
3478
3663
  items: [{ text: preview, variant: "normal" }],
3479
3664
  style: "codepath",
@@ -3760,8 +3945,7 @@ function chatAgent(options) {
3760
3945
  if (currentWirePayload.trigger === "submit-message" &&
3761
3946
  cleanedUIMessages.length > 0) {
3762
3947
  const lastUI = cleanedUIMessages[cleanedUIMessages.length - 1];
3763
- const matchedExisting = lastUI.id !== undefined &&
3764
- previouslyKnownMessageIds.has(lastUI.id);
3948
+ const matchedExisting = lastUI.id !== undefined && previouslyKnownMessageIds.has(lastUI.id);
3765
3949
  if (!matchedExisting) {
3766
3950
  turnNewUIMessages.push(lastUI);
3767
3951
  const lastModel = (await toModelMessages([lastUI]))[0];
@@ -3866,18 +4050,10 @@ function chatAgent(options) {
3866
4050
  // `UIMessageStreamError: No tool invocation found`.
3867
4051
  const pendingHandoverPartial = locals.get(chatHandoverPartialKey);
3868
4052
  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
- }
4053
+ spliceHandoverPartial(accumulatedMessages, accumulatedUIMessages, {
4054
+ partialAssistantMessage: pendingHandoverPartial,
4055
+ messageId: locals.get(chatHandoverMessageIdKey),
4056
+ });
3881
4057
  locals.set(chatHandoverPartialKey, []); // consume once
3882
4058
  }
3883
4059
  }
@@ -4104,7 +4280,8 @@ function chatAgent(options) {
4104
4280
  // We call toUIMessageStream ourselves to attach onFinish for response capture.
4105
4281
  // Pass originalMessages so the AI SDK reuses message IDs across turns
4106
4282
  // (e.g. for tool approval continuations / HITL flows).
4107
- if ((locals.get(chatPipeCountKey) ?? 0) === 0 && isUIMessageStreamable(runResult)) {
4283
+ if ((locals.get(chatPipeCountKey) ?? 0) === 0 &&
4284
+ isUIMessageStreamable(runResult)) {
4108
4285
  onFinishAttached = true;
4109
4286
  const resolvedOptions = resolveUIMessageStreamOptions();
4110
4287
  // For action turns, don't pass originalMessages: the response
@@ -4117,9 +4294,7 @@ function chatAgent(options) {
4117
4294
  // Pass originalMessages so the AI SDK reuses message IDs across
4118
4295
  // turns (e.g. for tool approval continuations / HITL flows).
4119
4296
  // Omit for action turns to force a fresh response ID.
4120
- ...(isActionTurn
4121
- ? {}
4122
- : { originalMessages: accumulatedUIMessages }),
4297
+ ...(isActionTurn ? {} : { originalMessages: accumulatedUIMessages }),
4123
4298
  // Always provide generateMessageId so the start chunk carries a
4124
4299
  // messageId. Without this, the frontend and backend generate IDs
4125
4300
  // independently and they won't match for ID-based dedup.
@@ -4130,7 +4305,10 @@ function chatAgent(options) {
4130
4305
  resolveOnFinish();
4131
4306
  },
4132
4307
  });
4133
- await pipeChat(uiStream, { signal: combinedSignal, spanName: "stream response" });
4308
+ await pipeChat(uiStream, {
4309
+ signal: combinedSignal,
4310
+ spanName: "stream response",
4311
+ });
4134
4312
  }
4135
4313
  }
4136
4314
  catch (error) {
@@ -4165,7 +4343,8 @@ function chatAgent(options) {
4165
4343
  // never reports final usage), which would block the turn loop
4166
4344
  // from ever firing onTurnComplete / writeTurnComplete.
4167
4345
  let turnUsage;
4168
- if (runResult != null && typeof runResult.totalUsage?.then === "function") {
4346
+ if (runResult != null &&
4347
+ typeof runResult.totalUsage?.then === "function") {
4169
4348
  try {
4170
4349
  turnUsage = (await Promise.race([
4171
4350
  runResult.totalUsage,
@@ -4251,7 +4430,10 @@ function chatAgent(options) {
4251
4430
  // may produce a message with an empty ID since IDs are normally
4252
4431
  // assigned by the frontend's useChat).
4253
4432
  if (!capturedResponseMessage.id) {
4254
- capturedResponseMessage = { ...capturedResponseMessage, id: generateMessageId() };
4433
+ capturedResponseMessage = {
4434
+ ...capturedResponseMessage,
4435
+ id: generateMessageId(),
4436
+ };
4255
4437
  }
4256
4438
  // Append any non-transient data parts queued via chat.response or writer.write()
4257
4439
  const queuedParts = locals.get(chatResponsePartsKey);
@@ -4552,7 +4734,9 @@ function chatAgent(options) {
4552
4734
  ? { "gen_ai.usage.total_tokens": turnUsage.totalTokens }
4553
4735
  : {}),
4554
4736
  ...(cumulativeUsage.totalTokens != null
4555
- ? { "gen_ai.usage.cumulative_total_tokens": cumulativeUsage.totalTokens }
4737
+ ? {
4738
+ "gen_ai.usage.cumulative_total_tokens": cumulativeUsage.totalTokens,
4739
+ }
4556
4740
  : {}),
4557
4741
  },
4558
4742
  });
@@ -4584,9 +4768,7 @@ function chatAgent(options) {
4584
4768
  savedAt: Date.now(),
4585
4769
  messages: accumulatedUIMessages,
4586
4770
  lastOutEventId: turnCompleteResult?.lastEventId,
4587
- lastInEventId: snapshotInCursor !== undefined
4588
- ? String(snapshotInCursor)
4589
- : undefined,
4771
+ lastInEventId: snapshotInCursor !== undefined ? String(snapshotInCursor) : undefined,
4590
4772
  });
4591
4773
  }, {
4592
4774
  attributes: {
@@ -4628,8 +4810,7 @@ function chatAgent(options) {
4628
4810
  // chat.requestUpgrade() was called — exit the loop so the
4629
4811
  // transport triggers a new run on the latest version.
4630
4812
  // chat.endRun() — same exit, no upgrade semantics.
4631
- if (locals.get(chatUpgradeRequestedKey) ||
4632
- locals.get(chatEndRunRequestedKey)) {
4813
+ if (locals.get(chatUpgradeRequestedKey) || locals.get(chatEndRunRequestedKey)) {
4633
4814
  return "exit";
4634
4815
  }
4635
4816
  // Wait for the next message — stay idle briefly, then suspend
@@ -4709,7 +4890,9 @@ function chatAgent(options) {
4709
4890
  // Turn error handler: write an error chunk + turn-complete to the stream
4710
4891
  // so the client sees the error, then wait for the next message instead
4711
4892
  // of killing the entire run. This keeps the conversation alive.
4712
- if (turnError instanceof Error && turnError.name === "AbortError" && runSignal.aborted) {
4893
+ if (turnError instanceof Error &&
4894
+ turnError.name === "AbortError" &&
4895
+ runSignal.aborted) {
4713
4896
  // Full run cancellation — exit immediately
4714
4897
  throw turnError;
4715
4898
  }
@@ -4805,9 +4988,7 @@ function chatAgent(options) {
4805
4988
  savedAt: Date.now(),
4806
4989
  messages: erroredUIMessages,
4807
4990
  lastOutEventId: errorTurnCompleteResult?.lastEventId,
4808
- lastInEventId: errorSnapshotInCursor !== undefined
4809
- ? String(errorSnapshotInCursor)
4810
- : undefined,
4991
+ lastInEventId: errorSnapshotInCursor !== undefined ? String(errorSnapshotInCursor) : undefined,
4811
4992
  });
4812
4993
  }
4813
4994
  catch (error) {
@@ -4818,8 +4999,7 @@ function chatAgent(options) {
4818
4999
  }
4819
5000
  }
4820
5001
  // chat.requestUpgrade() / chat.endRun() — exit after error turn too
4821
- if (locals.get(chatUpgradeRequestedKey) ||
4822
- locals.get(chatEndRunRequestedKey)) {
5002
+ if (locals.get(chatUpgradeRequestedKey) || locals.get(chatEndRunRequestedKey)) {
4823
5003
  return;
4824
5004
  }
4825
5005
  // Drain remaining recovered turns before idling — a thrown
@@ -4852,7 +5032,7 @@ function chatAgent(options) {
4852
5032
  // cleanup so a missing subscription doesn't throw.
4853
5033
  stopSub?.off();
4854
5034
  }
4855
- }
5035
+ },
4856
5036
  });
4857
5037
  // Register clientDataSchema so the CLI converts it to JSONSchema
4858
5038
  // and stores it as payloadSchema — used by the Playground UI
@@ -4944,7 +5124,7 @@ function createChatBuilder(config) {
4944
5124
  agent(options) {
4945
5125
  const mergedUiStream = config.uiStreamOptions && options.uiMessageStreamOptions
4946
5126
  ? { ...config.uiStreamOptions, ...options.uiMessageStreamOptions }
4947
- : options.uiMessageStreamOptions ?? config.uiStreamOptions;
5127
+ : (options.uiMessageStreamOptions ?? config.uiStreamOptions);
4948
5128
  return chatAgent({
4949
5129
  ...options,
4950
5130
  ...(config.clientDataSchema ? { clientDataSchema: config.clientDataSchema } : {}),
@@ -5437,8 +5617,19 @@ async function pipeChatAndCapture(source, options) {
5437
5617
  const onFinishPromise = new Promise((r) => {
5438
5618
  resolveOnFinish = r;
5439
5619
  });
5620
+ const resolvedOptions = resolveUIMessageStreamOptions();
5440
5621
  const uiStream = source.toUIMessageStream({
5441
- ...resolveUIMessageStreamOptions(),
5622
+ ...resolvedOptions,
5623
+ // Thread the prior chain (incl. a spliced handover partial) so a resumed
5624
+ // tool round's tool-output chunks merge into the originating tool-call
5625
+ // instead of throwing "No tool invocation found".
5626
+ ...(options?.originalMessages ? { originalMessages: options.originalMessages } : {}),
5627
+ // Stamp a server-generated id on the start chunk, same as chat.agent's
5628
+ // pipe. Without it the AI SDK regenerates the assistant id when a
5629
+ // prepareStep injection (steering) starts a new step mid-stream, and
5630
+ // the frontend replaces the partial message — wiping the
5631
+ // pre-injection text from the UI and the captured response.
5632
+ generateMessageId: resolvedOptions.generateMessageId ?? generateMessageId,
5442
5633
  onFinish: ({ responseMessage }) => {
5443
5634
  captured = responseMessage;
5444
5635
  resolveOnFinish();
@@ -5508,10 +5699,65 @@ class ChatMessageAccumulator {
5508
5699
  this.uiMessages = [...uiMessages];
5509
5700
  this.modelMessages = await toModelMessages(uiMessages);
5510
5701
  }
5702
+ /**
5703
+ * Splice a `chat.headStart` handover partial into the accumulator (the warm
5704
+ * step-1 response). Dedups by `messageId` so a seeded/hydrated history that
5705
+ * already carries the partial isn't doubled. Seed any prior history first
5706
+ * (e.g. `setMessages(payload.headStartMessages)`). Low-level — see
5707
+ * `consumeHandover` for the wait+seed+apply convenience.
5708
+ */
5709
+ applyHandover(signal) {
5710
+ spliceHandoverPartial(this.modelMessages, this.uiMessages, signal);
5711
+ }
5712
+ /**
5713
+ * One-call `chat.headStart` handover for a custom-agent loop: waits for the
5714
+ * handover signal, seeds prior history from `payload.headStartMessages`,
5715
+ * applies the warm step-1 partial, and reports what to do next.
5716
+ *
5717
+ * Returns `{ isFinal, skipped }`:
5718
+ * - `skipped: true` — not a `handover-prepare` run, the wait idled out, or the
5719
+ * warm handler aborted. Exit the run without a turn.
5720
+ * - `isFinal: true` — step 1 IS the response (pure text). Write turn-complete
5721
+ * and continue; do not call `streamText`.
5722
+ * - `isFinal: false` — fall through to `streamText`, which runs the pending
5723
+ * tool round handed over from step 1.
5724
+ */
5725
+ async consumeHandover(options) {
5726
+ const signal = await waitForHandover({
5727
+ payload: options.payload,
5728
+ idleTimeoutInSeconds: options.idleTimeoutInSeconds,
5729
+ timeout: options.timeout,
5730
+ });
5731
+ if (!signal || signal.kind === "handover-skip") {
5732
+ return { isFinal: false, skipped: true };
5733
+ }
5734
+ if (options.payload.headStartMessages && options.payload.headStartMessages.length > 0) {
5735
+ await this.setMessages(options.payload.headStartMessages);
5736
+ }
5737
+ this.applyHandover(signal);
5738
+ return { isFinal: signal.isFinal, skipped: false };
5739
+ }
5511
5740
  async addResponse(response) {
5512
5741
  if (!response.id) {
5513
5742
  response = { ...response, id: generateMessageId() };
5514
5743
  }
5744
+ // Tool-approval and handover-resume continuations reuse the trailing
5745
+ // assistant's ID (via originalMessages on the pipe), so the captured
5746
+ // response can carry the same ID as a message already in the chain
5747
+ // (e.g. a spliced handover partial). Replace in place instead of pushing
5748
+ // a duplicate, mirroring the chat.agent accumulator.
5749
+ const existingIdx = this.uiMessages.findIndex((m) => m.id === response.id);
5750
+ if (existingIdx !== -1) {
5751
+ this.uiMessages[existingIdx] = response;
5752
+ try {
5753
+ // Reconvert all model messages since we replaced rather than appended.
5754
+ this.modelMessages = await toModelMessages(this.uiMessages.map((m) => stripProviderMetadata(m)));
5755
+ }
5756
+ catch {
5757
+ // Conversion failed — leave the existing model messages in place
5758
+ }
5759
+ return;
5760
+ }
5515
5761
  this.uiMessages.push(response);
5516
5762
  try {
5517
5763
  const msgs = await toModelMessages([stripProviderMetadata(response)]);
@@ -5644,14 +5890,18 @@ class ChatMessageAccumulator {
5644
5890
  * signaling, and idle/suspend between turns. You control: initialization,
5645
5891
  * model/tool selection, persistence, and any custom per-turn logic.
5646
5892
  *
5893
+ * Call from inside a `chat.customAgent()` run — the wrapper binds the
5894
+ * backing Session that the iterator's stop signal and message channels
5895
+ * resolve to. (A plain `task()` does not bind it, so `createSession`
5896
+ * would throw "session handle is not initialized".)
5897
+ *
5647
5898
  * @example
5648
5899
  * ```ts
5649
- * import { task } from "@trigger.dev/sdk";
5650
5900
  * import { chat, type ChatTaskWirePayload } from "@trigger.dev/sdk/ai";
5651
5901
  * import { streamText } from "ai";
5652
5902
  * import { openai } from "@ai-sdk/openai";
5653
5903
  *
5654
- * export const myChat = task({
5904
+ * export const myChat = chat.customAgent({
5655
5905
  * id: "my-chat",
5656
5906
  * run: async (payload: ChatTaskWirePayload, { signal }) => {
5657
5907
  * const session = chat.createSession(payload, { signal });
@@ -5675,20 +5925,56 @@ function createChatSession(payload, options) {
5675
5925
  [Symbol.asyncIterator]() {
5676
5926
  let currentPayload = payload;
5677
5927
  let turn = -1;
5678
- const stop = createStopSignal();
5928
+ // Created on the first next() call, AFTER the resume-cursor seed —
5929
+ // createStopSignal attaches the `.in` SSE tail, and attaching
5930
+ // before the seed replays every record from seq 0 (the seed is a
5931
+ // no-op when the chatCustomAgent wrapper already ran it).
5932
+ let stop;
5933
+ let booted = false;
5679
5934
  const accumulator = new ChatMessageAccumulator();
5680
5935
  let previousTurnUsage;
5681
5936
  let cumulativeUsage = emptyUsage();
5682
5937
  return {
5683
5938
  async next() {
5939
+ if (!booted) {
5940
+ booted = true;
5941
+ await seedSessionInResumeCursorForCustomLoop(currentPayload);
5942
+ stop = createStopSignal();
5943
+ }
5684
5944
  turn++;
5945
+ // Head-start handover: the server triggered this run with
5946
+ // `trigger: "handover-prepare"` and signals the warm step-1 partial on
5947
+ // `session.in`. Wait for it BEFORE any `messagesInput.waitWithIdleTimeout`
5948
+ // (that facade consumes-and-discards non-message chunks and would swallow
5949
+ // the signal). Turn-0 only — continuation boots never carry this trigger.
5950
+ let handoverThisTurn = null;
5951
+ let pendingHandoverSignal = null;
5952
+ if (turn === 0 && currentPayload.trigger === "handover-prepare") {
5953
+ const signal = await waitForHandover({
5954
+ payload: currentPayload,
5955
+ idleTimeoutInSeconds: sessionIdleTimeoutOpt ??
5956
+ currentPayload.idleTimeoutInSeconds ??
5957
+ idleTimeoutInSeconds,
5958
+ timeout,
5959
+ });
5960
+ if (!signal || signal.kind === "handover-skip" || runSignal.aborted) {
5961
+ stop.cleanup();
5962
+ return { done: true, value: undefined };
5963
+ }
5964
+ pendingHandoverSignal = signal;
5965
+ handoverThisTurn = { isFinal: signal.isFinal };
5966
+ // Rewrite to a normal first-turn message turn so the rest of the loop
5967
+ // (steering setup, addIncoming, turnObj) runs unchanged.
5968
+ currentPayload = { ...currentPayload, trigger: "submit-message", message: undefined };
5969
+ }
5685
5970
  // First turn: wait when the boot payload carries no message.
5686
5971
  // Preload boots wait for the first real message; continuation
5687
5972
  // boots (fresh run via `ensureRunForSession` / end-and-continue)
5688
5973
  // arrive with the sticky boot-payload fields stripped, so running
5689
5974
  // a turn immediately would invoke the model with no user input.
5690
5975
  const isMessagelessContinuationBoot = currentPayload.continuation === true && !currentPayload.message;
5691
- if (turn === 0 && (currentPayload.trigger === "preload" || isMessagelessContinuationBoot)) {
5976
+ if (turn === 0 &&
5977
+ (currentPayload.trigger === "preload" || isMessagelessContinuationBoot)) {
5692
5978
  const result = await messagesInput.waitWithIdleTimeout({
5693
5979
  idleTimeoutInSeconds: sessionIdleTimeoutOpt ?? currentPayload.idleTimeoutInSeconds ?? 30,
5694
5980
  timeout,
@@ -5712,8 +5998,7 @@ function createChatSession(payload, options) {
5712
5998
  // Subsequent turns: wait for the next message
5713
5999
  if (turn > 0) {
5714
6000
  // chat.requestUpgrade() / chat.endRun() — exit before waiting
5715
- if (locals.get(chatUpgradeRequestedKey) ||
5716
- locals.get(chatEndRunRequestedKey)) {
6001
+ if (locals.get(chatUpgradeRequestedKey) || locals.get(chatEndRunRequestedKey)) {
5717
6002
  stop.cleanup();
5718
6003
  return { done: true, value: undefined };
5719
6004
  }
@@ -5788,6 +6073,16 @@ function createChatSession(payload, options) {
5788
6073
  ? [currentPayload.message]
5789
6074
  : [];
5790
6075
  const messages = await accumulator.addIncoming(incomingForAccumulator, currentPayload.trigger, turn);
6076
+ // Apply the head-start handover AFTER addIncoming — turn-0 addIncoming
6077
+ // replaces accumulator state, which would wipe a pre-applied splice.
6078
+ // Seed prior history first, then splice the warm step-1 partial.
6079
+ if (pendingHandoverSignal) {
6080
+ const priorHistory = currentPayload.headStartMessages;
6081
+ if (priorHistory && priorHistory.length > 0) {
6082
+ await accumulator.setMessages(priorHistory);
6083
+ }
6084
+ accumulator.applyHandover(pendingHandoverSignal);
6085
+ }
5791
6086
  // chat.requestUpgrade() called before this turn — signal transport and exit
5792
6087
  if (locals.get(chatUpgradeRequestedKey)) {
5793
6088
  await writeUpgradeRequiredChunk();
@@ -5814,13 +6109,38 @@ function createChatSession(payload, options) {
5814
6109
  continuation: currentPayload.continuation ?? false,
5815
6110
  previousTurnUsage,
5816
6111
  totalUsage: cumulativeUsage,
6112
+ handover: handoverThisTurn,
5817
6113
  async setMessages(uiMessages) {
5818
6114
  await accumulator.setMessages(uiMessages);
5819
6115
  },
5820
6116
  async complete(source) {
6117
+ // Head-start final turn: the warm step-1 partial is already spliced
6118
+ // into the accumulator and IS the response — nothing to pipe. Only
6119
+ // valid on a final handover; a missing source on any other turn is a
6120
+ // mistake (it would silently finalize without an assistant response).
6121
+ if (!source) {
6122
+ if (!handoverThisTurn?.isFinal) {
6123
+ throw new Error("turn.complete() requires a stream source unless turn.handover.isFinal is true");
6124
+ }
6125
+ const response = accumulator.uiMessages.at(-1);
6126
+ if (!response || response.role !== "assistant") {
6127
+ throw new Error("turn.complete() could not find the spliced handover response");
6128
+ }
6129
+ sessionMsgSub.off();
6130
+ await chatWriteTurnComplete();
6131
+ return response;
6132
+ }
5821
6133
  let response;
5822
6134
  try {
5823
- response = await pipeChatAndCapture(source, { signal: combinedSignal });
6135
+ response = await pipeChatAndCapture(source, {
6136
+ signal: combinedSignal,
6137
+ // On a non-final handover turn, thread the spliced partial so a
6138
+ // resumed tool round's tool-output chunks merge into the
6139
+ // handed-over tool-call. Gated on the handover turn only — a
6140
+ // normal turn must not pass originalMessages (it would merge the
6141
+ // fresh response into the prior assistant message).
6142
+ ...(handoverThisTurn ? { originalMessages: accumulator.uiMessages } : {}),
6143
+ });
5824
6144
  }
5825
6145
  catch (error) {
5826
6146
  if (error instanceof Error && error.name === "AbortError") {
@@ -5925,7 +6245,8 @@ function createChatSession(payload, options) {
5925
6245
  },
5926
6246
  ];
5927
6247
  if (sessionCompaction.compactUIMessages) {
5928
- accumulator.uiMessages = await sessionCompaction.compactUIMessages(compactEvent);
6248
+ accumulator.uiMessages =
6249
+ await sessionCompaction.compactUIMessages(compactEvent);
5929
6250
  }
5930
6251
  }
5931
6252
  }
@@ -5937,7 +6258,10 @@ function createChatSession(payload, options) {
5937
6258
  // Append any non-transient data parts queued via chat.response or writer.write()
5938
6259
  const queuedParts = locals.get(chatResponsePartsKey);
5939
6260
  if (queuedParts && queuedParts.length > 0) {
5940
- response = { ...response, parts: [...(response.parts ?? []), ...queuedParts] };
6261
+ response = {
6262
+ ...response,
6263
+ parts: [...(response.parts ?? []), ...queuedParts],
6264
+ };
5941
6265
  locals.set(chatResponsePartsKey, []);
5942
6266
  }
5943
6267
  await accumulator.addResponse(response);
@@ -5975,7 +6299,8 @@ function createChatSession(payload, options) {
5975
6299
  return { done: false, value: turnObj };
5976
6300
  },
5977
6301
  async return() {
5978
- stop.cleanup();
6302
+ // `stop` only exists once next() has booted the iterator.
6303
+ stop?.cleanup();
5979
6304
  return { done: true, value: undefined };
5980
6305
  },
5981
6306
  };
@@ -6205,6 +6530,12 @@ function createChatStartSessionAction(taskId, options) {
6205
6530
  if (!params.chatId) {
6206
6531
  throw new Error("chat.createStartSessionAction: params.chatId is required — used as the session externalId.");
6207
6532
  }
6533
+ // Scope the action to `apiClient`'s env: re-enter without it so the body
6534
+ // runs once, under that config (read via apiClientManager.accessToken/.baseURL).
6535
+ if (options?.apiClient) {
6536
+ const { apiClient, ...rest } = options;
6537
+ return apiClientManager.runWithConfig(apiClient, () => createChatStartSessionAction(taskId, rest)(params));
6538
+ }
6208
6539
  // The first run boots before the user's first message lands on
6209
6540
  // `.in/append`, so it sees an empty `messages` array and `trigger:
6210
6541
  // "preload"`. This matches the pre-Sessions preload semantics:
@@ -6218,8 +6549,8 @@ function createChatStartSessionAction(taskId, options) {
6218
6549
  // run-list filter by chat works without the customer having to wire it
6219
6550
  // up. Mirrors the browser-mediated `TriggerChatTransport.doStart` path.
6220
6551
  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);
6552
+ // SessionTriggerConfig.tags allows at most 5; the auto chat tag takes one slot.
6553
+ const tags = [`chat:${params.chatId}`, ...userTags].slice(0, 5);
6223
6554
  const clientDataMetadata = params.clientData !== undefined ? { metadata: params.clientData } : {};
6224
6555
  const triggerConfig = {
6225
6556
  basePayload: {
@@ -6243,6 +6574,20 @@ function createChatStartSessionAction(taskId, options) {
6243
6574
  maxAttempts: params.triggerConfig?.maxAttempts ?? options?.triggerConfig?.maxAttempts,
6244
6575
  }
6245
6576
  : {}),
6577
+ ...(options?.triggerConfig?.maxDuration !== undefined ||
6578
+ params.triggerConfig?.maxDuration !== undefined
6579
+ ? {
6580
+ maxDuration: params.triggerConfig?.maxDuration ?? options?.triggerConfig?.maxDuration,
6581
+ }
6582
+ : {}),
6583
+ ...(options?.triggerConfig?.region || params.triggerConfig?.region
6584
+ ? { region: params.triggerConfig?.region ?? options?.triggerConfig?.region }
6585
+ : {}),
6586
+ ...(options?.triggerConfig?.lockToVersion || params.triggerConfig?.lockToVersion
6587
+ ? {
6588
+ lockToVersion: params.triggerConfig?.lockToVersion ?? options?.triggerConfig?.lockToVersion,
6589
+ }
6590
+ : {}),
6246
6591
  ...(options?.triggerConfig?.idleTimeoutInSeconds !== undefined ||
6247
6592
  params.triggerConfig?.idleTimeoutInSeconds !== undefined
6248
6593
  ? {
@@ -6298,9 +6643,7 @@ function createChatStartSessionAction(taskId, options) {
6298
6643
  }
6299
6644
  function resolveChatStartBaseURL(endpoint, chatId, option) {
6300
6645
  const fallback = apiClientManager.baseURL ?? "https://api.trigger.dev";
6301
- const raw = typeof option === "function"
6302
- ? option({ endpoint, chatId })
6303
- : option ?? fallback;
6646
+ const raw = typeof option === "function" ? option({ endpoint, chatId }) : (option ?? fallback);
6304
6647
  return raw.replace(/\/$/, "");
6305
6648
  }
6306
6649
  function overrideRequestHeaders(accessToken) {
@@ -6421,10 +6764,20 @@ export const chat = {
6421
6764
  MessageAccumulator: ChatMessageAccumulator,
6422
6765
  /** Create a chat session (async iterator). See {@link createChatSession}. */
6423
6766
  createSession: createChatSession,
6767
+ /**
6768
+ * Wait for a `chat.headStart` handover signal inside a `chat.customAgent`
6769
+ * loop (turn 0). See {@link waitForHandover}. For most loops prefer the
6770
+ * `chat.MessageAccumulator.consumeHandover()` convenience, which also seeds
6771
+ * `payload.headStartMessages` and applies the partial.
6772
+ */
6773
+ waitForHandover,
6424
6774
  /**
6425
6775
  * Store and retrieve a resolved prompt for the current run.
6426
6776
  *
6427
6777
  * - `chat.prompt.set(resolved)` — store a `ResolvedPrompt` or plain string
6778
+ * - `chat.prompt.set(resolved, { providerOptions })` — also attach provider
6779
+ * options to the system block so a provider can cache it (e.g. Anthropic
6780
+ * prompt caching). See the prompt-caching guide.
6428
6781
  * - `chat.prompt()` — read the stored prompt (throws if not set)
6429
6782
  */
6430
6783
  prompt: Object.assign(getChatPrompt, { set: setChatPrompt }),