@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.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +12 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/action-continuation-guidance.ts +38 -0
- package/corpus/core/src/agent/production-agent.ts +209 -4
- package/corpus/core/src/agent/run-loop-with-resume.ts +128 -16
- package/corpus/core/src/agent/types.ts +8 -0
- package/corpus/core/src/client/agent-chat-adapter.ts +2 -40
- package/corpus/templates/design/app/components/design/CodeWorkbenchHost.tsx +486 -341
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +13 -0
- package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +76 -58
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +6 -0
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +80 -8
- package/corpus/templates/design/app/i18n/zh-TW.ts +2 -2
- package/corpus/templates/design/app/i18n-data.ts +22 -30
- package/corpus/templates/design/app/lib/design-import.ts +42 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +180 -109
- package/corpus/templates/design/changelog/2026-07-01-code-workspace-now-opens-a-vs-code-like-editor-with-real-cod.md +6 -0
- package/corpus/templates/design/changelog/2026-07-01-import-is-more-compact-local-app-setup-is-clearer-and-figma.md +6 -0
- package/corpus/templates/design/package.json +1 -0
- package/dist/agent/action-continuation-guidance.d.ts +3 -0
- package/dist/agent/action-continuation-guidance.d.ts.map +1 -0
- package/dist/agent/action-continuation-guidance.js +38 -0
- package/dist/agent/action-continuation-guidance.js.map +1 -0
- package/dist/agent/production-agent.d.ts +6 -2
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +160 -4
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
- package/dist/agent/run-loop-with-resume.js +69 -18
- package/dist/agent/run-loop-with-resume.js.map +1 -1
- package/dist/agent/types.d.ts +2 -0
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +2 -39
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/observability/routes.d.ts +5 -5
- package/package.json +1 -1
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -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
|
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.84.
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
91
|
+
function appendToolCallJournalNote(
|
|
61
92
|
messages: EngineMessage[],
|
|
62
|
-
|
|
63
|
-
):
|
|
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
|
|
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
|
-
|
|
201
|
-
|
|
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(
|
|
307
|
+
!(await hasCompletedSideEffectToolCallInCurrentTurn(
|
|
308
|
+
opts.threadId,
|
|
309
|
+
localTurnEvents,
|
|
310
|
+
))
|
|
212
311
|
) {
|
|
213
312
|
opts.send({ type: "clear" });
|
|
214
313
|
}
|
|
215
|
-
|
|
216
|
-
|
|
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(
|
|
338
|
+
!(await hasCompletedSideEffectToolCallInCurrentTurn(
|
|
339
|
+
opts.threadId,
|
|
340
|
+
localTurnEvents,
|
|
341
|
+
))
|
|
236
342
|
) {
|
|
237
343
|
opts.send({ type: "clear" });
|
|
238
344
|
}
|
|
239
|
-
|
|
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 (
|
|
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
|