@trigger.dev/sdk 4.6.3 → 4.7.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 (119) hide show
  1. package/dist/commonjs/v3/ai.d.ts +5 -2
  2. package/dist/commonjs/v3/ai.js +189 -266
  3. package/dist/commonjs/v3/ai.js.map +1 -1
  4. package/dist/commonjs/v3/chat-client.js +7 -0
  5. package/dist/commonjs/v3/chat-client.js.map +1 -1
  6. package/dist/commonjs/v3/chat-server.d.ts +1 -0
  7. package/dist/commonjs/v3/chat-server.js +8 -0
  8. package/dist/commonjs/v3/chat-server.js.map +1 -1
  9. package/dist/commonjs/v3/chat.d.ts +28 -4
  10. package/dist/commonjs/v3/chat.js +41 -9
  11. package/dist/commonjs/v3/chat.js.map +1 -1
  12. package/dist/commonjs/v3/chatRouteWait.d.ts +21 -0
  13. package/dist/commonjs/v3/chatRouteWait.js +43 -0
  14. package/dist/commonjs/v3/chatRouteWait.js.map +1 -0
  15. package/dist/commonjs/v3/compactionResponse.js +5 -0
  16. package/dist/commonjs/v3/compactionResponse.js.map +1 -1
  17. package/dist/commonjs/v3/concurrency-shared.d.ts +13 -0
  18. package/dist/commonjs/v3/concurrency-shared.js +35 -0
  19. package/dist/commonjs/v3/concurrency-shared.js.map +1 -0
  20. package/dist/commonjs/v3/concurrencyLimits.d.ts +73 -0
  21. package/dist/commonjs/v3/concurrencyLimits.js +166 -0
  22. package/dist/commonjs/v3/concurrencyLimits.js.map +1 -0
  23. package/dist/commonjs/v3/index.d.ts +2 -1
  24. package/dist/commonjs/v3/index.js +3 -1
  25. package/dist/commonjs/v3/index.js.map +1 -1
  26. package/dist/commonjs/v3/managedChatResponse.d.ts +44 -0
  27. package/dist/commonjs/v3/managedChatResponse.js +233 -0
  28. package/dist/commonjs/v3/managedChatResponse.js.map +1 -0
  29. package/dist/commonjs/v3/queues.d.ts +31 -0
  30. package/dist/commonjs/v3/queues.js +31 -0
  31. package/dist/commonjs/v3/queues.js.map +1 -1
  32. package/dist/commonjs/v3/shared.d.ts +18 -1
  33. package/dist/commonjs/v3/shared.js +137 -47
  34. package/dist/commonjs/v3/shared.js.map +1 -1
  35. package/dist/commonjs/v3/steeringContext.d.ts +41 -0
  36. package/dist/commonjs/v3/steeringContext.js +118 -0
  37. package/dist/commonjs/v3/steeringContext.js.map +1 -0
  38. package/dist/commonjs/v3/transcriptStorage.d.ts +4 -1
  39. package/dist/commonjs/v3/transcriptStorage.js +51 -4
  40. package/dist/commonjs/v3/transcriptStorage.js.map +1 -1
  41. package/dist/commonjs/version.js +1 -1
  42. package/dist/esm/v3/ai.d.ts +5 -2
  43. package/dist/esm/v3/ai.js +189 -266
  44. package/dist/esm/v3/ai.js.map +1 -1
  45. package/dist/esm/v3/chat-client.js +7 -0
  46. package/dist/esm/v3/chat-client.js.map +1 -1
  47. package/dist/esm/v3/chat-server.d.ts +1 -0
  48. package/dist/esm/v3/chat-server.js +8 -0
  49. package/dist/esm/v3/chat-server.js.map +1 -1
  50. package/dist/esm/v3/chat.d.ts +28 -4
  51. package/dist/esm/v3/chat.js +41 -9
  52. package/dist/esm/v3/chat.js.map +1 -1
  53. package/dist/esm/v3/chatRouteWait.d.ts +21 -0
  54. package/dist/esm/v3/chatRouteWait.js +40 -0
  55. package/dist/esm/v3/chatRouteWait.js.map +1 -0
  56. package/dist/esm/v3/compactionResponse.js +5 -0
  57. package/dist/esm/v3/compactionResponse.js.map +1 -1
  58. package/dist/esm/v3/concurrency-shared.d.ts +13 -0
  59. package/dist/esm/v3/concurrency-shared.js +31 -0
  60. package/dist/esm/v3/concurrency-shared.js.map +1 -0
  61. package/dist/esm/v3/concurrencyLimits.d.ts +73 -0
  62. package/dist/esm/v3/concurrencyLimits.js +158 -0
  63. package/dist/esm/v3/concurrencyLimits.js.map +1 -0
  64. package/dist/esm/v3/index.d.ts +2 -1
  65. package/dist/esm/v3/index.js +2 -1
  66. package/dist/esm/v3/index.js.map +1 -1
  67. package/dist/esm/v3/managedChatResponse.d.ts +44 -0
  68. package/dist/esm/v3/managedChatResponse.js +228 -0
  69. package/dist/esm/v3/managedChatResponse.js.map +1 -0
  70. package/dist/esm/v3/queues.d.ts +31 -0
  71. package/dist/esm/v3/queues.js +31 -0
  72. package/dist/esm/v3/queues.js.map +1 -1
  73. package/dist/esm/v3/shared.d.ts +18 -1
  74. package/dist/esm/v3/shared.js +136 -47
  75. package/dist/esm/v3/shared.js.map +1 -1
  76. package/dist/esm/v3/steeringContext.d.ts +41 -0
  77. package/dist/esm/v3/steeringContext.js +113 -0
  78. package/dist/esm/v3/steeringContext.js.map +1 -0
  79. package/dist/esm/v3/transcriptStorage.d.ts +4 -1
  80. package/dist/esm/v3/transcriptStorage.js +51 -4
  81. package/dist/esm/v3/transcriptStorage.js.map +1 -1
  82. package/dist/esm/version.js +1 -1
  83. package/docs/ai-chat/client-protocol.mdx +3 -1
  84. package/docs/ai-chat/error-handling.mdx +44 -76
  85. package/docs/ai-chat/fast-starts.mdx +1 -1
  86. package/docs/ai-chat/frontend.mdx +27 -21
  87. package/docs/ai-chat/patterns/branching-conversations.mdx +95 -230
  88. package/docs/ai-chat/patterns/human-in-the-loop.mdx +166 -164
  89. package/docs/ai-chat/patterns/tool-result-auditing.mdx +28 -27
  90. package/docs/ai-chat/patterns/version-upgrades.mdx +4 -4
  91. package/docs/ai-chat/pending-messages.mdx +19 -5
  92. package/docs/ai-chat/quick-start.mdx +26 -20
  93. package/docs/ai-chat/reference.mdx +21 -3
  94. package/docs/ai-chat/sessions.mdx +1 -1
  95. package/docs/ai-chat/testing.mdx +16 -4
  96. package/docs/concurrency.mdx +384 -0
  97. package/docs/database-connections.mdx +3 -3
  98. package/docs/deploy-environment-variables.mdx +6 -0
  99. package/docs/deployment/atomic-deployment.mdx +416 -132
  100. package/docs/deployment/overview.mdx +2 -2
  101. package/docs/github-actions.mdx +2 -2
  102. package/docs/github-integration.mdx +2 -2
  103. package/docs/idempotency.mdx +43 -5
  104. package/docs/introduction.mdx +1 -1
  105. package/docs/limits.mdx +16 -6
  106. package/docs/observability/query.mdx +25 -0
  107. package/docs/queues.mdx +271 -0
  108. package/docs/reports.mdx +1 -1
  109. package/docs/runs/priority.mdx +2 -25
  110. package/docs/self-hosting/env/webapp.mdx +7 -0
  111. package/docs/tasks/overview.mdx +3 -5
  112. package/docs/troubleshooting-alerts.mdx +124 -1
  113. package/docs/troubleshooting.mdx +12 -0
  114. package/docs/vercel-integration.mdx +6 -7
  115. package/docs/versioning.mdx +1 -1
  116. package/docs/writing-tasks-introduction.mdx +2 -1
  117. package/package.json +2 -2
  118. package/docs/deployment/version-skew-protection.mdx +0 -492
  119. package/docs/queue-concurrency.mdx +0 -358
@@ -16,9 +16,12 @@ const v3_1 = require("@trigger.dev/core/v3");
16
16
  // ESM-only `ai@7` (see ../imports/ai-runtime.ts).
17
17
  const api_1 = require("@opentelemetry/api");
18
18
  const sessionTracing_js_1 = require("./sessionTracing.js");
19
+ const chatRouteWait_js_1 = require("./chatRouteWait.js");
19
20
  const ai_runtime_js_1 = require("../imports/ai-runtime.js");
20
21
  const transcriptStorage_js_1 = require("./transcriptStorage.js");
21
22
  const compactionResponse_js_1 = require("./compactionResponse.js");
23
+ const managedChatResponse_js_1 = require("./managedChatResponse.js");
24
+ const steeringContext_js_1 = require("./steeringContext.js");
22
25
  let transcriptStorageOverride;
23
26
  /**
24
27
  * Test-only override for the storage `chat.agent` persists through, so a
@@ -48,6 +51,7 @@ const externalDeploymentId_js_1 = require("./externalDeploymentId.js");
48
51
  const chatVersionSkew_js_1 = require("./chatVersionSkew.js");
49
52
  const sessions_js_1 = require("./sessions.js");
50
53
  const shared_js_1 = require("./shared.js");
54
+ const concurrency_shared_js_1 = require("./concurrency-shared.js");
51
55
  const streams_js_1 = require("./streams.js");
52
56
  const tracer_js_1 = require("./tracer.js");
53
57
  const METADATA_KEY = "tool.execute.options";
@@ -56,17 +60,17 @@ const METADATA_KEY = "tool.execute.options";
56
60
  * `ignoreIncompleteToolCalls: true` to prevent failures from
57
61
  * stopped/aborted conversations with partial tool parts.
58
62
  */
59
- function toModelMessages(messages) {
63
+ function toModelMessages(messages, context) {
60
64
  // Pass the resolved per-turn `tools` (if any) so the AI SDK can look up each
61
65
  // tool's `toModelOutput` and re-apply it to prior-turn tool results. Without
62
66
  // `tools` it falls back to JSON-stringifying the raw output (TRI-10149). The
63
67
  // conditional spread keeps the options object byte-identical to the no-tools
64
68
  // path when nothing was declared.
65
69
  const tools = locals_js_1.locals.get(chatResolvedToolsKey);
66
- return (0, ai_runtime_js_1.convertToModelMessages)(messages, {
70
+ return (0, steeringContext_js_1.convertSteeredMessages)(messages, async (batch) => (0, ai_runtime_js_1.convertToModelMessages)(batch, {
67
71
  ignoreIncompleteToolCalls: true,
68
72
  ...(tools ? { tools } : {}),
69
- });
73
+ }), locals_js_1.locals.get(chatSteeringInjectionsKey) ?? new Map(), context ?? locals_js_1.locals.get(chatCurrentUIMessagesKey) ?? messages, context !== undefined || locals_js_1.locals.get(chatCurrentUIMessagesKey) !== undefined);
70
74
  }
71
75
  const chatTurnContextKey = locals_js_1.locals.create("chat.turnContext");
72
76
  /**
@@ -871,7 +875,7 @@ const chatStream = {
871
875
  * `onTurnComplete`'s `responseMessage` and `uiMessages`.
872
876
  *
873
877
  * Non-transient data chunks (`type` starts with `data-`, no `transient: true`)
874
- * are queued for accumulation into the assistant response message.
878
+ * are accumulated in emission order into the assistant response message.
875
879
  * Transient or non-data chunks are streamed only (same as `chat.stream`).
876
880
  *
877
881
  * @example
@@ -889,15 +893,7 @@ const chatResponse = {
889
893
  * response message; everything else is stream-only.
890
894
  */
891
895
  write(part) {
892
- queueResponsePart(part);
893
- const { waitUntilComplete } = chatStream.writer({
894
- spanName: "chat.response.write",
895
- collapsed: true,
896
- execute: ({ write }) => {
897
- write(part);
898
- },
899
- });
900
- waitUntilComplete().catch(() => { });
896
+ managedResponse().writeData(part);
901
897
  },
902
898
  };
903
899
  /**
@@ -906,60 +902,13 @@ const chatResponse = {
906
902
  * @internal
907
903
  */
908
904
  function createLazyChatWriter() {
909
- let writeImpl = null;
910
- let mergeImpl = null;
911
- let waitPromise = null;
912
- let resolveExecute = null;
913
- let started = false;
914
- const bufferedParts = [];
915
- const bufferedStreams = [];
916
- function ensureInitialized() {
917
- if (started)
918
- return;
919
- started = true;
920
- const executePromise = new Promise((resolve) => {
921
- resolveExecute = resolve;
922
- });
923
- const { waitUntilComplete } = chatStream.writer({
905
+ return (0, managedChatResponse_js_1.createOrderedChatWriter)(() => (locals_js_1.locals.get(chatManagedResponseActiveKey) ? managedResponse() : undefined), async (stream) => {
906
+ const { waitUntilComplete } = chatStream.pipe(stream, {
924
907
  collapsed: true,
925
908
  spanName: "callback writer",
926
- execute: ({ write, merge }) => {
927
- writeImpl = write;
928
- mergeImpl = merge;
929
- for (const part of bufferedParts.splice(0))
930
- write(part);
931
- for (const stream of bufferedStreams.splice(0))
932
- merge(stream);
933
- return executePromise;
934
- },
935
909
  });
936
- waitPromise = waitUntilComplete;
937
- }
938
- return {
939
- writer: {
940
- write(part) {
941
- ensureInitialized();
942
- queueResponsePart(part);
943
- if (writeImpl)
944
- writeImpl(part);
945
- else
946
- bufferedParts.push(part);
947
- },
948
- merge(stream) {
949
- ensureInitialized();
950
- if (mergeImpl)
951
- mergeImpl(stream);
952
- else
953
- bufferedStreams.push(stream);
954
- },
955
- },
956
- async flush() {
957
- if (resolveExecute) {
958
- resolveExecute(); // Signal execute to complete
959
- await waitPromise(); // Wait for stream to finish piping
960
- }
961
- },
962
- };
910
+ await waitUntilComplete();
911
+ });
963
912
  }
964
913
  /**
965
914
  * Runs a callback with a lazy ChatWriter, flushing the stream after completion.
@@ -1135,31 +1084,20 @@ async function waitOnChatRoute(route, options) {
1135
1084
  if (options.onSuspend)
1136
1085
  await options.onSuspend();
1137
1086
  span.setAttribute("wait.resolved", "suspended");
1138
- while (true) {
1139
- /**
1140
- * The floor doubles as the wake cursor: the server completes the
1141
- * waitpoint immediately if anything sits after this sequence, so a
1142
- * floor that has advanced past an unread record parks a waitpoint
1143
- * nothing will complete. Recorded on the span so a run that never woke
1144
- * can be diagnosed from its trace alone.
1145
- */
1146
- const wakeFrom = router.resumeFloor();
1147
- span.setAttribute("wait.lastSeqNum", wakeFrom ?? -1);
1148
- const wake = await session.in.awaitWake({
1149
- timeout: options.timeout,
1150
- lastSeqNum: wakeFrom,
1151
- });
1152
- if (!wake.ok) {
1153
- span.recordException(wake.error);
1154
- return { ok: false, error: wake.error };
1155
- }
1156
- const record = await router.next(route);
1157
- if (!record)
1158
- continue;
1159
- if (options.onResume)
1160
- await options.onResume();
1161
- return { ok: true, output: record.data, record };
1087
+ const result = await (0, chatRouteWait_js_1.waitForChatRouteAfterIdle)(router, route, {
1088
+ timeout: options.timeout,
1089
+ wake: async (timeout, lastSeqNum) => {
1090
+ span.setAttribute("wait.lastSeqNum", lastSeqNum ?? -1);
1091
+ return session.in.awaitWake({ timeout, lastSeqNum });
1092
+ },
1093
+ });
1094
+ if (!result.ok) {
1095
+ span.recordException(result.error);
1096
+ return result;
1162
1097
  }
1098
+ if (options.onResume)
1099
+ await options.onResume();
1100
+ return { ok: true, output: result.record.data, record: result.record };
1163
1101
  }, {
1164
1102
  attributes: {
1165
1103
  [v3_1.SemanticInternalAttributes.STYLE_ICON]: "sessions",
@@ -2550,29 +2488,19 @@ const chatTurnNewUIMessagesKey = locals_js_1.locals.create("chat.turnNewUIMessag
2550
2488
  const chatPendingSteerKey = locals_js_1.locals.create("chat.pendingSteer");
2551
2489
  /** @internal — IDs of messages that were successfully injected via prepareStep */
2552
2490
  const chatInjectedMessageIdsKey = locals_js_1.locals.create("chat.injectedMessageIds");
2553
- /** @internal — non-transient data parts queued via chat.response or writer.write() for accumulation into the response message */
2554
- const chatResponsePartsKey = locals_js_1.locals.create("chat.responseParts");
2555
- /**
2556
- * Check if a chunk is a non-transient data part that should persist to the response message.
2557
- * @internal
2558
- */
2559
- function isNonTransientDataPart(part) {
2560
- if (typeof part !== "object" || part === null)
2561
- return false;
2562
- const p = part;
2563
- return typeof p.type === "string" && p.type.startsWith("data-") && p.transient !== true;
2564
- }
2565
- /**
2566
- * Queue a chunk for accumulation into the response message (if it's a non-transient data part).
2567
- * Called by `chat.response.write()` and `ChatWriter.write()`.
2568
- * @internal
2569
- */
2570
- function queueResponsePart(part) {
2571
- if (!isNonTransientDataPart(part))
2572
- return;
2573
- const parts = locals_js_1.locals.get(chatResponsePartsKey) ?? [];
2574
- parts.push(part);
2575
- locals_js_1.locals.set(chatResponsePartsKey, parts);
2491
+ const chatManagedResponseKey = locals_js_1.locals.create("chat.managedResponse");
2492
+ const chatManagedResponseActiveKey = locals_js_1.locals.create("chat.managedResponseActive");
2493
+ const chatSteeringInjectionsKey = locals_js_1.locals.create("chat.steeringInjections");
2494
+ function managedResponse() {
2495
+ let response = locals_js_1.locals.get(chatManagedResponseKey);
2496
+ if (!response || response.isClosed) {
2497
+ response = new managedChatResponse_js_1.ManagedChatResponse(async (stream) => {
2498
+ const { waitUntilComplete } = chatStream.pipe(stream, { spanName: "managed chat response" });
2499
+ await waitUntilComplete();
2500
+ });
2501
+ locals_js_1.locals.set(chatManagedResponseKey, response);
2502
+ }
2503
+ return response;
2576
2504
  }
2577
2505
  /**
2578
2506
  * Check that no tool calls are in-flight in a step's content.
@@ -2924,6 +2852,8 @@ function modelFormOf(m, batch, injected) {
2924
2852
  * @internal
2925
2853
  */
2926
2854
  async function drainSteeringQueue(config, messages, steps, queueOverride) {
2855
+ if (steps.length === 0)
2856
+ managedResponse().beginGeneration();
2927
2857
  const queue = queueOverride ?? locals_js_1.locals.get(chatSteeringQueueKey);
2928
2858
  if (!queue || queue.length === 0)
2929
2859
  return EMPTY_DRAIN;
@@ -3056,31 +2986,24 @@ async function drainSteeringQueue(config, messages, steps, queueOverride) {
3056
2986
  }
3057
2987
  locals_js_1.locals.set(chatPendingSteerKey, pendingSteer);
3058
2988
  }
3059
- // Write injection confirmation chunk to the stream so the frontend
3060
- // knows which messages were injected and where in the response.
3061
2989
  if (injected.length > 0) {
3062
- try {
3063
- const { waitUntilComplete } = chatStream.writer({
3064
- collapsed: true,
3065
- execute: ({ write }) => {
3066
- write({
3067
- type: ai_shared_js_1.PENDING_MESSAGE_INJECTED_TYPE,
3068
- id: (0, ai_runtime_js_1.generateId)(),
3069
- data: {
3070
- messageIds: claimedUIMessages.map((m) => m.id),
3071
- messages: claimedUIMessages.map((m) => ({
3072
- id: m.id,
3073
- text: textOfUIMessage(m),
3074
- })),
3075
- },
3076
- });
3077
- },
3078
- });
3079
- await waitUntilComplete();
3080
- }
3081
- catch {
3082
- /* non-fatal — stream write failed */
3083
- }
2990
+ const id = (0, ai_runtime_js_1.generateId)();
2991
+ const messageIds = claimedUIMessages.map((m) => m.id);
2992
+ const injections = locals_js_1.locals.get(chatSteeringInjectionsKey) ?? new Map();
2993
+ injections.set(id, { id, messageIds, messages: injected });
2994
+ locals_js_1.locals.set(chatSteeringInjectionsKey, injections);
2995
+ const response = managedResponse();
2996
+ // prepareStep can run ahead of the UI consumer. Admit the marker only
2997
+ // once the actual preceding finish-step chunk has entered this stream.
2998
+ response.afterStep(steps.length);
2999
+ response.writeData({
3000
+ type: ai_shared_js_1.PENDING_MESSAGE_INJECTED_TYPE,
3001
+ id,
3002
+ data: {
3003
+ messageIds,
3004
+ messages: claimedUIMessages.map((m) => ({ id: m.id, text: textOfUIMessage(m) })),
3005
+ },
3006
+ });
3084
3007
  }
3085
3008
  // Fire onInjected callback
3086
3009
  if (config.onInjected && injected.length > 0) {
@@ -3381,7 +3304,7 @@ function buildManagedStreamTextOptions(options, config) {
3381
3304
  * regenerate needs: without it a regenerated answer can call nothing.
3382
3305
  */
3383
3306
  tools: (tools ?? agentTools),
3384
- });
3307
+ }, false);
3385
3308
  const promptSystem = locals_js_1.locals.get(chatPromptKey)?.text;
3386
3309
  /**
3387
3310
  * Two managed sources conflict too, not only a caller against a managed one.
@@ -3408,6 +3331,9 @@ function buildManagedStreamTextOptions(options, config) {
3408
3331
  return { ...(first ?? {}), ...(second ?? {}) };
3409
3332
  };
3410
3333
  }
3334
+ if (typeof managed.prepareStep === "function") {
3335
+ managed.prepareStep = (0, steeringContext_js_1.retainStepMessages)(managed.prepareStep);
3336
+ }
3411
3337
  return { ...managed, ...rest };
3412
3338
  }
3413
3339
  /** @internal Test hook for {@link buildManagedStreamTextOptions}. */
@@ -3423,7 +3349,7 @@ function createBoundStreamText(registry, agentSystem, agentCacheControl, agentSy
3423
3349
  }));
3424
3350
  return bound;
3425
3351
  }
3426
- function toStreamTextOptions(options) {
3352
+ function toStreamTextOptions(options, retain = true) {
3427
3353
  const agentDefaults = locals_js_1.locals.get(chatAgentManagedConfigKey);
3428
3354
  if (agentDefaults) {
3429
3355
  options = {
@@ -3630,6 +3556,9 @@ function toStreamTextOptions(options) {
3630
3556
  return resultMessages ? { messages: resultMessages } : undefined;
3631
3557
  };
3632
3558
  }
3559
+ if (retain && typeof result.prepareStep === "function") {
3560
+ result.prepareStep = (0, steeringContext_js_1.retainStepMessages)(result.prepareStep);
3561
+ }
3633
3562
  return result;
3634
3563
  }
3635
3564
  const actionTurnBrand = Symbol.for("trigger.dev/chat/actionTurn");
@@ -3764,7 +3693,7 @@ function isReadableStream(value) {
3764
3693
  * }
3765
3694
  * ```
3766
3695
  */
3767
- async function pipeChat(source, options) {
3696
+ async function pipeChat(source, options, capture) {
3768
3697
  locals_js_1.locals.set(chatPipeCountKey, (locals_js_1.locals.get(chatPipeCountKey) ?? 0) + 1);
3769
3698
  let stream;
3770
3699
  if (isUIMessageStreamable(source)) {
@@ -3793,8 +3722,18 @@ async function pipeChat(source, options) {
3793
3722
  // accepts opaque UIMessageStreamable / raw iterables whose element
3794
3723
  // type we don't know at compile time. Cast — runtime behaviour is
3795
3724
  // identical (bytes go to session.out either way).
3796
- const { waitUntilComplete } = chatStream.pipe(stream, pipeOptions);
3797
- await waitUntilComplete();
3725
+ if (capture) {
3726
+ const response = managedResponse();
3727
+ response.seed(capture.originalMessages);
3728
+ await response.pipe(stream, options?.signal);
3729
+ }
3730
+ else {
3731
+ // Raw/manual pipes intentionally do not feed the managed capture. Never
3732
+ // leave injection events waiting for finish-step chunks it cannot see.
3733
+ managedResponse().useRawPipe();
3734
+ const { waitUntilComplete } = chatStream.pipe(stream, pipeOptions);
3735
+ await waitUntilComplete();
3736
+ }
3798
3737
  }
3799
3738
  function chatCustomAgent(options) {
3800
3739
  const { clientDataSchema, onClientDataValidationError, clientDataReportErrorAt, run: userRun, ...restOptions } = options;
@@ -4047,11 +3986,13 @@ function chatAgent(options) {
4047
3986
  if (!pending || pending.length === 0)
4048
3987
  return [];
4049
3988
  locals_js_1.locals.set(chatPendingSteerKey, []);
4050
- for (const entry of pending) {
3989
+ const inlineIds = new Set((0, steeringContext_js_1.steeringMarkers)(options?.response ? [options.response] : []).flatMap((m) => m.messageIds));
3990
+ const standalone = pending.filter((entry) => !inlineIds.has(entry.ui.id));
3991
+ for (const entry of standalone) {
4051
3992
  accumulatedMessages.push(...entry.model);
4052
3993
  options?.turnNew?.push(...entry.model);
4053
3994
  }
4054
- return pending;
3995
+ return standalone;
4055
3996
  };
4056
3997
  // Accumulated UI messages for persistence. Mirrors the model accumulator
4057
3998
  // but in frontend-friendly UIMessage format (with parts, id, etc.).
@@ -4145,7 +4086,10 @@ function chatAgent(options) {
4145
4086
  });
4146
4087
  const throughId = opts.messages.at(-1)?.id ?? "";
4147
4088
  const queued = locals_js_1.locals.get(chatBackgroundQueueKey) ?? [];
4148
- const runtimeState = laneCompacted || laneInjections.length > 0 || queued.length > 0
4089
+ const transcriptIds = new Set(opts.messages.map((message) => message.id));
4090
+ const markerIds = new Set((0, steeringContext_js_1.steeringMarkers)(opts.messages).map((marker) => marker.id));
4091
+ const steering = [...(locals_js_1.locals.get(chatSteeringInjectionsKey)?.values() ?? [])].filter((entry) => markerIds.has(entry.id) || entry.messageIds.some((id) => transcriptIds.has(id)));
4092
+ const runtimeState = laneCompacted || laneInjections.length > 0 || queued.length > 0 || steering.length > 0
4149
4093
  ? {
4150
4094
  v: 1,
4151
4095
  ...(laneCompacted
@@ -4158,6 +4102,7 @@ function chatAgent(options) {
4158
4102
  : {}),
4159
4103
  ...(laneInjections.length > 0 ? { injections: laneInjections } : {}),
4160
4104
  ...(queued.length > 0 ? { queued: [...queued] } : {}),
4105
+ ...(steering.length > 0 ? { steering } : {}),
4161
4106
  }
4162
4107
  : null;
4163
4108
  if (runtimeState !== null || persistedStateSet) {
@@ -4627,6 +4572,8 @@ function chatAgent(options) {
4627
4572
  }
4628
4573
  try {
4629
4574
  const bootRuntimeState = (0, transcriptStorage_js_1.parseTranscriptRuntimeState)(bootTranscriptState);
4575
+ locals_js_1.locals.set(chatSteeringInjectionsKey, new Map((bootRuntimeState?.steering ?? []).map((entry) => [entry.id, entry])));
4576
+ locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
4630
4577
  const restored = await (0, transcriptStorage_js_1.restoreModelLane)(accumulatedUIMessages, bootRuntimeState, (messages) => toModelMessages(messages));
4631
4578
  accumulatedMessages = restored.messages;
4632
4579
  laneCompacted = restored.compacted;
@@ -5074,7 +5021,9 @@ function chatAgent(options) {
5074
5021
  locals_js_1.locals.set(chatCompactionStateKey, undefined);
5075
5022
  locals_js_1.locals.set(chatSteeringQueueKey, []);
5076
5023
  locals_js_1.locals.set(chatPendingBackgroundKey, []);
5077
- locals_js_1.locals.set(chatResponsePartsKey, []);
5024
+ await locals_js_1.locals.get(chatManagedResponseKey)?.close();
5025
+ locals_js_1.locals.set(chatManagedResponseKey, undefined);
5026
+ locals_js_1.locals.set(chatManagedResponseActiveKey, true);
5078
5027
  // NOTE: chatBackgroundQueueKey is NOT reset here — messages injected
5079
5028
  // by deferred work from the previous turn's onTurnComplete need to
5080
5029
  // survive into the next turn. The queue is drained before run().
@@ -5238,7 +5187,7 @@ function chatAgent(options) {
5238
5187
  if (actionOverride) {
5239
5188
  locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
5240
5189
  accumulatedUIMessages = [...actionOverride];
5241
- accumulatedMessages = await toModelMessages(actionOverride);
5190
+ accumulatedMessages = await toModelMessages(actionOverride, actionOverride);
5242
5191
  laneCompacted = false;
5243
5192
  laneInjections = [];
5244
5193
  locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
@@ -5396,7 +5345,7 @@ function chatAgent(options) {
5396
5345
  accumulatedUIMessages[accumulatedUIMessages.length - 1].role !== "user") {
5397
5346
  accumulatedUIMessages.pop();
5398
5347
  }
5399
- accumulatedMessages = await toModelMessages(accumulatedUIMessages);
5348
+ accumulatedMessages = await toModelMessages(accumulatedUIMessages, accumulatedUIMessages);
5400
5349
  laneCompacted = false;
5401
5350
  laneInjections = [];
5402
5351
  }
@@ -5448,7 +5397,7 @@ function chatAgent(options) {
5448
5397
  }
5449
5398
  if (!inPlace) {
5450
5399
  v3_1.logger.warn("chat.agent: replaced message not found at the model lane tail; reconverting the lane");
5451
- accumulatedMessages = await toModelMessages(accumulatedUIMessages);
5400
+ accumulatedMessages = await toModelMessages(accumulatedUIMessages, accumulatedUIMessages);
5452
5401
  laneCompacted = false;
5453
5402
  laneInjections = [];
5454
5403
  }
@@ -5665,7 +5614,7 @@ function chatAgent(options) {
5665
5614
  if (turnStartOverride) {
5666
5615
  locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
5667
5616
  accumulatedUIMessages = [...turnStartOverride];
5668
- accumulatedMessages = await toModelMessages(turnStartOverride);
5617
+ accumulatedMessages = await toModelMessages(turnStartOverride, turnStartOverride);
5669
5618
  laneCompacted = false;
5670
5619
  laneInjections = [];
5671
5620
  locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
@@ -5745,6 +5694,7 @@ function chatAgent(options) {
5745
5694
  capturedResponseMessage = lastUI;
5746
5695
  capturedFinishReason = "stop";
5747
5696
  }
5697
+ managedResponse().seed(accumulatedUIMessages);
5748
5698
  // Don't call userRun. Don't pipe. Skip directly
5749
5699
  // to the post-turn flow below.
5750
5700
  }
@@ -5804,7 +5754,7 @@ function chatAgent(options) {
5804
5754
  await pipeChat(tapUIMessageChunks(uiStream, turnBufferedChunks), {
5805
5755
  signal: combinedSignal,
5806
5756
  spanName: "stream response",
5807
- });
5757
+ }, { originalMessages: isActionTurn ? undefined : accumulatedUIMessages });
5808
5758
  }
5809
5759
  }
5810
5760
  catch (error) {
@@ -5832,6 +5782,10 @@ function chatAgent(options) {
5832
5782
  new Promise((r) => setTimeout(r, 2_000)),
5833
5783
  ]);
5834
5784
  }
5785
+ capturedResponseMessage =
5786
+ (await locals_js_1.locals.get(chatManagedResponseKey)?.snapshot()) ?? capturedResponseMessage;
5787
+ if (capturedResponseMessage)
5788
+ capturedPartialResponse = capturedResponseMessage;
5835
5789
  // Capture token usage from the streamText result (if available).
5836
5790
  // totalUsage is a PromiseLike that resolves after the stream is consumed.
5837
5791
  // Race with a 2s timeout — on stop-abort the AI SDK's totalUsage
@@ -5955,6 +5909,7 @@ function chatAgent(options) {
5955
5909
  // branches below, so a turn that captured no response is covered.
5956
5910
  const steerTailThisTurn = reconcilePendingSteer({
5957
5911
  turnNew: turnNewModelMessages,
5912
+ response: capturedResponseMessage,
5958
5913
  }).reduce((n, e) => n + e.model.length, 0) + reconcilePendingBackground();
5959
5914
  // Append the assistant's response (partial or complete) to the accumulator.
5960
5915
  // The onFinish callback fires even on abort/stop, so partial responses
@@ -5977,15 +5932,6 @@ function chatAgent(options) {
5977
5932
  id: (0, ai_runtime_js_1.generateId)(),
5978
5933
  };
5979
5934
  }
5980
- // Append any non-transient data parts queued via chat.response or writer.write()
5981
- const queuedParts = locals_js_1.locals.get(chatResponsePartsKey);
5982
- if (queuedParts && queuedParts.length > 0) {
5983
- capturedResponseMessage = {
5984
- ...capturedResponseMessage,
5985
- parts: [...capturedResponseMessage.parts, ...queuedParts],
5986
- };
5987
- locals_js_1.locals.set(chatResponsePartsKey, []);
5988
- }
5989
5935
  const responseHasContent = capturedResponseMessage.parts.some((part) => part.type !== "step-start");
5990
5936
  if (responseHasContent) {
5991
5937
  // Tool-approval continuations: the AI SDK reuses the trailing
@@ -6047,7 +5993,7 @@ function chatAgent(options) {
6047
5993
  locals_js_1.locals.set(chatHandoverSplicedRunKey, undefined);
6048
5994
  if (!ok) {
6049
5995
  v3_1.logger.warn("chat.agent: replaced response not found at the model lane tail; reconverting the lane");
6050
- accumulatedMessages = await toModelMessages(accumulatedUIMessages);
5996
+ accumulatedMessages = await toModelMessages(accumulatedUIMessages, accumulatedUIMessages);
6051
5997
  laneCompacted = false;
6052
5998
  laneInjections = [];
6053
5999
  }
@@ -6065,22 +6011,6 @@ function chatAgent(options) {
6065
6011
  responseWasSkipped = true;
6066
6012
  }
6067
6013
  }
6068
- // If there's no captured response (manual pipe mode) but there are
6069
- // queued data parts, create a minimal response message to hold them.
6070
- if (!capturedResponseMessage) {
6071
- const remainingParts = locals_js_1.locals.get(chatResponsePartsKey);
6072
- if (remainingParts && remainingParts.length > 0) {
6073
- capturedResponseMessage = {
6074
- id: (0, ai_runtime_js_1.generateId)(),
6075
- role: "assistant",
6076
- parts: [...remainingParts],
6077
- };
6078
- locals_js_1.locals.set(chatResponsePartsKey, []);
6079
- accumulatedUIMessages.push(capturedResponseMessage);
6080
- turnNewUIMessages.push(capturedResponseMessage);
6081
- locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
6082
- }
6083
- }
6084
6014
  if (capturedResponseMessage) {
6085
6015
  responseCommitted = true;
6086
6016
  capturedPartialResponse = capturedResponseMessage;
@@ -6242,6 +6172,8 @@ function chatAgent(options) {
6242
6172
  totalUsage: cumulativeUsage,
6243
6173
  finishReason: capturedFinishReason,
6244
6174
  };
6175
+ const beforeHookRevision = locals_js_1.locals.get(chatManagedResponseKey)?.revision ?? 0;
6176
+ let beforeHookHistoryEdited = false;
6245
6177
  // Fire onBeforeTurnComplete — stream is still open so the hook
6246
6178
  // can write custom chunks to the frontend (e.g. compaction progress).
6247
6179
  if (onBeforeTurnComplete) {
@@ -6252,9 +6184,10 @@ function chatAgent(options) {
6252
6184
  // Check if the hook replaced messages (compaction or chat.history)
6253
6185
  const override = locals_js_1.locals.get(chatOverrideMessagesKey);
6254
6186
  if (override) {
6187
+ beforeHookHistoryEdited = true;
6255
6188
  locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
6256
6189
  accumulatedUIMessages = [...override];
6257
- accumulatedMessages = await toModelMessages(override);
6190
+ accumulatedMessages = await toModelMessages(override, override);
6258
6191
  laneCompacted = false;
6259
6192
  laneInjections = [];
6260
6193
  locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
@@ -6271,35 +6204,33 @@ function chatAgent(options) {
6271
6204
  },
6272
6205
  });
6273
6206
  }
6274
- // Drain any late response parts added during onBeforeTurnComplete
6275
- const lateParts = locals_js_1.locals.get(chatResponsePartsKey);
6276
- if (lateParts && lateParts.length > 0 && capturedResponseMessage) {
6277
- const idx = accumulatedUIMessages.findIndex((m) => m.id === capturedResponseMessage.id);
6278
- if (idx !== -1) {
6279
- const msg = accumulatedUIMessages[idx];
6280
- accumulatedUIMessages[idx] = {
6281
- ...msg,
6282
- parts: [...(msg.parts ?? []), ...lateParts],
6283
- };
6284
- capturedResponseMessage = accumulatedUIMessages[idx];
6285
- capturedPartialResponse = capturedResponseMessage;
6286
- turnCompleteEvent.responseMessage = capturedResponseMessage;
6287
- turnCompleteEvent.uiMessages = accumulatedUIMessages;
6288
- locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
6289
- }
6290
- else if (responseWasSkipped) {
6291
- capturedResponseMessage = {
6292
- ...capturedResponseMessage,
6293
- parts: [...(capturedResponseMessage.parts ?? []), ...lateParts],
6294
- };
6295
- accumulatedUIMessages.push(capturedResponseMessage);
6296
- turnNewUIMessages.push(capturedResponseMessage);
6297
- capturedPartialResponse = capturedResponseMessage;
6298
- turnCompleteEvent.responseMessage = capturedResponseMessage;
6207
+ const editedResponse = beforeHookHistoryEdited && capturedResponseMessage
6208
+ ? accumulatedUIMessages.find((message) => message.id === capturedResponseMessage?.id)
6209
+ : undefined;
6210
+ const finalManagedResponse = await locals_js_1.locals
6211
+ .get(chatManagedResponseKey)
6212
+ ?.snapshot(editedResponse
6213
+ ? { message: editedResponse, from: beforeHookRevision }
6214
+ : undefined);
6215
+ if (finalManagedResponse?.parts.some((part) => part.type !== "step-start")) {
6216
+ const idx = accumulatedUIMessages.findIndex((m) => m.id === finalManagedResponse.id);
6217
+ const finalized = (wasStopped ? cleanupAbortedParts(finalManagedResponse) : finalManagedResponse);
6218
+ if (idx !== -1 || responseWasSkipped || !capturedResponseMessage) {
6219
+ if (idx !== -1)
6220
+ accumulatedUIMessages[idx] = finalized;
6221
+ else
6222
+ accumulatedUIMessages.push(finalized);
6223
+ const deltaIdx = turnNewUIMessages.findIndex((m) => m.id === finalized.id);
6224
+ if (deltaIdx !== -1)
6225
+ turnNewUIMessages[deltaIdx] = finalized;
6226
+ else
6227
+ turnNewUIMessages.push(finalized);
6228
+ capturedResponseMessage = finalized;
6229
+ capturedPartialResponse = finalized;
6230
+ turnCompleteEvent.responseMessage = finalized;
6299
6231
  turnCompleteEvent.uiMessages = accumulatedUIMessages;
6300
6232
  locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
6301
6233
  }
6302
- locals_js_1.locals.set(chatResponsePartsKey, []);
6303
6234
  }
6304
6235
  settleRecoveredTurn(currentWirePayload);
6305
6236
  // Write turn-complete control chunk — closes the frontend stream.
@@ -6316,7 +6247,7 @@ function chatAgent(options) {
6316
6247
  if (turnCompleteOverride) {
6317
6248
  locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
6318
6249
  accumulatedUIMessages = [...turnCompleteOverride];
6319
- accumulatedMessages = await toModelMessages(turnCompleteOverride);
6250
+ accumulatedMessages = await toModelMessages(turnCompleteOverride, turnCompleteOverride);
6320
6251
  laneCompacted = false;
6321
6252
  laneInjections = [];
6322
6253
  locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
@@ -6552,7 +6483,8 @@ function chatAgent(options) {
6552
6483
  !accumulatedUIMessages.some((m) => m.id === erroredWireMessage.id)
6553
6484
  ? [...accumulatedUIMessages, erroredWireMessage]
6554
6485
  : accumulatedUIMessages;
6555
- let partialResponse = capturedPartialResponse ??
6486
+ let partialResponse = (await locals_js_1.locals.get(chatManagedResponseKey)?.snapshot()) ??
6487
+ capturedPartialResponse ??
6556
6488
  (await assemblePartialFromChunks(turnBufferedChunks));
6557
6489
  if (partialResponse) {
6558
6490
  partialResponse = cleanupAbortedParts(partialResponse);
@@ -6560,23 +6492,16 @@ function chatAgent(options) {
6560
6492
  let partialIdx = partialResponse?.id
6561
6493
  ? erroredUIMessages.findIndex((m) => m.id === partialResponse.id)
6562
6494
  : -1;
6563
- if (partialResponse && capturedPartialResponse === undefined && partialIdx !== -1) {
6495
+ if (partialResponse &&
6496
+ capturedPartialResponse === undefined &&
6497
+ !locals_js_1.locals.get(chatManagedResponseKey)?.continues(partialResponse.id) &&
6498
+ partialIdx !== -1) {
6564
6499
  partialResponse = undefined;
6565
6500
  partialIdx = -1;
6566
6501
  }
6567
6502
  if (partialResponse && !partialResponse.id) {
6568
6503
  partialResponse = { ...partialResponse, id: (0, ai_runtime_js_1.generateId)() };
6569
6504
  }
6570
- if (partialResponse && !responseCommitted) {
6571
- const queuedParts = locals_js_1.locals.get(chatResponsePartsKey);
6572
- if (queuedParts && queuedParts.length > 0) {
6573
- partialResponse = {
6574
- ...partialResponse,
6575
- parts: [...partialResponse.parts, ...queuedParts],
6576
- };
6577
- locals_js_1.locals.set(chatResponsePartsKey, []);
6578
- }
6579
- }
6580
6505
  const includePartial = partialResponse != null && !responseCommitted;
6581
6506
  // What the stream left behind, by content. After `onTurnComplete` the
6582
6507
  // partial is still unfinished only if the message under its id is
@@ -6609,7 +6534,7 @@ function chatAgent(options) {
6609
6534
  };
6610
6535
  let erroredNewUIMessages = buildErroredNew();
6611
6536
  let erroredNewModelMessages = [];
6612
- const reconciledSteer = reconcilePendingSteer();
6537
+ const reconciledSteer = reconcilePendingSteer({ response: partialResponse });
6613
6538
  const backgroundTailThisTurn = reconcilePendingBackground();
6614
6539
  if (!responseCommitted) {
6615
6540
  try {
@@ -6620,14 +6545,7 @@ function chatAgent(options) {
6620
6545
  * the model received it (what `prepare` produced), matching the
6621
6546
  * lane. The wire message and partial are converted as before.
6622
6547
  */
6623
- const steerModelById = new Map(reconciledSteer.map((e) => [e.ui.id, e.model]));
6624
- for (const m of erroredNewUIMessages) {
6625
- const recorded = steerModelById.get(m.id);
6626
- if (recorded)
6627
- erroredNewModelMessages.push(...recorded);
6628
- else
6629
- erroredNewModelMessages.push(...(await toModelMessages([stripProviderMetadata(m)])));
6630
- }
6548
+ erroredNewModelMessages = await toModelMessages(erroredNewUIMessages.map(stripProviderMetadata));
6631
6549
  }
6632
6550
  if (erroredUIMessagesWithPartial !== accumulatedUIMessages) {
6633
6551
  if (partialIdx === -1) {
@@ -6639,7 +6557,7 @@ function chatAgent(options) {
6639
6557
  backgroundTailThisTurn);
6640
6558
  if (!ok) {
6641
6559
  v3_1.logger.warn("chat.agent: replaced partial not found at the model lane tail; reconverting the lane");
6642
- accumulatedMessages = await toModelMessages(erroredUIMessagesWithPartial);
6560
+ accumulatedMessages = await toModelMessages(erroredUIMessagesWithPartial, erroredUIMessagesWithPartial);
6643
6561
  laneCompacted = false;
6644
6562
  laneInjections = [];
6645
6563
  }
@@ -6696,7 +6614,7 @@ function chatAgent(options) {
6696
6614
  // Convert first: a rejected conversion (a tool's `toModelOutput`
6697
6615
  // can throw) must leave every lane on the history it had.
6698
6616
  const overrideUIMessages = [...errorTurnOverride];
6699
- const overrideModelMessages = await toModelMessages(errorTurnOverride);
6617
+ const overrideModelMessages = await toModelMessages(errorTurnOverride, errorTurnOverride);
6700
6618
  erroredUIMessagesWithPartial = overrideUIMessages;
6701
6619
  accumulatedUIMessages = overrideUIMessages;
6702
6620
  accumulatedMessages = overrideModelMessages;
@@ -6789,6 +6707,11 @@ function chatAgent(options) {
6789
6707
  }
6790
6708
  finally {
6791
6709
  turnMsgSub?.off();
6710
+ locals_js_1.locals.set(chatManagedResponseActiveKey, false);
6711
+ await locals_js_1.locals
6712
+ .get(chatManagedResponseKey)
6713
+ ?.close()
6714
+ .catch(() => { });
6792
6715
  }
6793
6716
  }
6794
6717
  }
@@ -7758,7 +7681,7 @@ async function pipeChatAndCapture(source, options) {
7758
7681
  await pipeChat(tappedStream, {
7759
7682
  signal: options?.signal,
7760
7683
  spanName: options?.spanName ?? "stream response",
7761
- });
7684
+ }, { originalMessages: options?.originalMessages });
7762
7685
  // The pipe can drain cleanly on a stop — the source stream just ends
7763
7686
  // early — so classify by the signal rather than relying on a throw.
7764
7687
  if (options?.signal?.aborted) {
@@ -7784,6 +7707,9 @@ async function pipeChatAndCapture(source, options) {
7784
7707
  if (!captured && bufferedChunks.length > 0) {
7785
7708
  captured = await assemblePartialFromChunks(bufferedChunks);
7786
7709
  }
7710
+ captured = (await locals_js_1.locals.get(chatManagedResponseKey)?.snapshot()) ?? captured;
7711
+ if (!locals_js_1.locals.get(chatTurnContextKey))
7712
+ await locals_js_1.locals.get(chatManagedResponseKey)?.close();
7787
7713
  return {
7788
7714
  message: captured,
7789
7715
  status,
@@ -7817,6 +7743,7 @@ class ChatMessageAccumulator {
7817
7743
  _handoverRun;
7818
7744
  _pendingMessages;
7819
7745
  _steeringQueue = [];
7746
+ _pendingSteer = [];
7820
7747
  constructor(options) {
7821
7748
  this._compaction = options?.compaction;
7822
7749
  this._pendingMessages = options?.pendingMessages;
@@ -7889,6 +7816,19 @@ class ChatMessageAccumulator {
7889
7816
  return { isFinal: signal.isFinal, skipped: false };
7890
7817
  }
7891
7818
  async addResponse(response) {
7819
+ const inlineIds = new Set((0, steeringContext_js_1.steeringMarkers)([response]).flatMap((m) => m.messageIds));
7820
+ for (const entry of this._pendingSteer.splice(0)) {
7821
+ if (!inlineIds.has(entry.ui.id))
7822
+ continue;
7823
+ // absorbSteering is public and updates context immediately. Move just
7824
+ // those exact appended objects into the response's chronological run;
7825
+ // never rebuild a possibly compacted lane from the UI transcript.
7826
+ for (const message of entry.model) {
7827
+ const index = this.modelMessages.lastIndexOf(message);
7828
+ if (index !== -1)
7829
+ this.modelMessages.splice(index, 1);
7830
+ }
7831
+ }
7892
7832
  if (!response.id) {
7893
7833
  response = { ...response, id: (0, ai_runtime_js_1.generateId)() };
7894
7834
  }
@@ -7965,7 +7905,10 @@ class ChatMessageAccumulator {
7965
7905
  this.uiMessages.push(...fresh);
7966
7906
  // Record what the model received. Only when the whole batch is new is
7967
7907
  // `injected` known to describe exactly these messages.
7968
- this.modelMessages.push(...(injected && fresh.length === claimed.length ? injected : await toModelMessages(fresh)));
7908
+ const model = injected && fresh.length === claimed.length ? injected : await toModelMessages(fresh);
7909
+ this.modelMessages.push(...model);
7910
+ for (const ui of fresh)
7911
+ this._pendingSteer.push({ ui, model: modelFormOf(ui, fresh, model) });
7969
7912
  }
7970
7913
  /**
7971
7914
  * Get and clear unconsumed steering messages.
@@ -7985,7 +7928,7 @@ class ChatMessageAccumulator {
7985
7928
  const comp = this._compaction;
7986
7929
  const pm = this._pendingMessages;
7987
7930
  const queue = this._steeringQueue;
7988
- return async ({ messages, steps }) => {
7931
+ return (0, steeringContext_js_1.retainStepMessages)(async ({ messages, steps }) => {
7989
7932
  let resultMessages;
7990
7933
  // 1. Compaction
7991
7934
  if (comp) {
@@ -7998,7 +7941,7 @@ class ChatMessageAccumulator {
7998
7941
  }
7999
7942
  }
8000
7943
  // 2. Pending message injection
8001
- if (pm && queue.length > 0) {
7944
+ if (pm) {
8002
7945
  const { injected, claimed } = await drainSteeringQueue(pm, resultMessages ?? messages, steps, queue);
8003
7946
  await this.absorbSteering(claimed, injected);
8004
7947
  if (injected.length > 0) {
@@ -8006,7 +7949,7 @@ class ChatMessageAccumulator {
8006
7949
  }
8007
7950
  }
8008
7951
  return resultMessages ? { messages: resultMessages } : undefined;
8009
- };
7952
+ });
8010
7953
  }
8011
7954
  /**
8012
7955
  * Run outer-loop compaction if needed. Call after adding the response
@@ -8318,7 +8261,9 @@ function createChatSession(payload, options) {
8318
8261
  // Reset stop signal for this turn
8319
8262
  stop.reset();
8320
8263
  // Reset per-turn state
8321
- locals_js_1.locals.set(chatResponsePartsKey, []);
8264
+ await locals_js_1.locals.get(chatManagedResponseKey)?.close();
8265
+ locals_js_1.locals.set(chatManagedResponseKey, undefined);
8266
+ locals_js_1.locals.set(chatManagedResponseActiveKey, true);
8322
8267
  // Set up steering queue and pending messages config in locals
8323
8268
  // so toStreamTextOptions() auto-injects prepareStep for steering
8324
8269
  const turnSteeringQueue = [];
@@ -8463,11 +8408,6 @@ function createChatSession(payload, options) {
8463
8408
  if (captured.status === "error") {
8464
8409
  if (captured.message) {
8465
8410
  const partial = cleanupAbortedParts(captured.message);
8466
- const queuedParts = locals_js_1.locals.get(chatResponsePartsKey);
8467
- if (queuedParts && queuedParts.length > 0) {
8468
- partial.parts = [...(partial.parts ?? []), ...queuedParts];
8469
- locals_js_1.locals.set(chatResponsePartsKey, []);
8470
- }
8471
8411
  await accumulator.addResponse(partial);
8472
8412
  }
8473
8413
  throw captured.error;
@@ -8483,26 +8423,8 @@ function createChatSession(payload, options) {
8483
8423
  const cleaned = stop.signal.aborted && !runSignal.aborted
8484
8424
  ? cleanupAbortedParts(response)
8485
8425
  : response;
8486
- // Append any non-transient data parts queued via chat.response or writer.write()
8487
- const queuedParts = locals_js_1.locals.get(chatResponsePartsKey);
8488
- if (queuedParts && queuedParts.length > 0) {
8489
- cleaned.parts = [...(cleaned.parts ?? []), ...queuedParts];
8490
- locals_js_1.locals.set(chatResponsePartsKey, []);
8491
- }
8492
8426
  await accumulator.addResponse(cleaned);
8493
8427
  }
8494
- else {
8495
- // No response (manual pipe mode) but there are queued data parts
8496
- const queuedParts = locals_js_1.locals.get(chatResponsePartsKey);
8497
- if (queuedParts && queuedParts.length > 0) {
8498
- await accumulator.addResponse({
8499
- id: (0, ai_runtime_js_1.generateId)(),
8500
- role: "assistant",
8501
- parts: queuedParts,
8502
- });
8503
- locals_js_1.locals.set(chatResponsePartsKey, []);
8504
- }
8505
- }
8506
8428
  // Capture token usage from the streamText result. Race with a 2s
8507
8429
  // timeout — on stop-abort the AI SDK's totalUsage promise can hang
8508
8430
  // indefinitely, which would wedge the turn loop (same guard as
@@ -8593,15 +8515,6 @@ function createChatSession(payload, options) {
8593
8515
  return response;
8594
8516
  },
8595
8517
  async addResponse(response) {
8596
- // Append any non-transient data parts queued via chat.response or writer.write()
8597
- const queuedParts = locals_js_1.locals.get(chatResponsePartsKey);
8598
- if (queuedParts && queuedParts.length > 0) {
8599
- response = {
8600
- ...response,
8601
- parts: [...(response.parts ?? []), ...queuedParts],
8602
- };
8603
- locals_js_1.locals.set(chatResponsePartsKey, []);
8604
- }
8605
8518
  await accumulator.addResponse(response);
8606
8519
  },
8607
8520
  async done() {
@@ -8613,7 +8526,7 @@ function createChatSession(payload, options) {
8613
8526
  const hasPending = !!sessionPendingMessages;
8614
8527
  if (!hasCompaction && !hasPending)
8615
8528
  return undefined;
8616
- return async ({ messages: stepMsgs, steps, }) => {
8529
+ return (0, steeringContext_js_1.retainStepMessages)(async ({ messages: stepMsgs, steps, }) => {
8617
8530
  let resultMessages;
8618
8531
  if (sessionCompaction) {
8619
8532
  const compactResult = await chatCompact(stepMsgs, steps, {
@@ -8632,7 +8545,7 @@ function createChatSession(payload, options) {
8632
8545
  }
8633
8546
  }
8634
8547
  return resultMessages ? { messages: resultMessages } : undefined;
8635
- };
8548
+ });
8636
8549
  },
8637
8550
  };
8638
8551
  return { done: false, value: turnObj };
@@ -8900,6 +8813,10 @@ function createChatStartSessionAction(taskId, options) {
8900
8813
  const clientDataMetadata = params.clientData !== undefined ? { metadata: params.clientData } : {};
8901
8814
  const maxAttempts = params.triggerConfig?.maxAttempts ?? options?.triggerConfig?.maxAttempts;
8902
8815
  const maxDuration = params.triggerConfig?.maxDuration ?? options?.triggerConfig?.maxDuration;
8816
+ const concurrency = params.triggerConfig?.concurrency !== undefined
8817
+ ? params.triggerConfig.concurrency
8818
+ : options?.triggerConfig?.concurrency;
8819
+ const concurrencyKey = params.triggerConfig?.concurrencyKey ?? options?.triggerConfig?.concurrencyKey;
8903
8820
  const idleTimeoutInSeconds = params.triggerConfig?.idleTimeoutInSeconds ?? options?.triggerConfig?.idleTimeoutInSeconds;
8904
8821
  // Only `undefined` means "not supplied": a per-call `null` (opt out) has to beat a pinning
8905
8822
  // action default, which neither truthiness nor `??` would allow.
@@ -8921,6 +8838,8 @@ function createChatStartSessionAction(taskId, options) {
8921
8838
  ...(options?.triggerConfig?.queue || params.triggerConfig?.queue
8922
8839
  ? { queue: params.triggerConfig?.queue ?? options?.triggerConfig?.queue }
8923
8840
  : {}),
8841
+ ...(concurrency !== undefined ? (0, concurrency_shared_js_1.triggerConcurrencyBody)(concurrency) : {}),
8842
+ ...(concurrencyKey !== undefined ? { concurrencyKey } : {}),
8924
8843
  tags,
8925
8844
  ...(maxAttempts !== undefined ? { maxAttempts } : {}),
8926
8845
  ...(maxDuration !== undefined ? { maxDuration } : {}),
@@ -9268,6 +9187,10 @@ exports.chat = {
9268
9187
  * @internal
9269
9188
  */
9270
9189
  async function writeTurnCompleteChunk(_chatId, publicAccessToken) {
9190
+ locals_js_1.locals.set(chatManagedResponseActiveKey, false);
9191
+ const response = locals_js_1.locals.get(chatManagedResponseKey);
9192
+ if (response)
9193
+ await response.close();
9271
9194
  const session = getChatSession();
9272
9195
  // A handover-prepare boot claims the handover kinds so a signal arriving
9273
9196
  // before `waitForHandover` attaches is not drained. Released here rather than