@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
@@ -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,45 @@ 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", {
159
+ error: error instanceof Error ? error.message : String(error),
160
+ });
161
+ }
162
+ }
120
163
  let readChatSnapshotImpl;
121
164
  function __setReadChatSnapshotImplForTests(impl) {
122
165
  readChatSnapshotImpl = impl;
@@ -668,6 +711,16 @@ function createTaskToolExecuteHandler(task) {
668
711
  toolMeta.continuation = chatCtx.continuation;
669
712
  toolMeta.clientData = chatCtx.clientData;
670
713
  }
714
+ else {
715
+ // Hand-rolled chat.customAgent loops never set per-turn context, but
716
+ // the wrapper records the boot payload's external chatId at run boot
717
+ // — thread it so subtask chat helpers (`chat.stream.writer` with
718
+ // target "root") can open the parent's session.
719
+ const chatExternalId = locals_js_1.locals.get(chatExternalIdKey);
720
+ if (chatExternalId) {
721
+ toolMeta.chatId = chatExternalId;
722
+ }
723
+ }
671
724
  const chatLocals = {};
672
725
  for (const entry of chatLocalRegistry) {
673
726
  const value = locals_js_1.locals.get(entry.key);
@@ -1190,8 +1243,7 @@ const handoverInput = {
1190
1243
  const result = await getChatSession().in.waitWithIdleTimeout(options);
1191
1244
  if (!result.ok)
1192
1245
  return result;
1193
- if (result.output.kind === "handover" ||
1194
- result.output.kind === "handover-skip") {
1246
+ if (result.output.kind === "handover" || result.output.kind === "handover-skip") {
1195
1247
  return { ok: true, output: result.output };
1196
1248
  }
1197
1249
  // Other kinds (message, stop) are not expected during handover-prepare.
@@ -1200,6 +1252,36 @@ const handoverInput = {
1200
1252
  }
1201
1253
  },
1202
1254
  };
1255
+ /**
1256
+ * Wait for a `chat.headStart` handover signal inside a custom-agent loop or
1257
+ * `chat.createSession`. Returns:
1258
+ * - `null` — this run is not a `handover-prepare` boot, or the wait idled out /
1259
+ * the warm handler crashed before signaling. Treat as "no handover".
1260
+ * - `{ kind: "handover-skip" }` — the warm handler aborted; exit without a turn.
1261
+ * - `{ kind: "handover", partialAssistantMessage, messageId?, isFinal }` — splice
1262
+ * the partial (`chat.MessageAccumulator.applyHandover`) and, when `isFinal` is
1263
+ * false, fall through to `streamText` to run the handed-over tool round.
1264
+ *
1265
+ * For the common case prefer `accumulator.consumeHandover()`, which also seeds
1266
+ * `payload.headStartMessages` and applies the partial for you.
1267
+ *
1268
+ * Must be called at turn 0 before any `chat.messages.waitWithIdleTimeout` —
1269
+ * that facade consumes and discards non-message chunks, which would swallow the
1270
+ * handover signal.
1271
+ */
1272
+ async function waitForHandover(options) {
1273
+ if (options.payload.trigger !== "handover-prepare")
1274
+ return null;
1275
+ const result = await handoverInput.waitWithIdleTimeout({
1276
+ idleTimeoutInSeconds: options.idleTimeoutInSeconds ?? options.payload.idleTimeoutInSeconds ?? 60,
1277
+ timeout: options.timeout,
1278
+ spanName: options.spanName ?? "waiting for handover signal",
1279
+ });
1280
+ // Non-ok = idle timeout or the warm handler crashed without signaling.
1281
+ if (!result.ok)
1282
+ return null;
1283
+ return result.output;
1284
+ }
1203
1285
  /**
1204
1286
  * Per-turn deferred promises. Registered via `chat.defer()`, awaited
1205
1287
  * before `onTurnComplete` fires. Reset each turn.
@@ -1296,6 +1378,27 @@ function synthesizeHandoverUIMessage(partial, messageId) {
1296
1378
  parts,
1297
1379
  };
1298
1380
  }
1381
+ /**
1382
+ * Splice a head-start handover partial into an accumulating message pair
1383
+ * (model + UI). Dedups by `messageId` against the UI chain (so a hydrated
1384
+ * history that already persisted the partial isn't doubled), then pushes the
1385
+ * partial into `modelMessages` and the synthesized UIMessage into `uiMessages`.
1386
+ * Shared by the `chat.agent` turn-0 splice and `ChatMessageAccumulator.applyHandover`.
1387
+ * @internal
1388
+ */
1389
+ function spliceHandoverPartial(modelMessages, uiMessages, signal) {
1390
+ if (!signal.partialAssistantMessage || signal.partialAssistantMessage.length === 0) {
1391
+ return;
1392
+ }
1393
+ // Skip if the hydrated chain already persisted the partial under this id.
1394
+ const alreadyInChain = signal.messageId !== undefined && uiMessages.some((m) => m.id === signal.messageId);
1395
+ if (alreadyInChain)
1396
+ return;
1397
+ modelMessages.push(...signal.partialAssistantMessage);
1398
+ const partialUI = synthesizeHandoverUIMessage(signal.partialAssistantMessage, signal.messageId);
1399
+ if (partialUI)
1400
+ uiMessages.push(partialUI);
1401
+ }
1299
1402
  /**
1300
1403
  * Per-turn background context queue. Messages added via `chat.backgroundWork.inject()`
1301
1404
  * are drained at the next `prepareStep` boundary and appended to the model messages.
@@ -1853,6 +1956,55 @@ function isStepBoundarySafe(step) {
1853
1956
  .map((p) => p.toolCallId));
1854
1957
  return ![...callIds].some((id) => !settledIds.has(id));
1855
1958
  }
1959
+ /**
1960
+ * True when a model message is a `tool` message carrying a
1961
+ * `tool-approval-response` part — the trailing row a head-start handover
1962
+ * reshapes a pending first-turn tool call into. AI SDK's `collectToolApprovals`
1963
+ * only inspects the conversation's last message, so this row must survive to
1964
+ * `streamText` intact for the agent to execute the handed-over call.
1965
+ * @internal
1966
+ */
1967
+ function hasToolApprovalResponse(message) {
1968
+ return (message?.role === "tool" &&
1969
+ Array.isArray(message.content) &&
1970
+ message.content.some((part) => part != null &&
1971
+ typeof part === "object" &&
1972
+ part.type === "tool-approval-response"));
1973
+ }
1974
+ /**
1975
+ * Keep a head-start handover's tool-approval tail intact across `prepareMessages`.
1976
+ *
1977
+ * The handover reshapes the warm step-1's pending tool call into AI SDK's
1978
+ * tool-approval round: a `tool-approval-request` on the assistant plus a
1979
+ * trailing `tool` message with `tool-approval-response { approved: true }`. The
1980
+ * agent's next `streamText` runs `collectToolApprovals`, which ONLY looks at the
1981
+ * last message — so that tool row must stay last and unmodified for the pending
1982
+ * call to execute. A user `prepareMessages` hook that rewrites or drops the last
1983
+ * message (e.g. rolling a provider cache breakpoint onto it) silently breaks the
1984
+ * resume: the agent sends a bare `tool_use` and the turn dies with
1985
+ * "tool_use ids were found without tool_result". If the hook's input ended with
1986
+ * that approval tail, re-assert the original tail as the last message.
1987
+ *
1988
+ * No-op for every normal turn — only fires when the input genuinely ended with a
1989
+ * pending tool-approval response (i.e. a head-start resume).
1990
+ * @internal
1991
+ */
1992
+ function preserveToolApprovalTail(original, prepared) {
1993
+ const originalTail = original[original.length - 1];
1994
+ if (!hasToolApprovalResponse(originalTail))
1995
+ return prepared;
1996
+ // Hook left the exact tail object in place — nothing to do.
1997
+ if (prepared[prepared.length - 1] === originalTail)
1998
+ return prepared;
1999
+ // Otherwise drop only the trailing approval tail the hook produced (the
2000
+ // original moved, or a rewritten copy) and re-append the original so it is
2001
+ // last and intact. Older approval rounds deeper in history must survive.
2002
+ const withoutMovedOriginal = prepared.filter((m) => m !== originalTail);
2003
+ while (hasToolApprovalResponse(withoutMovedOriginal[withoutMovedOriginal.length - 1])) {
2004
+ withoutMovedOriginal.pop();
2005
+ }
2006
+ return [...withoutMovedOriginal, originalTail];
2007
+ }
1856
2008
  /**
1857
2009
  * Apply the prepareMessages hook if one is set in locals.
1858
2010
  * @internal
@@ -1862,7 +2014,7 @@ async function applyPrepareMessages(messages, reason) {
1862
2014
  if (!hook)
1863
2015
  return messages;
1864
2016
  const turnCtx = locals_js_1.locals.get(chatTurnContextKey);
1865
- return tracer_js_1.tracer.startActiveSpan("prepareMessages()", async () => {
2017
+ const prepared = await tracer_js_1.tracer.startActiveSpan("prepareMessages()", async () => {
1866
2018
  return hook({
1867
2019
  messages,
1868
2020
  reason,
@@ -1878,6 +2030,9 @@ async function applyPrepareMessages(messages, reason) {
1878
2030
  "chat.prepareMessages.messageCount": messages.length,
1879
2031
  },
1880
2032
  });
2033
+ // A user hook must never be able to break the head-start handover resume by
2034
+ // disturbing the trailing tool-approval row (see preserveToolApprovalTail).
2035
+ return preserveToolApprovalTail(messages, prepared);
1881
2036
  }
1882
2037
  /**
1883
2038
  * Resolve the `tools` option into a concrete `ToolSet` and cache it in locals so
@@ -2250,11 +2405,18 @@ function isCompactionSafe(messages) {
2250
2405
  }
2251
2406
  /** @internal */
2252
2407
  const chatPromptKey = locals_js_1.locals.create("chat.prompt");
2408
+ /**
2409
+ * @internal Provider options attached to the system message that
2410
+ * `toStreamTextOptions()` builds from the stored prompt — lets a provider cache
2411
+ * the system block. Stored separately so it works for both the `ResolvedPrompt`
2412
+ * and plain-string forms without mutating the prompt object.
2413
+ */
2414
+ const chatPromptProviderOptionsKey = locals_js_1.locals.create("chat.prompt.providerOptions");
2253
2415
  /**
2254
2416
  * Store a resolved prompt (or plain string) for the current run.
2255
2417
  * Call from any hook (`onPreload`, `onChatStart`, `onTurnStart`) or `run()`.
2256
2418
  */
2257
- function setChatPrompt(resolved) {
2419
+ function setChatPrompt(resolved, options) {
2258
2420
  if (typeof resolved === "string") {
2259
2421
  locals_js_1.locals.set(chatPromptKey, {
2260
2422
  text: resolved,
@@ -2271,6 +2433,9 @@ function setChatPrompt(resolved) {
2271
2433
  else {
2272
2434
  locals_js_1.locals.set(chatPromptKey, resolved);
2273
2435
  }
2436
+ // Always overwrite the slot (even with undefined) so a later prompt.set with
2437
+ // no options clears a previous prompt's cache opt-in rather than leaking it.
2438
+ locals_js_1.locals.set(chatPromptProviderOptionsKey, options?.providerOptions);
2274
2439
  }
2275
2440
  /**
2276
2441
  * Read the stored prompt. Throws if `chat.prompt.set()` has not been called.
@@ -2436,7 +2601,21 @@ function toStreamTextOptions(options) {
2436
2601
  const promptText = prompt?.text ?? "";
2437
2602
  const skillsText = skills && skills.length > 0 ? buildSkillsSystemPrompt(skills) : "";
2438
2603
  if (promptText || skillsText) {
2439
- result.system = [promptText, skillsText].filter(Boolean).join("\n\n");
2604
+ const systemText = [promptText, skillsText].filter(Boolean).join("\n\n");
2605
+ // Resolve system-prompt provider options for caching. Precedence (most
2606
+ // specific wins, no deep merge): explicit `systemProviderOptions` →
2607
+ // `cacheControl` sugar → `providerOptions` stored on `chat.prompt.set()`.
2608
+ const systemProviderOptions = options?.systemProviderOptions ??
2609
+ (options?.cacheControl
2610
+ ? { anthropic: { cacheControl: options.cacheControl } }
2611
+ : undefined) ??
2612
+ locals_js_1.locals.get(chatPromptProviderOptionsKey);
2613
+ // A bare string stays a bare string (the unchanged default). With provider
2614
+ // options, emit a structured `SystemModelMessage` so the provider can cache
2615
+ // the system block — `streamText`'s `system` accepts string | message.
2616
+ result.system = systemProviderOptions
2617
+ ? { role: "system", content: systemText, providerOptions: systemProviderOptions }
2618
+ : systemText;
2440
2619
  }
2441
2620
  // Prompt-related options (only if chat.prompt.set() was called)
2442
2621
  if (prompt) {
@@ -2618,6 +2797,7 @@ function chatCustomAgent(options) {
2618
2797
  // `chat.createStartSessionAction`) before this run is triggered.
2619
2798
  // No client-side upsert needed.
2620
2799
  locals_js_1.locals.set(chatSessionHandleKey, sessions_js_1.sessions.open(payload.chatId));
2800
+ locals_js_1.locals.set(chatExternalIdKey, payload.chatId);
2621
2801
  locals_js_1.locals.set(chatAgentRunContextKey, runOptions.ctx);
2622
2802
  // Initialize the turn-complete trim slot so `chat.writeTurnComplete`
2623
2803
  // trims `session.out` back to the previous turn boundary. Without
@@ -2627,6 +2807,10 @@ function chatCustomAgent(options) {
2627
2807
  (0, streams_js_1.markChatAgentRunForStreamsWarning)();
2628
2808
  v3_1.taskContext.setConversationId(payload.chatId);
2629
2809
  stampConversationIdOnActiveSpan(payload.chatId);
2810
+ // Seed the `.in` resume cursor before user code attaches any `.in`
2811
+ // listener — otherwise a continuation boot replays already-answered
2812
+ // messages into the loop's first wait.
2813
+ await seedSessionInResumeCursorForCustomLoop(payload);
2630
2814
  return userRun(payload, runOptions);
2631
2815
  },
2632
2816
  });
@@ -2673,6 +2857,7 @@ function chatAgent(options) {
2673
2857
  // `chat.createStartSessionAction` or browser-direct) before this
2674
2858
  // run is triggered — no client-side upsert needed here.
2675
2859
  locals_js_1.locals.set(chatSessionHandleKey, sessions_js_1.sessions.open(payload.chatId));
2860
+ locals_js_1.locals.set(chatExternalIdKey, payload.chatId);
2676
2861
  // Mutable holder; advances in `writeTurnCompleteChunk` after each turn
2677
2862
  // and is the trim target for the NEXT turn's trim record.
2678
2863
  locals_js_1.locals.set(lastTurnCompleteSeqNumKey, { value: undefined });
@@ -2796,7 +2981,9 @@ function chatAgent(options) {
2796
2981
  const replayOutPhase = async () => {
2797
2982
  const replayOutStart = Date.now();
2798
2983
  try {
2799
- const replayResult = await replaySessionOutTail(sessionIdForSnapshot, { lastEventId: bootSnapshot?.lastOutEventId });
2984
+ const replayResult = await replaySessionOutTail(sessionIdForSnapshot, {
2985
+ lastEventId: bootSnapshot?.lastOutEventId,
2986
+ });
2800
2987
  replayedSettled = replayResult.settled;
2801
2988
  replayedPartial = replayResult.partial;
2802
2989
  replayedPartialRaw = replayResult.partialRaw;
@@ -2893,9 +3080,7 @@ function chatAgent(options) {
2893
3080
  // crashed / was canceled / requested upgrade
2894
3081
  // - Snapshot exists at all (catches edge cases where the wire
2895
3082
  // didn't set `continuation` but a snapshot indicates prior turns)
2896
- const needsResumeCursor = ctx.attempt.number > 1 ||
2897
- payload.continuation === true ||
2898
- bootSnapshot !== undefined;
3083
+ const needsResumeCursor = ctx.attempt.number > 1 || payload.continuation === true || bootSnapshot !== undefined;
2899
3084
  if (needsResumeCursor) {
2900
3085
  try {
2901
3086
  // Reuse the cursor the boot block already resolved (snapshot
@@ -3242,9 +3427,7 @@ function chatAgent(options) {
3242
3427
  // Wait for the first real message — task-level idle settings win over
3243
3428
  // `transport.preload(..., { idleTimeoutInSeconds })` / wire payload so
3244
3429
  // `chat.agent({ idleTimeoutInSeconds, preloadIdleTimeoutInSeconds })` is authoritative.
3245
- const effectivePreloadIdleTimeout = preloadIdleTimeoutInSeconds ??
3246
- idleTimeoutInSeconds ??
3247
- payload.idleTimeoutInSeconds;
3430
+ const effectivePreloadIdleTimeout = preloadIdleTimeoutInSeconds ?? idleTimeoutInSeconds ?? payload.idleTimeoutInSeconds;
3248
3431
  const effectivePreloadTimeout = metadata_js_1.metadata.get(TURN_TIMEOUT_METADATA_KEY) ??
3249
3432
  preloadTimeout ??
3250
3433
  turnTimeout;
@@ -3489,7 +3672,9 @@ function chatAgent(options) {
3489
3672
  if (lastUserMessage) {
3490
3673
  turnAttributes["chat.user_message"] = lastUserMessage;
3491
3674
  // Show a truncated preview of the user message as an accessory
3492
- const preview = lastUserMessage.length > 80 ? lastUserMessage.slice(0, 80) + "..." : lastUserMessage;
3675
+ const preview = lastUserMessage.length > 80
3676
+ ? lastUserMessage.slice(0, 80) + "..."
3677
+ : lastUserMessage;
3493
3678
  Object.assign(turnAttributes, (0, v3_1.accessoryAttributes)({
3494
3679
  items: [{ text: preview, variant: "normal" }],
3495
3680
  style: "codepath",
@@ -3776,8 +3961,7 @@ function chatAgent(options) {
3776
3961
  if (currentWirePayload.trigger === "submit-message" &&
3777
3962
  cleanedUIMessages.length > 0) {
3778
3963
  const lastUI = cleanedUIMessages[cleanedUIMessages.length - 1];
3779
- const matchedExisting = lastUI.id !== undefined &&
3780
- previouslyKnownMessageIds.has(lastUI.id);
3964
+ const matchedExisting = lastUI.id !== undefined && previouslyKnownMessageIds.has(lastUI.id);
3781
3965
  if (!matchedExisting) {
3782
3966
  turnNewUIMessages.push(lastUI);
3783
3967
  const lastModel = (await toModelMessages([lastUI]))[0];
@@ -3882,18 +4066,10 @@ function chatAgent(options) {
3882
4066
  // `UIMessageStreamError: No tool invocation found`.
3883
4067
  const pendingHandoverPartial = locals_js_1.locals.get(chatHandoverPartialKey);
3884
4068
  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
- }
4069
+ spliceHandoverPartial(accumulatedMessages, accumulatedUIMessages, {
4070
+ partialAssistantMessage: pendingHandoverPartial,
4071
+ messageId: locals_js_1.locals.get(chatHandoverMessageIdKey),
4072
+ });
3897
4073
  locals_js_1.locals.set(chatHandoverPartialKey, []); // consume once
3898
4074
  }
3899
4075
  }
@@ -4120,7 +4296,8 @@ function chatAgent(options) {
4120
4296
  // We call toUIMessageStream ourselves to attach onFinish for response capture.
4121
4297
  // Pass originalMessages so the AI SDK reuses message IDs across turns
4122
4298
  // (e.g. for tool approval continuations / HITL flows).
4123
- if ((locals_js_1.locals.get(chatPipeCountKey) ?? 0) === 0 && isUIMessageStreamable(runResult)) {
4299
+ if ((locals_js_1.locals.get(chatPipeCountKey) ?? 0) === 0 &&
4300
+ isUIMessageStreamable(runResult)) {
4124
4301
  onFinishAttached = true;
4125
4302
  const resolvedOptions = resolveUIMessageStreamOptions();
4126
4303
  // For action turns, don't pass originalMessages: the response
@@ -4133,9 +4310,7 @@ function chatAgent(options) {
4133
4310
  // Pass originalMessages so the AI SDK reuses message IDs across
4134
4311
  // turns (e.g. for tool approval continuations / HITL flows).
4135
4312
  // Omit for action turns to force a fresh response ID.
4136
- ...(isActionTurn
4137
- ? {}
4138
- : { originalMessages: accumulatedUIMessages }),
4313
+ ...(isActionTurn ? {} : { originalMessages: accumulatedUIMessages }),
4139
4314
  // Always provide generateMessageId so the start chunk carries a
4140
4315
  // messageId. Without this, the frontend and backend generate IDs
4141
4316
  // independently and they won't match for ID-based dedup.
@@ -4146,7 +4321,10 @@ function chatAgent(options) {
4146
4321
  resolveOnFinish();
4147
4322
  },
4148
4323
  });
4149
- await pipeChat(uiStream, { signal: combinedSignal, spanName: "stream response" });
4324
+ await pipeChat(uiStream, {
4325
+ signal: combinedSignal,
4326
+ spanName: "stream response",
4327
+ });
4150
4328
  }
4151
4329
  }
4152
4330
  catch (error) {
@@ -4181,7 +4359,8 @@ function chatAgent(options) {
4181
4359
  // never reports final usage), which would block the turn loop
4182
4360
  // from ever firing onTurnComplete / writeTurnComplete.
4183
4361
  let turnUsage;
4184
- if (runResult != null && typeof runResult.totalUsage?.then === "function") {
4362
+ if (runResult != null &&
4363
+ typeof runResult.totalUsage?.then === "function") {
4185
4364
  try {
4186
4365
  turnUsage = (await Promise.race([
4187
4366
  runResult.totalUsage,
@@ -4267,7 +4446,10 @@ function chatAgent(options) {
4267
4446
  // may produce a message with an empty ID since IDs are normally
4268
4447
  // assigned by the frontend's useChat).
4269
4448
  if (!capturedResponseMessage.id) {
4270
- capturedResponseMessage = { ...capturedResponseMessage, id: (0, ai_runtime_js_1.generateId)() };
4449
+ capturedResponseMessage = {
4450
+ ...capturedResponseMessage,
4451
+ id: (0, ai_runtime_js_1.generateId)(),
4452
+ };
4271
4453
  }
4272
4454
  // Append any non-transient data parts queued via chat.response or writer.write()
4273
4455
  const queuedParts = locals_js_1.locals.get(chatResponsePartsKey);
@@ -4568,7 +4750,9 @@ function chatAgent(options) {
4568
4750
  ? { "gen_ai.usage.total_tokens": turnUsage.totalTokens }
4569
4751
  : {}),
4570
4752
  ...(cumulativeUsage.totalTokens != null
4571
- ? { "gen_ai.usage.cumulative_total_tokens": cumulativeUsage.totalTokens }
4753
+ ? {
4754
+ "gen_ai.usage.cumulative_total_tokens": cumulativeUsage.totalTokens,
4755
+ }
4572
4756
  : {}),
4573
4757
  },
4574
4758
  });
@@ -4600,9 +4784,7 @@ function chatAgent(options) {
4600
4784
  savedAt: Date.now(),
4601
4785
  messages: accumulatedUIMessages,
4602
4786
  lastOutEventId: turnCompleteResult?.lastEventId,
4603
- lastInEventId: snapshotInCursor !== undefined
4604
- ? String(snapshotInCursor)
4605
- : undefined,
4787
+ lastInEventId: snapshotInCursor !== undefined ? String(snapshotInCursor) : undefined,
4606
4788
  });
4607
4789
  }, {
4608
4790
  attributes: {
@@ -4644,8 +4826,7 @@ function chatAgent(options) {
4644
4826
  // chat.requestUpgrade() was called — exit the loop so the
4645
4827
  // transport triggers a new run on the latest version.
4646
4828
  // chat.endRun() — same exit, no upgrade semantics.
4647
- if (locals_js_1.locals.get(chatUpgradeRequestedKey) ||
4648
- locals_js_1.locals.get(chatEndRunRequestedKey)) {
4829
+ if (locals_js_1.locals.get(chatUpgradeRequestedKey) || locals_js_1.locals.get(chatEndRunRequestedKey)) {
4649
4830
  return "exit";
4650
4831
  }
4651
4832
  // Wait for the next message — stay idle briefly, then suspend
@@ -4725,7 +4906,9 @@ function chatAgent(options) {
4725
4906
  // Turn error handler: write an error chunk + turn-complete to the stream
4726
4907
  // so the client sees the error, then wait for the next message instead
4727
4908
  // of killing the entire run. This keeps the conversation alive.
4728
- if (turnError instanceof Error && turnError.name === "AbortError" && runSignal.aborted) {
4909
+ if (turnError instanceof Error &&
4910
+ turnError.name === "AbortError" &&
4911
+ runSignal.aborted) {
4729
4912
  // Full run cancellation — exit immediately
4730
4913
  throw turnError;
4731
4914
  }
@@ -4821,9 +5004,7 @@ function chatAgent(options) {
4821
5004
  savedAt: Date.now(),
4822
5005
  messages: erroredUIMessages,
4823
5006
  lastOutEventId: errorTurnCompleteResult?.lastEventId,
4824
- lastInEventId: errorSnapshotInCursor !== undefined
4825
- ? String(errorSnapshotInCursor)
4826
- : undefined,
5007
+ lastInEventId: errorSnapshotInCursor !== undefined ? String(errorSnapshotInCursor) : undefined,
4827
5008
  });
4828
5009
  }
4829
5010
  catch (error) {
@@ -4834,8 +5015,7 @@ function chatAgent(options) {
4834
5015
  }
4835
5016
  }
4836
5017
  // chat.requestUpgrade() / chat.endRun() — exit after error turn too
4837
- if (locals_js_1.locals.get(chatUpgradeRequestedKey) ||
4838
- locals_js_1.locals.get(chatEndRunRequestedKey)) {
5018
+ if (locals_js_1.locals.get(chatUpgradeRequestedKey) || locals_js_1.locals.get(chatEndRunRequestedKey)) {
4839
5019
  return;
4840
5020
  }
4841
5021
  // Drain remaining recovered turns before idling — a thrown
@@ -4868,7 +5048,7 @@ function chatAgent(options) {
4868
5048
  // cleanup so a missing subscription doesn't throw.
4869
5049
  stopSub?.off();
4870
5050
  }
4871
- }
5051
+ },
4872
5052
  });
4873
5053
  // Register clientDataSchema so the CLI converts it to JSONSchema
4874
5054
  // and stores it as payloadSchema — used by the Playground UI
@@ -4960,7 +5140,7 @@ function createChatBuilder(config) {
4960
5140
  agent(options) {
4961
5141
  const mergedUiStream = config.uiStreamOptions && options.uiMessageStreamOptions
4962
5142
  ? { ...config.uiStreamOptions, ...options.uiMessageStreamOptions }
4963
- : options.uiMessageStreamOptions ?? config.uiStreamOptions;
5143
+ : (options.uiMessageStreamOptions ?? config.uiStreamOptions);
4964
5144
  return chatAgent({
4965
5145
  ...options,
4966
5146
  ...(config.clientDataSchema ? { clientDataSchema: config.clientDataSchema } : {}),
@@ -5453,8 +5633,19 @@ async function pipeChatAndCapture(source, options) {
5453
5633
  const onFinishPromise = new Promise((r) => {
5454
5634
  resolveOnFinish = r;
5455
5635
  });
5636
+ const resolvedOptions = resolveUIMessageStreamOptions();
5456
5637
  const uiStream = source.toUIMessageStream({
5457
- ...resolveUIMessageStreamOptions(),
5638
+ ...resolvedOptions,
5639
+ // Thread the prior chain (incl. a spliced handover partial) so a resumed
5640
+ // tool round's tool-output chunks merge into the originating tool-call
5641
+ // instead of throwing "No tool invocation found".
5642
+ ...(options?.originalMessages ? { originalMessages: options.originalMessages } : {}),
5643
+ // Stamp a server-generated id on the start chunk, same as chat.agent's
5644
+ // pipe. Without it the AI SDK regenerates the assistant id when a
5645
+ // prepareStep injection (steering) starts a new step mid-stream, and
5646
+ // the frontend replaces the partial message — wiping the
5647
+ // pre-injection text from the UI and the captured response.
5648
+ generateMessageId: resolvedOptions.generateMessageId ?? ai_runtime_js_1.generateId,
5458
5649
  onFinish: ({ responseMessage }) => {
5459
5650
  captured = responseMessage;
5460
5651
  resolveOnFinish();
@@ -5524,10 +5715,65 @@ class ChatMessageAccumulator {
5524
5715
  this.uiMessages = [...uiMessages];
5525
5716
  this.modelMessages = await toModelMessages(uiMessages);
5526
5717
  }
5718
+ /**
5719
+ * Splice a `chat.headStart` handover partial into the accumulator (the warm
5720
+ * step-1 response). Dedups by `messageId` so a seeded/hydrated history that
5721
+ * already carries the partial isn't doubled. Seed any prior history first
5722
+ * (e.g. `setMessages(payload.headStartMessages)`). Low-level — see
5723
+ * `consumeHandover` for the wait+seed+apply convenience.
5724
+ */
5725
+ applyHandover(signal) {
5726
+ spliceHandoverPartial(this.modelMessages, this.uiMessages, signal);
5727
+ }
5728
+ /**
5729
+ * One-call `chat.headStart` handover for a custom-agent loop: waits for the
5730
+ * handover signal, seeds prior history from `payload.headStartMessages`,
5731
+ * applies the warm step-1 partial, and reports what to do next.
5732
+ *
5733
+ * Returns `{ isFinal, skipped }`:
5734
+ * - `skipped: true` — not a `handover-prepare` run, the wait idled out, or the
5735
+ * warm handler aborted. Exit the run without a turn.
5736
+ * - `isFinal: true` — step 1 IS the response (pure text). Write turn-complete
5737
+ * and continue; do not call `streamText`.
5738
+ * - `isFinal: false` — fall through to `streamText`, which runs the pending
5739
+ * tool round handed over from step 1.
5740
+ */
5741
+ async consumeHandover(options) {
5742
+ const signal = await waitForHandover({
5743
+ payload: options.payload,
5744
+ idleTimeoutInSeconds: options.idleTimeoutInSeconds,
5745
+ timeout: options.timeout,
5746
+ });
5747
+ if (!signal || signal.kind === "handover-skip") {
5748
+ return { isFinal: false, skipped: true };
5749
+ }
5750
+ if (options.payload.headStartMessages && options.payload.headStartMessages.length > 0) {
5751
+ await this.setMessages(options.payload.headStartMessages);
5752
+ }
5753
+ this.applyHandover(signal);
5754
+ return { isFinal: signal.isFinal, skipped: false };
5755
+ }
5527
5756
  async addResponse(response) {
5528
5757
  if (!response.id) {
5529
5758
  response = { ...response, id: (0, ai_runtime_js_1.generateId)() };
5530
5759
  }
5760
+ // Tool-approval and handover-resume continuations reuse the trailing
5761
+ // assistant's ID (via originalMessages on the pipe), so the captured
5762
+ // response can carry the same ID as a message already in the chain
5763
+ // (e.g. a spliced handover partial). Replace in place instead of pushing
5764
+ // a duplicate, mirroring the chat.agent accumulator.
5765
+ const existingIdx = this.uiMessages.findIndex((m) => m.id === response.id);
5766
+ if (existingIdx !== -1) {
5767
+ this.uiMessages[existingIdx] = response;
5768
+ try {
5769
+ // Reconvert all model messages since we replaced rather than appended.
5770
+ this.modelMessages = await toModelMessages(this.uiMessages.map((m) => stripProviderMetadata(m)));
5771
+ }
5772
+ catch {
5773
+ // Conversion failed — leave the existing model messages in place
5774
+ }
5775
+ return;
5776
+ }
5531
5777
  this.uiMessages.push(response);
5532
5778
  try {
5533
5779
  const msgs = await toModelMessages([stripProviderMetadata(response)]);
@@ -5660,14 +5906,18 @@ class ChatMessageAccumulator {
5660
5906
  * signaling, and idle/suspend between turns. You control: initialization,
5661
5907
  * model/tool selection, persistence, and any custom per-turn logic.
5662
5908
  *
5909
+ * Call from inside a `chat.customAgent()` run — the wrapper binds the
5910
+ * backing Session that the iterator's stop signal and message channels
5911
+ * resolve to. (A plain `task()` does not bind it, so `createSession`
5912
+ * would throw "session handle is not initialized".)
5913
+ *
5663
5914
  * @example
5664
5915
  * ```ts
5665
- * import { task } from "@trigger.dev/sdk";
5666
5916
  * import { chat, type ChatTaskWirePayload } from "@trigger.dev/sdk/ai";
5667
5917
  * import { streamText } from "ai";
5668
5918
  * import { openai } from "@ai-sdk/openai";
5669
5919
  *
5670
- * export const myChat = task({
5920
+ * export const myChat = chat.customAgent({
5671
5921
  * id: "my-chat",
5672
5922
  * run: async (payload: ChatTaskWirePayload, { signal }) => {
5673
5923
  * const session = chat.createSession(payload, { signal });
@@ -5691,20 +5941,56 @@ function createChatSession(payload, options) {
5691
5941
  [Symbol.asyncIterator]() {
5692
5942
  let currentPayload = payload;
5693
5943
  let turn = -1;
5694
- const stop = createStopSignal();
5944
+ // Created on the first next() call, AFTER the resume-cursor seed —
5945
+ // createStopSignal attaches the `.in` SSE tail, and attaching
5946
+ // before the seed replays every record from seq 0 (the seed is a
5947
+ // no-op when the chatCustomAgent wrapper already ran it).
5948
+ let stop;
5949
+ let booted = false;
5695
5950
  const accumulator = new ChatMessageAccumulator();
5696
5951
  let previousTurnUsage;
5697
5952
  let cumulativeUsage = emptyUsage();
5698
5953
  return {
5699
5954
  async next() {
5955
+ if (!booted) {
5956
+ booted = true;
5957
+ await seedSessionInResumeCursorForCustomLoop(currentPayload);
5958
+ stop = createStopSignal();
5959
+ }
5700
5960
  turn++;
5961
+ // Head-start handover: the server triggered this run with
5962
+ // `trigger: "handover-prepare"` and signals the warm step-1 partial on
5963
+ // `session.in`. Wait for it BEFORE any `messagesInput.waitWithIdleTimeout`
5964
+ // (that facade consumes-and-discards non-message chunks and would swallow
5965
+ // the signal). Turn-0 only — continuation boots never carry this trigger.
5966
+ let handoverThisTurn = null;
5967
+ let pendingHandoverSignal = null;
5968
+ if (turn === 0 && currentPayload.trigger === "handover-prepare") {
5969
+ const signal = await waitForHandover({
5970
+ payload: currentPayload,
5971
+ idleTimeoutInSeconds: sessionIdleTimeoutOpt ??
5972
+ currentPayload.idleTimeoutInSeconds ??
5973
+ idleTimeoutInSeconds,
5974
+ timeout,
5975
+ });
5976
+ if (!signal || signal.kind === "handover-skip" || runSignal.aborted) {
5977
+ stop.cleanup();
5978
+ return { done: true, value: undefined };
5979
+ }
5980
+ pendingHandoverSignal = signal;
5981
+ handoverThisTurn = { isFinal: signal.isFinal };
5982
+ // Rewrite to a normal first-turn message turn so the rest of the loop
5983
+ // (steering setup, addIncoming, turnObj) runs unchanged.
5984
+ currentPayload = { ...currentPayload, trigger: "submit-message", message: undefined };
5985
+ }
5701
5986
  // First turn: wait when the boot payload carries no message.
5702
5987
  // Preload boots wait for the first real message; continuation
5703
5988
  // boots (fresh run via `ensureRunForSession` / end-and-continue)
5704
5989
  // arrive with the sticky boot-payload fields stripped, so running
5705
5990
  // a turn immediately would invoke the model with no user input.
5706
5991
  const isMessagelessContinuationBoot = currentPayload.continuation === true && !currentPayload.message;
5707
- if (turn === 0 && (currentPayload.trigger === "preload" || isMessagelessContinuationBoot)) {
5992
+ if (turn === 0 &&
5993
+ (currentPayload.trigger === "preload" || isMessagelessContinuationBoot)) {
5708
5994
  const result = await messagesInput.waitWithIdleTimeout({
5709
5995
  idleTimeoutInSeconds: sessionIdleTimeoutOpt ?? currentPayload.idleTimeoutInSeconds ?? 30,
5710
5996
  timeout,
@@ -5728,8 +6014,7 @@ function createChatSession(payload, options) {
5728
6014
  // Subsequent turns: wait for the next message
5729
6015
  if (turn > 0) {
5730
6016
  // chat.requestUpgrade() / chat.endRun() — exit before waiting
5731
- if (locals_js_1.locals.get(chatUpgradeRequestedKey) ||
5732
- locals_js_1.locals.get(chatEndRunRequestedKey)) {
6017
+ if (locals_js_1.locals.get(chatUpgradeRequestedKey) || locals_js_1.locals.get(chatEndRunRequestedKey)) {
5733
6018
  stop.cleanup();
5734
6019
  return { done: true, value: undefined };
5735
6020
  }
@@ -5804,6 +6089,16 @@ function createChatSession(payload, options) {
5804
6089
  ? [currentPayload.message]
5805
6090
  : [];
5806
6091
  const messages = await accumulator.addIncoming(incomingForAccumulator, currentPayload.trigger, turn);
6092
+ // Apply the head-start handover AFTER addIncoming — turn-0 addIncoming
6093
+ // replaces accumulator state, which would wipe a pre-applied splice.
6094
+ // Seed prior history first, then splice the warm step-1 partial.
6095
+ if (pendingHandoverSignal) {
6096
+ const priorHistory = currentPayload.headStartMessages;
6097
+ if (priorHistory && priorHistory.length > 0) {
6098
+ await accumulator.setMessages(priorHistory);
6099
+ }
6100
+ accumulator.applyHandover(pendingHandoverSignal);
6101
+ }
5807
6102
  // chat.requestUpgrade() called before this turn — signal transport and exit
5808
6103
  if (locals_js_1.locals.get(chatUpgradeRequestedKey)) {
5809
6104
  await writeUpgradeRequiredChunk();
@@ -5830,13 +6125,38 @@ function createChatSession(payload, options) {
5830
6125
  continuation: currentPayload.continuation ?? false,
5831
6126
  previousTurnUsage,
5832
6127
  totalUsage: cumulativeUsage,
6128
+ handover: handoverThisTurn,
5833
6129
  async setMessages(uiMessages) {
5834
6130
  await accumulator.setMessages(uiMessages);
5835
6131
  },
5836
6132
  async complete(source) {
6133
+ // Head-start final turn: the warm step-1 partial is already spliced
6134
+ // into the accumulator and IS the response — nothing to pipe. Only
6135
+ // valid on a final handover; a missing source on any other turn is a
6136
+ // mistake (it would silently finalize without an assistant response).
6137
+ if (!source) {
6138
+ if (!handoverThisTurn?.isFinal) {
6139
+ throw new Error("turn.complete() requires a stream source unless turn.handover.isFinal is true");
6140
+ }
6141
+ const response = accumulator.uiMessages.at(-1);
6142
+ if (!response || response.role !== "assistant") {
6143
+ throw new Error("turn.complete() could not find the spliced handover response");
6144
+ }
6145
+ sessionMsgSub.off();
6146
+ await chatWriteTurnComplete();
6147
+ return response;
6148
+ }
5837
6149
  let response;
5838
6150
  try {
5839
- response = await pipeChatAndCapture(source, { signal: combinedSignal });
6151
+ response = await pipeChatAndCapture(source, {
6152
+ signal: combinedSignal,
6153
+ // On a non-final handover turn, thread the spliced partial so a
6154
+ // resumed tool round's tool-output chunks merge into the
6155
+ // handed-over tool-call. Gated on the handover turn only — a
6156
+ // normal turn must not pass originalMessages (it would merge the
6157
+ // fresh response into the prior assistant message).
6158
+ ...(handoverThisTurn ? { originalMessages: accumulator.uiMessages } : {}),
6159
+ });
5840
6160
  }
5841
6161
  catch (error) {
5842
6162
  if (error instanceof Error && error.name === "AbortError") {
@@ -5941,7 +6261,8 @@ function createChatSession(payload, options) {
5941
6261
  },
5942
6262
  ];
5943
6263
  if (sessionCompaction.compactUIMessages) {
5944
- accumulator.uiMessages = await sessionCompaction.compactUIMessages(compactEvent);
6264
+ accumulator.uiMessages =
6265
+ await sessionCompaction.compactUIMessages(compactEvent);
5945
6266
  }
5946
6267
  }
5947
6268
  }
@@ -5953,7 +6274,10 @@ function createChatSession(payload, options) {
5953
6274
  // Append any non-transient data parts queued via chat.response or writer.write()
5954
6275
  const queuedParts = locals_js_1.locals.get(chatResponsePartsKey);
5955
6276
  if (queuedParts && queuedParts.length > 0) {
5956
- response = { ...response, parts: [...(response.parts ?? []), ...queuedParts] };
6277
+ response = {
6278
+ ...response,
6279
+ parts: [...(response.parts ?? []), ...queuedParts],
6280
+ };
5957
6281
  locals_js_1.locals.set(chatResponsePartsKey, []);
5958
6282
  }
5959
6283
  await accumulator.addResponse(response);
@@ -5991,7 +6315,8 @@ function createChatSession(payload, options) {
5991
6315
  return { done: false, value: turnObj };
5992
6316
  },
5993
6317
  async return() {
5994
- stop.cleanup();
6318
+ // `stop` only exists once next() has booted the iterator.
6319
+ stop?.cleanup();
5995
6320
  return { done: true, value: undefined };
5996
6321
  },
5997
6322
  };
@@ -6221,6 +6546,12 @@ function createChatStartSessionAction(taskId, options) {
6221
6546
  if (!params.chatId) {
6222
6547
  throw new Error("chat.createStartSessionAction: params.chatId is required — used as the session externalId.");
6223
6548
  }
6549
+ // Scope the action to `apiClient`'s env: re-enter without it so the body
6550
+ // runs once, under that config (read via apiClientManager.accessToken/.baseURL).
6551
+ if (options?.apiClient) {
6552
+ const { apiClient, ...rest } = options;
6553
+ return v3_1.apiClientManager.runWithConfig(apiClient, () => createChatStartSessionAction(taskId, rest)(params));
6554
+ }
6224
6555
  // The first run boots before the user's first message lands on
6225
6556
  // `.in/append`, so it sees an empty `messages` array and `trigger:
6226
6557
  // "preload"`. This matches the pre-Sessions preload semantics:
@@ -6234,8 +6565,8 @@ function createChatStartSessionAction(taskId, options) {
6234
6565
  // run-list filter by chat works without the customer having to wire it
6235
6566
  // up. Mirrors the browser-mediated `TriggerChatTransport.doStart` path.
6236
6567
  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);
6568
+ // SessionTriggerConfig.tags allows at most 5; the auto chat tag takes one slot.
6569
+ const tags = [`chat:${params.chatId}`, ...userTags].slice(0, 5);
6239
6570
  const clientDataMetadata = params.clientData !== undefined ? { metadata: params.clientData } : {};
6240
6571
  const triggerConfig = {
6241
6572
  basePayload: {
@@ -6259,6 +6590,20 @@ function createChatStartSessionAction(taskId, options) {
6259
6590
  maxAttempts: params.triggerConfig?.maxAttempts ?? options?.triggerConfig?.maxAttempts,
6260
6591
  }
6261
6592
  : {}),
6593
+ ...(options?.triggerConfig?.maxDuration !== undefined ||
6594
+ params.triggerConfig?.maxDuration !== undefined
6595
+ ? {
6596
+ maxDuration: params.triggerConfig?.maxDuration ?? options?.triggerConfig?.maxDuration,
6597
+ }
6598
+ : {}),
6599
+ ...(options?.triggerConfig?.region || params.triggerConfig?.region
6600
+ ? { region: params.triggerConfig?.region ?? options?.triggerConfig?.region }
6601
+ : {}),
6602
+ ...(options?.triggerConfig?.lockToVersion || params.triggerConfig?.lockToVersion
6603
+ ? {
6604
+ lockToVersion: params.triggerConfig?.lockToVersion ?? options?.triggerConfig?.lockToVersion,
6605
+ }
6606
+ : {}),
6262
6607
  ...(options?.triggerConfig?.idleTimeoutInSeconds !== undefined ||
6263
6608
  params.triggerConfig?.idleTimeoutInSeconds !== undefined
6264
6609
  ? {
@@ -6314,9 +6659,7 @@ function createChatStartSessionAction(taskId, options) {
6314
6659
  }
6315
6660
  function resolveChatStartBaseURL(endpoint, chatId, option) {
6316
6661
  const fallback = v3_1.apiClientManager.baseURL ?? "https://api.trigger.dev";
6317
- const raw = typeof option === "function"
6318
- ? option({ endpoint, chatId })
6319
- : option ?? fallback;
6662
+ const raw = typeof option === "function" ? option({ endpoint, chatId }) : (option ?? fallback);
6320
6663
  return raw.replace(/\/$/, "");
6321
6664
  }
6322
6665
  function overrideRequestHeaders(accessToken) {
@@ -6437,10 +6780,20 @@ exports.chat = {
6437
6780
  MessageAccumulator: ChatMessageAccumulator,
6438
6781
  /** Create a chat session (async iterator). See {@link createChatSession}. */
6439
6782
  createSession: createChatSession,
6783
+ /**
6784
+ * Wait for a `chat.headStart` handover signal inside a `chat.customAgent`
6785
+ * loop (turn 0). See {@link waitForHandover}. For most loops prefer the
6786
+ * `chat.MessageAccumulator.consumeHandover()` convenience, which also seeds
6787
+ * `payload.headStartMessages` and applies the partial.
6788
+ */
6789
+ waitForHandover,
6440
6790
  /**
6441
6791
  * Store and retrieve a resolved prompt for the current run.
6442
6792
  *
6443
6793
  * - `chat.prompt.set(resolved)` — store a `ResolvedPrompt` or plain string
6794
+ * - `chat.prompt.set(resolved, { providerOptions })` — also attach provider
6795
+ * options to the system block so a provider can cache it (e.g. Anthropic
6796
+ * prompt caching). See the prompt-caching guide.
6444
6797
  * - `chat.prompt()` — read the stored prompt (throws if not set)
6445
6798
  */
6446
6799
  prompt: Object.assign(getChatPrompt, { set: setChatPrompt }),