@trigger.dev/sdk 4.6.4 → 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
package/dist/esm/v3/ai.js CHANGED
@@ -3,9 +3,12 @@ import { accessoryAttributes, apiClientManager, controlSubtype, generateJWT, get
3
3
  // ESM-only `ai@7` (see ../imports/ai-runtime.ts).
4
4
  import { trace } from "@opentelemetry/api";
5
5
  import { traceSessionIdle } from "./sessionTracing.js";
6
+ import { waitForChatRouteAfterIdle } from "./chatRouteWait.js";
6
7
  import { tool as aiTool, convertToModelMessages, dynamicTool, generateId as generateMessageId, getToolName, isToolUIPart, jsonSchema, readUIMessageStream, streamText as aiStreamText, zodSchema, } from "../imports/ai-runtime.js";
7
8
  import { createTranscriptShadow, defaultStorage, diffTranscript, fingerprintMessage, parseTranscriptRuntimeState, restoreModelLane, } from "./transcriptStorage.js";
8
9
  import { responseAfterCompaction } from "./compactionResponse.js";
10
+ import { ManagedChatResponse, createOrderedChatWriter } from "./managedChatResponse.js";
11
+ import { convertSteeredMessages, retainStepMessages, steeringMarkers, } from "./steeringContext.js";
9
12
  let transcriptStorageOverride;
10
13
  /**
11
14
  * Test-only override for the storage `chat.agent` persists through, so a
@@ -33,6 +36,7 @@ import { withResolvedExternalDeploymentId } from "./externalDeploymentId.js";
33
36
  import { resolvePinToFollow } from "./chatVersionSkew.js";
34
37
  import { sessions, } from "./sessions.js";
35
38
  import { createTask } from "./shared.js";
39
+ import { triggerConcurrencyBody } from "./concurrency-shared.js";
36
40
  import { markChatAgentRunForStreamsWarning } from "./streams.js";
37
41
  import { tracer } from "./tracer.js";
38
42
  const METADATA_KEY = "tool.execute.options";
@@ -41,17 +45,17 @@ const METADATA_KEY = "tool.execute.options";
41
45
  * `ignoreIncompleteToolCalls: true` to prevent failures from
42
46
  * stopped/aborted conversations with partial tool parts.
43
47
  */
44
- function toModelMessages(messages) {
48
+ function toModelMessages(messages, context) {
45
49
  // Pass the resolved per-turn `tools` (if any) so the AI SDK can look up each
46
50
  // tool's `toModelOutput` and re-apply it to prior-turn tool results. Without
47
51
  // `tools` it falls back to JSON-stringifying the raw output (TRI-10149). The
48
52
  // conditional spread keeps the options object byte-identical to the no-tools
49
53
  // path when nothing was declared.
50
54
  const tools = locals.get(chatResolvedToolsKey);
51
- return convertToModelMessages(messages, {
55
+ return convertSteeredMessages(messages, async (batch) => convertToModelMessages(batch, {
52
56
  ignoreIncompleteToolCalls: true,
53
57
  ...(tools ? { tools } : {}),
54
- });
58
+ }), locals.get(chatSteeringInjectionsKey) ?? new Map(), context ?? locals.get(chatCurrentUIMessagesKey) ?? messages, context !== undefined || locals.get(chatCurrentUIMessagesKey) !== undefined);
55
59
  }
56
60
  const chatTurnContextKey = locals.create("chat.turnContext");
57
61
  /**
@@ -848,7 +852,7 @@ const chatStream = {
848
852
  * `onTurnComplete`'s `responseMessage` and `uiMessages`.
849
853
  *
850
854
  * Non-transient data chunks (`type` starts with `data-`, no `transient: true`)
851
- * are queued for accumulation into the assistant response message.
855
+ * are accumulated in emission order into the assistant response message.
852
856
  * Transient or non-data chunks are streamed only (same as `chat.stream`).
853
857
  *
854
858
  * @example
@@ -866,15 +870,7 @@ const chatResponse = {
866
870
  * response message; everything else is stream-only.
867
871
  */
868
872
  write(part) {
869
- queueResponsePart(part);
870
- const { waitUntilComplete } = chatStream.writer({
871
- spanName: "chat.response.write",
872
- collapsed: true,
873
- execute: ({ write }) => {
874
- write(part);
875
- },
876
- });
877
- waitUntilComplete().catch(() => { });
873
+ managedResponse().writeData(part);
878
874
  },
879
875
  };
880
876
  /**
@@ -883,60 +879,13 @@ const chatResponse = {
883
879
  * @internal
884
880
  */
885
881
  function createLazyChatWriter() {
886
- let writeImpl = null;
887
- let mergeImpl = null;
888
- let waitPromise = null;
889
- let resolveExecute = null;
890
- let started = false;
891
- const bufferedParts = [];
892
- const bufferedStreams = [];
893
- function ensureInitialized() {
894
- if (started)
895
- return;
896
- started = true;
897
- const executePromise = new Promise((resolve) => {
898
- resolveExecute = resolve;
899
- });
900
- const { waitUntilComplete } = chatStream.writer({
882
+ return createOrderedChatWriter(() => (locals.get(chatManagedResponseActiveKey) ? managedResponse() : undefined), async (stream) => {
883
+ const { waitUntilComplete } = chatStream.pipe(stream, {
901
884
  collapsed: true,
902
885
  spanName: "callback writer",
903
- execute: ({ write, merge }) => {
904
- writeImpl = write;
905
- mergeImpl = merge;
906
- for (const part of bufferedParts.splice(0))
907
- write(part);
908
- for (const stream of bufferedStreams.splice(0))
909
- merge(stream);
910
- return executePromise;
911
- },
912
886
  });
913
- waitPromise = waitUntilComplete;
914
- }
915
- return {
916
- writer: {
917
- write(part) {
918
- ensureInitialized();
919
- queueResponsePart(part);
920
- if (writeImpl)
921
- writeImpl(part);
922
- else
923
- bufferedParts.push(part);
924
- },
925
- merge(stream) {
926
- ensureInitialized();
927
- if (mergeImpl)
928
- mergeImpl(stream);
929
- else
930
- bufferedStreams.push(stream);
931
- },
932
- },
933
- async flush() {
934
- if (resolveExecute) {
935
- resolveExecute(); // Signal execute to complete
936
- await waitPromise(); // Wait for stream to finish piping
937
- }
938
- },
939
- };
887
+ await waitUntilComplete();
888
+ });
940
889
  }
941
890
  /**
942
891
  * Runs a callback with a lazy ChatWriter, flushing the stream after completion.
@@ -1112,31 +1061,20 @@ async function waitOnChatRoute(route, options) {
1112
1061
  if (options.onSuspend)
1113
1062
  await options.onSuspend();
1114
1063
  span.setAttribute("wait.resolved", "suspended");
1115
- while (true) {
1116
- /**
1117
- * The floor doubles as the wake cursor: the server completes the
1118
- * waitpoint immediately if anything sits after this sequence, so a
1119
- * floor that has advanced past an unread record parks a waitpoint
1120
- * nothing will complete. Recorded on the span so a run that never woke
1121
- * can be diagnosed from its trace alone.
1122
- */
1123
- const wakeFrom = router.resumeFloor();
1124
- span.setAttribute("wait.lastSeqNum", wakeFrom ?? -1);
1125
- const wake = await session.in.awaitWake({
1126
- timeout: options.timeout,
1127
- lastSeqNum: wakeFrom,
1128
- });
1129
- if (!wake.ok) {
1130
- span.recordException(wake.error);
1131
- return { ok: false, error: wake.error };
1132
- }
1133
- const record = await router.next(route);
1134
- if (!record)
1135
- continue;
1136
- if (options.onResume)
1137
- await options.onResume();
1138
- return { ok: true, output: record.data, record };
1064
+ const result = await waitForChatRouteAfterIdle(router, route, {
1065
+ timeout: options.timeout,
1066
+ wake: async (timeout, lastSeqNum) => {
1067
+ span.setAttribute("wait.lastSeqNum", lastSeqNum ?? -1);
1068
+ return session.in.awaitWake({ timeout, lastSeqNum });
1069
+ },
1070
+ });
1071
+ if (!result.ok) {
1072
+ span.recordException(result.error);
1073
+ return result;
1139
1074
  }
1075
+ if (options.onResume)
1076
+ await options.onResume();
1077
+ return { ok: true, output: result.record.data, record: result.record };
1140
1078
  }, {
1141
1079
  attributes: {
1142
1080
  [SemanticInternalAttributes.STYLE_ICON]: "sessions",
@@ -2537,29 +2475,19 @@ const chatTurnNewUIMessagesKey = locals.create("chat.turnNewUIMessages");
2537
2475
  const chatPendingSteerKey = locals.create("chat.pendingSteer");
2538
2476
  /** @internal — IDs of messages that were successfully injected via prepareStep */
2539
2477
  const chatInjectedMessageIdsKey = locals.create("chat.injectedMessageIds");
2540
- /** @internal — non-transient data parts queued via chat.response or writer.write() for accumulation into the response message */
2541
- const chatResponsePartsKey = locals.create("chat.responseParts");
2542
- /**
2543
- * Check if a chunk is a non-transient data part that should persist to the response message.
2544
- * @internal
2545
- */
2546
- function isNonTransientDataPart(part) {
2547
- if (typeof part !== "object" || part === null)
2548
- return false;
2549
- const p = part;
2550
- return typeof p.type === "string" && p.type.startsWith("data-") && p.transient !== true;
2551
- }
2552
- /**
2553
- * Queue a chunk for accumulation into the response message (if it's a non-transient data part).
2554
- * Called by `chat.response.write()` and `ChatWriter.write()`.
2555
- * @internal
2556
- */
2557
- function queueResponsePart(part) {
2558
- if (!isNonTransientDataPart(part))
2559
- return;
2560
- const parts = locals.get(chatResponsePartsKey) ?? [];
2561
- parts.push(part);
2562
- locals.set(chatResponsePartsKey, parts);
2478
+ const chatManagedResponseKey = locals.create("chat.managedResponse");
2479
+ const chatManagedResponseActiveKey = locals.create("chat.managedResponseActive");
2480
+ const chatSteeringInjectionsKey = locals.create("chat.steeringInjections");
2481
+ function managedResponse() {
2482
+ let response = locals.get(chatManagedResponseKey);
2483
+ if (!response || response.isClosed) {
2484
+ response = new ManagedChatResponse(async (stream) => {
2485
+ const { waitUntilComplete } = chatStream.pipe(stream, { spanName: "managed chat response" });
2486
+ await waitUntilComplete();
2487
+ });
2488
+ locals.set(chatManagedResponseKey, response);
2489
+ }
2490
+ return response;
2563
2491
  }
2564
2492
  /**
2565
2493
  * Check that no tool calls are in-flight in a step's content.
@@ -2911,6 +2839,8 @@ function modelFormOf(m, batch, injected) {
2911
2839
  * @internal
2912
2840
  */
2913
2841
  async function drainSteeringQueue(config, messages, steps, queueOverride) {
2842
+ if (steps.length === 0)
2843
+ managedResponse().beginGeneration();
2914
2844
  const queue = queueOverride ?? locals.get(chatSteeringQueueKey);
2915
2845
  if (!queue || queue.length === 0)
2916
2846
  return EMPTY_DRAIN;
@@ -3043,31 +2973,24 @@ async function drainSteeringQueue(config, messages, steps, queueOverride) {
3043
2973
  }
3044
2974
  locals.set(chatPendingSteerKey, pendingSteer);
3045
2975
  }
3046
- // Write injection confirmation chunk to the stream so the frontend
3047
- // knows which messages were injected and where in the response.
3048
2976
  if (injected.length > 0) {
3049
- try {
3050
- const { waitUntilComplete } = chatStream.writer({
3051
- collapsed: true,
3052
- execute: ({ write }) => {
3053
- write({
3054
- type: PENDING_MESSAGE_INJECTED_TYPE,
3055
- id: generateMessageId(),
3056
- data: {
3057
- messageIds: claimedUIMessages.map((m) => m.id),
3058
- messages: claimedUIMessages.map((m) => ({
3059
- id: m.id,
3060
- text: textOfUIMessage(m),
3061
- })),
3062
- },
3063
- });
3064
- },
3065
- });
3066
- await waitUntilComplete();
3067
- }
3068
- catch {
3069
- /* non-fatal — stream write failed */
3070
- }
2977
+ const id = generateMessageId();
2978
+ const messageIds = claimedUIMessages.map((m) => m.id);
2979
+ const injections = locals.get(chatSteeringInjectionsKey) ?? new Map();
2980
+ injections.set(id, { id, messageIds, messages: injected });
2981
+ locals.set(chatSteeringInjectionsKey, injections);
2982
+ const response = managedResponse();
2983
+ // prepareStep can run ahead of the UI consumer. Admit the marker only
2984
+ // once the actual preceding finish-step chunk has entered this stream.
2985
+ response.afterStep(steps.length);
2986
+ response.writeData({
2987
+ type: PENDING_MESSAGE_INJECTED_TYPE,
2988
+ id,
2989
+ data: {
2990
+ messageIds,
2991
+ messages: claimedUIMessages.map((m) => ({ id: m.id, text: textOfUIMessage(m) })),
2992
+ },
2993
+ });
3071
2994
  }
3072
2995
  // Fire onInjected callback
3073
2996
  if (config.onInjected && injected.length > 0) {
@@ -3368,7 +3291,7 @@ function buildManagedStreamTextOptions(options, config) {
3368
3291
  * regenerate needs: without it a regenerated answer can call nothing.
3369
3292
  */
3370
3293
  tools: (tools ?? agentTools),
3371
- });
3294
+ }, false);
3372
3295
  const promptSystem = locals.get(chatPromptKey)?.text;
3373
3296
  /**
3374
3297
  * Two managed sources conflict too, not only a caller against a managed one.
@@ -3395,6 +3318,9 @@ function buildManagedStreamTextOptions(options, config) {
3395
3318
  return { ...(first ?? {}), ...(second ?? {}) };
3396
3319
  };
3397
3320
  }
3321
+ if (typeof managed.prepareStep === "function") {
3322
+ managed.prepareStep = retainStepMessages(managed.prepareStep);
3323
+ }
3398
3324
  return { ...managed, ...rest };
3399
3325
  }
3400
3326
  /** @internal Test hook for {@link buildManagedStreamTextOptions}. */
@@ -3410,7 +3336,7 @@ function createBoundStreamText(registry, agentSystem, agentCacheControl, agentSy
3410
3336
  }));
3411
3337
  return bound;
3412
3338
  }
3413
- function toStreamTextOptions(options) {
3339
+ function toStreamTextOptions(options, retain = true) {
3414
3340
  const agentDefaults = locals.get(chatAgentManagedConfigKey);
3415
3341
  if (agentDefaults) {
3416
3342
  options = {
@@ -3617,6 +3543,9 @@ function toStreamTextOptions(options) {
3617
3543
  return resultMessages ? { messages: resultMessages } : undefined;
3618
3544
  };
3619
3545
  }
3546
+ if (retain && typeof result.prepareStep === "function") {
3547
+ result.prepareStep = retainStepMessages(result.prepareStep);
3548
+ }
3620
3549
  return result;
3621
3550
  }
3622
3551
  const actionTurnBrand = Symbol.for("trigger.dev/chat/actionTurn");
@@ -3751,7 +3680,7 @@ function isReadableStream(value) {
3751
3680
  * }
3752
3681
  * ```
3753
3682
  */
3754
- async function pipeChat(source, options) {
3683
+ async function pipeChat(source, options, capture) {
3755
3684
  locals.set(chatPipeCountKey, (locals.get(chatPipeCountKey) ?? 0) + 1);
3756
3685
  let stream;
3757
3686
  if (isUIMessageStreamable(source)) {
@@ -3780,8 +3709,18 @@ async function pipeChat(source, options) {
3780
3709
  // accepts opaque UIMessageStreamable / raw iterables whose element
3781
3710
  // type we don't know at compile time. Cast — runtime behaviour is
3782
3711
  // identical (bytes go to session.out either way).
3783
- const { waitUntilComplete } = chatStream.pipe(stream, pipeOptions);
3784
- await waitUntilComplete();
3712
+ if (capture) {
3713
+ const response = managedResponse();
3714
+ response.seed(capture.originalMessages);
3715
+ await response.pipe(stream, options?.signal);
3716
+ }
3717
+ else {
3718
+ // Raw/manual pipes intentionally do not feed the managed capture. Never
3719
+ // leave injection events waiting for finish-step chunks it cannot see.
3720
+ managedResponse().useRawPipe();
3721
+ const { waitUntilComplete } = chatStream.pipe(stream, pipeOptions);
3722
+ await waitUntilComplete();
3723
+ }
3785
3724
  }
3786
3725
  function chatCustomAgent(options) {
3787
3726
  const { clientDataSchema, onClientDataValidationError, clientDataReportErrorAt, run: userRun, ...restOptions } = options;
@@ -4034,11 +3973,13 @@ function chatAgent(options) {
4034
3973
  if (!pending || pending.length === 0)
4035
3974
  return [];
4036
3975
  locals.set(chatPendingSteerKey, []);
4037
- for (const entry of pending) {
3976
+ const inlineIds = new Set(steeringMarkers(options?.response ? [options.response] : []).flatMap((m) => m.messageIds));
3977
+ const standalone = pending.filter((entry) => !inlineIds.has(entry.ui.id));
3978
+ for (const entry of standalone) {
4038
3979
  accumulatedMessages.push(...entry.model);
4039
3980
  options?.turnNew?.push(...entry.model);
4040
3981
  }
4041
- return pending;
3982
+ return standalone;
4042
3983
  };
4043
3984
  // Accumulated UI messages for persistence. Mirrors the model accumulator
4044
3985
  // but in frontend-friendly UIMessage format (with parts, id, etc.).
@@ -4132,7 +4073,10 @@ function chatAgent(options) {
4132
4073
  });
4133
4074
  const throughId = opts.messages.at(-1)?.id ?? "";
4134
4075
  const queued = locals.get(chatBackgroundQueueKey) ?? [];
4135
- const runtimeState = laneCompacted || laneInjections.length > 0 || queued.length > 0
4076
+ const transcriptIds = new Set(opts.messages.map((message) => message.id));
4077
+ const markerIds = new Set(steeringMarkers(opts.messages).map((marker) => marker.id));
4078
+ const steering = [...(locals.get(chatSteeringInjectionsKey)?.values() ?? [])].filter((entry) => markerIds.has(entry.id) || entry.messageIds.some((id) => transcriptIds.has(id)));
4079
+ const runtimeState = laneCompacted || laneInjections.length > 0 || queued.length > 0 || steering.length > 0
4136
4080
  ? {
4137
4081
  v: 1,
4138
4082
  ...(laneCompacted
@@ -4145,6 +4089,7 @@ function chatAgent(options) {
4145
4089
  : {}),
4146
4090
  ...(laneInjections.length > 0 ? { injections: laneInjections } : {}),
4147
4091
  ...(queued.length > 0 ? { queued: [...queued] } : {}),
4092
+ ...(steering.length > 0 ? { steering } : {}),
4148
4093
  }
4149
4094
  : null;
4150
4095
  if (runtimeState !== null || persistedStateSet) {
@@ -4614,6 +4559,8 @@ function chatAgent(options) {
4614
4559
  }
4615
4560
  try {
4616
4561
  const bootRuntimeState = parseTranscriptRuntimeState(bootTranscriptState);
4562
+ locals.set(chatSteeringInjectionsKey, new Map((bootRuntimeState?.steering ?? []).map((entry) => [entry.id, entry])));
4563
+ locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
4617
4564
  const restored = await restoreModelLane(accumulatedUIMessages, bootRuntimeState, (messages) => toModelMessages(messages));
4618
4565
  accumulatedMessages = restored.messages;
4619
4566
  laneCompacted = restored.compacted;
@@ -5061,7 +5008,9 @@ function chatAgent(options) {
5061
5008
  locals.set(chatCompactionStateKey, undefined);
5062
5009
  locals.set(chatSteeringQueueKey, []);
5063
5010
  locals.set(chatPendingBackgroundKey, []);
5064
- locals.set(chatResponsePartsKey, []);
5011
+ await locals.get(chatManagedResponseKey)?.close();
5012
+ locals.set(chatManagedResponseKey, undefined);
5013
+ locals.set(chatManagedResponseActiveKey, true);
5065
5014
  // NOTE: chatBackgroundQueueKey is NOT reset here — messages injected
5066
5015
  // by deferred work from the previous turn's onTurnComplete need to
5067
5016
  // survive into the next turn. The queue is drained before run().
@@ -5225,7 +5174,7 @@ function chatAgent(options) {
5225
5174
  if (actionOverride) {
5226
5175
  locals.set(chatOverrideMessagesKey, undefined);
5227
5176
  accumulatedUIMessages = [...actionOverride];
5228
- accumulatedMessages = await toModelMessages(actionOverride);
5177
+ accumulatedMessages = await toModelMessages(actionOverride, actionOverride);
5229
5178
  laneCompacted = false;
5230
5179
  laneInjections = [];
5231
5180
  locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
@@ -5383,7 +5332,7 @@ function chatAgent(options) {
5383
5332
  accumulatedUIMessages[accumulatedUIMessages.length - 1].role !== "user") {
5384
5333
  accumulatedUIMessages.pop();
5385
5334
  }
5386
- accumulatedMessages = await toModelMessages(accumulatedUIMessages);
5335
+ accumulatedMessages = await toModelMessages(accumulatedUIMessages, accumulatedUIMessages);
5387
5336
  laneCompacted = false;
5388
5337
  laneInjections = [];
5389
5338
  }
@@ -5435,7 +5384,7 @@ function chatAgent(options) {
5435
5384
  }
5436
5385
  if (!inPlace) {
5437
5386
  logger.warn("chat.agent: replaced message not found at the model lane tail; reconverting the lane");
5438
- accumulatedMessages = await toModelMessages(accumulatedUIMessages);
5387
+ accumulatedMessages = await toModelMessages(accumulatedUIMessages, accumulatedUIMessages);
5439
5388
  laneCompacted = false;
5440
5389
  laneInjections = [];
5441
5390
  }
@@ -5652,7 +5601,7 @@ function chatAgent(options) {
5652
5601
  if (turnStartOverride) {
5653
5602
  locals.set(chatOverrideMessagesKey, undefined);
5654
5603
  accumulatedUIMessages = [...turnStartOverride];
5655
- accumulatedMessages = await toModelMessages(turnStartOverride);
5604
+ accumulatedMessages = await toModelMessages(turnStartOverride, turnStartOverride);
5656
5605
  laneCompacted = false;
5657
5606
  laneInjections = [];
5658
5607
  locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
@@ -5732,6 +5681,7 @@ function chatAgent(options) {
5732
5681
  capturedResponseMessage = lastUI;
5733
5682
  capturedFinishReason = "stop";
5734
5683
  }
5684
+ managedResponse().seed(accumulatedUIMessages);
5735
5685
  // Don't call userRun. Don't pipe. Skip directly
5736
5686
  // to the post-turn flow below.
5737
5687
  }
@@ -5791,7 +5741,7 @@ function chatAgent(options) {
5791
5741
  await pipeChat(tapUIMessageChunks(uiStream, turnBufferedChunks), {
5792
5742
  signal: combinedSignal,
5793
5743
  spanName: "stream response",
5794
- });
5744
+ }, { originalMessages: isActionTurn ? undefined : accumulatedUIMessages });
5795
5745
  }
5796
5746
  }
5797
5747
  catch (error) {
@@ -5819,6 +5769,10 @@ function chatAgent(options) {
5819
5769
  new Promise((r) => setTimeout(r, 2_000)),
5820
5770
  ]);
5821
5771
  }
5772
+ capturedResponseMessage =
5773
+ (await locals.get(chatManagedResponseKey)?.snapshot()) ?? capturedResponseMessage;
5774
+ if (capturedResponseMessage)
5775
+ capturedPartialResponse = capturedResponseMessage;
5822
5776
  // Capture token usage from the streamText result (if available).
5823
5777
  // totalUsage is a PromiseLike that resolves after the stream is consumed.
5824
5778
  // Race with a 2s timeout — on stop-abort the AI SDK's totalUsage
@@ -5942,6 +5896,7 @@ function chatAgent(options) {
5942
5896
  // branches below, so a turn that captured no response is covered.
5943
5897
  const steerTailThisTurn = reconcilePendingSteer({
5944
5898
  turnNew: turnNewModelMessages,
5899
+ response: capturedResponseMessage,
5945
5900
  }).reduce((n, e) => n + e.model.length, 0) + reconcilePendingBackground();
5946
5901
  // Append the assistant's response (partial or complete) to the accumulator.
5947
5902
  // The onFinish callback fires even on abort/stop, so partial responses
@@ -5964,15 +5919,6 @@ function chatAgent(options) {
5964
5919
  id: generateMessageId(),
5965
5920
  };
5966
5921
  }
5967
- // Append any non-transient data parts queued via chat.response or writer.write()
5968
- const queuedParts = locals.get(chatResponsePartsKey);
5969
- if (queuedParts && queuedParts.length > 0) {
5970
- capturedResponseMessage = {
5971
- ...capturedResponseMessage,
5972
- parts: [...capturedResponseMessage.parts, ...queuedParts],
5973
- };
5974
- locals.set(chatResponsePartsKey, []);
5975
- }
5976
5922
  const responseHasContent = capturedResponseMessage.parts.some((part) => part.type !== "step-start");
5977
5923
  if (responseHasContent) {
5978
5924
  // Tool-approval continuations: the AI SDK reuses the trailing
@@ -6034,7 +5980,7 @@ function chatAgent(options) {
6034
5980
  locals.set(chatHandoverSplicedRunKey, undefined);
6035
5981
  if (!ok) {
6036
5982
  logger.warn("chat.agent: replaced response not found at the model lane tail; reconverting the lane");
6037
- accumulatedMessages = await toModelMessages(accumulatedUIMessages);
5983
+ accumulatedMessages = await toModelMessages(accumulatedUIMessages, accumulatedUIMessages);
6038
5984
  laneCompacted = false;
6039
5985
  laneInjections = [];
6040
5986
  }
@@ -6052,22 +5998,6 @@ function chatAgent(options) {
6052
5998
  responseWasSkipped = true;
6053
5999
  }
6054
6000
  }
6055
- // If there's no captured response (manual pipe mode) but there are
6056
- // queued data parts, create a minimal response message to hold them.
6057
- if (!capturedResponseMessage) {
6058
- const remainingParts = locals.get(chatResponsePartsKey);
6059
- if (remainingParts && remainingParts.length > 0) {
6060
- capturedResponseMessage = {
6061
- id: generateMessageId(),
6062
- role: "assistant",
6063
- parts: [...remainingParts],
6064
- };
6065
- locals.set(chatResponsePartsKey, []);
6066
- accumulatedUIMessages.push(capturedResponseMessage);
6067
- turnNewUIMessages.push(capturedResponseMessage);
6068
- locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
6069
- }
6070
- }
6071
6001
  if (capturedResponseMessage) {
6072
6002
  responseCommitted = true;
6073
6003
  capturedPartialResponse = capturedResponseMessage;
@@ -6229,6 +6159,8 @@ function chatAgent(options) {
6229
6159
  totalUsage: cumulativeUsage,
6230
6160
  finishReason: capturedFinishReason,
6231
6161
  };
6162
+ const beforeHookRevision = locals.get(chatManagedResponseKey)?.revision ?? 0;
6163
+ let beforeHookHistoryEdited = false;
6232
6164
  // Fire onBeforeTurnComplete — stream is still open so the hook
6233
6165
  // can write custom chunks to the frontend (e.g. compaction progress).
6234
6166
  if (onBeforeTurnComplete) {
@@ -6239,9 +6171,10 @@ function chatAgent(options) {
6239
6171
  // Check if the hook replaced messages (compaction or chat.history)
6240
6172
  const override = locals.get(chatOverrideMessagesKey);
6241
6173
  if (override) {
6174
+ beforeHookHistoryEdited = true;
6242
6175
  locals.set(chatOverrideMessagesKey, undefined);
6243
6176
  accumulatedUIMessages = [...override];
6244
- accumulatedMessages = await toModelMessages(override);
6177
+ accumulatedMessages = await toModelMessages(override, override);
6245
6178
  laneCompacted = false;
6246
6179
  laneInjections = [];
6247
6180
  locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
@@ -6258,35 +6191,33 @@ function chatAgent(options) {
6258
6191
  },
6259
6192
  });
6260
6193
  }
6261
- // Drain any late response parts added during onBeforeTurnComplete
6262
- const lateParts = locals.get(chatResponsePartsKey);
6263
- if (lateParts && lateParts.length > 0 && capturedResponseMessage) {
6264
- const idx = accumulatedUIMessages.findIndex((m) => m.id === capturedResponseMessage.id);
6265
- if (idx !== -1) {
6266
- const msg = accumulatedUIMessages[idx];
6267
- accumulatedUIMessages[idx] = {
6268
- ...msg,
6269
- parts: [...(msg.parts ?? []), ...lateParts],
6270
- };
6271
- capturedResponseMessage = accumulatedUIMessages[idx];
6272
- capturedPartialResponse = capturedResponseMessage;
6273
- turnCompleteEvent.responseMessage = capturedResponseMessage;
6274
- turnCompleteEvent.uiMessages = accumulatedUIMessages;
6275
- locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
6276
- }
6277
- else if (responseWasSkipped) {
6278
- capturedResponseMessage = {
6279
- ...capturedResponseMessage,
6280
- parts: [...(capturedResponseMessage.parts ?? []), ...lateParts],
6281
- };
6282
- accumulatedUIMessages.push(capturedResponseMessage);
6283
- turnNewUIMessages.push(capturedResponseMessage);
6284
- capturedPartialResponse = capturedResponseMessage;
6285
- turnCompleteEvent.responseMessage = capturedResponseMessage;
6194
+ const editedResponse = beforeHookHistoryEdited && capturedResponseMessage
6195
+ ? accumulatedUIMessages.find((message) => message.id === capturedResponseMessage?.id)
6196
+ : undefined;
6197
+ const finalManagedResponse = await locals
6198
+ .get(chatManagedResponseKey)
6199
+ ?.snapshot(editedResponse
6200
+ ? { message: editedResponse, from: beforeHookRevision }
6201
+ : undefined);
6202
+ if (finalManagedResponse?.parts.some((part) => part.type !== "step-start")) {
6203
+ const idx = accumulatedUIMessages.findIndex((m) => m.id === finalManagedResponse.id);
6204
+ const finalized = (wasStopped ? cleanupAbortedParts(finalManagedResponse) : finalManagedResponse);
6205
+ if (idx !== -1 || responseWasSkipped || !capturedResponseMessage) {
6206
+ if (idx !== -1)
6207
+ accumulatedUIMessages[idx] = finalized;
6208
+ else
6209
+ accumulatedUIMessages.push(finalized);
6210
+ const deltaIdx = turnNewUIMessages.findIndex((m) => m.id === finalized.id);
6211
+ if (deltaIdx !== -1)
6212
+ turnNewUIMessages[deltaIdx] = finalized;
6213
+ else
6214
+ turnNewUIMessages.push(finalized);
6215
+ capturedResponseMessage = finalized;
6216
+ capturedPartialResponse = finalized;
6217
+ turnCompleteEvent.responseMessage = finalized;
6286
6218
  turnCompleteEvent.uiMessages = accumulatedUIMessages;
6287
6219
  locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
6288
6220
  }
6289
- locals.set(chatResponsePartsKey, []);
6290
6221
  }
6291
6222
  settleRecoveredTurn(currentWirePayload);
6292
6223
  // Write turn-complete control chunk — closes the frontend stream.
@@ -6303,7 +6234,7 @@ function chatAgent(options) {
6303
6234
  if (turnCompleteOverride) {
6304
6235
  locals.set(chatOverrideMessagesKey, undefined);
6305
6236
  accumulatedUIMessages = [...turnCompleteOverride];
6306
- accumulatedMessages = await toModelMessages(turnCompleteOverride);
6237
+ accumulatedMessages = await toModelMessages(turnCompleteOverride, turnCompleteOverride);
6307
6238
  laneCompacted = false;
6308
6239
  laneInjections = [];
6309
6240
  locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
@@ -6539,7 +6470,8 @@ function chatAgent(options) {
6539
6470
  !accumulatedUIMessages.some((m) => m.id === erroredWireMessage.id)
6540
6471
  ? [...accumulatedUIMessages, erroredWireMessage]
6541
6472
  : accumulatedUIMessages;
6542
- let partialResponse = capturedPartialResponse ??
6473
+ let partialResponse = (await locals.get(chatManagedResponseKey)?.snapshot()) ??
6474
+ capturedPartialResponse ??
6543
6475
  (await assemblePartialFromChunks(turnBufferedChunks));
6544
6476
  if (partialResponse) {
6545
6477
  partialResponse = cleanupAbortedParts(partialResponse);
@@ -6547,23 +6479,16 @@ function chatAgent(options) {
6547
6479
  let partialIdx = partialResponse?.id
6548
6480
  ? erroredUIMessages.findIndex((m) => m.id === partialResponse.id)
6549
6481
  : -1;
6550
- if (partialResponse && capturedPartialResponse === undefined && partialIdx !== -1) {
6482
+ if (partialResponse &&
6483
+ capturedPartialResponse === undefined &&
6484
+ !locals.get(chatManagedResponseKey)?.continues(partialResponse.id) &&
6485
+ partialIdx !== -1) {
6551
6486
  partialResponse = undefined;
6552
6487
  partialIdx = -1;
6553
6488
  }
6554
6489
  if (partialResponse && !partialResponse.id) {
6555
6490
  partialResponse = { ...partialResponse, id: generateMessageId() };
6556
6491
  }
6557
- if (partialResponse && !responseCommitted) {
6558
- const queuedParts = locals.get(chatResponsePartsKey);
6559
- if (queuedParts && queuedParts.length > 0) {
6560
- partialResponse = {
6561
- ...partialResponse,
6562
- parts: [...partialResponse.parts, ...queuedParts],
6563
- };
6564
- locals.set(chatResponsePartsKey, []);
6565
- }
6566
- }
6567
6492
  const includePartial = partialResponse != null && !responseCommitted;
6568
6493
  // What the stream left behind, by content. After `onTurnComplete` the
6569
6494
  // partial is still unfinished only if the message under its id is
@@ -6596,7 +6521,7 @@ function chatAgent(options) {
6596
6521
  };
6597
6522
  let erroredNewUIMessages = buildErroredNew();
6598
6523
  let erroredNewModelMessages = [];
6599
- const reconciledSteer = reconcilePendingSteer();
6524
+ const reconciledSteer = reconcilePendingSteer({ response: partialResponse });
6600
6525
  const backgroundTailThisTurn = reconcilePendingBackground();
6601
6526
  if (!responseCommitted) {
6602
6527
  try {
@@ -6607,14 +6532,7 @@ function chatAgent(options) {
6607
6532
  * the model received it (what `prepare` produced), matching the
6608
6533
  * lane. The wire message and partial are converted as before.
6609
6534
  */
6610
- const steerModelById = new Map(reconciledSteer.map((e) => [e.ui.id, e.model]));
6611
- for (const m of erroredNewUIMessages) {
6612
- const recorded = steerModelById.get(m.id);
6613
- if (recorded)
6614
- erroredNewModelMessages.push(...recorded);
6615
- else
6616
- erroredNewModelMessages.push(...(await toModelMessages([stripProviderMetadata(m)])));
6617
- }
6535
+ erroredNewModelMessages = await toModelMessages(erroredNewUIMessages.map(stripProviderMetadata));
6618
6536
  }
6619
6537
  if (erroredUIMessagesWithPartial !== accumulatedUIMessages) {
6620
6538
  if (partialIdx === -1) {
@@ -6626,7 +6544,7 @@ function chatAgent(options) {
6626
6544
  backgroundTailThisTurn);
6627
6545
  if (!ok) {
6628
6546
  logger.warn("chat.agent: replaced partial not found at the model lane tail; reconverting the lane");
6629
- accumulatedMessages = await toModelMessages(erroredUIMessagesWithPartial);
6547
+ accumulatedMessages = await toModelMessages(erroredUIMessagesWithPartial, erroredUIMessagesWithPartial);
6630
6548
  laneCompacted = false;
6631
6549
  laneInjections = [];
6632
6550
  }
@@ -6683,7 +6601,7 @@ function chatAgent(options) {
6683
6601
  // Convert first: a rejected conversion (a tool's `toModelOutput`
6684
6602
  // can throw) must leave every lane on the history it had.
6685
6603
  const overrideUIMessages = [...errorTurnOverride];
6686
- const overrideModelMessages = await toModelMessages(errorTurnOverride);
6604
+ const overrideModelMessages = await toModelMessages(errorTurnOverride, errorTurnOverride);
6687
6605
  erroredUIMessagesWithPartial = overrideUIMessages;
6688
6606
  accumulatedUIMessages = overrideUIMessages;
6689
6607
  accumulatedMessages = overrideModelMessages;
@@ -6776,6 +6694,11 @@ function chatAgent(options) {
6776
6694
  }
6777
6695
  finally {
6778
6696
  turnMsgSub?.off();
6697
+ locals.set(chatManagedResponseActiveKey, false);
6698
+ await locals
6699
+ .get(chatManagedResponseKey)
6700
+ ?.close()
6701
+ .catch(() => { });
6779
6702
  }
6780
6703
  }
6781
6704
  }
@@ -7745,7 +7668,7 @@ async function pipeChatAndCapture(source, options) {
7745
7668
  await pipeChat(tappedStream, {
7746
7669
  signal: options?.signal,
7747
7670
  spanName: options?.spanName ?? "stream response",
7748
- });
7671
+ }, { originalMessages: options?.originalMessages });
7749
7672
  // The pipe can drain cleanly on a stop — the source stream just ends
7750
7673
  // early — so classify by the signal rather than relying on a throw.
7751
7674
  if (options?.signal?.aborted) {
@@ -7771,6 +7694,9 @@ async function pipeChatAndCapture(source, options) {
7771
7694
  if (!captured && bufferedChunks.length > 0) {
7772
7695
  captured = await assemblePartialFromChunks(bufferedChunks);
7773
7696
  }
7697
+ captured = (await locals.get(chatManagedResponseKey)?.snapshot()) ?? captured;
7698
+ if (!locals.get(chatTurnContextKey))
7699
+ await locals.get(chatManagedResponseKey)?.close();
7774
7700
  return {
7775
7701
  message: captured,
7776
7702
  status,
@@ -7804,6 +7730,7 @@ class ChatMessageAccumulator {
7804
7730
  _handoverRun;
7805
7731
  _pendingMessages;
7806
7732
  _steeringQueue = [];
7733
+ _pendingSteer = [];
7807
7734
  constructor(options) {
7808
7735
  this._compaction = options?.compaction;
7809
7736
  this._pendingMessages = options?.pendingMessages;
@@ -7876,6 +7803,19 @@ class ChatMessageAccumulator {
7876
7803
  return { isFinal: signal.isFinal, skipped: false };
7877
7804
  }
7878
7805
  async addResponse(response) {
7806
+ const inlineIds = new Set(steeringMarkers([response]).flatMap((m) => m.messageIds));
7807
+ for (const entry of this._pendingSteer.splice(0)) {
7808
+ if (!inlineIds.has(entry.ui.id))
7809
+ continue;
7810
+ // absorbSteering is public and updates context immediately. Move just
7811
+ // those exact appended objects into the response's chronological run;
7812
+ // never rebuild a possibly compacted lane from the UI transcript.
7813
+ for (const message of entry.model) {
7814
+ const index = this.modelMessages.lastIndexOf(message);
7815
+ if (index !== -1)
7816
+ this.modelMessages.splice(index, 1);
7817
+ }
7818
+ }
7879
7819
  if (!response.id) {
7880
7820
  response = { ...response, id: generateMessageId() };
7881
7821
  }
@@ -7952,7 +7892,10 @@ class ChatMessageAccumulator {
7952
7892
  this.uiMessages.push(...fresh);
7953
7893
  // Record what the model received. Only when the whole batch is new is
7954
7894
  // `injected` known to describe exactly these messages.
7955
- this.modelMessages.push(...(injected && fresh.length === claimed.length ? injected : await toModelMessages(fresh)));
7895
+ const model = injected && fresh.length === claimed.length ? injected : await toModelMessages(fresh);
7896
+ this.modelMessages.push(...model);
7897
+ for (const ui of fresh)
7898
+ this._pendingSteer.push({ ui, model: modelFormOf(ui, fresh, model) });
7956
7899
  }
7957
7900
  /**
7958
7901
  * Get and clear unconsumed steering messages.
@@ -7972,7 +7915,7 @@ class ChatMessageAccumulator {
7972
7915
  const comp = this._compaction;
7973
7916
  const pm = this._pendingMessages;
7974
7917
  const queue = this._steeringQueue;
7975
- return async ({ messages, steps }) => {
7918
+ return retainStepMessages(async ({ messages, steps }) => {
7976
7919
  let resultMessages;
7977
7920
  // 1. Compaction
7978
7921
  if (comp) {
@@ -7985,7 +7928,7 @@ class ChatMessageAccumulator {
7985
7928
  }
7986
7929
  }
7987
7930
  // 2. Pending message injection
7988
- if (pm && queue.length > 0) {
7931
+ if (pm) {
7989
7932
  const { injected, claimed } = await drainSteeringQueue(pm, resultMessages ?? messages, steps, queue);
7990
7933
  await this.absorbSteering(claimed, injected);
7991
7934
  if (injected.length > 0) {
@@ -7993,7 +7936,7 @@ class ChatMessageAccumulator {
7993
7936
  }
7994
7937
  }
7995
7938
  return resultMessages ? { messages: resultMessages } : undefined;
7996
- };
7939
+ });
7997
7940
  }
7998
7941
  /**
7999
7942
  * Run outer-loop compaction if needed. Call after adding the response
@@ -8305,7 +8248,9 @@ function createChatSession(payload, options) {
8305
8248
  // Reset stop signal for this turn
8306
8249
  stop.reset();
8307
8250
  // Reset per-turn state
8308
- locals.set(chatResponsePartsKey, []);
8251
+ await locals.get(chatManagedResponseKey)?.close();
8252
+ locals.set(chatManagedResponseKey, undefined);
8253
+ locals.set(chatManagedResponseActiveKey, true);
8309
8254
  // Set up steering queue and pending messages config in locals
8310
8255
  // so toStreamTextOptions() auto-injects prepareStep for steering
8311
8256
  const turnSteeringQueue = [];
@@ -8450,11 +8395,6 @@ function createChatSession(payload, options) {
8450
8395
  if (captured.status === "error") {
8451
8396
  if (captured.message) {
8452
8397
  const partial = cleanupAbortedParts(captured.message);
8453
- const queuedParts = locals.get(chatResponsePartsKey);
8454
- if (queuedParts && queuedParts.length > 0) {
8455
- partial.parts = [...(partial.parts ?? []), ...queuedParts];
8456
- locals.set(chatResponsePartsKey, []);
8457
- }
8458
8398
  await accumulator.addResponse(partial);
8459
8399
  }
8460
8400
  throw captured.error;
@@ -8470,26 +8410,8 @@ function createChatSession(payload, options) {
8470
8410
  const cleaned = stop.signal.aborted && !runSignal.aborted
8471
8411
  ? cleanupAbortedParts(response)
8472
8412
  : response;
8473
- // Append any non-transient data parts queued via chat.response or writer.write()
8474
- const queuedParts = locals.get(chatResponsePartsKey);
8475
- if (queuedParts && queuedParts.length > 0) {
8476
- cleaned.parts = [...(cleaned.parts ?? []), ...queuedParts];
8477
- locals.set(chatResponsePartsKey, []);
8478
- }
8479
8413
  await accumulator.addResponse(cleaned);
8480
8414
  }
8481
- else {
8482
- // No response (manual pipe mode) but there are queued data parts
8483
- const queuedParts = locals.get(chatResponsePartsKey);
8484
- if (queuedParts && queuedParts.length > 0) {
8485
- await accumulator.addResponse({
8486
- id: generateMessageId(),
8487
- role: "assistant",
8488
- parts: queuedParts,
8489
- });
8490
- locals.set(chatResponsePartsKey, []);
8491
- }
8492
- }
8493
8415
  // Capture token usage from the streamText result. Race with a 2s
8494
8416
  // timeout — on stop-abort the AI SDK's totalUsage promise can hang
8495
8417
  // indefinitely, which would wedge the turn loop (same guard as
@@ -8580,15 +8502,6 @@ function createChatSession(payload, options) {
8580
8502
  return response;
8581
8503
  },
8582
8504
  async addResponse(response) {
8583
- // Append any non-transient data parts queued via chat.response or writer.write()
8584
- const queuedParts = locals.get(chatResponsePartsKey);
8585
- if (queuedParts && queuedParts.length > 0) {
8586
- response = {
8587
- ...response,
8588
- parts: [...(response.parts ?? []), ...queuedParts],
8589
- };
8590
- locals.set(chatResponsePartsKey, []);
8591
- }
8592
8505
  await accumulator.addResponse(response);
8593
8506
  },
8594
8507
  async done() {
@@ -8600,7 +8513,7 @@ function createChatSession(payload, options) {
8600
8513
  const hasPending = !!sessionPendingMessages;
8601
8514
  if (!hasCompaction && !hasPending)
8602
8515
  return undefined;
8603
- return async ({ messages: stepMsgs, steps, }) => {
8516
+ return retainStepMessages(async ({ messages: stepMsgs, steps, }) => {
8604
8517
  let resultMessages;
8605
8518
  if (sessionCompaction) {
8606
8519
  const compactResult = await chatCompact(stepMsgs, steps, {
@@ -8619,7 +8532,7 @@ function createChatSession(payload, options) {
8619
8532
  }
8620
8533
  }
8621
8534
  return resultMessages ? { messages: resultMessages } : undefined;
8622
- };
8535
+ });
8623
8536
  },
8624
8537
  };
8625
8538
  return { done: false, value: turnObj };
@@ -8887,6 +8800,10 @@ function createChatStartSessionAction(taskId, options) {
8887
8800
  const clientDataMetadata = params.clientData !== undefined ? { metadata: params.clientData } : {};
8888
8801
  const maxAttempts = params.triggerConfig?.maxAttempts ?? options?.triggerConfig?.maxAttempts;
8889
8802
  const maxDuration = params.triggerConfig?.maxDuration ?? options?.triggerConfig?.maxDuration;
8803
+ const concurrency = params.triggerConfig?.concurrency !== undefined
8804
+ ? params.triggerConfig.concurrency
8805
+ : options?.triggerConfig?.concurrency;
8806
+ const concurrencyKey = params.triggerConfig?.concurrencyKey ?? options?.triggerConfig?.concurrencyKey;
8890
8807
  const idleTimeoutInSeconds = params.triggerConfig?.idleTimeoutInSeconds ?? options?.triggerConfig?.idleTimeoutInSeconds;
8891
8808
  // Only `undefined` means "not supplied": a per-call `null` (opt out) has to beat a pinning
8892
8809
  // action default, which neither truthiness nor `??` would allow.
@@ -8908,6 +8825,8 @@ function createChatStartSessionAction(taskId, options) {
8908
8825
  ...(options?.triggerConfig?.queue || params.triggerConfig?.queue
8909
8826
  ? { queue: params.triggerConfig?.queue ?? options?.triggerConfig?.queue }
8910
8827
  : {}),
8828
+ ...(concurrency !== undefined ? triggerConcurrencyBody(concurrency) : {}),
8829
+ ...(concurrencyKey !== undefined ? { concurrencyKey } : {}),
8911
8830
  tags,
8912
8831
  ...(maxAttempts !== undefined ? { maxAttempts } : {}),
8913
8832
  ...(maxDuration !== undefined ? { maxDuration } : {}),
@@ -9255,6 +9174,10 @@ export const chat = {
9255
9174
  * @internal
9256
9175
  */
9257
9176
  async function writeTurnCompleteChunk(_chatId, publicAccessToken) {
9177
+ locals.set(chatManagedResponseActiveKey, false);
9178
+ const response = locals.get(chatManagedResponseKey);
9179
+ if (response)
9180
+ await response.close();
9258
9181
  const session = getChatSession();
9259
9182
  // A handover-prepare boot claims the handover kinds so a signal arriving
9260
9183
  // before `waitForHandover` attaches is not drained. Released here rather than