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

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 (61) hide show
  1. package/dist/commonjs/imports/ai-runtime-cjs.cjs.map +1 -0
  2. package/dist/commonjs/imports/ai-runtime.d.ts +1 -0
  3. package/dist/commonjs/imports/ai-runtime.js +27 -0
  4. package/dist/commonjs/v3/ai.d.ts +17 -2
  5. package/dist/commonjs/v3/ai.js +349 -139
  6. package/dist/commonjs/v3/ai.js.map +1 -1
  7. package/dist/commonjs/v3/aiAutoTelemetry.d.ts +2 -0
  8. package/dist/commonjs/v3/aiAutoTelemetry.js +81 -0
  9. package/dist/commonjs/v3/aiAutoTelemetry.js.map +1 -0
  10. package/dist/commonjs/v3/chat-client.js +8 -3
  11. package/dist/commonjs/v3/chat-client.js.map +1 -1
  12. package/dist/commonjs/v3/chat-react.js +10 -7
  13. package/dist/commonjs/v3/chat-react.js.map +1 -1
  14. package/dist/commonjs/v3/chat-server.d.ts +29 -6
  15. package/dist/commonjs/v3/chat-server.js +6 -4
  16. package/dist/commonjs/v3/chat-server.js.map +1 -1
  17. package/dist/commonjs/v3/chat.d.ts +11 -0
  18. package/dist/commonjs/v3/chat.js +95 -7
  19. package/dist/commonjs/v3/chat.js.map +1 -1
  20. package/dist/commonjs/v3/chat.test.js +53 -0
  21. package/dist/commonjs/v3/chat.test.js.map +1 -1
  22. package/dist/commonjs/v3/sessions.d.ts +8 -4
  23. package/dist/commonjs/v3/sessions.js +7 -3
  24. package/dist/commonjs/v3/sessions.js.map +1 -1
  25. package/dist/commonjs/v3/shared.js +17 -9
  26. package/dist/commonjs/v3/shared.js.map +1 -1
  27. package/dist/commonjs/v3/test/mock-chat-agent.d.ts +6 -0
  28. package/dist/commonjs/v3/test/mock-chat-agent.js +1 -0
  29. package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
  30. package/dist/commonjs/version.js +1 -1
  31. package/dist/esm/imports/ai-runtime.d.ts +2 -0
  32. package/dist/esm/imports/ai-runtime.js +16 -0
  33. package/dist/esm/imports/ai-runtime.js.map +1 -0
  34. package/dist/esm/v3/ai.d.ts +17 -2
  35. package/dist/esm/v3/ai.js +319 -110
  36. package/dist/esm/v3/ai.js.map +1 -1
  37. package/dist/esm/v3/aiAutoTelemetry.d.ts +2 -0
  38. package/dist/esm/v3/aiAutoTelemetry.js +78 -0
  39. package/dist/esm/v3/aiAutoTelemetry.js.map +1 -0
  40. package/dist/esm/v3/chat-client.js +6 -1
  41. package/dist/esm/v3/chat-client.js.map +1 -1
  42. package/dist/esm/v3/chat-react.js +10 -7
  43. package/dist/esm/v3/chat-react.js.map +1 -1
  44. package/dist/esm/v3/chat-server.d.ts +29 -6
  45. package/dist/esm/v3/chat-server.js +3 -1
  46. package/dist/esm/v3/chat-server.js.map +1 -1
  47. package/dist/esm/v3/chat.d.ts +11 -0
  48. package/dist/esm/v3/chat.js +95 -7
  49. package/dist/esm/v3/chat.js.map +1 -1
  50. package/dist/esm/v3/chat.test.js +53 -0
  51. package/dist/esm/v3/chat.test.js.map +1 -1
  52. package/dist/esm/v3/sessions.d.ts +8 -4
  53. package/dist/esm/v3/sessions.js +7 -3
  54. package/dist/esm/v3/sessions.js.map +1 -1
  55. package/dist/esm/v3/shared.js +18 -10
  56. package/dist/esm/v3/shared.js.map +1 -1
  57. package/dist/esm/v3/test/mock-chat-agent.d.ts +6 -0
  58. package/dist/esm/v3/test/mock-chat-agent.js +1 -0
  59. package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
  60. package/dist/esm/version.js +1 -1
  61. package/package.json +11 -5
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.chat = exports.upsertIncomingMessage = exports.PENDING_MESSAGE_INJECTED_TYPE = exports.ai = void 0;
4
+ exports.__findLatestSessionInCursorForTests = __findLatestSessionInCursorForTests;
4
5
  exports.__setReadChatSnapshotImplForTests = __setReadChatSnapshotImplForTests;
5
6
  exports.__setWriteChatSnapshotImplForTests = __setWriteChatSnapshotImplForTests;
6
7
  exports.__readChatSnapshotProductionPathForTests = __readChatSnapshotProductionPathForTests;
@@ -12,7 +13,9 @@ exports.__setReplaySessionInTailImplForTests = __setReplaySessionInTailImplForTe
12
13
  exports.__replaySessionInTailProductionPathForTests = __replaySessionInTailProductionPathForTests;
13
14
  exports.buildSkillTools = buildSkillTools;
14
15
  const v3_1 = require("@trigger.dev/core/v3");
15
- const ai_1 = require("ai");
16
+ // Runtime VALUES go through the ESM/CJS shim so the CJS build can `require`
17
+ // ESM-only `ai@7` (see ../imports/ai-runtime.ts).
18
+ const ai_runtime_js_1 = require("../imports/ai-runtime.js");
16
19
  const api_1 = require("@opentelemetry/api");
17
20
  const auth_js_1 = require("./auth.js");
18
21
  const locals_js_1 = require("./locals.js");
@@ -29,6 +32,7 @@ const agentSkillsRuntime_js_1 = require("./agentSkillsRuntime.js");
29
32
  const streams_js_1 = require("./streams.js");
30
33
  const sessions_js_1 = require("./sessions.js");
31
34
  const shared_js_1 = require("./shared.js");
35
+ const aiAutoTelemetry_js_1 = require("./aiAutoTelemetry.js");
32
36
  const v3_2 = require("@trigger.dev/core/v3");
33
37
  const tracer_js_1 = require("./tracer.js");
34
38
  const METADATA_KEY = "tool.execute.options";
@@ -44,7 +48,7 @@ function toModelMessages(messages) {
44
48
  // conditional spread keeps the options object byte-identical to the no-tools
45
49
  // path when nothing was declared.
46
50
  const tools = locals_js_1.locals.get(chatResolvedToolsKey);
47
- return (0, ai_1.convertToModelMessages)(messages, {
51
+ return (0, ai_runtime_js_1.convertToModelMessages)(messages, {
48
52
  ignoreIncompleteToolCalls: true,
49
53
  ...(tools ? { tools } : {}),
50
54
  });
@@ -78,51 +82,41 @@ const lastTurnCompleteSeqNumKey = locals_js_1.locals.create("chat.lastTurnComple
78
82
  * the `.in` subscription so already-processed user messages don't get
79
83
  * replayed from S2.
80
84
  *
81
- * Implementation streams the SSE endpoint and listens for `turn-complete`
82
- * via the transport's `onControl` callback; the data-chunk for-await is
83
- * just there to drive the stream. The scan is O(1 turn) because
84
- * `session.out` is bounded to roughly one turn at steady state every
85
- * successful turn-complete is followed by an S2 trim back to the
86
- * previous one (see `writeTurnCompleteChunk`).
85
+ * Implementation is a non-blocking records read (`wait=0`) the
86
+ * endpoint returns everything currently stored (including pre-trim
87
+ * records, since S2 trims are eventually consistent) in one shot, and
88
+ * we keep the LAST matching header. The previous SSE-based scan had to
89
+ * idle-wait a full 5s window to know it reached the tail, which put a
90
+ * constant ~6s tax on every continuation boot.
87
91
  *
88
92
  * Returns `undefined` if no `turn-complete` carrying the header has been
89
93
  * written yet — first-turn-ever, first turn post-OOM-with-no-prior-runs,
90
- * or a `turn-complete` written before this header existed (cross-version
91
- * boot). Callers fall back to subscribing `.in` from seq 0 in that case;
92
- * the slim-wire merge handles any dedup against snapshot-restored
93
- * messages.
94
+ * a `turn-complete` written before this header existed, or a server old
95
+ * enough that the records endpoint doesn't serialize headers. Callers
96
+ * fall back to subscribing `.in` from seq 0 in that case; the slim-wire
97
+ * merge handles any dedup against snapshot-restored messages.
94
98
  * @internal
95
99
  */
96
100
  async function findLatestSessionInCursor(chatId) {
97
101
  const apiClient = v3_1.apiClientManager.clientOrThrow();
102
+ const response = await apiClient.readSessionStreamRecords(chatId, "out");
98
103
  let latestCursor;
99
- const stream = await apiClient.subscribeToSessionStream(chatId, "out", {
100
- // 5s rather than 1s: S2 trim is eventually-consistent (10-60s
101
- // window), so a worker booting just after a trim could still see
102
- // pre-trim records and need a bit longer to drain them all before
103
- // the SSE long-poll closes. Without enough headroom the scan would
104
- // fall back to `undefined`, the `.in` cursor wouldn't be seeded,
105
- // and the next subscribe would replay messages already processed.
106
- timeoutInSeconds: 5,
107
- onControl: (event) => {
108
- if (event.subtype !== v3_1.TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE)
109
- return;
110
- const raw = (0, v3_1.headerValue)(event.headers, v3_1.SESSION_IN_EVENT_ID_HEADER);
111
- if (!raw)
112
- return;
113
- const parsed = Number.parseInt(raw, 10);
114
- if (Number.isFinite(parsed))
115
- latestCursor = parsed;
116
- },
117
- });
118
- // Drain the stream so the underlying SSE reader runs to completion. We
119
- // don't accumulate chunks; `onControl` fires inline as turn-complete
120
- // records arrive.
121
- for await (const _ of stream) {
122
- // intentionally empty
104
+ for (const record of response.records) {
105
+ if ((0, v3_1.controlSubtype)(record.headers) !== v3_1.TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE)
106
+ continue;
107
+ const raw = (0, v3_1.headerValue)(record.headers, v3_1.SESSION_IN_EVENT_ID_HEADER);
108
+ if (!raw)
109
+ continue;
110
+ const parsed = Number.parseInt(raw, 10);
111
+ if (Number.isFinite(parsed))
112
+ latestCursor = parsed;
123
113
  }
124
114
  return latestCursor;
125
115
  }
116
+ /** Test-only entry point for the records-based cursor scan. @internal */
117
+ async function __findLatestSessionInCursorForTests(chatId) {
118
+ return findLatestSessionInCursor(chatId);
119
+ }
126
120
  let readChatSnapshotImpl;
127
121
  function __setReadChatSnapshotImplForTests(impl) {
128
122
  readChatSnapshotImpl = impl;
@@ -451,7 +445,7 @@ async function replaySessionOutTail(sessionId, options) {
451
445
  });
452
446
  let last;
453
447
  try {
454
- for await (const snapshot of (0, ai_1.readUIMessageStream)({ stream: segmentStream })) {
448
+ for await (const snapshot of (0, ai_runtime_js_1.readUIMessageStream)({ stream: segmentStream })) {
455
449
  last = snapshot;
456
450
  }
457
451
  }
@@ -654,9 +648,14 @@ function createTaskToolExecuteHandler(task) {
654
648
  const toolMeta = {
655
649
  toolCallId: toolOpts?.toolCallId ?? "",
656
650
  };
657
- if (toolOpts?.experimental_context !== undefined) {
651
+ // v6 passes user context as `experimental_context`, v7 as `context`. Read
652
+ // whichever is set and stamp both so subtasks reading either name work.
653
+ const toolContext = toolOpts?.context ?? toolOpts?.experimental_context;
654
+ if (toolContext !== undefined) {
658
655
  try {
659
- toolMeta.experimental_context = JSON.parse(JSON.stringify(toolOpts.experimental_context));
656
+ const serialized = JSON.parse(JSON.stringify(toolContext));
657
+ toolMeta.experimental_context = serialized;
658
+ toolMeta.context = serialized;
660
659
  }
661
660
  catch {
662
661
  /* non-serializable */
@@ -699,9 +698,9 @@ function toolFromTask(task, options) {
699
698
  // Zod-backed tasks: use static `tool()` so runtime shape matches `ToolSet`. Generic task context
700
699
  // prevents `tool()` overloads from inferring input; `as any` is localized to this call only.
701
700
  if ("schema" in task && task.schema && (0, v3_1.isSchemaZodEsque)(task.schema)) {
702
- const staticTool = (0, ai_1.tool)({
701
+ const staticTool = (0, ai_runtime_js_1.tool)({
703
702
  description: task.description ?? "",
704
- inputSchema: (0, ai_1.zodSchema)(task.schema),
703
+ inputSchema: (0, ai_runtime_js_1.zodSchema)(task.schema),
705
704
  execute: async (input, toolOpts) => executeFromTaskInput(input, toolOpts),
706
705
  ...(options?.experimental_toToolResultContent !== undefined
707
706
  ? { experimental_toToolResultContent: options.experimental_toToolResultContent }
@@ -709,7 +708,7 @@ function toolFromTask(task, options) {
709
708
  });
710
709
  return staticTool;
711
710
  }
712
- const toolDefinition = (0, ai_1.dynamicTool)({
711
+ const toolDefinition = (0, ai_runtime_js_1.dynamicTool)({
713
712
  description: task.description,
714
713
  inputSchema: convertTaskSchemaToToolParameters(task),
715
714
  ...(options?.experimental_toToolResultContent !== undefined
@@ -777,15 +776,15 @@ function convertTaskSchemaToToolParameters(task) {
777
776
  if ("schema" in task) {
778
777
  // If TaskSchema is ArkTypeEsque, use ai.jsonSchema to convert it to a Schema
779
778
  if ("toJsonSchema" in task.schema && typeof task.schema.toJsonSchema === "function") {
780
- return (0, ai_1.jsonSchema)(task.schema.toJsonSchema());
779
+ return (0, ai_runtime_js_1.jsonSchema)(task.schema.toJsonSchema());
781
780
  }
782
781
  // If TaskSchema is ZodEsque, use ai.zodSchema to convert it to a Schema
783
782
  if ((0, v3_1.isSchemaZodEsque)(task.schema)) {
784
- return (0, ai_1.zodSchema)(task.schema);
783
+ return (0, ai_runtime_js_1.zodSchema)(task.schema);
785
784
  }
786
785
  }
787
786
  if ("jsonSchema" in task) {
788
- return (0, ai_1.jsonSchema)(task.jsonSchema);
787
+ return (0, ai_runtime_js_1.jsonSchema)(task.jsonSchema);
789
788
  }
790
789
  throw new Error("Cannot convert task to a tool. Make sure to use a task with a schema or jsonSchema.");
791
790
  }
@@ -985,8 +984,15 @@ const messagesInput = {
985
984
  on(handler) {
986
985
  return getChatSession().in.on((chunk) => {
987
986
  if (chunk.kind === "message") {
988
- return handler(chunk.payload);
987
+ // Returning `true` marks the record CONSUMED at the manager level:
988
+ // it is neither buffered for a later `once()` nor re-delivered by
989
+ // the buffer drain when the next turn re-attaches its handler.
990
+ // Without this, a message arriving mid-stream was delivered twice
991
+ // and ran a duplicate turn.
992
+ void Promise.resolve(handler(chunk.payload)).catch(() => { });
993
+ return true;
989
994
  }
995
+ return undefined;
990
996
  });
991
997
  },
992
998
  once(options) {
@@ -1080,8 +1086,13 @@ const stopInput = {
1080
1086
  on(handler) {
1081
1087
  return getChatSession().in.on((chunk) => {
1082
1088
  if (chunk.kind === "stop") {
1083
- return handler({ stop: true, message: chunk.message });
1089
+ // Consume stop records (see the messages facade above). A stop is
1090
+ // only meaningful to the turn it interrupts — buffering it would
1091
+ // let a stale stop abort a future turn.
1092
+ void Promise.resolve(handler({ stop: true, message: chunk.message })).catch(() => { });
1093
+ return true;
1084
1094
  }
1095
+ return undefined;
1085
1096
  });
1086
1097
  },
1087
1098
  once(options) {
@@ -1235,6 +1246,9 @@ const chatHandoverIsFinalKey = locals_js_1.locals.create("chat.handoverIsFinal")
1235
1246
  * `tool-approval-response` rows are AI-SDK-internal and don't need a
1236
1247
  * UIMessage representation. We map:
1237
1248
  * - `text` parts → `{ type: "text", text }`
1249
+ * - `reasoning` parts → `{ type: "reasoning", text, state: "done" }`
1250
+ * (provider metadata carried so an Anthropic thinking signature
1251
+ * survives a UIMessage → ModelMessage round trip)
1238
1252
  * - `tool-call` parts → `{ type: "tool-${name}", toolCallId,
1239
1253
  * state: "input-available", input }`
1240
1254
  * - `tool-approval-request` parts → skipped (AI SDK derives the
@@ -1251,6 +1265,14 @@ function synthesizeHandoverUIMessage(partial, messageId) {
1251
1265
  if (part.type === "text" && typeof part.text === "string") {
1252
1266
  parts.push({ type: "text", text: part.text });
1253
1267
  }
1268
+ else if (part.type === "reasoning" && typeof part.text === "string") {
1269
+ parts.push({
1270
+ type: "reasoning",
1271
+ text: part.text,
1272
+ state: "done",
1273
+ ...(part.providerOptions ? { providerMetadata: part.providerOptions } : {}),
1274
+ });
1275
+ }
1254
1276
  else if (part.type === "tool-call" && part.toolCallId && part.toolName) {
1255
1277
  parts.push({
1256
1278
  type: `tool-${part.toolName}`,
@@ -1269,7 +1291,7 @@ function synthesizeHandoverUIMessage(partial, messageId) {
1269
1291
  // browser). Fall back to a fresh id only if the handover signal
1270
1292
  // didn't carry one.
1271
1293
  return {
1272
- id: messageId ?? (0, ai_1.generateId)(),
1294
+ id: messageId ?? (0, ai_runtime_js_1.generateId)(),
1273
1295
  role: "assistant",
1274
1296
  parts,
1275
1297
  };
@@ -1425,7 +1447,7 @@ function* iterateToolParts(message) {
1425
1447
  if (message.role !== "assistant")
1426
1448
  return;
1427
1449
  for (const part of (message.parts ?? [])) {
1428
- if (!(0, ai_1.isToolUIPart)(part))
1450
+ if (!(0, ai_runtime_js_1.isToolUIPart)(part))
1429
1451
  continue;
1430
1452
  const toolCallId = part.toolCallId;
1431
1453
  if (typeof toolCallId !== "string" || toolCallId.length === 0)
@@ -1433,7 +1455,7 @@ function* iterateToolParts(message) {
1433
1455
  yield {
1434
1456
  part,
1435
1457
  toolCallId,
1436
- toolName: (0, ai_1.getToolName)(part),
1458
+ toolName: (0, ai_runtime_js_1.getToolName)(part),
1437
1459
  state: part.state,
1438
1460
  };
1439
1461
  }
@@ -1457,7 +1479,7 @@ function extractPendingToolCallsFromPartial(partial) {
1457
1479
  const parts = (partial.parts ?? []);
1458
1480
  for (let i = 0; i < parts.length; i++) {
1459
1481
  const part = parts[i];
1460
- if (!(0, ai_1.isToolUIPart)(part))
1482
+ if (!(0, ai_runtime_js_1.isToolUIPart)(part))
1461
1483
  continue;
1462
1484
  if (!isPendingToolState(part.state))
1463
1485
  continue;
@@ -1466,7 +1488,7 @@ function extractPendingToolCallsFromPartial(partial) {
1466
1488
  continue;
1467
1489
  out.push({
1468
1490
  toolCallId,
1469
- toolName: (0, ai_1.getToolName)(part),
1491
+ toolName: (0, ai_runtime_js_1.getToolName)(part),
1470
1492
  input: part.input,
1471
1493
  partIndex: i,
1472
1494
  });
@@ -1569,7 +1591,7 @@ function extractNewToolResultsFromHistory(message, messages) {
1569
1591
  function mergeIncomingIntoHydrated(hydrated, incoming) {
1570
1592
  const incomingAdvancedByCallId = new Map();
1571
1593
  for (const part of (incoming.parts ?? [])) {
1572
- if (!(0, ai_1.isToolUIPart)(part))
1594
+ if (!(0, ai_runtime_js_1.isToolUIPart)(part))
1573
1595
  continue;
1574
1596
  const toolCallId = part.toolCallId;
1575
1597
  if (typeof toolCallId !== "string" || toolCallId.length === 0)
@@ -1583,7 +1605,7 @@ function mergeIncomingIntoHydrated(hydrated, incoming) {
1583
1605
  let mutated = false;
1584
1606
  const hydratedParts = (hydrated.parts ?? []);
1585
1607
  const mergedParts = hydratedParts.map((part) => {
1586
- if (!(0, ai_1.isToolUIPart)(part))
1608
+ if (!(0, ai_runtime_js_1.isToolUIPart)(part))
1587
1609
  return part;
1588
1610
  const toolCallId = part.toolCallId;
1589
1611
  if (typeof toolCallId !== "string" || toolCallId.length === 0)
@@ -1973,7 +1995,7 @@ async function chatCompact(messages, steps, options) {
1973
1995
  return { type: "skipped" };
1974
1996
  }
1975
1997
  const result = await tracer_js_1.tracer.startActiveSpan("context compaction", async (span) => {
1976
- const compactionId = (0, ai_1.generateId)();
1998
+ const compactionId = (0, ai_runtime_js_1.generateId)();
1977
1999
  let summary;
1978
2000
  const { waitUntilComplete } = chatStream.writer({
1979
2001
  spanName: "stream compaction chunks",
@@ -2148,7 +2170,7 @@ async function drainSteeringQueue(config, messages, steps, queueOverride) {
2148
2170
  execute: ({ write }) => {
2149
2171
  write({
2150
2172
  type: ai_shared_js_2.PENDING_MESSAGE_INJECTED_TYPE,
2151
- id: (0, ai_1.generateId)(),
2173
+ id: (0, ai_runtime_js_1.generateId)(),
2152
2174
  data: {
2153
2175
  messageIds: uiMessages.map((m) => m.id),
2154
2176
  messages: uiMessages.map((m, idx) => ({
@@ -2302,9 +2324,9 @@ function findSkillByName(skills, name) {
2302
2324
  * (e.g. in a `chat.createSession` loop with custom streamText).
2303
2325
  */
2304
2326
  function buildSkillTools(skills) {
2305
- const loadSkill = (0, ai_1.tool)({
2327
+ const loadSkill = (0, ai_runtime_js_1.tool)({
2306
2328
  description: "Load the full instructions for a skill by its name. Call this first before using a skill.",
2307
- inputSchema: (0, ai_1.jsonSchema)({
2329
+ inputSchema: (0, ai_runtime_js_1.jsonSchema)({
2308
2330
  type: "object",
2309
2331
  properties: {
2310
2332
  name: {
@@ -2332,9 +2354,9 @@ function buildSkillTools(skills) {
2332
2354
  };
2333
2355
  },
2334
2356
  });
2335
- const readFile = (0, ai_1.tool)({
2357
+ const readFile = (0, ai_runtime_js_1.tool)({
2336
2358
  description: "Read a file from a skill's bundled folder. Paths must be relative to the skill's root.",
2337
- inputSchema: (0, ai_1.jsonSchema)({
2359
+ inputSchema: (0, ai_runtime_js_1.jsonSchema)({
2338
2360
  type: "object",
2339
2361
  properties: {
2340
2362
  skill: { type: "string", description: "The skill's name (from frontmatter)." },
@@ -2362,9 +2384,9 @@ function buildSkillTools(skills) {
2362
2384
  }
2363
2385
  },
2364
2386
  });
2365
- const bash = (0, ai_1.tool)({
2387
+ const bash = (0, ai_runtime_js_1.tool)({
2366
2388
  description: "Run a bash command inside a skill's bundled folder. Use this to invoke the skill's scripts. The working directory is the skill's root.",
2367
- inputSchema: (0, ai_1.jsonSchema)({
2389
+ inputSchema: (0, ai_runtime_js_1.jsonSchema)({
2368
2390
  type: "object",
2369
2391
  properties: {
2370
2392
  skill: { type: "string", description: "The skill's name (from frontmatter)." },
@@ -2597,6 +2619,11 @@ function chatCustomAgent(options) {
2597
2619
  // No client-side upsert needed.
2598
2620
  locals_js_1.locals.set(chatSessionHandleKey, sessions_js_1.sessions.open(payload.chatId));
2599
2621
  locals_js_1.locals.set(chatAgentRunContextKey, runOptions.ctx);
2622
+ // Initialize the turn-complete trim slot so `chat.writeTurnComplete`
2623
+ // trims `session.out` back to the previous turn boundary. Without
2624
+ // this the slot is undefined and the trim never runs, so `.out`
2625
+ // grows without bound for the whole custom-agent surface.
2626
+ locals_js_1.locals.set(lastTurnCompleteSeqNumKey, { value: undefined });
2600
2627
  (0, streams_js_1.markChatAgentRunForStreamsWarning)();
2601
2628
  v3_1.taskContext.setConversationId(payload.chatId);
2602
2629
  stampConversationIdOnActiveSpan(payload.chatId);
@@ -2630,6 +2657,11 @@ function chatAgent(options) {
2630
2657
  agentConfig: { type: "ai-sdk-chat" },
2631
2658
  run: async (payload, { signal: runSignal, ctx }) => {
2632
2659
  locals_js_1.locals.set(chatAgentRunContextKey, ctx);
2660
+ // On AI SDK 7, register the `@ai-sdk/otel` integration (once per process)
2661
+ // so `experimental_telemetry` spans flow into the run trace. Awaited here
2662
+ // at run boot — before any `streamText` — and a no-op on v5/v6 or when the
2663
+ // optional `@ai-sdk/otel` peer isn't installed. See ./aiAutoTelemetry.ts.
2664
+ await (0, aiAutoTelemetry_js_1.ensureAiSdkTelemetry)();
2633
2665
  // Bind the run to its backing Session so every module-level helper
2634
2666
  // (chat.stream, chat.messages, chat.stopSignal) resolves to this
2635
2667
  // chat's `.in` / `.out` channels.
@@ -2717,6 +2749,11 @@ function chatAgent(options) {
2717
2749
  // `messagesInput.waitWithIdleTimeout` so recovered turns fire first.
2718
2750
  const bootInjectedQueue = [];
2719
2751
  const couldHavePriorState = payload.continuation === true || ctx.attempt.number > 1;
2752
+ // `.in` resume cursor, computed at most once per boot. The boot
2753
+ // block below resolves it (snapshot field or records scan) and the
2754
+ // resume-cursor block reuses it instead of re-scanning.
2755
+ let bootInCursor;
2756
+ let bootInCursorResolved = false;
2720
2757
  if (!hydrateMessages && couldHavePriorState) {
2721
2758
  // Single parent span for the whole boot read phase — snapshot
2722
2759
  // read, session.out replay, session.in replay. Per-phase timing
@@ -2752,23 +2789,28 @@ function chatAgent(options) {
2752
2789
  slot.value = seeded;
2753
2790
  }
2754
2791
  }
2755
- // session.out replay
2756
- const replayOutStart = Date.now();
2757
- try {
2758
- const replayResult = await replaySessionOutTail(sessionIdForSnapshot, { lastEventId: bootSnapshot?.lastOutEventId });
2759
- replayedSettled = replayResult.settled;
2760
- replayedPartial = replayResult.partial;
2761
- replayedPartialRaw = replayResult.partialRaw;
2762
- }
2763
- catch (error) {
2764
- v3_1.logger.warn("chat.agent: session.out replay failed; using snapshot only", {
2765
- error: error instanceof Error ? error.message : String(error),
2766
- sessionId: sessionIdForSnapshot,
2767
- });
2768
- }
2769
- bootSpan.setAttribute("chat.boot.replay.out.durationMs", Date.now() - replayOutStart);
2770
- bootSpan.setAttribute("chat.boot.replay.out.settledCount", replayedSettled.length);
2771
- bootSpan.setAttribute("chat.boot.replay.out.partialPresent", replayedPartial !== undefined);
2792
+ // The `.out` replay and the `.in` cursor + tail read are
2793
+ // independent (both depend only on the snapshot) — run them
2794
+ // concurrently. Each phase keeps its own catch + duration
2795
+ // attribute.
2796
+ const replayOutPhase = async () => {
2797
+ const replayOutStart = Date.now();
2798
+ try {
2799
+ const replayResult = await replaySessionOutTail(sessionIdForSnapshot, { lastEventId: bootSnapshot?.lastOutEventId });
2800
+ replayedSettled = replayResult.settled;
2801
+ replayedPartial = replayResult.partial;
2802
+ replayedPartialRaw = replayResult.partialRaw;
2803
+ }
2804
+ catch (error) {
2805
+ v3_1.logger.warn("chat.agent: session.out replay failed; using snapshot only", {
2806
+ error: error instanceof Error ? error.message : String(error),
2807
+ sessionId: sessionIdForSnapshot,
2808
+ });
2809
+ }
2810
+ bootSpan.setAttribute("chat.boot.replay.out.durationMs", Date.now() - replayOutStart);
2811
+ bootSpan.setAttribute("chat.boot.replay.out.settledCount", replayedSettled.length);
2812
+ bootSpan.setAttribute("chat.boot.replay.out.partialPresent", replayedPartial !== undefined);
2813
+ };
2772
2814
  // session.in tail read
2773
2815
  //
2774
2816
  // session.in carries the user-side of the conversation
@@ -2779,20 +2821,43 @@ function chatAgent(options) {
2779
2821
  // visible via the live SSE subscription — by which point they
2780
2822
  // would arrive AFTER the partial-assistant orphan and look like
2781
2823
  // brand-new turns to the model, producing inverted chains.
2782
- const replayInStart = Date.now();
2783
- const lastInEventId = await findLatestSessionInCursor(payload.chatId)
2784
- .then((cursor) => (cursor !== undefined ? String(cursor) : undefined))
2785
- .catch(() => undefined);
2786
- try {
2787
- replayedInTail = await replaySessionInTail(payload.chatId, {
2788
- lastEventId: lastInEventId,
2789
- });
2790
- }
2791
- catch (error) {
2792
- v3_1.logger.warn("chat.agent: session.in replay failed; in-flight users may not be recovered", { error: error instanceof Error ? error.message : String(error) });
2793
- }
2794
- bootSpan.setAttribute("chat.boot.replay.in.durationMs", Date.now() - replayInStart);
2795
- bootSpan.setAttribute("chat.boot.replay.in.userCount", replayedInTail.length);
2824
+ //
2825
+ // The cursor comes from the snapshot when present (written
2826
+ // there since `lastInEventId` was added) otherwise from a
2827
+ // records scan of `.out`'s latest turn-complete header.
2828
+ const replayInPhase = async () => {
2829
+ const replayInStart = Date.now();
2830
+ const snapshotInCursor = bootSnapshot?.lastInEventId !== undefined
2831
+ ? Number.parseInt(bootSnapshot.lastInEventId, 10)
2832
+ : undefined;
2833
+ if (snapshotInCursor !== undefined && Number.isFinite(snapshotInCursor)) {
2834
+ bootInCursor = snapshotInCursor;
2835
+ bootInCursorResolved = true;
2836
+ }
2837
+ else {
2838
+ try {
2839
+ bootInCursor = await findLatestSessionInCursor(payload.chatId);
2840
+ bootInCursorResolved = true;
2841
+ }
2842
+ catch {
2843
+ // Transient scan failure: leave unresolved so the
2844
+ // resume-cursor block below retries the lookup.
2845
+ bootInCursor = undefined;
2846
+ }
2847
+ }
2848
+ bootSpan.setAttribute("chat.boot.replay.in.cursorFromSnapshot", snapshotInCursor !== undefined);
2849
+ try {
2850
+ replayedInTail = await replaySessionInTail(payload.chatId, {
2851
+ lastEventId: bootInCursor !== undefined ? String(bootInCursor) : undefined,
2852
+ });
2853
+ }
2854
+ catch (error) {
2855
+ v3_1.logger.warn("chat.agent: session.in replay failed; in-flight users may not be recovered", { error: error instanceof Error ? error.message : String(error) });
2856
+ }
2857
+ bootSpan.setAttribute("chat.boot.replay.in.durationMs", Date.now() - replayInStart);
2858
+ bootSpan.setAttribute("chat.boot.replay.in.userCount", replayedInTail.length);
2859
+ };
2860
+ await Promise.all([replayOutPhase(), replayInPhase()]);
2796
2861
  }, {
2797
2862
  attributes: {
2798
2863
  [v3_1.SemanticInternalAttributes.STYLE_ICON]: "tabler-rotate-clockwise",
@@ -2833,7 +2898,12 @@ function chatAgent(options) {
2833
2898
  bootSnapshot !== undefined;
2834
2899
  if (needsResumeCursor) {
2835
2900
  try {
2836
- const cursor = await findLatestSessionInCursor(payload.chatId);
2901
+ // Reuse the cursor the boot block already resolved (snapshot
2902
+ // field or records scan) — only scan here when the boot block
2903
+ // was skipped (hydrateMessages, or snapshot-only signals).
2904
+ const cursor = bootInCursorResolved
2905
+ ? bootInCursor
2906
+ : await findLatestSessionInCursor(payload.chatId);
2837
2907
  if (cursor !== undefined) {
2838
2908
  v3_1.sessionStreams.setLastSeqNum(payload.chatId, "in", cursor);
2839
2909
  v3_1.sessionStreams.setLastDispatchedSeqNum(payload.chatId, "in", cursor);
@@ -3597,6 +3667,18 @@ function chatAgent(options) {
3597
3667
  // therefore a delta merge, not a full-history reset.
3598
3668
  if (currentWirePayload.trigger !== "action") {
3599
3669
  let cleanedUIMessages = cleanedIncomingMessages;
3670
+ // Turn-0 head-start with hydrateMessages: the boot seeding from
3671
+ // `payload.headStartMessages` is non-hydrate-only, so ship the
3672
+ // route handler's first-turn history to the hydrate hook as
3673
+ // incoming messages instead (gated on the pending handover).
3674
+ if (turn === 0 &&
3675
+ hydrateMessages &&
3676
+ cleanedUIMessages.length === 0 &&
3677
+ (locals_js_1.locals.get(chatHandoverPartialKey)?.length ?? 0) > 0 &&
3678
+ Array.isArray(payload.headStartMessages) &&
3679
+ payload.headStartMessages.length > 0) {
3680
+ cleanedUIMessages = payload.headStartMessages;
3681
+ }
3600
3682
  // Validate/transform UIMessages before conversion — catches malformed
3601
3683
  // messages from storage or untrusted input before they reach the model.
3602
3684
  // Slim wire: triggers like `regenerate-message` carry no incoming
@@ -3775,40 +3857,47 @@ function chatAgent(options) {
3775
3857
  // `preload` / `close` / `handover-prepare` and submits
3776
3858
  // with no incoming message fall through with the boot-
3777
3859
  // seeded accumulator unchanged.
3778
- if (turn === 0) {
3779
- // Head-start handover splice (turn 0 only): the
3780
- // `chat.handover` route handler signalled a mid-turn
3781
- // handover, so splice its partial assistant response
3782
- // (text + pending tool-calls + the synthesized
3783
- // tool-approval round) onto the accumulator.
3784
- // `streamText` then hits AI SDK's initial-tool-
3785
- // execution branch, runs the agent-side tool executes,
3786
- // and resumes from step 2 — skipping the first model
3787
- // call (already done by the handler).
3788
- //
3789
- // We also synthesize a UIMessage form of the partial
3790
- // assistant and push it to `accumulatedUIMessages` so
3791
- // AI SDK's `processUIMessageStream` (invoked when the
3792
- // run loop calls `runResult.toUIMessageStream({
3793
- // onFinish })`) can initialize `state.message` from
3794
- // the trailing assistant in `originalMessages`. Without
3795
- // that, the `tool-output-available` chunks emitted by
3796
- // the initial-tool-execution branch can't find their
3797
- // matching tool-call in state and AI SDK throws
3798
- // `UIMessageStreamError: No tool invocation found`.
3799
- const pendingHandoverPartial = locals_js_1.locals.get(chatHandoverPartialKey);
3800
- if (pendingHandoverPartial && pendingHandoverPartial.length > 0) {
3860
+ }
3861
+ if (turn === 0) {
3862
+ // Head-start handover splice (turn 0 only, BOTH
3863
+ // accumulation branches hydrate and default): the
3864
+ // `chat.handover` route handler signalled a mid-turn
3865
+ // handover, so splice its partial assistant response
3866
+ // (text + pending tool-calls + the synthesized
3867
+ // tool-approval round) onto the accumulator.
3868
+ // `streamText` then hits AI SDK's initial-tool-
3869
+ // execution branch, runs the agent-side tool executes,
3870
+ // and resumes from step 2 — skipping the first model
3871
+ // call (already done by the handler).
3872
+ //
3873
+ // We also synthesize a UIMessage form of the partial
3874
+ // assistant and push it to `accumulatedUIMessages` so
3875
+ // AI SDK's `processUIMessageStream` (invoked when the
3876
+ // run loop calls `runResult.toUIMessageStream({
3877
+ // onFinish })`) can initialize `state.message` from
3878
+ // the trailing assistant in `originalMessages`. Without
3879
+ // that, the `tool-output-available` chunks emitted by
3880
+ // the initial-tool-execution branch can't find their
3881
+ // matching tool-call in state and AI SDK throws
3882
+ // `UIMessageStreamError: No tool invocation found`.
3883
+ const pendingHandoverPartial = locals_js_1.locals.get(chatHandoverPartialKey);
3884
+ 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) {
3801
3891
  accumulatedMessages.push(...pendingHandoverPartial);
3802
- const handoverMessageId = locals_js_1.locals.get(chatHandoverMessageIdKey);
3803
3892
  const partialUI = synthesizeHandoverUIMessage(pendingHandoverPartial, handoverMessageId);
3804
3893
  if (partialUI) {
3805
3894
  accumulatedUIMessages.push(partialUI);
3806
3895
  }
3807
- locals_js_1.locals.set(chatHandoverPartialKey, []); // consume once
3808
3896
  }
3897
+ locals_js_1.locals.set(chatHandoverPartialKey, []); // consume once
3809
3898
  }
3810
- locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
3811
3899
  }
3900
+ locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
3812
3901
  } // end if (trigger !== "action")
3813
3902
  // ── Action result handling ──────────────────────────────
3814
3903
  // For action turns, skip the turn machinery entirely.
@@ -3826,7 +3915,7 @@ function chatAgent(options) {
3826
3915
  const resolvedOptions = resolveUIMessageStreamOptions();
3827
3916
  const uiStream = actionStreamResult.toUIMessageStream({
3828
3917
  ...resolvedOptions,
3829
- generateMessageId: resolvedOptions.generateMessageId ?? ai_1.generateId,
3918
+ generateMessageId: resolvedOptions.generateMessageId ?? ai_runtime_js_1.generateId,
3830
3919
  });
3831
3920
  await pipeChat(uiStream, {
3832
3921
  signal: combinedSignal,
@@ -4050,7 +4139,7 @@ function chatAgent(options) {
4050
4139
  // Always provide generateMessageId so the start chunk carries a
4051
4140
  // messageId. Without this, the frontend and backend generate IDs
4052
4141
  // independently and they won't match for ID-based dedup.
4053
- generateMessageId: resolvedOptions.generateMessageId ?? ai_1.generateId,
4142
+ generateMessageId: resolvedOptions.generateMessageId ?? ai_runtime_js_1.generateId,
4054
4143
  onFinish: ({ responseMessage, finishReason, }) => {
4055
4144
  capturedResponseMessage = responseMessage;
4056
4145
  capturedFinishReason = finishReason;
@@ -4178,7 +4267,7 @@ function chatAgent(options) {
4178
4267
  // may produce a message with an empty ID since IDs are normally
4179
4268
  // assigned by the frontend's useChat).
4180
4269
  if (!capturedResponseMessage.id) {
4181
- capturedResponseMessage = { ...capturedResponseMessage, id: (0, ai_1.generateId)() };
4270
+ capturedResponseMessage = { ...capturedResponseMessage, id: (0, ai_runtime_js_1.generateId)() };
4182
4271
  }
4183
4272
  // Append any non-transient data parts queued via chat.response or writer.write()
4184
4273
  const queuedParts = locals_js_1.locals.get(chatResponsePartsKey);
@@ -4234,7 +4323,7 @@ function chatAgent(options) {
4234
4323
  const remainingParts = locals_js_1.locals.get(chatResponsePartsKey);
4235
4324
  if (remainingParts && remainingParts.length > 0) {
4236
4325
  capturedResponseMessage = {
4237
- id: (0, ai_1.generateId)(),
4326
+ id: (0, ai_runtime_js_1.generateId)(),
4238
4327
  role: "assistant",
4239
4328
  parts: [...remainingParts],
4240
4329
  };
@@ -4276,7 +4365,7 @@ function chatAgent(options) {
4276
4365
  });
4277
4366
  if (shouldTrigger) {
4278
4367
  await tracer_js_1.tracer.startActiveSpan("context compaction (outer loop)", async (compactionSpan) => {
4279
- const compactionId = (0, ai_1.generateId)();
4368
+ const compactionId = (0, ai_runtime_js_1.generateId)();
4280
4369
  const { waitUntilComplete } = chatStream.writer({
4281
4370
  spanName: "stream compaction chunks",
4282
4371
  collapsed: true,
@@ -4505,11 +4594,15 @@ function chatAgent(options) {
4505
4594
  if (!hydrateMessages) {
4506
4595
  try {
4507
4596
  await tracer_js_1.tracer.startActiveSpan("snapshot.write", async () => {
4597
+ const snapshotInCursor = getChatSession().in.lastDispatchedSeqNum();
4508
4598
  await writeChatSnapshot(sessionIdForSnapshot, {
4509
4599
  version: 1,
4510
4600
  savedAt: Date.now(),
4511
4601
  messages: accumulatedUIMessages,
4512
4602
  lastOutEventId: turnCompleteResult?.lastEventId,
4603
+ lastInEventId: snapshotInCursor !== undefined
4604
+ ? String(snapshotInCursor)
4605
+ : undefined,
4513
4606
  });
4514
4607
  }, {
4515
4608
  attributes: {
@@ -4646,17 +4739,100 @@ function chatAgent(options) {
4646
4739
  if (turnError instanceof v3_1.OutOfMemoryError) {
4647
4740
  throw turnError;
4648
4741
  }
4742
+ let errorTurnCompleteResult;
4649
4743
  try {
4650
4744
  await withChatWriter(async (writer) => {
4651
4745
  const errorText = turnError instanceof Error ? turnError.message : "An unexpected error occurred";
4652
4746
  writer.write({ type: "error", errorText });
4653
4747
  });
4654
4748
  // Signal turn complete so the client knows this turn is done
4655
- await writeTurnCompleteChunk(currentWirePayload.chatId);
4749
+ errorTurnCompleteResult = await writeTurnCompleteChunk(currentWirePayload.chatId);
4656
4750
  }
4657
4751
  catch {
4658
4752
  // Best-effort — if stream write fails, let the run continue anyway
4659
4753
  }
4754
+ // The submit-message merge into the accumulator may not have run
4755
+ // yet (a pre-run hook threw), so fold the wire message in for the
4756
+ // error event + snapshot — the cursor has already advanced past it,
4757
+ // so otherwise it survives in neither the snapshot nor the `.in` tail.
4758
+ const erroredWireMessage = currentWirePayload.message;
4759
+ const erroredUIMessages = erroredWireMessage &&
4760
+ !accumulatedUIMessages.some((m) => m.id === erroredWireMessage.id)
4761
+ ? [...accumulatedUIMessages, erroredWireMessage]
4762
+ : accumulatedUIMessages;
4763
+ // Fire onTurnComplete on the error path too — the docs promise it
4764
+ // runs "after every turn, successful or errored" so customers can
4765
+ // mark the turn failed. `responseMessage` is undefined/partial and
4766
+ // `error` carries the thrown value.
4767
+ if (onTurnComplete) {
4768
+ try {
4769
+ await tracer_js_1.tracer.startActiveSpan("onTurnComplete()", async () => {
4770
+ await onTurnComplete({
4771
+ ctx,
4772
+ chatId: currentWirePayload.chatId,
4773
+ messages: accumulatedMessages,
4774
+ uiMessages: erroredUIMessages,
4775
+ newMessages: [],
4776
+ newUIMessages: erroredWireMessage ? [erroredWireMessage] : [],
4777
+ responseMessage: undefined,
4778
+ rawResponseMessage: undefined,
4779
+ turn,
4780
+ runId: ctx.run.id,
4781
+ chatAccessToken: "",
4782
+ // Parsed `clientData` isn't reliably in scope here (parsing
4783
+ // may itself be the failure), and the raw metadata is the
4784
+ // wrong shape — leave it undefined on the error path.
4785
+ clientData: undefined,
4786
+ stopped: false,
4787
+ continuation,
4788
+ previousRunId,
4789
+ preloaded,
4790
+ totalUsage: cumulativeUsage,
4791
+ finishReason: "error",
4792
+ error: turnError,
4793
+ lastEventId: errorTurnCompleteResult?.lastEventId,
4794
+ });
4795
+ }, {
4796
+ attributes: {
4797
+ [v3_1.SemanticInternalAttributes.STYLE_ICON]: "task-hook-onComplete",
4798
+ [v3_1.SemanticInternalAttributes.COLLAPSED]: true,
4799
+ "chat.id": currentWirePayload.chatId,
4800
+ "chat.turn": turn + 1,
4801
+ "chat.errored": true,
4802
+ },
4803
+ });
4804
+ }
4805
+ catch {
4806
+ // A throwing onTurnComplete on the error path must not crash
4807
+ // the run — keep the conversation alive for the next message.
4808
+ }
4809
+ }
4810
+ // Persist a snapshot so the failed turn's user message isn't
4811
+ // stranded. `writeTurnCompleteChunk` already advanced the `.in`
4812
+ // cursor past it (via the session-in-event-id header), and the
4813
+ // success-path snapshot write is skipped on error — without this
4814
+ // the next boot would resume past a message that exists in
4815
+ // neither the snapshot nor the replayable `.in` tail.
4816
+ if (!hydrateMessages) {
4817
+ try {
4818
+ const errorSnapshotInCursor = getChatSession().in.lastDispatchedSeqNum();
4819
+ await writeChatSnapshot(sessionIdForSnapshot, {
4820
+ version: 1,
4821
+ savedAt: Date.now(),
4822
+ messages: erroredUIMessages,
4823
+ lastOutEventId: errorTurnCompleteResult?.lastEventId,
4824
+ lastInEventId: errorSnapshotInCursor !== undefined
4825
+ ? String(errorSnapshotInCursor)
4826
+ : undefined,
4827
+ });
4828
+ }
4829
+ catch (error) {
4830
+ v3_1.logger.warn("chat.agent: error-path snapshot write failed", {
4831
+ error: error instanceof Error ? error.message : String(error),
4832
+ sessionId: sessionIdForSnapshot,
4833
+ });
4834
+ }
4835
+ }
4660
4836
  // chat.requestUpgrade() / chat.endRun() — exit after error turn too
4661
4837
  if (locals_js_1.locals.get(chatUpgradeRequestedKey) ||
4662
4838
  locals_js_1.locals.get(chatEndRunRequestedKey)) {
@@ -5350,7 +5526,7 @@ class ChatMessageAccumulator {
5350
5526
  }
5351
5527
  async addResponse(response) {
5352
5528
  if (!response.id) {
5353
- response = { ...response, id: (0, ai_1.generateId)() };
5529
+ response = { ...response, id: (0, ai_runtime_js_1.generateId)() };
5354
5530
  }
5355
5531
  this.uiMessages.push(response);
5356
5532
  try {
@@ -5522,18 +5698,32 @@ function createChatSession(payload, options) {
5522
5698
  return {
5523
5699
  async next() {
5524
5700
  turn++;
5525
- // First turn: handle preload wait for the first real message
5526
- if (turn === 0 && currentPayload.trigger === "preload") {
5701
+ // First turn: wait when the boot payload carries no message.
5702
+ // Preload boots wait for the first real message; continuation
5703
+ // boots (fresh run via `ensureRunForSession` / end-and-continue)
5704
+ // arrive with the sticky boot-payload fields stripped, so running
5705
+ // a turn immediately would invoke the model with no user input.
5706
+ const isMessagelessContinuationBoot = currentPayload.continuation === true && !currentPayload.message;
5707
+ if (turn === 0 && (currentPayload.trigger === "preload" || isMessagelessContinuationBoot)) {
5527
5708
  const result = await messagesInput.waitWithIdleTimeout({
5528
5709
  idleTimeoutInSeconds: sessionIdleTimeoutOpt ?? currentPayload.idleTimeoutInSeconds ?? 30,
5529
5710
  timeout,
5530
- spanName: "waiting for first message",
5711
+ spanName: currentPayload.trigger === "preload"
5712
+ ? "waiting for first message"
5713
+ : "waiting for first message (continuation)",
5531
5714
  });
5532
5715
  if (!result.ok || runSignal.aborted) {
5533
5716
  stop.cleanup();
5534
5717
  return { done: true, value: undefined };
5535
5718
  }
5719
+ const continuationBoot = isMessagelessContinuationBoot;
5536
5720
  currentPayload = result.output;
5721
+ // Preserve the continuation flag — the wire payload of the next
5722
+ // message doesn't carry it, and `turn.continuation` is how the
5723
+ // user knows to seed history (e.g. `turn.setMessages(stored)`).
5724
+ if (continuationBoot && currentPayload.continuation === undefined) {
5725
+ currentPayload = { ...currentPayload, continuation: true };
5726
+ }
5537
5727
  }
5538
5728
  // Subsequent turns: wait for the next message
5539
5729
  if (turn > 0) {
@@ -5679,21 +5869,29 @@ function createChatSession(payload, options) {
5679
5869
  const queuedParts = locals_js_1.locals.get(chatResponsePartsKey);
5680
5870
  if (queuedParts && queuedParts.length > 0) {
5681
5871
  await accumulator.addResponse({
5682
- id: (0, ai_1.generateId)(),
5872
+ id: (0, ai_runtime_js_1.generateId)(),
5683
5873
  role: "assistant",
5684
5874
  parts: queuedParts,
5685
5875
  });
5686
5876
  locals_js_1.locals.set(chatResponsePartsKey, []);
5687
5877
  }
5688
5878
  }
5689
- // Capture token usage from the streamText result
5879
+ // Capture token usage from the streamText result. Race with a 2s
5880
+ // timeout — on stop-abort the AI SDK's totalUsage promise can hang
5881
+ // indefinitely, which would wedge the turn loop (same guard as
5882
+ // chat.agent's turn loop).
5690
5883
  let turnUsage;
5691
5884
  if (typeof source.totalUsage?.then === "function") {
5692
5885
  try {
5693
- const usage = await source.totalUsage;
5694
- turnUsage = usage;
5695
- previousTurnUsage = usage;
5696
- cumulativeUsage = addUsage(cumulativeUsage, usage);
5886
+ const usage = (await Promise.race([
5887
+ source.totalUsage,
5888
+ new Promise((r) => setTimeout(() => r(undefined), 2_000)),
5889
+ ]));
5890
+ if (usage) {
5891
+ turnUsage = usage;
5892
+ previousTurnUsage = usage;
5893
+ cumulativeUsage = addUsage(cumulativeUsage, usage);
5894
+ }
5697
5895
  }
5698
5896
  catch {
5699
5897
  /* non-fatal */
@@ -6036,7 +6234,8 @@ function createChatStartSessionAction(taskId, options) {
6036
6234
  // run-list filter by chat works without the customer having to wire it
6037
6235
  // up. Mirrors the browser-mediated `TriggerChatTransport.doStart` path.
6038
6236
  const userTags = params.triggerConfig?.tags ?? options?.triggerConfig?.tags ?? [];
6039
- const tags = [`chat:${params.chatId}`, ...userTags].slice(0, 5);
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);
6040
6239
  const clientDataMetadata = params.clientData !== undefined ? { metadata: params.clientData } : {};
6041
6240
  const triggerConfig = {
6042
6241
  basePayload: {
@@ -6334,8 +6533,19 @@ async function writeTurnCompleteChunk(_chatId, publicAccessToken) {
6334
6533
  // 2. Trim back to the previous turn-complete, if we have one. Skipping on
6335
6534
  // first-turn-ever (or first turn post-OOM without a snapshot seed) is
6336
6535
  // fine — the chain catches up next turn.
6337
- const slot = locals_js_1.locals.get(lastTurnCompleteSeqNumKey);
6338
- const prev = slot?.value;
6536
+ //
6537
+ // Lazily create the slot if a caller reached here without one (a plain
6538
+ // `task()` driving `chat.createSession` / `chat.writeTurnComplete`, vs.
6539
+ // chatAgent/chatCustomAgent which seed it at boot). The first call then
6540
+ // does no trim (nothing before it) and records its seq; later calls trim
6541
+ // — so `.out` is bounded for every writeTurnComplete caller, not just the
6542
+ // built-in agents.
6543
+ let slot = locals_js_1.locals.get(lastTurnCompleteSeqNumKey);
6544
+ if (!slot) {
6545
+ slot = { value: undefined };
6546
+ locals_js_1.locals.set(lastTurnCompleteSeqNumKey, slot);
6547
+ }
6548
+ const prev = slot.value;
6339
6549
  if (slot && prev !== undefined) {
6340
6550
  try {
6341
6551
  await session.out.trimTo(prev);