@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.
- package/corpus/README.md +1 -1
- 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 +344 -34
- 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/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 +8 -5
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +267 -32
- 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/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- 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.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
|
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.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
|
-
:
|
|
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
|
|
4034
|
-
* still under its budget.
|
|
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
|
-
|
|
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
|
-
|
|
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 &&
|
|
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
|
-
!
|
|
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
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
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
|
},
|