@agent-native/core 0.84.38 → 0.84.40

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 (41) hide show
  1. package/corpus/README.md +2 -2
  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 +209 -4
  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/corpus/templates/design/app/components/design/CodeWorkbenchHost.tsx +486 -341
  10. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +13 -0
  11. package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +76 -58
  12. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +6 -0
  13. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +80 -8
  14. package/corpus/templates/design/app/i18n/zh-TW.ts +2 -2
  15. package/corpus/templates/design/app/i18n-data.ts +22 -30
  16. package/corpus/templates/design/app/lib/design-import.ts +42 -0
  17. package/corpus/templates/design/app/pages/DesignEditor.tsx +180 -109
  18. package/corpus/templates/design/changelog/2026-07-01-code-workspace-now-opens-a-vs-code-like-editor-with-real-cod.md +6 -0
  19. package/corpus/templates/design/changelog/2026-07-01-import-is-more-compact-local-app-setup-is-clearer-and-figma.md +6 -0
  20. package/corpus/templates/design/package.json +1 -0
  21. package/dist/agent/action-continuation-guidance.d.ts +3 -0
  22. package/dist/agent/action-continuation-guidance.d.ts.map +1 -0
  23. package/dist/agent/action-continuation-guidance.js +38 -0
  24. package/dist/agent/action-continuation-guidance.js.map +1 -0
  25. package/dist/agent/production-agent.d.ts +6 -2
  26. package/dist/agent/production-agent.d.ts.map +1 -1
  27. package/dist/agent/production-agent.js +160 -4
  28. package/dist/agent/production-agent.js.map +1 -1
  29. package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
  30. package/dist/agent/run-loop-with-resume.js +69 -18
  31. package/dist/agent/run-loop-with-resume.js.map +1 -1
  32. package/dist/agent/types.d.ts +2 -0
  33. package/dist/agent/types.d.ts.map +1 -1
  34. package/dist/agent/types.js.map +1 -1
  35. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  36. package/dist/client/agent-chat-adapter.js +2 -39
  37. package/dist/client/agent-chat-adapter.js.map +1 -1
  38. package/dist/collab/routes.d.ts +1 -1
  39. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  40. package/dist/observability/routes.d.ts +5 -5
  41. 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
31
- - template files: 4995
30
+ - core files: 2044
31
+ - template files: 4998
@@ -1,5 +1,17 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.84.40
4
+
5
+ ### Patch Changes
6
+
7
+ - 96feefd: Checkpoint agent runs that stall while preparing an action input without streaming bytes.
8
+
9
+ ## 0.84.39
10
+
11
+ ### Patch Changes
12
+
13
+ - ca6912b: Fix hosted Google Analytics / Tag Manager injection by baking the measurement id into Nitro server bundles and merging the required GA/GTM script, connect, and image hosts into existing stricter document CSPs.
14
+
3
15
  ## 0.84.38
4
16
 
5
17
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.84.38",
3
+ "version": "0.84.40",
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,83 @@ 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
+ let active: {
4141
+ id?: string;
4142
+ tool: string;
4143
+ } | null = null;
4144
+ for (const event of events) {
4145
+ if (isPreparingActionActivityEvent(event)) {
4146
+ const tool = event.tool?.trim();
4147
+ if (tool) {
4148
+ active = {
4149
+ tool,
4150
+ ...(event.id?.trim() ? { id: event.id.trim() } : {}),
4151
+ };
4152
+ }
4153
+ continue;
4154
+ }
4155
+ if (event.type === "tool_start" || event.type === "tool_done") {
4156
+ const tool = event.tool?.trim();
4157
+ const id = event.id?.trim();
4158
+ if (
4159
+ active &&
4160
+ ((id && active.id === id) || (!id && tool && active.tool === tool))
4161
+ ) {
4162
+ active = null;
4163
+ }
4164
+ continue;
4165
+ }
4166
+ if (event.type === "error" && isRecoverableContinuationError(event)) {
4167
+ continue;
4168
+ }
4169
+ if (
4170
+ event.type === "clear" ||
4171
+ event.type === "done" ||
4172
+ event.type === "error" ||
4173
+ event.type === "missing_api_key"
4174
+ ) {
4175
+ active = null;
4176
+ }
4177
+ }
4178
+ return active?.tool;
4179
+ }
4180
+
4181
+ function lastUnfinishedPreparingActionTool(run: ActiveRun): string | undefined {
4182
+ return lastUnfinishedPreparingActionToolFromEvents(
4183
+ run.events.map(({ event }) => event),
4184
+ );
4185
+ }
4186
+
4187
+ export function backgroundContinuationReasonForRun(
4188
+ run: ActiveRun,
4189
+ ): AgentLoopContinuationReason {
4190
+ const last = run.events.at(-1)?.event;
4191
+ if (last?.type === "loop_limit") return "loop_limit";
4192
+ if (
4193
+ last?.type === "auto_continue" &&
4194
+ isAgentLoopContinuationReason(last.reason)
4195
+ ) {
4196
+ return last.reason;
4197
+ }
4198
+ if (last?.type === "error" && isRecoverableContinuationError(last)) {
4199
+ return continuationReasonForResumableError(
4200
+ new EngineError(last.error, { errorCode: last.errorCode }),
4201
+ );
4202
+ }
4203
+ return "run_timeout";
4204
+ }
4205
+
4021
4206
  /**
4022
4207
  * Hard cap on server-driven background→background continuation chunks for a
4023
4208
  * single logical turn. A `backgroundFunction` run gets a ~13-min soft timeout,
@@ -4981,7 +5166,19 @@ export function createProductionAgentHandler(
4981
5166
  ?.threadData;
4982
5167
  const resumed = threadDataToEngineMessages(priorThreadData);
4983
5168
  if (resumed.length > 0) {
4984
- appendAgentLoopContinuation(resumed, "run_timeout");
5169
+ const actionPreparationTool =
5170
+ typeof backgroundRunMarker?.actionPreparationTool === "string" &&
5171
+ backgroundRunMarker.actionPreparationTool.trim()
5172
+ ? backgroundRunMarker.actionPreparationTool.trim()
5173
+ : undefined;
5174
+ const continuationReason = isAgentLoopContinuationReason(
5175
+ backgroundRunMarker?.continuationReason,
5176
+ )
5177
+ ? backgroundRunMarker.continuationReason
5178
+ : "run_timeout";
5179
+ appendAgentLoopContinuation(resumed, continuationReason, {
5180
+ ...(actionPreparationTool ? { actionPreparationTool } : {}),
5181
+ });
4985
5182
  messages.length = 0;
4986
5183
  messages.push(...resumed);
4987
5184
  }
@@ -5334,6 +5531,10 @@ export function createProductionAgentHandler(
5334
5531
  // its seq log starts clean; same turnId folds the assistant
5335
5532
  // message across chunks.
5336
5533
  const nextRunId = generateRunId();
5534
+ const actionPreparationTool =
5535
+ lastUnfinishedPreparingActionTool(run);
5536
+ const continuationReason =
5537
+ backgroundContinuationReasonForRun(run);
5337
5538
  const continuationDispatchPath =
5338
5539
  resolveAgentChatProcessRunDispatchPath();
5339
5540
  const continuationExpectsNetlifyBackgroundFunction =
@@ -5358,6 +5559,10 @@ export function createProductionAgentHandler(
5358
5559
  runId: nextRunId,
5359
5560
  turnId: effectiveTurnId,
5360
5561
  continuationCount: backgroundContinuationCount + 1,
5562
+ continuationReason,
5563
+ ...(actionPreparationTool
5564
+ ? { actionPreparationTool }
5565
+ : {}),
5361
5566
  backgroundFunctionRuntimeExpected:
5362
5567
  continuationExpectsNetlifyBackgroundFunction,
5363
5568
  },
@@ -27,6 +27,8 @@ import {
27
27
  appendAgentLoopContinuation,
28
28
  isResumableEngineError,
29
29
  continuationReasonForResumableError,
30
+ lastUnfinishedPreparingActionToolFromEvents,
31
+ type AgentLoopContinuationReason,
30
32
  } from "./production-agent.js";
31
33
  import { resolveRunSoftTimeoutMs } from "./run-manager.js";
32
34
  import type { ResolveRunSoftTimeoutOptions } from "./run-manager.js";
@@ -35,6 +37,35 @@ import {
35
37
  classifyToolCallJournal,
36
38
  buildResumeJournalNote,
37
39
  } from "./tool-call-journal.js";
40
+ import type { AgentChatEvent } from "./types.js";
41
+
42
+ async function readCurrentTurnEventsForResume(
43
+ threadId: string | undefined,
44
+ localEvents: readonly AgentChatEvent[] = [],
45
+ ): Promise<AgentChatEvent[]> {
46
+ let persistedEvents: AgentChatEvent[] = [];
47
+ try {
48
+ persistedEvents = threadId
49
+ ? await getCurrentTurnEventsForThread(threadId)
50
+ : [];
51
+ } catch {
52
+ persistedEvents = [];
53
+ }
54
+ if (localEvents.length === 0) return persistedEvents;
55
+ const seen = new Set(persistedEvents.map((event) => JSON.stringify(event)));
56
+ return [
57
+ ...persistedEvents,
58
+ ...localEvents.filter((event) => !seen.has(JSON.stringify(event))),
59
+ ];
60
+ }
61
+
62
+ function actionPreparationContinuationOptions(
63
+ events: readonly AgentChatEvent[],
64
+ ): { actionPreparationTool?: string } {
65
+ const actionPreparationTool =
66
+ lastUnfinishedPreparingActionToolFromEvents(events);
67
+ return actionPreparationTool ? { actionPreparationTool } : {};
68
+ }
38
69
 
39
70
  /**
40
71
  * Derive the per-turn tool-call journal from the durable run-event ledger and,
@@ -57,13 +88,11 @@ import {
57
88
  * complete write tool (returning the journaled result instead). See
58
89
  * `tool-call-journal.ts` (`findCompletedJournalEntry`) for the keying used.
59
90
  */
60
- async function appendToolCallJournalNote(
91
+ function appendToolCallJournalNote(
61
92
  messages: EngineMessage[],
62
- threadId: string | undefined,
63
- ): Promise<void> {
64
- if (!threadId) return;
93
+ events: readonly AgentChatEvent[],
94
+ ): void {
65
95
  try {
66
- const events = await getCurrentTurnEventsForThread(threadId);
67
96
  if (events.length === 0) return;
68
97
  const journal = classifyToolCallJournal(events);
69
98
  const note = buildResumeJournalNote(journal);
@@ -79,12 +108,27 @@ async function appendToolCallJournalNote(
79
108
  }
80
109
  }
81
110
 
111
+ async function appendContinuationAndJournal(
112
+ messages: EngineMessage[],
113
+ reason: AgentLoopContinuationReason,
114
+ threadId: string | undefined,
115
+ localEvents: readonly AgentChatEvent[] = [],
116
+ ): Promise<void> {
117
+ const events = await readCurrentTurnEventsForResume(threadId, localEvents);
118
+ appendAgentLoopContinuation(
119
+ messages,
120
+ reason,
121
+ actionPreparationContinuationOptions(events),
122
+ );
123
+ appendToolCallJournalNote(messages, events);
124
+ }
125
+
82
126
  async function hasCompletedSideEffectToolCallInCurrentTurn(
83
127
  threadId: string | undefined,
128
+ localEvents: readonly AgentChatEvent[] = [],
84
129
  ): Promise<boolean> {
85
- if (!threadId) return false;
86
130
  try {
87
- const events = await getCurrentTurnEventsForThread(threadId);
131
+ const events = await readCurrentTurnEventsForResume(threadId, localEvents);
88
132
  if (events.length === 0) return false;
89
133
  return events.some(
90
134
  (event) =>
@@ -97,6 +141,24 @@ async function hasCompletedSideEffectToolCallInCurrentTurn(
97
141
  }
98
142
  }
99
143
 
144
+ function internalContinuationReasonForAttempt(
145
+ events: readonly AgentChatEvent[],
146
+ ): AgentLoopContinuationReason | undefined {
147
+ const last = events.at(-1);
148
+ if (last?.type !== "auto_continue") return undefined;
149
+ if (
150
+ last.reason === "run_timeout" ||
151
+ last.reason === "loop_limit" ||
152
+ last.reason === "no_progress" ||
153
+ last.reason === "stream_ended" ||
154
+ last.reason === "gateway_timeout" ||
155
+ last.reason === "network_interrupted"
156
+ ) {
157
+ return last.reason;
158
+ }
159
+ return undefined;
160
+ }
161
+
100
162
  /**
101
163
  * Cap on continuation iterations inside a single
102
164
  * `runAgentLoopDirectWithSoftTimeout` invocation. The host's hard function
@@ -161,6 +223,7 @@ export async function runAgentLoopDirectWithSoftTimeout(
161
223
  usage.model = next.model;
162
224
  };
163
225
 
226
+ const localTurnEvents: AgentChatEvent[] = [];
164
227
  let attempts = 0;
165
228
  // Tracks whether the most recent attempt ended by scheduling another
166
229
  // continuation (soft-timeout or resumable error → `continue`) rather than
@@ -189,16 +252,49 @@ export async function runAgentLoopDirectWithSoftTimeout(
189
252
  controller.abort();
190
253
  }, timeoutMs);
191
254
 
255
+ const attemptStartIndex = localTurnEvents.length;
256
+ const send = (event: AgentChatEvent) => {
257
+ localTurnEvents.push(event);
258
+ opts.send(event);
259
+ };
260
+
192
261
  try {
193
262
  const nextUsage = await runAgentLoop({
194
263
  ...opts,
264
+ send,
195
265
  signal: controller.signal,
196
266
  });
197
267
  addUsage(nextUsage);
268
+ const attemptEvents = localTurnEvents.slice(attemptStartIndex);
269
+ const internalContinuationReason =
270
+ internalContinuationReasonForAttempt(attemptEvents);
271
+ if (internalContinuationReason && !upstreamSignal.aborted) {
272
+ lastAttemptWasUnfinishedContinuation = true;
273
+ const continuationEvents = [...localTurnEvents];
274
+ if (
275
+ !(await hasCompletedSideEffectToolCallInCurrentTurn(
276
+ opts.threadId,
277
+ continuationEvents,
278
+ ))
279
+ ) {
280
+ opts.send({ type: "clear" });
281
+ }
282
+ await appendContinuationAndJournal(
283
+ opts.messages,
284
+ internalContinuationReason,
285
+ opts.threadId,
286
+ continuationEvents,
287
+ );
288
+ continue;
289
+ }
198
290
  if (softTimedOut && !upstreamSignal.aborted) {
199
291
  lastAttemptWasUnfinishedContinuation = true;
200
- appendAgentLoopContinuation(opts.messages, "run_timeout");
201
- await appendToolCallJournalNote(opts.messages, opts.threadId);
292
+ await appendContinuationAndJournal(
293
+ opts.messages,
294
+ "run_timeout",
295
+ opts.threadId,
296
+ localTurnEvents,
297
+ );
202
298
  continue;
203
299
  }
204
300
  return usage;
@@ -208,12 +304,19 @@ export async function runAgentLoopDirectWithSoftTimeout(
208
304
  // resumed model doesn't re-emit it and produce duplicated output.
209
305
  lastAttemptWasUnfinishedContinuation = true;
210
306
  if (
211
- !(await hasCompletedSideEffectToolCallInCurrentTurn(opts.threadId))
307
+ !(await hasCompletedSideEffectToolCallInCurrentTurn(
308
+ opts.threadId,
309
+ localTurnEvents,
310
+ ))
212
311
  ) {
213
312
  opts.send({ type: "clear" });
214
313
  }
215
- appendAgentLoopContinuation(opts.messages, "run_timeout");
216
- await appendToolCallJournalNote(opts.messages, opts.threadId);
314
+ await appendContinuationAndJournal(
315
+ opts.messages,
316
+ "run_timeout",
317
+ opts.threadId,
318
+ localTurnEvents,
319
+ );
217
320
  continue;
218
321
  }
219
322
  // Resumable transport / gateway interruptions: the LLM call was cut off
@@ -232,15 +335,19 @@ export async function runAgentLoopDirectWithSoftTimeout(
232
335
  if (!upstreamSignal.aborted && isResumableEngineError(err)) {
233
336
  lastAttemptWasUnfinishedContinuation = true;
234
337
  if (
235
- !(await hasCompletedSideEffectToolCallInCurrentTurn(opts.threadId))
338
+ !(await hasCompletedSideEffectToolCallInCurrentTurn(
339
+ opts.threadId,
340
+ localTurnEvents,
341
+ ))
236
342
  ) {
237
343
  opts.send({ type: "clear" });
238
344
  }
239
- appendAgentLoopContinuation(
345
+ await appendContinuationAndJournal(
240
346
  opts.messages,
241
347
  continuationReasonForResumableError(err),
348
+ opts.threadId,
349
+ localTurnEvents,
242
350
  );
243
- await appendToolCallJournalNote(opts.messages, opts.threadId);
244
351
  continue;
245
352
  }
246
353
  throw err;
@@ -262,7 +369,12 @@ export async function runAgentLoopDirectWithSoftTimeout(
262
369
  // the terminal message stands alone instead of trailing a half sentence.
263
370
  // Preserve completed tool cards: they are the user's only durable proof
264
371
  // that a side effect landed before the final assistant note timed out.
265
- if (!(await hasCompletedSideEffectToolCallInCurrentTurn(opts.threadId))) {
372
+ if (
373
+ !(await hasCompletedSideEffectToolCallInCurrentTurn(
374
+ opts.threadId,
375
+ localTurnEvents,
376
+ ))
377
+ ) {
266
378
  opts.send({ type: "clear" });
267
379
  }
268
380
  opts.send({
@@ -163,6 +163,14 @@ export interface AgentChatRequest {
163
163
  __backgroundRun?: {
164
164
  runId: string;
165
165
  turnId?: string;
166
+ continuationReason?:
167
+ | "run_timeout"
168
+ | "loop_limit"
169
+ | "no_progress"
170
+ | "stream_ended"
171
+ | "gateway_timeout"
172
+ | "network_interrupted";
173
+ actionPreparationTool?: string;
166
174
  /**
167
175
  * Number of server-driven background→background continuations already
168
176
  * chained into this logical turn (0 on the first chunk). The worker