@ferris1225/pi-subagents 0.25.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -7
- package/package.json +53 -53
- package/src/index.ts +9 -3
- package/src/spawn.ts +137 -20
package/README.md
CHANGED
|
@@ -31,8 +31,11 @@ report their results back to the main agent automatically.
|
|
|
31
31
|
- **Per-agent configuration** — enable agents, choose a model and thinking level per
|
|
32
32
|
agent, and tune limits from `/subagents-setup`.
|
|
33
33
|
- **Automatic model fallback** — if an agent's model fails at the provider level before
|
|
34
|
-
producing any output, the
|
|
35
|
-
|
|
34
|
+
producing any output, the SAME model is retried up to five times (bounded backoff) for
|
|
35
|
+
transient errors (503/429/timeout/network/...); if it still fails, the run is retried
|
|
36
|
+
once with the main window's current model. Terminal errors (quota exhausted, billing,
|
|
37
|
+
an invalid API key) skip both and are handed straight back to the main agent. The
|
|
38
|
+
fallback is per-run only and never persisted.
|
|
36
39
|
- **Idle watchdog** — a sub-agent that produces no output for a configurable duration is
|
|
37
40
|
terminated and retried with the fallback model.
|
|
38
41
|
- **Leaf processes** — sub-agents cannot access the `subagent` tool, so delegation
|
|
@@ -53,10 +56,12 @@ Several tools now offer some form of sub-agents. What this extension does differ
|
|
|
53
56
|
calls `subagent_wait` (event-driven, returns the actual result) instead of
|
|
54
57
|
sleeping or polling.
|
|
55
58
|
- **Failures are handled, not reported.** Three layers of resilience: a provider-
|
|
56
|
-
level model failure retries
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
level model failure first retries the same model up to five times on a transient
|
|
60
|
+
provider error, then retries once with the main window's model; terminal errors
|
|
61
|
+
(quota/auth) short-circuit straight to the main agent; an idle watchdog terminates
|
|
62
|
+
a run that goes silent (a stalled stream) and retries it; and a concurrent-startup
|
|
63
|
+
race is retried with backoff automatically. The widget and the completion message
|
|
64
|
+
tell you when any of these happened.
|
|
60
65
|
- **A quality gate that closes the loop.** When a reviewer returns `REVIEW_FAIL`,
|
|
61
66
|
the extension dispatches a worker briefed with the concrete findings, then a
|
|
62
67
|
re-review — up to `maxFixRounds` times — and only then wakes the main agent with
|
|
@@ -466,7 +471,7 @@ idle timeout; **Full re-setup** re-runs the whole first-time wizard.
|
|
|
466
471
|
| `agentScope` | `user`, `project`, or `both`; controls which user/project agent directories are discovered. |
|
|
467
472
|
| `maxConcurrency` | Max sub-agent processes running at once (1–16, default 4), and the max tasks one parallel `subagent` call accepts. Extra work waits in the queue. |
|
|
468
473
|
| `maxFixRounds` | Auto-fix rounds when a reviewer returns `REVIEW_FAIL`: the extension dispatches a `worker` (briefed with the review's findings) then a `reviewer` re-review, repeating up to this many times before waking the main agent with the full chain. `0` disables it (the main agent handles fixes itself). Default 2. |
|
|
469
|
-
| `idleTimeoutSec` | Idle timeout in seconds: a sub-agent
|
|
474
|
+
| `idleTimeoutSec` | Idle timeout in seconds: a sub-agent whose stdout goes silent for this long is terminated and retried (same model first, then the main-window fallback, like any transient provider failure). `0` disables the idle watchdog. Default 90. A long but active run is never interrupted. |
|
|
470
475
|
|
|
471
476
|
### Configuration migration
|
|
472
477
|
|
package/package.json
CHANGED
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@ferris1225/pi-subagents",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Focused sub-agent delegation for pi: explore / worker / reviewer agents in isolated context, with proactive dispatch injection and per-agent model selection.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"license": "MIT",
|
|
7
|
-
"publishConfig": {
|
|
8
|
-
"access": "public"
|
|
9
|
-
},
|
|
10
|
-
"keywords": [
|
|
11
|
-
"pi-package",
|
|
12
|
-
"pi-extension",
|
|
13
|
-
"subagent",
|
|
14
|
-
"sub-agent",
|
|
15
|
-
"delegation"
|
|
16
|
-
],
|
|
17
|
-
"files": [
|
|
18
|
-
"src",
|
|
19
|
-
"agents",
|
|
20
|
-
"README.md",
|
|
21
|
-
"LICENSE"
|
|
22
|
-
],
|
|
23
|
-
"pi": {
|
|
24
|
-
"extensions": [
|
|
25
|
-
"./src/index.ts"
|
|
26
|
-
]
|
|
27
|
-
},
|
|
28
|
-
"scripts": {
|
|
29
|
-
"check": "tsc --noEmit",
|
|
30
|
-
"test": "vitest run tests",
|
|
31
|
-
"prepack": "npm run check && npm test"
|
|
32
|
-
},
|
|
33
|
-
"peerDependencies": {
|
|
34
|
-
"@earendil-works/pi-agent-core": ">=0.80.6",
|
|
35
|
-
"@earendil-works/pi-ai": ">=0.80.6",
|
|
36
|
-
"@earendil-works/pi-coding-agent": ">=0.80.6",
|
|
37
|
-
"@earendil-works/pi-tui": ">=0.80.6",
|
|
38
|
-
"typebox": "*"
|
|
39
|
-
},
|
|
40
|
-
"devDependencies": {
|
|
41
|
-
"@earendil-works/pi-agent-core": "^0.83.0",
|
|
42
|
-
"@earendil-works/pi-ai": "^0.83.0",
|
|
43
|
-
"@earendil-works/pi-coding-agent": "^0.83.0",
|
|
44
|
-
"@earendil-works/pi-tui": "^0.83.0",
|
|
45
|
-
"@types/node": "^22.10.0",
|
|
46
|
-
"typebox": "^1.3.9",
|
|
47
|
-
"typescript": "^5.9.0",
|
|
48
|
-
"vitest": "^4.1.0"
|
|
49
|
-
},
|
|
50
|
-
"engines": {
|
|
51
|
-
"node": ">=22.19.0"
|
|
52
|
-
}
|
|
53
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@ferris1225/pi-subagents",
|
|
3
|
+
"version": "0.26.0",
|
|
4
|
+
"description": "Focused sub-agent delegation for pi: explore / worker / reviewer agents in isolated context, with proactive dispatch injection and per-agent model selection.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"pi-package",
|
|
12
|
+
"pi-extension",
|
|
13
|
+
"subagent",
|
|
14
|
+
"sub-agent",
|
|
15
|
+
"delegation"
|
|
16
|
+
],
|
|
17
|
+
"files": [
|
|
18
|
+
"src",
|
|
19
|
+
"agents",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"pi": {
|
|
24
|
+
"extensions": [
|
|
25
|
+
"./src/index.ts"
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"check": "tsc --noEmit",
|
|
30
|
+
"test": "vitest run tests",
|
|
31
|
+
"prepack": "npm run check && npm test"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@earendil-works/pi-agent-core": ">=0.80.6",
|
|
35
|
+
"@earendil-works/pi-ai": ">=0.80.6",
|
|
36
|
+
"@earendil-works/pi-coding-agent": ">=0.80.6",
|
|
37
|
+
"@earendil-works/pi-tui": ">=0.80.6",
|
|
38
|
+
"typebox": "*"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@earendil-works/pi-agent-core": "^0.83.0",
|
|
42
|
+
"@earendil-works/pi-ai": "^0.83.0",
|
|
43
|
+
"@earendil-works/pi-coding-agent": "^0.83.0",
|
|
44
|
+
"@earendil-works/pi-tui": "^0.83.0",
|
|
45
|
+
"@types/node": "^22.10.0",
|
|
46
|
+
"typebox": "^1.3.9",
|
|
47
|
+
"typescript": "^5.9.0",
|
|
48
|
+
"vitest": "^4.1.0"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=22.19.0"
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -167,10 +167,13 @@ function formatCompletionBlock(result: SingleResult, maxResultLines: number, cwd
|
|
|
167
167
|
const fallbackNote = result.modelFallbackFrom
|
|
168
168
|
? ` (model fell back from ${result.modelFallbackFrom} to ${result.model ?? "main-window model"})`
|
|
169
169
|
: "";
|
|
170
|
-
const
|
|
170
|
+
const startupRetryNote = result.startupRetries
|
|
171
171
|
? ` (recovered after ${result.startupRetries} startup retr${result.startupRetries === 1 ? "y" : "ies"} — concurrent pi startup race)`
|
|
172
172
|
: "";
|
|
173
|
-
const
|
|
173
|
+
const modelRetryNote = result.modelRetries
|
|
174
|
+
? ` (recovered after ${result.modelRetries} same-model retr${result.modelRetries === 1 ? "y" : "ies"} on a transient provider error)`
|
|
175
|
+
: "";
|
|
176
|
+
const lines = [`### [${result.agent}] ${status}${usage ? ` (${usage})` : ""}${fallbackNote}${startupRetryNote}${modelRetryNote}`, "", `Task: ${formatTaskSummary(result.task, 80, false)}`, "", text];
|
|
174
177
|
// A run can exit cleanly while its last tools failed (e.g. a build that broke):
|
|
175
178
|
// the final text alone may claim more than the tools achieved, so surface the
|
|
176
179
|
// failures explicitly and tell the main agent to verify before relying on it.
|
|
@@ -196,8 +199,11 @@ function formatCompletionBlock(result: SingleResult, maxResultLines: number, cwd
|
|
|
196
199
|
* produced usable output (or the run stalled), so the task is handed back to the
|
|
197
200
|
* main window instead of being left as a dead failure. */
|
|
198
201
|
function modelLevelTakeoverNote(result: SingleResult): string {
|
|
202
|
+
const sameModel = result.modelRetries
|
|
203
|
+
? `, after ${result.modelRetries} same-model retr${result.modelRetries === 1 ? "y" : "ies"} on transient errors`
|
|
204
|
+
: "";
|
|
199
205
|
const retry = result.modelFallbackFrom ? ", and the retry with the main-window model also failed" : "";
|
|
200
|
-
return `The sub-agent could not complete this task: its model was unavailable or failed (or the run stalled)${retry}. Please execute this task in the main window with your own tools; do not re-dispatch it as a sub-agent.`;
|
|
206
|
+
return `The sub-agent could not complete this task: its model was unavailable or failed (or the run stalled)${sameModel}${retry}. Please execute this task in the main window with your own tools; do not re-dispatch it as a sub-agent.`;
|
|
201
207
|
}
|
|
202
208
|
|
|
203
209
|
/** Resolve a run-id request to actual ids: an exact numeric match always wins
|
package/src/spawn.ts
CHANGED
|
@@ -41,6 +41,20 @@ export const SUBAGENT_STARTUP_RETRY_DELAYS_MS = [250, 750, 1500] as const;
|
|
|
41
41
|
/** A genuine startup race fails well before a model request can complete. */
|
|
42
42
|
export const MAX_SUBAGENT_STARTUP_FAILURE_DURATION_MS = 2000;
|
|
43
43
|
|
|
44
|
+
/** Backoff schedule (ms) for retrying a run whose configured model failed at the
|
|
45
|
+
* provider level with a TRANSIENT error (503/429/timeout/network/overloaded/...) —
|
|
46
|
+
* i.e. NOT a terminal error (quota exhausted, billing, invalid API key). The same
|
|
47
|
+
* model is relaunched (each relaunch gets its own startup-retry inner loop), so a
|
|
48
|
+
* one-off provider hiccup recovers without demoting the configured agent model.
|
|
49
|
+
*
|
|
50
|
+
* This sits OUTSIDE pi-ai's per-request provider retry (default 3 attempts, 2/4/8s
|
|
51
|
+
* backoff): when the provider still can't recover after its own retries, the
|
|
52
|
+
* child exits carrying the final error, and this layer relaunches the whole run
|
|
53
|
+
* up to len(delays) more times before falling back to the main-window model.
|
|
54
|
+
*
|
|
55
|
+
* Bounded and capped so a stubborn outage does not stall a dispatch forever. */
|
|
56
|
+
export const SUBAGENT_RUN_LEVEL_RETRY_DELAYS_MS = [2_000, 4_000, 8_000, 16_000, 30_000] as const;
|
|
57
|
+
|
|
44
58
|
export interface UsageStats {
|
|
45
59
|
input: number;
|
|
46
60
|
output: number;
|
|
@@ -74,6 +88,12 @@ export interface SingleResult {
|
|
|
74
88
|
* exit (a concurrent pi startup race) before it produced a result. Set only when
|
|
75
89
|
* the run actually recovered after retrying, so callers can surface it. */
|
|
76
90
|
startupRetries?: number;
|
|
91
|
+
/** How many times the SAME configured model was relaunched after a transient
|
|
92
|
+
* provider-level failure (503/429/timeout/network/...) before the run produced
|
|
93
|
+
* a result. Set on recovery and on fall-back to the main-window model; left
|
|
94
|
+
* undefined for a terminal (quota/billing/invalid-key) error that short-
|
|
95
|
+
* circuits before any retry, since no relaunch happened. */
|
|
96
|
+
modelRetries?: number;
|
|
77
97
|
/** Tool calls that failed inside the run (from tool_execution_end events). A
|
|
78
98
|
* clean process exit can still hide a failed build/test/tool — the completion
|
|
79
99
|
* message must surface these so the main agent is never misled by a rosy final
|
|
@@ -218,6 +238,39 @@ export function isModelLevelFailure(result: SingleResult): boolean {
|
|
|
218
238
|
return result.messages.length > 0 || result.stderr.trim().length > 0;
|
|
219
239
|
}
|
|
220
240
|
|
|
241
|
+
/** Patterns that signal a TERMINAL provider/account error: retrying the same
|
|
242
|
+
* model (or falling back to the main-window model under the same account) cannot
|
|
243
|
+
* fix it, so the run skips both run-level retry and model fallback and is handed
|
|
244
|
+
* back to the main agent. This is the complement of pi-ai's transient-error set
|
|
245
|
+
* (429/5xx/overloaded/network/timeout/...): anything NOT matching here is treated
|
|
246
|
+
* as transient and retried on the same model before degrading.
|
|
247
|
+
*
|
|
248
|
+
* Mirrors pi-ai's NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN (quota/billing/
|
|
249
|
+
* subscription-limit text) and adds the auth/credential failures the user cited
|
|
250
|
+
* ("key无效"). Auth is account-scoped, so a fallback model on the same provider
|
|
251
|
+
* would fail identically — hand it to the main agent immediately. */
|
|
252
|
+
const TERMINAL_MODEL_ERROR_PATTERN =
|
|
253
|
+
/insufficient_quota|quota\s+exceeded|exceeded[^.\n]{0,40}quota|out\s+of\s+budget|billing|usage\s+limit|usage_limit|gousagelimiterror|freeusagelimiterror|monthly\s+usage\s+limit\s+reached|available\s+balance|invalid\s+(?:api\s+)?key|incorrect\s+api\s+key|unauthori[sz]ed|\b401\b|\b403\b|forbidden|permission\s+denied/i;
|
|
254
|
+
|
|
255
|
+
/** True when a model-level failure carries a TERMINAL error message — quota
|
|
256
|
+
* exhaustion, billing, an invalid API key, auth rejection. Such a run is NEVER
|
|
257
|
+
* retried on the same model and never falls back to the main-window model: the
|
|
258
|
+
* account is the bottleneck, so it is handed back to the main agent to fix.
|
|
259
|
+
*
|
|
260
|
+
* Caller must first confirm `isModelLevelFailure(result)` — aborts and
|
|
261
|
+
* dispatch-crafted results never reach this classifier. */
|
|
262
|
+
export function isTerminalModelError(result: SingleResult): boolean {
|
|
263
|
+
const message = result.errorMessage?.trim();
|
|
264
|
+
if (message) return TERMINAL_MODEL_ERROR_PATTERN.test(message);
|
|
265
|
+
// Only consult stderr when there is no structured errorMessage: pi-ai surfaces
|
|
266
|
+
// provider errors via message_end -> errorMessage, so a transient errorMessage
|
|
267
|
+
// (e.g. "503 Service Unavailable") must not be overridden by noisy stderr that
|
|
268
|
+
// happens to mention a terminal-looking word (an npm warning, a proxy banner).
|
|
269
|
+
// This keeps transient failures retryable even when stderr is chatty.
|
|
270
|
+
const stderr = result.stderr.trim();
|
|
271
|
+
return stderr.length > 0 && TERMINAL_MODEL_ERROR_PATTERN.test(stderr);
|
|
272
|
+
}
|
|
273
|
+
|
|
221
274
|
/**
|
|
222
275
|
* True when a failed run produced NO model, tool, output, or usage activity
|
|
223
276
|
* within the startup window — the signature of a concurrent pi startup race,
|
|
@@ -365,6 +418,12 @@ export interface RunSingleOptions {
|
|
|
365
418
|
* (a concurrent pi startup race). Defaults to SUBAGENT_STARTUP_RETRY_DELAYS_MS;
|
|
366
419
|
* pass a shorter array in tests to keep them fast. */
|
|
367
420
|
startupRetryDelaysMs?: readonly number[];
|
|
421
|
+
/** Run-level backoff schedule (ms) for relaunching the SAME configured model
|
|
422
|
+
* after a transient provider-level failure (503/429/timeout/network/...). Each
|
|
423
|
+
* relaunch gets its own startup-retry inner loop. Defaults to
|
|
424
|
+
* SUBAGENT_RUN_LEVEL_RETRY_DELAYS_MS; pass [] to disable (e.g. when an isolated
|
|
425
|
+
* test wants to assert only the fallback path runs once). */
|
|
426
|
+
runLevelRetryDelaysMs?: readonly number[];
|
|
368
427
|
signal?: AbortSignal;
|
|
369
428
|
onLive?: (e: SubagentLiveEvent) => void;
|
|
370
429
|
makeDetails: (results: SingleResult[]) => SubagentDetails;
|
|
@@ -666,17 +725,31 @@ export async function runSingleAgent(options: RunSingleOptions): Promise<SingleR
|
|
|
666
725
|
}
|
|
667
726
|
|
|
668
727
|
/**
|
|
669
|
-
* Run one agent with
|
|
728
|
+
* Run one agent with three layers of resilience against transient dispatch failures:
|
|
670
729
|
*
|
|
671
730
|
* 1. Startup retry (inner loop): a concurrent pi startup race can make the child
|
|
672
731
|
* exit before any model/tool activity. Relaunch with backoff so the startup
|
|
673
732
|
* lock clears. The SAME model is retried — the race is in the host, not the
|
|
674
733
|
* model — and only a clean, silent, zero-activity exit qualifies (see
|
|
675
734
|
* isRetryableStartupFailure), so retrying can never duplicate real work.
|
|
676
|
-
* 2.
|
|
677
|
-
* before producing output
|
|
678
|
-
*
|
|
679
|
-
*
|
|
735
|
+
* 2. Run-level retry on the SAME configured model (middle): when the provider
|
|
736
|
+
* rejects the model before producing output with a TRANSIENT error
|
|
737
|
+
* (503/429/timeout/network/stream/...) — i.e. NOT a terminal one (quota
|
|
738
|
+
* exhausted, billing, an invalid API key, auth rejected — see
|
|
739
|
+
* isTerminalModelError) — relaunch the whole run up to
|
|
740
|
+
* SUBAGENT_RUN_LEVEL_RETRY_DELAYS_MS.length more times with backoff, so a
|
|
741
|
+
* one-off provider hiccup recovers without demoting the configured agent
|
|
742
|
+
* model. Each relaunch gets its own inner startup-retry loop. This sits
|
|
743
|
+
* outside pi-ai's per-request provider retry, which by then has already
|
|
744
|
+
* tried (default 3 attempts) and given up.
|
|
745
|
+
* 3. Model fallback (outer): when the same model is still failing after all its
|
|
746
|
+
* run-level retries, retry once with the main window's current model. The
|
|
747
|
+
* fallback gets its own startup-retry loop, since a startup race can hit any
|
|
748
|
+
* relaunch regardless of model.
|
|
749
|
+
*
|
|
750
|
+
* Terminal model errors short-circuit straight to the caller (modelRetries is
|
|
751
|
+
* set): the account is the bottleneck, so neither same-model retry nor a
|
|
752
|
+
* same-account fallback can help, and the run is left for the main agent to fix.
|
|
680
753
|
*
|
|
681
754
|
* The fallback is per-run only and never persisted: a transient provider hiccup
|
|
682
755
|
* must not silently downgrade the configured agent model.
|
|
@@ -687,7 +760,11 @@ export async function runSingleAgentWithModelFallback(
|
|
|
687
760
|
): Promise<SingleResult> {
|
|
688
761
|
const agent = options.agent;
|
|
689
762
|
const launchedRef = agent?.model;
|
|
690
|
-
const
|
|
763
|
+
const startupDelays = options.startupRetryDelaysMs ?? SUBAGENT_STARTUP_RETRY_DELAYS_MS;
|
|
764
|
+
// Run-level retry is opted out of with an explicit empty array (e.g. a test
|
|
765
|
+
// that wants to assert ONLY the fallback path runs once); undefined means
|
|
766
|
+
// "use the default 5-attempt transient-error schedule".
|
|
767
|
+
const runDelays = options.runLevelRetryDelaysMs ?? SUBAGENT_RUN_LEVEL_RETRY_DELAYS_MS;
|
|
691
768
|
|
|
692
769
|
const runWithStartupRetry = async (opts: RunSingleOptions): Promise<SingleResult> => {
|
|
693
770
|
let lastResult: SingleResult;
|
|
@@ -700,12 +777,12 @@ export async function runSingleAgentWithModelFallback(
|
|
|
700
777
|
if (retries > 0 && !isFailedResult(lastResult)) lastResult.startupRetries = retries;
|
|
701
778
|
return lastResult;
|
|
702
779
|
}
|
|
703
|
-
const delay =
|
|
780
|
+
const delay = startupDelays[attempt];
|
|
704
781
|
if (delay === undefined) {
|
|
705
782
|
// Exhausted: the agent never reached a model. Surface the concurrency-race
|
|
706
783
|
// cause as a dispatch-level failure (no model was ever reached, so this
|
|
707
|
-
// must NOT trigger model fallback) so the main agent
|
|
708
|
-
// maxConcurrency.
|
|
784
|
+
// must NOT trigger run-level retry or model fallback) so the main agent
|
|
785
|
+
// can retry or lower maxConcurrency.
|
|
709
786
|
lastResult.errorMessage = formatStartupRetryExhaustedError(
|
|
710
787
|
lastResult.model ?? opts.agent?.model ?? "default",
|
|
711
788
|
attempt + 1,
|
|
@@ -725,15 +802,55 @@ export async function runSingleAgentWithModelFallback(
|
|
|
725
802
|
}
|
|
726
803
|
};
|
|
727
804
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
//
|
|
733
|
-
//
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
//
|
|
737
|
-
//
|
|
738
|
-
|
|
805
|
+
let result = await runWithStartupRetry(options);
|
|
806
|
+
|
|
807
|
+
// After a model-level failure, classify before reacting. A TERMINAL error
|
|
808
|
+
// (quota/billing/invalid key/auth) is account-scoped: neither same-model
|
|
809
|
+
// retry nor a same-account fallback can help, so hand the run straight back
|
|
810
|
+
// to the main agent instead of burning its time on a doomed retry.
|
|
811
|
+
if (agent && launchedRef && isModelLevelFailure(result) && isTerminalModelError(result)) return result;
|
|
812
|
+
|
|
813
|
+
// A TRANSIENT provider failure (503/429/timeout/network/stream/...) is usually
|
|
814
|
+
// a one-off hiccup. Relaunch the SAME configured model up to runDelays.length
|
|
815
|
+
// more times with backoff before degrading to a fallback model — the run's own
|
|
816
|
+
// provider retry already tried and failed, so each relaunch here is an
|
|
817
|
+
// independent, fresh attempt that can recover without losing the configured
|
|
818
|
+
// model's capability to review.
|
|
819
|
+
let modelRetries = 0;
|
|
820
|
+
if (agent && launchedRef && isModelLevelFailure(result) && runDelays.length > 0) {
|
|
821
|
+
for (let attempt = 0; ; attempt++) {
|
|
822
|
+
const delay = runDelays[attempt];
|
|
823
|
+
if (delay === undefined) break;
|
|
824
|
+
try {
|
|
825
|
+
options.onLive?.({ kind: "status", status: "running" });
|
|
826
|
+
} catch { /* never throw from event handling */ }
|
|
827
|
+
const shouldRetry = await waitForStartupRetry(delay, options.signal);
|
|
828
|
+
if (!shouldRetry) return { ...result, modelRetries };
|
|
829
|
+
const retried = await runWithStartupRetry(options);
|
|
830
|
+
modelRetries++;
|
|
831
|
+
if (!isModelLevelFailure(retried) || isTerminalModelError(retried)) {
|
|
832
|
+
// Each relaunch redoes the work; failedTools reflect ONLY the final
|
|
833
|
+
// attempt (no stale build errors from earlier transient failures),
|
|
834
|
+
// so the completion message's claim stays accurate.
|
|
835
|
+
return { ...retried, modelRetries };
|
|
836
|
+
}
|
|
837
|
+
result = retried;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// Same-model retries exhausted (or none configured) and still failing: fall
|
|
842
|
+
// back to the main window's current model exactly once. Skipped when there is
|
|
843
|
+
// no fallback ref or it equals the configured model — a same-ref rerun would
|
|
844
|
+
// just repeat the already-exhausted failure for nothing.
|
|
845
|
+
if (agent && launchedRef && fallbackModelRef && launchedRef !== fallbackModelRef && isModelLevelFailure(result)) {
|
|
846
|
+
const retried = await runWithStartupRetry({ ...options, agent: { ...agent, model: fallbackModelRef } });
|
|
847
|
+
// The fallback replaces the result wholesale: `retried.failedTools` reflect
|
|
848
|
+
// ONLY the fallback (final) attempt. The original attempt's failedTools are
|
|
849
|
+
// intentionally not merged — a fallback relaunch redoes the work, so attaching
|
|
850
|
+
// the first attempt's stale build errors to a clean final attempt would
|
|
851
|
+
// misattribute failures the worker already fixed. This makes the README's
|
|
852
|
+
// "failed tool calls from the run's final attempt" claim accurate.
|
|
853
|
+
return { ...retried, modelFallbackFrom: launchedRef, modelRetries };
|
|
854
|
+
}
|
|
855
|
+
return { ...result, modelRetries };
|
|
739
856
|
}
|