@agent-native/core 0.84.39 → 0.84.41

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 (31) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/action-continuation-guidance.ts +38 -0
  5. package/corpus/core/src/agent/production-agent.ts +344 -34
  6. package/corpus/core/src/agent/run-loop-with-resume.ts +128 -16
  7. package/corpus/core/src/agent/types.ts +8 -0
  8. package/corpus/core/src/client/agent-chat-adapter.ts +2 -40
  9. package/dist/agent/action-continuation-guidance.d.ts +3 -0
  10. package/dist/agent/action-continuation-guidance.d.ts.map +1 -0
  11. package/dist/agent/action-continuation-guidance.js +38 -0
  12. package/dist/agent/action-continuation-guidance.js.map +1 -0
  13. package/dist/agent/production-agent.d.ts +8 -5
  14. package/dist/agent/production-agent.d.ts.map +1 -1
  15. package/dist/agent/production-agent.js +267 -32
  16. package/dist/agent/production-agent.js.map +1 -1
  17. package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
  18. package/dist/agent/run-loop-with-resume.js +69 -18
  19. package/dist/agent/run-loop-with-resume.js.map +1 -1
  20. package/dist/agent/types.d.ts +2 -0
  21. package/dist/agent/types.d.ts.map +1 -1
  22. package/dist/agent/types.js.map +1 -1
  23. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  24. package/dist/client/agent-chat-adapter.js +2 -39
  25. package/dist/client/agent-chat-adapter.js.map +1 -1
  26. package/dist/collab/routes.d.ts +1 -1
  27. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  28. package/dist/observability/routes.d.ts +5 -5
  29. package/dist/progress/routes.d.ts +1 -1
  30. package/dist/resources/handlers.d.ts +1 -1
  31. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -27,5 +27,5 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
27
27
 
28
28
  ## Generated Counts
29
29
 
30
- - core files: 2043
30
+ - core files: 2044
31
31
  - template files: 4998
@@ -1,5 +1,17 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.84.41
4
+
5
+ ### Patch Changes
6
+
7
+ - a8548bc: Continue durable background runs when a chunk completes tool calls without final assistant text.
8
+
9
+ ## 0.84.40
10
+
11
+ ### Patch Changes
12
+
13
+ - 96feefd: Checkpoint agent runs that stall while preparing an action input without streaming bytes.
14
+
3
15
  ## 0.84.39
4
16
 
5
17
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.84.39",
3
+ "version": "0.84.41",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -0,0 +1,38 @@
1
+ // Concrete "ship a compact first version, then refine incrementally" guidance
2
+ // keyed by the large-payload action the model was cut off while preparing.
3
+ // A run cut off before the tool starts tends to reassemble the same oversized
4
+ // payload on every continuation; pointing the model at the incremental path is
5
+ // what breaks that loop.
6
+ export function incrementalActionGuidance(tool: string): string | undefined {
7
+ switch (tool) {
8
+ case "create-extension":
9
+ case "update-extension":
10
+ return "create a compact working v1 with `create-extension`, then use focused `update-extension` edits for refinements";
11
+ case "generate-design":
12
+ case "update-design":
13
+ return 'if an existing design file or snapshot is already in history, especially after a Design variant pick, stop retrying `generate-design`: call `get-design-snapshot` for the selected file, then call `edit-design` once on that same `fileId` (`mode: "replace-file"` for a compact full-file replacement, or search/replace for smaller edits). Use `generate-design` only for a brand-new compact first file when no target file exists yet';
14
+ case "edit-design":
15
+ return "retry with a smaller `edit-design` payload: prefer a handful of exact search/replace edits against the already-snapshotted file, and avoid `replacementContent` for a huge full-file rewrite. If a selected variant needs expansion, upgrade the highest-value visible sections first, save once, and summarize any remaining refinements";
16
+ case "present-design-variants":
17
+ return 'call `present-design-variants` with concise labels, descriptions, accent colors, and feature bullets; omit large `content` HTML when needed so the action can render compact representative screens. After the user picks a direction, delete unchosen screens, snapshot the selected `fileId`, and refine that same file with `edit-design` (`mode: "replace-file"` for placeholder expansion). Do not call `generate-design` after the variant pick';
18
+ case "create-visual-plan":
19
+ case "create-ui-plan":
20
+ case "create-plan-design":
21
+ case "create-prototype-plan":
22
+ return "create the plan with its core sections or first screen, then expand it with `update-visual-plan`/`patch-visual-plan-source` follow-up edits";
23
+ case "update-visual-plan":
24
+ case "patch-visual-plan-source":
25
+ return "apply smaller, targeted `patch-visual-plan-source` edits rather than rewriting the whole plan in one call";
26
+ case "update-dashboard":
27
+ return "save a small dashboard first, then add panels one at a time with `update-dashboard` incremental `ops` edits instead of authoring the whole config in one call";
28
+ default:
29
+ return undefined;
30
+ }
31
+ }
32
+
33
+ export function actionPreparationContinuationNote(tool: string): string {
34
+ const guidance = incrementalActionGuidance(tool);
35
+ return guidance
36
+ ? `\n\nThe previous run was cut off while preparing the \`${tool}\` action input before the action could finish. Avoid spending another whole run assembling one large tool payload - ${guidance}.`
37
+ : `\n\nThe previous run was cut off while preparing the \`${tool}\` action input before the action could finish. Avoid re-assembling one large tool payload: produce a compact first result you can finish in a single run, then refine it with smaller follow-up edits.`;
38
+ }
@@ -44,6 +44,7 @@ import {
44
44
  normalizeReasoningEffortForModel,
45
45
  type ReasoningEffort,
46
46
  } from "../shared/reasoning-effort.js";
47
+ import { actionPreparationContinuationNote } from "./action-continuation-guidance.js";
47
48
  import { applyContextDirectives } from "./context-xray/apply-directives.js";
48
49
  import { loadContextDirectives } from "./context-xray/directives-store.js";
49
50
  import {
@@ -984,6 +985,7 @@ function maxRetriesForError(err: unknown): number {
984
985
  return MAX_RETRIES;
985
986
  }
986
987
  const TOOL_INPUT_ACTIVITY_INTERVAL_MS = 1500;
988
+ const ACTION_PREPARATION_NO_PROGRESS_TIMEOUT_MS = 90_000;
987
989
  const MAX_TEXT_ATTACHMENT_CHARS = 60_000;
988
990
  const MAX_SELECTION_CONTEXT_CHARS = 8_000;
989
991
  const MAX_RESOURCE_INVENTORY_ITEMS = 40;
@@ -1693,11 +1695,13 @@ export type AgentLoopContinuationReason =
1693
1695
  | "max_tokens"
1694
1696
  | "stream_ended"
1695
1697
  | "gateway_timeout"
1696
- | "network_interrupted";
1698
+ | "network_interrupted"
1699
+ | "no_progress";
1697
1700
 
1698
1701
  export function appendAgentLoopContinuation(
1699
1702
  messages: EngineMessage[],
1700
1703
  reason: AgentLoopContinuationReason,
1704
+ options: { actionPreparationTool?: string } = {},
1701
1705
  ) {
1702
1706
  const note =
1703
1707
  reason === "loop_limit"
@@ -1710,18 +1714,37 @@ export function appendAgentLoopContinuation(
1710
1714
  ? "The previous LLM call hit an upstream gateway timeout before the response finished streaming."
1711
1715
  : reason === "network_interrupted"
1712
1716
  ? "The previous LLM call was cut off by a transport-level interruption (socket dropped, connection reset, or stream closed unexpectedly)."
1713
- : "The previous run reached an internal execution budget.";
1717
+ : reason === "no_progress"
1718
+ ? "The previous run stopped producing progress events while the connection stayed open."
1719
+ : "The previous run reached an internal execution budget.";
1720
+ const actionInputNote = options.actionPreparationTool
1721
+ ? actionPreparationContinuationNote(options.actionPreparationTool)
1722
+ : "";
1714
1723
  messages.push({
1715
1724
  role: "user",
1716
1725
  content: [
1717
1726
  {
1718
1727
  type: "text",
1719
- text: `${AGENT_INTERNAL_CONTINUE_PROMPT}\n\nInternal note: ${note}`,
1728
+ text: `${AGENT_INTERNAL_CONTINUE_PROMPT}\n\nInternal note: ${note}${actionInputNote}`,
1720
1729
  },
1721
1730
  ],
1722
1731
  });
1723
1732
  }
1724
1733
 
1734
+ function isAgentLoopContinuationReason(
1735
+ reason: unknown,
1736
+ ): reason is AgentLoopContinuationReason {
1737
+ return (
1738
+ reason === "run_timeout" ||
1739
+ reason === "loop_limit" ||
1740
+ reason === "max_tokens" ||
1741
+ reason === "stream_ended" ||
1742
+ reason === "gateway_timeout" ||
1743
+ reason === "network_interrupted" ||
1744
+ reason === "no_progress"
1745
+ );
1746
+ }
1747
+
1725
1748
  /**
1726
1749
  * True when an error thrown by `runAgentLoop` is a recoverable transport- or
1727
1750
  * gateway-level interruption that the agent can resume from rather than a
@@ -2792,6 +2815,15 @@ export async function runAgentLoop(opts: {
2792
2815
  const toolInputNames = new Map<string, string>();
2793
2816
  const toolInputBytes = new Map<string, number>();
2794
2817
  let lastToolInputActivityAt = 0;
2818
+ type ActiveToolInputPreparation = {
2819
+ id: string;
2820
+ toolName?: string;
2821
+ startedAt: number;
2822
+ lastProgressAt: number;
2823
+ bytes: number;
2824
+ };
2825
+ const activeToolInputs = new Map<string, ActiveToolInputPreparation>();
2826
+ let endedForActionPreparationNoProgress = false;
2795
2827
  const sendToolInputActivity = (
2796
2828
  toolName: string | undefined,
2797
2829
  toolInputId?: string,
@@ -2814,8 +2846,65 @@ export async function runAgentLoop(opts: {
2814
2846
  ...(typeof progressBytes === "number" ? { progressBytes } : {}),
2815
2847
  });
2816
2848
  };
2849
+ const resetActiveToolInput = (
2850
+ toolName: string | undefined,
2851
+ toolInputId?: string,
2852
+ ) => {
2853
+ if (toolInputId) {
2854
+ activeToolInputs.delete(toolInputId);
2855
+ return;
2856
+ }
2857
+ if (toolName) {
2858
+ for (const [id, active] of activeToolInputs) {
2859
+ if (active.toolName === toolName) {
2860
+ activeToolInputs.delete(id);
2861
+ }
2862
+ }
2863
+ }
2864
+ };
2865
+ const hasActionPreparationStalled = () => {
2866
+ if (activeToolInputs.size === 0) return false;
2867
+ const now = Date.now();
2868
+ for (const active of activeToolInputs.values()) {
2869
+ if (
2870
+ now - active.lastProgressAt >=
2871
+ ACTION_PREPARATION_NO_PROGRESS_TIMEOUT_MS
2872
+ ) {
2873
+ return true;
2874
+ }
2875
+ }
2876
+ return false;
2877
+ };
2878
+ const trackActiveToolInput = (
2879
+ key: string,
2880
+ toolName: string | undefined,
2881
+ bytes: number,
2882
+ ) => {
2883
+ const now = Date.now();
2884
+ const previous = activeToolInputs.get(key);
2885
+ activeToolInputs.set(key, {
2886
+ id: key,
2887
+ ...((toolName ?? previous?.toolName)
2888
+ ? { toolName: toolName ?? previous?.toolName }
2889
+ : {}),
2890
+ startedAt: previous?.startedAt ?? now,
2891
+ lastProgressAt: now,
2892
+ bytes,
2893
+ });
2894
+ };
2895
+ const clearActiveToolInputs = () => {
2896
+ activeToolInputs.clear();
2897
+ };
2817
2898
 
2818
2899
  for await (const event of eventStream) {
2900
+ if (hasActionPreparationStalled()) {
2901
+ send({
2902
+ type: "auto_continue",
2903
+ reason: "no_progress",
2904
+ });
2905
+ endedForActionPreparationNoProgress = true;
2906
+ break;
2907
+ }
2819
2908
  // In-loop processor seam (stream hook). Each chunk is offered to every
2820
2909
  // processor's `processOutputStream` before the loop handles it. A
2821
2910
  // processor `abort()` throws a TripWire; catch it locally so it is not
@@ -2845,6 +2934,7 @@ export async function runAgentLoop(opts: {
2845
2934
  if (key && event.name) {
2846
2935
  toolInputNames.set(key, event.name);
2847
2936
  toolInputBytes.set(key, 0);
2937
+ trackActiveToolInput(key, event.name, 0);
2848
2938
  }
2849
2939
  sendToolInputActivity(event.name, key, undefined, true);
2850
2940
  } else if (event.type === "tool-input-delta") {
@@ -2859,19 +2949,25 @@ export async function runAgentLoop(opts: {
2859
2949
  previous +
2860
2950
  new TextEncoder().encode(event.text ?? "").byteLength;
2861
2951
  toolInputBytes.set(key, progressBytes);
2952
+ if (progressBytes > previous) {
2953
+ trackActiveToolInput(key, toolName, progressBytes);
2954
+ }
2862
2955
  }
2863
2956
  sendToolInputActivity(toolName, key, progressBytes);
2864
2957
  } else if (event.type === "gateway-heartbeat") {
2865
2958
  send({ type: "stream_keepalive" });
2866
2959
  } else if (event.type === "tool-call") {
2867
2960
  // The authoritative tool-call blocks arrive in assistant-content.
2961
+ resetActiveToolInput(event.name, event.id);
2868
2962
  } else if (event.type === "tool-call-error") {
2963
+ resetActiveToolInput(event.name, event.id);
2869
2964
  toolCallErrors.set(event.id, {
2870
2965
  name: event.name,
2871
2966
  input: event.input,
2872
2967
  error: event.error,
2873
2968
  });
2874
2969
  } else if (event.type === "assistant-content") {
2970
+ clearActiveToolInputs();
2875
2971
  assistantContent = event.parts;
2876
2972
  } else if (event.type === "usage") {
2877
2973
  usage.inputTokens += event.inputTokens;
@@ -2889,6 +2985,18 @@ export async function runAgentLoop(opts: {
2889
2985
  });
2890
2986
  }
2891
2987
  }
2988
+ if (hasActionPreparationStalled()) {
2989
+ send({
2990
+ type: "auto_continue",
2991
+ reason: "no_progress",
2992
+ });
2993
+ endedForActionPreparationNoProgress = true;
2994
+ break;
2995
+ }
2996
+ }
2997
+
2998
+ if (endedForActionPreparationNoProgress) {
2999
+ return usage;
2892
3000
  }
2893
3001
 
2894
3002
  break;
@@ -4018,6 +4126,189 @@ function endsAtInternalContinuationBoundary(run: ActiveRun): boolean {
4018
4126
  return last.type === "error" && isRecoverableContinuationError(last);
4019
4127
  }
4020
4128
 
4129
+ function isPreparingActionActivityEvent(
4130
+ event: AgentChatEvent,
4131
+ ): event is Extract<AgentChatEvent, { type: "activity" }> {
4132
+ if (event.type !== "activity") return false;
4133
+ const label = event.label.trim().toLowerCase();
4134
+ return label.startsWith("preparing ") && label.includes(" action");
4135
+ }
4136
+
4137
+ export function lastUnfinishedPreparingActionToolFromEvents(
4138
+ events: readonly AgentChatEvent[],
4139
+ ): string | undefined {
4140
+ const active = new Map<
4141
+ string,
4142
+ {
4143
+ id?: string;
4144
+ order: number;
4145
+ tool: string;
4146
+ }
4147
+ >();
4148
+ const idlessToolStarts = new Map<string, number>();
4149
+ const removeOldestMatchingActivePreparation = (
4150
+ tool: string,
4151
+ shouldRemove: (value: {
4152
+ id?: string;
4153
+ order: number;
4154
+ tool: string;
4155
+ }) => boolean = () => true,
4156
+ ) => {
4157
+ let oldest:
4158
+ | {
4159
+ key: string;
4160
+ order: number;
4161
+ }
4162
+ | undefined;
4163
+ for (const [key, value] of active) {
4164
+ if (value.tool !== tool || !shouldRemove(value)) continue;
4165
+ if (!oldest || value.order < oldest.order) {
4166
+ oldest = {
4167
+ key,
4168
+ order: value.order,
4169
+ };
4170
+ }
4171
+ }
4172
+ if (oldest) active.delete(oldest.key);
4173
+ return Boolean(oldest);
4174
+ };
4175
+ const removeMatchingActivePreparation = (event: {
4176
+ id?: string;
4177
+ tool?: string;
4178
+ type: "tool_done" | "tool_start";
4179
+ }) => {
4180
+ const id = event.id?.trim();
4181
+ const tool = event.tool?.trim();
4182
+ if (!tool) return;
4183
+ if (id) {
4184
+ if (!active.delete(`id:${id}`)) {
4185
+ removeOldestMatchingActivePreparation(tool, (value) => !value.id);
4186
+ }
4187
+ return;
4188
+ }
4189
+
4190
+ if (event.type === "tool_start") {
4191
+ if (removeOldestMatchingActivePreparation(tool)) {
4192
+ idlessToolStarts.set(tool, (idlessToolStarts.get(tool) ?? 0) + 1);
4193
+ }
4194
+ return;
4195
+ }
4196
+
4197
+ const startedCount = idlessToolStarts.get(tool) ?? 0;
4198
+ if (startedCount > 0) {
4199
+ if (startedCount === 1) {
4200
+ idlessToolStarts.delete(tool);
4201
+ } else {
4202
+ idlessToolStarts.set(tool, startedCount - 1);
4203
+ }
4204
+ return;
4205
+ }
4206
+ removeOldestMatchingActivePreparation(tool);
4207
+ };
4208
+ events.forEach((event, order) => {
4209
+ if (isPreparingActionActivityEvent(event)) {
4210
+ const tool = event.tool?.trim();
4211
+ if (tool) {
4212
+ const id = event.id?.trim();
4213
+ const key = id ? `id:${id}` : `tool:${tool}:${order}`;
4214
+ active.set(key, {
4215
+ tool,
4216
+ order,
4217
+ ...(id ? { id } : {}),
4218
+ });
4219
+ }
4220
+ return;
4221
+ }
4222
+ if (event.type === "tool_start" || event.type === "tool_done") {
4223
+ removeMatchingActivePreparation(event);
4224
+ return;
4225
+ }
4226
+ if (event.type === "error" && isRecoverableContinuationError(event)) {
4227
+ return;
4228
+ }
4229
+ if (
4230
+ event.type === "clear" ||
4231
+ event.type === "done" ||
4232
+ event.type === "error" ||
4233
+ event.type === "missing_api_key"
4234
+ ) {
4235
+ active.clear();
4236
+ idlessToolStarts.clear();
4237
+ }
4238
+ });
4239
+ let latest:
4240
+ | {
4241
+ order: number;
4242
+ tool: string;
4243
+ }
4244
+ | undefined;
4245
+ for (const value of active.values()) {
4246
+ if (!latest || value.order > latest.order) {
4247
+ latest = value;
4248
+ }
4249
+ }
4250
+ return latest?.tool;
4251
+ }
4252
+
4253
+ function endsAfterCompletedToolWithoutAssistantFinal(run: ActiveRun): boolean {
4254
+ let completedToolAfterLastAssistantText = false;
4255
+ for (const { event } of run.events) {
4256
+ if (event.type === "text" && event.text.trim().length > 0) {
4257
+ completedToolAfterLastAssistantText = false;
4258
+ continue;
4259
+ }
4260
+ if (event.type === "tool_done" && event.isError !== true) {
4261
+ completedToolAfterLastAssistantText = true;
4262
+ continue;
4263
+ }
4264
+ if (
4265
+ event.type === "clear" ||
4266
+ event.type === "error" ||
4267
+ event.type === "missing_api_key" ||
4268
+ event.type === "auto_continue" ||
4269
+ event.type === "loop_limit"
4270
+ ) {
4271
+ completedToolAfterLastAssistantText = false;
4272
+ }
4273
+ }
4274
+ return completedToolAfterLastAssistantText;
4275
+ }
4276
+
4277
+ function lastUnfinishedPreparingActionTool(run: ActiveRun): string | undefined {
4278
+ return lastUnfinishedPreparingActionToolFromEvents(
4279
+ run.events.map(({ event }) => event),
4280
+ );
4281
+ }
4282
+
4283
+ export function backgroundContinuationReasonForRun(
4284
+ run: ActiveRun,
4285
+ ): AgentLoopContinuationReason {
4286
+ const last = run.events.at(-1)?.event;
4287
+ if (last?.type === "loop_limit") return "loop_limit";
4288
+ if (
4289
+ last?.type === "auto_continue" &&
4290
+ isAgentLoopContinuationReason(last.reason)
4291
+ ) {
4292
+ return last.reason;
4293
+ }
4294
+ if (last?.type === "error" && isRecoverableContinuationError(last)) {
4295
+ return continuationReasonForResumableError(
4296
+ new EngineError(last.error, { errorCode: last.errorCode }),
4297
+ );
4298
+ }
4299
+ if (endsAfterCompletedToolWithoutAssistantFinal(run)) {
4300
+ return "stream_ended";
4301
+ }
4302
+ return "run_timeout";
4303
+ }
4304
+
4305
+ function endsAtContinuationBoundary(run: ActiveRun): boolean {
4306
+ return (
4307
+ endsAtInternalContinuationBoundary(run) ||
4308
+ endsAfterCompletedToolWithoutAssistantFinal(run)
4309
+ );
4310
+ }
4311
+
4021
4312
  /**
4022
4313
  * Hard cap on server-driven background→background continuation chunks for a
4023
4314
  * single logical turn. A `backgroundFunction` run gets a ~13-min soft timeout,
@@ -4030,9 +4321,8 @@ export const MAX_BACKGROUND_RUN_CONTINUATIONS = 20;
4030
4321
  /**
4031
4322
  * Whether the background worker should self-fire the next server-driven
4032
4323
  * continuation chunk. True only when this is a background worker run that ended
4033
- * at a recoverable soft-timeout boundary (not aborted/stopped) and the chain is
4034
- * still under its budget. Extracted so the decision is unit testable without
4035
- * booting the whole handler.
4324
+ * at a recoverable unfinished boundary (not aborted/stopped) and the chain is
4325
+ * still under its budget. Aborted / user-stopped runs do NOT chain.
4036
4326
  */
4037
4327
  export function shouldChainBackgroundContinuation(opts: {
4038
4328
  isBackgroundWorker: boolean;
@@ -4042,7 +4332,7 @@ export function shouldChainBackgroundContinuation(opts: {
4042
4332
  return (
4043
4333
  opts.isBackgroundWorker &&
4044
4334
  opts.run.status !== "aborted" &&
4045
- endsAtInternalContinuationBoundary(opts.run) &&
4335
+ endsAtContinuationBoundary(opts.run) &&
4046
4336
  opts.continuationCount < MAX_BACKGROUND_RUN_CONTINUATIONS
4047
4337
  );
4048
4338
  }
@@ -4981,7 +5271,19 @@ export function createProductionAgentHandler(
4981
5271
  ?.threadData;
4982
5272
  const resumed = threadDataToEngineMessages(priorThreadData);
4983
5273
  if (resumed.length > 0) {
4984
- appendAgentLoopContinuation(resumed, "run_timeout");
5274
+ const actionPreparationTool =
5275
+ typeof backgroundRunMarker?.actionPreparationTool === "string" &&
5276
+ backgroundRunMarker.actionPreparationTool.trim()
5277
+ ? backgroundRunMarker.actionPreparationTool.trim()
5278
+ : undefined;
5279
+ const continuationReason = isAgentLoopContinuationReason(
5280
+ backgroundRunMarker?.continuationReason,
5281
+ )
5282
+ ? backgroundRunMarker.continuationReason
5283
+ : "run_timeout";
5284
+ appendAgentLoopContinuation(resumed, continuationReason, {
5285
+ ...(actionPreparationTool ? { actionPreparationTool } : {}),
5286
+ });
4985
5287
  messages.length = 0;
4986
5288
  messages.push(...resumed);
4987
5289
  }
@@ -5173,13 +5475,19 @@ export function createProductionAgentHandler(
5173
5475
  turnId: effectiveTurnId,
5174
5476
  }
5175
5477
  : null;
5478
+ const willChainBackgroundContinuation = (run: ActiveRun) =>
5479
+ shouldChainBackgroundContinuation({
5480
+ isBackgroundWorker,
5481
+ run,
5482
+ continuationCount: backgroundContinuationCount,
5483
+ });
5176
5484
 
5177
5485
  const completeTrackedProgressRun = async (
5178
5486
  run: ActiveRun,
5179
5487
  completionError?: unknown,
5180
5488
  ) => {
5181
5489
  if (!trackedProgressRunId || !trackedProgressOwner) return;
5182
- if (!completionError && endsAtInternalContinuationBoundary(run)) {
5490
+ if (!completionError && willChainBackgroundContinuation(run)) {
5183
5491
  return;
5184
5492
  }
5185
5493
  const terminalStatus =
@@ -5275,7 +5583,7 @@ export function createProductionAgentHandler(
5275
5583
  if (
5276
5584
  isBackgroundWorker &&
5277
5585
  run.status === "errored" &&
5278
- !endsAtInternalContinuationBoundary(run)
5586
+ !willChainBackgroundContinuation(run)
5279
5587
  ) {
5280
5588
  const errEvent = [...run.events]
5281
5589
  .reverse()
@@ -5304,36 +5612,34 @@ export function createProductionAgentHandler(
5304
5612
  // agent-teams `fireInternalDispatch({ body: { mode: "continue" }})`
5305
5613
  // chain. Bounded by MAX_BACKGROUND_RUN_CONTINUATIONS. Aborted /
5306
5614
  // user-stopped runs do NOT chain.
5307
- if (
5308
- shouldChainBackgroundContinuation({
5309
- // Self-chain server-side for EVERY durable worker, not only the
5310
- // ones inside a `-background` function. Server-driven
5311
- // continuation is the whole point of durable background: the run
5312
- // must survive the client disconnecting (closed tab), so it
5313
- // cannot depend on the browser re-POSTing `auto_continue`. A
5314
- // worker on the regular ~60s function — a Netlify routing miss,
5315
- // or a non-Netlify host (Vercel/Cloudflare/Render/Fly) that
5316
- // never emits a `-background` function checkpoints at the 40s
5317
- // soft-timeout and self-dispatches the next 40s chunk; a worker
5318
- // in a real `-background` function chains ~13-min chunks. Only
5319
- // the per-chunk BUDGET differs by function type (gated by
5320
- // `runsInBackgroundFunction` at the startRun call below); the
5321
- // continuation itself must stay server-driven on both. (The
5322
- // self-chain is only reachable when the initial dispatch already
5323
- // succeeded a dispatch fast-fail degrades to the inline
5324
- // foreground fallback, which is not a worker and rides the
5325
- // connected client's auto_continue instead.)
5326
- isBackgroundWorker,
5327
- run,
5328
- continuationCount: backgroundContinuationCount,
5329
- })
5330
- ) {
5615
+ // Self-chain server-side for EVERY durable worker, not only the
5616
+ // ones inside a `-background` function. Server-driven
5617
+ // continuation is the whole point of durable background: the run
5618
+ // must survive the client disconnecting (closed tab), so it
5619
+ // cannot depend on the browser re-POSTing `auto_continue`. A
5620
+ // worker on the regular ~60s function a Netlify routing miss,
5621
+ // or a non-Netlify host (Vercel/Cloudflare/Render/Fly) that
5622
+ // never emits a `-background` function — checkpoints at the 40s
5623
+ // soft-timeout and self-dispatches the next 40s chunk; a worker
5624
+ // in a real `-background` function chains ~13-min chunks. Only
5625
+ // the per-chunk BUDGET differs by function type (gated by
5626
+ // `runsInBackgroundFunction` at the startRun call below); the
5627
+ // continuation itself must stay server-driven on both. (The
5628
+ // self-chain is only reachable when the initial dispatch already
5629
+ // succeeded a dispatch fast-fail degrades to the inline
5630
+ // foreground fallback, which is not a worker and rides the
5631
+ // connected client's auto_continue instead.)
5632
+ if (willChainBackgroundContinuation(run)) {
5331
5633
  // Mint the next chunk's runId here and sign the dispatch token
5332
5634
  // over it, so the `_process-run` route's HMAC check and the
5333
5635
  // worker's run identity agree. Fresh runId (not this chunk's) so
5334
5636
  // its seq log starts clean; same turnId folds the assistant
5335
5637
  // message across chunks.
5336
5638
  const nextRunId = generateRunId();
5639
+ const actionPreparationTool =
5640
+ lastUnfinishedPreparingActionTool(run);
5641
+ const continuationReason =
5642
+ backgroundContinuationReasonForRun(run);
5337
5643
  const continuationDispatchPath =
5338
5644
  resolveAgentChatProcessRunDispatchPath();
5339
5645
  const continuationExpectsNetlifyBackgroundFunction =
@@ -5358,6 +5664,10 @@ export function createProductionAgentHandler(
5358
5664
  runId: nextRunId,
5359
5665
  turnId: effectiveTurnId,
5360
5666
  continuationCount: backgroundContinuationCount + 1,
5667
+ continuationReason,
5668
+ ...(actionPreparationTool
5669
+ ? { actionPreparationTool }
5670
+ : {}),
5361
5671
  backgroundFunctionRuntimeExpected:
5362
5672
  continuationExpectsNetlifyBackgroundFunction,
5363
5673
  },