@aexol/spectral 0.9.222 → 0.9.224
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/dist/agent/index.d.ts +13 -0
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +251 -72
- package/dist/sdk/agent-core/agent-loop.d.ts.map +1 -1
- package/dist/sdk/agent-core/agent-loop.js +2 -4
- package/dist/sdk/ai/index.d.ts +1 -0
- package/dist/sdk/ai/index.d.ts.map +1 -1
- package/dist/sdk/ai/index.js +1 -0
- package/dist/sdk/ai/utils/transient-error.d.ts +28 -0
- package/dist/sdk/ai/utils/transient-error.d.ts.map +1 -0
- package/dist/sdk/ai/utils/transient-error.js +55 -0
- package/dist/sdk/coding-agent/core/agent-session.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/agent-session.js +7 -3
- package/dist/server/agent-bridge/pricing.d.ts +17 -0
- package/dist/server/agent-bridge/pricing.d.ts.map +1 -1
- package/dist/server/agent-bridge/pricing.js +24 -0
- package/dist/server/agent-bridge/tps-metrics.d.ts +38 -0
- package/dist/server/agent-bridge/tps-metrics.d.ts.map +1 -0
- package/dist/server/agent-bridge/tps-metrics.js +50 -0
- package/dist/server/agent-bridge.d.ts +6 -0
- package/dist/server/agent-bridge.d.ts.map +1 -1
- package/dist/server/agent-bridge.js +68 -20
- package/dist/server/error-humanizer.d.ts +11 -3
- package/dist/server/error-humanizer.d.ts.map +1 -1
- package/dist/server/error-humanizer.js +11 -3
- package/dist/server/session-stream/turn-events.d.ts +15 -0
- package/dist/server/session-stream/turn-events.d.ts.map +1 -1
- package/dist/server/session-stream/turn-events.js +31 -0
- package/dist/server/session-stream.d.ts +1 -1
- package/dist/server/session-stream.d.ts.map +1 -1
- package/dist/server/session-stream.js +39 -2
- package/dist/server/storage/rows.d.ts +4 -0
- package/dist/server/storage/rows.d.ts.map +1 -1
- package/dist/server/storage/schema.d.ts +1 -1
- package/dist/server/storage/schema.d.ts.map +1 -1
- package/dist/server/storage/schema.js +4 -0
- package/dist/server/storage/types.d.ts +8 -0
- package/dist/server/storage/types.d.ts.map +1 -1
- package/dist/server/storage.d.ts +15 -0
- package/dist/server/storage.d.ts.map +1 -1
- package/dist/server/storage.js +66 -18
- package/dist/server/wire.d.ts +39 -0
- package/dist/server/wire.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/agent/index.d.ts
CHANGED
|
@@ -50,6 +50,19 @@ export declare function buildSubagentSystemPrompt(baseSystemPrompt: string | und
|
|
|
50
50
|
*/
|
|
51
51
|
export declare function createSubagentSessionManagerSnapshot(sessionManager: ExtensionContext["sessionManager"]): Pick<ReadonlySessionManager, "getBranch" | "getSessionId">;
|
|
52
52
|
export declare function resolveSubagentTools(cwd: string, ext: ExtensionAPI, ctx: ExtensionContext): AgentTool[];
|
|
53
|
+
/** Test hook: replace the run-level retry sleep (e.g. with an instant/collecting stub). */
|
|
54
|
+
export declare function setSubagentRetrySleepForTesting(fn?: (ms: number, signal?: AbortSignal) => Promise<void>): void;
|
|
55
|
+
/**
|
|
56
|
+
* Resolve the max number of child-run attempts for the run-level retry loop.
|
|
57
|
+
* An explicit tool param (clamped to 0-10) overrides everything — including a
|
|
58
|
+
* globally disabled retry switch. Without the param, the session default
|
|
59
|
+
* (3 retries) applies unless retry is explicitly disabled, in which case a
|
|
60
|
+
* single attempt is made.
|
|
61
|
+
*/
|
|
62
|
+
export declare function resolveSubagentMaxAttempts(explicit: number | undefined, settings: {
|
|
63
|
+
enabled?: boolean;
|
|
64
|
+
maxRetries?: number;
|
|
65
|
+
} | undefined): number;
|
|
53
66
|
export default function subagentExtension(ext: ExtensionAPI, opts?: {
|
|
54
67
|
disabledAgents?: string[];
|
|
55
68
|
emitSubagentEvent?: (event: ServerEvent) => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/agent/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,KAAK,EAIX,SAAS,EAET,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/agent/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,KAAK,EAIX,SAAS,EAET,MAAM,4BAA4B,CAAC;AAqBpC,OAAO,EACN,KAAK,YAAY,EACjB,KAAK,gBAAgB,EAErB,MAAM,8BAA8B,CAAC;AAItC,OAAO,EACN,KAAK,WAAW,EAKhB,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAQrD,OAAO,KAAK,EAAE,sBAAsB,EAAgB,MAAM,6CAA6C,CAAC;AAuGxG;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CACxC,gBAAgB,EAAE,MAAM,GAAG,SAAS,EACpC,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,GAAG,cAAc,GAAG,OAAO,GAAG,QAAQ,CAAC,GACpF,MAAM,CAYR;AA4JD;;;GAGG;AACH,wBAAgB,oCAAoC,CACnD,cAAc,EAAE,gBAAgB,CAAC,gBAAgB,CAAC,GAChD,IAAI,CAAC,sBAAsB,EAAE,WAAW,GAAG,cAAc,CAAC,CAe5D;AA6BD,wBAAgB,oBAAoB,CACnC,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,YAAY,EACjB,GAAG,EAAE,gBAAgB,GACnB,SAAS,EAAE,CAwCb;AAqHD,2FAA2F;AAC3F,wBAAgB,+BAA+B,CAC9C,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,GACtD,IAAI,CAEN;AAmBD;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CACzC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,QAAQ,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,GAC9D,MAAM,CAiBR;AA2eD,MAAM,CAAC,OAAO,UAAU,iBAAiB,CACxC,GAAG,EAAE,YAAY,EACjB,IAAI,CAAC,EAAE;IACN,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB,GACC,IAAI,CA2aN"}
|
package/dist/agent/index.js
CHANGED
|
@@ -28,10 +28,11 @@
|
|
|
28
28
|
* (`share_project_observation`, `receive_agent_observations`,
|
|
29
29
|
* `compact_context`, `subagent`) remain excluded
|
|
30
30
|
*/
|
|
31
|
-
import { agentLoop } from "../sdk/agent-core/index.js";
|
|
31
|
+
import { agentLoop, agentLoopContinue, } from "../sdk/agent-core/index.js";
|
|
32
32
|
import { buildIsolatedRequestTimePrunedMessagesResult, COMPACT_CONTEXT_TOOL_NAME, } from "../memory/tools/compact-context.js";
|
|
33
|
-
import { StringEnum } from "../sdk/ai/index.js";
|
|
33
|
+
import { isRateLimitError, isTransientApiError, isContextOverflow, isPaymentRequiredError, StringEnum, streamSimple, } from "../sdk/ai/index.js";
|
|
34
34
|
import { mutateSystemPrompt } from "../sdk/coding-agent/core/system-prompt-mutator.js";
|
|
35
|
+
import { sleep } from "../sdk/coding-agent/utils/sleep.js";
|
|
35
36
|
import { Type } from "typebox";
|
|
36
37
|
import { discoverAgents, agentMetadataBlock, } from "./agents.js";
|
|
37
38
|
import { loadAgentSettings } from "../server/handlers/agent-settings.js";
|
|
@@ -334,15 +335,17 @@ async function mapWithConcurrencyLimit(items, concurrency, fn) {
|
|
|
334
335
|
return results;
|
|
335
336
|
}
|
|
336
337
|
/**
|
|
337
|
-
*
|
|
338
|
+
* Runs a child agent with an independent per-subagent timeout.
|
|
338
339
|
*
|
|
339
340
|
* Combines the parent's abort signal with a fresh timeout AbortController so
|
|
340
341
|
* that no single subagent can exhaust the parent prompt's full timeout budget.
|
|
342
|
+
* The budget covers the WHOLE child run including any resume retries (see
|
|
343
|
+
* `runSingleAgent`), so wall time stays bounded by SUBAGENT_TIMEOUT_MS.
|
|
341
344
|
* On timeout, returns a clear error result instead of throwing so the parent
|
|
342
345
|
* agent can continue (e.g. stop a chain, or surface the error as the tool
|
|
343
346
|
* result for parallel/single modes).
|
|
344
347
|
*/
|
|
345
|
-
async function runSingleAgentWithTimeout(defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails, parentToolCallId, onInternalEvent, ext, ctx) {
|
|
348
|
+
async function runSingleAgentWithTimeout(defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails, parentToolCallId, onInternalEvent, ext, ctx, maxRetries) {
|
|
346
349
|
const timeoutMs = SUBAGENT_TIMEOUT_MS;
|
|
347
350
|
const timeoutSeconds = timeoutMs / 1000;
|
|
348
351
|
const controller = new AbortController();
|
|
@@ -356,7 +359,7 @@ async function runSingleAgentWithTimeout(defaultCwd, agents, agentName, task, cw
|
|
|
356
359
|
signal.addEventListener("abort", onParentAbort, { once: true });
|
|
357
360
|
}
|
|
358
361
|
try {
|
|
359
|
-
const result = await runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, controller.signal, onUpdate, makeDetails, parentToolCallId, onInternalEvent, ext, ctx);
|
|
362
|
+
const result = await runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, controller.signal, onUpdate, makeDetails, parentToolCallId, onInternalEvent, ext, ctx, maxRetries);
|
|
360
363
|
// If the timeout fired (and parent signal did not), surface a clean
|
|
361
364
|
// timeout error result rather than a bare "aborted" result.
|
|
362
365
|
if (controller.signal.aborted &&
|
|
@@ -379,10 +382,61 @@ async function runSingleAgentWithTimeout(defaultCwd, agents, agentName, task, cw
|
|
|
379
382
|
clearTimeout(timeoutId);
|
|
380
383
|
}
|
|
381
384
|
}
|
|
385
|
+
/**
|
|
386
|
+
* Backoff sleep for the run-level retry loop; overridable in tests. Abort-aware
|
|
387
|
+
* (mirrors the main session's abortable retry sleep): it rejects as soon as the
|
|
388
|
+
* run signal aborts instead of stalling for the full delay.
|
|
389
|
+
*/
|
|
390
|
+
let retrySleep = (ms, signal) => sleep(ms, signal);
|
|
391
|
+
/** Test hook: replace the run-level retry sleep (e.g. with an instant/collecting stub). */
|
|
392
|
+
export function setSubagentRetrySleepForTesting(fn) {
|
|
393
|
+
retrySleep = fn ?? ((ms, signal) => sleep(ms, signal));
|
|
394
|
+
}
|
|
395
|
+
/** Upper bound for the tool-level `maxRetries` param (defense against bad values). */
|
|
396
|
+
const SUBAGENT_MAX_RETRIES_CAP = 10;
|
|
397
|
+
/**
|
|
398
|
+
* Minimal assistant-shaped probe used to ask `isContextOverflow` about a bare
|
|
399
|
+
* error string during retry classification.
|
|
400
|
+
*/
|
|
401
|
+
function buildOverflowProbe(errorMessage) {
|
|
402
|
+
return {
|
|
403
|
+
role: "assistant",
|
|
404
|
+
content: [],
|
|
405
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
406
|
+
stopReason: "error",
|
|
407
|
+
errorMessage,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Resolve the max number of child-run attempts for the run-level retry loop.
|
|
412
|
+
* An explicit tool param (clamped to 0-10) overrides everything — including a
|
|
413
|
+
* globally disabled retry switch. Without the param, the session default
|
|
414
|
+
* (3 retries) applies unless retry is explicitly disabled, in which case a
|
|
415
|
+
* single attempt is made.
|
|
416
|
+
*/
|
|
417
|
+
export function resolveSubagentMaxAttempts(explicit, settings) {
|
|
418
|
+
// Non-finite values (NaN/Infinity) from direct internal callers are treated
|
|
419
|
+
// as absent — otherwise `1 + NaN` would poison the attempts cap and loop.
|
|
420
|
+
const capped = explicit !== undefined && Number.isFinite(explicit)
|
|
421
|
+
? Math.max(0, Math.min(SUBAGENT_MAX_RETRIES_CAP, Math.floor(explicit)))
|
|
422
|
+
: undefined;
|
|
423
|
+
if (capped !== undefined)
|
|
424
|
+
return 1 + capped;
|
|
425
|
+
if (settings && settings.enabled === false)
|
|
426
|
+
return 1;
|
|
427
|
+
// Settings-derived values are hardened the same way as the explicit param:
|
|
428
|
+
// a non-finite maxRetries would otherwise poison the cap (`segments >= NaN`
|
|
429
|
+
// is never true → unbounded retries).
|
|
430
|
+
const settingsRetries = Number.isFinite(settings?.maxRetries)
|
|
431
|
+
? Math.min(SUBAGENT_MAX_RETRIES_CAP, settings.maxRetries)
|
|
432
|
+
: undefined;
|
|
433
|
+
const retries = settingsRetries ?? 3;
|
|
434
|
+
return 1 + retries;
|
|
435
|
+
}
|
|
382
436
|
// ---------------------------------------------------------------------------
|
|
383
437
|
// Core — run a single subagent in-process via agentLoop
|
|
384
438
|
// ---------------------------------------------------------------------------
|
|
385
|
-
async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails, parentToolCallId, onInternalEvent, ext, ctx) {
|
|
439
|
+
async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails, parentToolCallId, onInternalEvent, ext, ctx, maxRetries) {
|
|
386
440
|
const normalizedAgentName = agentName.replace(/^@/, "");
|
|
387
441
|
const agent = agents.find((a) => a.name === normalizedAgentName);
|
|
388
442
|
if (!agent) {
|
|
@@ -493,7 +547,29 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, si
|
|
|
493
547
|
});
|
|
494
548
|
}
|
|
495
549
|
};
|
|
550
|
+
// Run-level retry policy for the resume loop below (mirrors
|
|
551
|
+
// AgentSession._prepareRetry + its settings-driven backoff).
|
|
552
|
+
const retrySettings = ctx.settingsManager?.getRetrySettings?.();
|
|
553
|
+
const maxSegments = resolveSubagentMaxAttempts(maxRetries, retrySettings);
|
|
554
|
+
const baseDelayMs = Number.isFinite(retrySettings?.baseDelayMs)
|
|
555
|
+
? retrySettings?.baseDelayMs
|
|
556
|
+
: 2000;
|
|
496
557
|
try {
|
|
558
|
+
// Provider retry settings: subagents must inherit the same configurable
|
|
559
|
+
// retry policy as the main session (see sdk.ts streamFn wrapper) instead
|
|
560
|
+
// of silently falling back to provider SDK defaults (e.g. OpenAI's
|
|
561
|
+
// maxRetries=2 is too few for multi-minute outages). `ctx.settingsManager`
|
|
562
|
+
// is optional on ExtensionContext — when absent the wrapper below simply
|
|
563
|
+
// forwards options untouched, so provider/SDK defaults still apply.
|
|
564
|
+
// Optional call: symmetric with getRetrySettings?.() above — partial
|
|
565
|
+
// settingsManager stubs (extension contexts, tests) must not throw.
|
|
566
|
+
const providerRetrySettings = ctx.settingsManager?.getProviderRetrySettings?.();
|
|
567
|
+
const subagentStreamFn = (streamModel, streamContext, options) => streamSimple(streamModel, streamContext, {
|
|
568
|
+
...options,
|
|
569
|
+
timeoutMs: options?.timeoutMs ?? providerRetrySettings?.timeoutMs,
|
|
570
|
+
maxRetries: options?.maxRetries ?? providerRetrySettings?.maxRetries,
|
|
571
|
+
maxRetryDelayMs: options?.maxRetryDelayMs ?? providerRetrySettings?.maxRetryDelayMs,
|
|
572
|
+
});
|
|
497
573
|
// Invocation-local hook state; never reuse the parent extension context/runtime.
|
|
498
574
|
const config = {
|
|
499
575
|
model,
|
|
@@ -523,81 +599,166 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, si
|
|
|
523
599
|
}
|
|
524
600
|
},
|
|
525
601
|
};
|
|
526
|
-
const
|
|
602
|
+
const initialPrompt = [
|
|
603
|
+
{
|
|
604
|
+
role: "user",
|
|
605
|
+
content: [{ type: "text", text: task }],
|
|
606
|
+
timestamp: Date.now(),
|
|
607
|
+
},
|
|
608
|
+
];
|
|
609
|
+
const baseContext = {
|
|
527
610
|
systemPrompt: `${effectivePrompt}\n\n---\n\n# Task\n\n${task}`,
|
|
528
611
|
messages: [],
|
|
529
612
|
tools,
|
|
530
|
-
}
|
|
613
|
+
};
|
|
531
614
|
// Collect messages from the stream
|
|
532
615
|
const collectedMessages = [];
|
|
533
616
|
let hadError = false;
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
if (inner?.type === "text_delta" && inner.delta) {
|
|
545
|
-
streamingText += inner.delta;
|
|
546
|
-
emitUpdate();
|
|
617
|
+
let segments = 0;
|
|
618
|
+
// One child run may span several segments: the first one starts the
|
|
619
|
+
// loop, any later one resumes it. Tool results already recorded in
|
|
620
|
+
// `collectedMessages` are part of the resumed context, so tools are
|
|
621
|
+
// never re-executed by a retry.
|
|
622
|
+
const consumeStream = async (stream) => {
|
|
623
|
+
for await (const event of stream) {
|
|
624
|
+
if (signal?.aborted) {
|
|
625
|
+
hadError = true;
|
|
626
|
+
break;
|
|
547
627
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
628
|
+
onInternalEvent?.(event, parentToolCallId);
|
|
629
|
+
if (event.type === "message_update" && event.message?.role === "assistant") {
|
|
630
|
+
// Forward streaming text/thinking deltas so the UI can show the
|
|
631
|
+
// subagent process as it's generated, not just the final result.
|
|
632
|
+
const inner = event.assistantMessageEvent;
|
|
633
|
+
if (inner?.type === "text_delta" && inner.delta) {
|
|
634
|
+
streamingText += inner.delta;
|
|
635
|
+
emitUpdate();
|
|
552
636
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
collectedMessages.push(msg);
|
|
560
|
-
if (msg.role === "assistant") {
|
|
561
|
-
// Reset streaming accumulator — final text is now in messages
|
|
562
|
-
streamingText = "";
|
|
563
|
-
currentResult.usage.turns++;
|
|
564
|
-
const usage = msg.usage;
|
|
565
|
-
if (usage) {
|
|
566
|
-
currentResult.usage.input += usage.input || 0;
|
|
567
|
-
currentResult.usage.output += usage.output || 0;
|
|
568
|
-
currentResult.usage.cacheRead += usage.cacheRead || 0;
|
|
569
|
-
currentResult.usage.cacheWrite += usage.cacheWrite || 0;
|
|
570
|
-
currentResult.usage.cost += usage.cost?.total || 0;
|
|
571
|
-
currentResult.usage.contextTokens = usage.totalTokens || 0;
|
|
637
|
+
else if (inner?.type === "thinking_delta" && inner.delta) {
|
|
638
|
+
thinkingText += inner.delta;
|
|
639
|
+
if (inner.metadata) {
|
|
640
|
+
thinkingMetadata = { ...(thinkingMetadata ?? {}), ...inner.metadata };
|
|
641
|
+
}
|
|
642
|
+
emitUpdate();
|
|
572
643
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
if (event.type === "message_end" && event.message) {
|
|
647
|
+
const msg = event.message;
|
|
648
|
+
collectedMessages.push(msg);
|
|
649
|
+
if (msg.role === "assistant") {
|
|
650
|
+
// Reset streaming accumulator — final text is now in messages
|
|
651
|
+
streamingText = "";
|
|
652
|
+
currentResult.usage.turns++;
|
|
653
|
+
const usage = msg.usage;
|
|
654
|
+
if (usage) {
|
|
655
|
+
currentResult.usage.input += usage.input || 0;
|
|
656
|
+
currentResult.usage.output += usage.output || 0;
|
|
657
|
+
currentResult.usage.cacheRead += usage.cacheRead || 0;
|
|
658
|
+
currentResult.usage.cacheWrite += usage.cacheWrite || 0;
|
|
659
|
+
currentResult.usage.cost += usage.cost?.total || 0;
|
|
660
|
+
currentResult.usage.contextTokens = usage.totalTokens || 0;
|
|
661
|
+
}
|
|
662
|
+
if (!currentResult.model && msg.model)
|
|
663
|
+
currentResult.model = msg.model;
|
|
664
|
+
if (msg.stopReason)
|
|
665
|
+
currentResult.stopReason = msg.stopReason;
|
|
666
|
+
if (msg.errorMessage)
|
|
667
|
+
currentResult.errorMessage = msg.errorMessage;
|
|
668
|
+
if (msg.stopReason === "error" || msg.stopReason === "aborted") {
|
|
669
|
+
hadError = true;
|
|
670
|
+
}
|
|
581
671
|
}
|
|
672
|
+
currentResult.messages = [...collectedMessages];
|
|
673
|
+
emitUpdate();
|
|
674
|
+
}
|
|
675
|
+
if (event.type === "tool_execution_end" && event.isError) {
|
|
676
|
+
emitUpdate();
|
|
677
|
+
}
|
|
678
|
+
if (event.type === "agent_end" && event.error) {
|
|
679
|
+
hadError = true;
|
|
680
|
+
currentResult.errorMessage = event.error;
|
|
681
|
+
currentResult.stderr = event.error;
|
|
582
682
|
}
|
|
583
|
-
currentResult.messages = [...collectedMessages];
|
|
584
|
-
emitUpdate();
|
|
585
683
|
}
|
|
586
|
-
|
|
587
|
-
|
|
684
|
+
};
|
|
685
|
+
for (;;) {
|
|
686
|
+
segments++;
|
|
687
|
+
// Per-segment transient state: a resumed segment must be able to
|
|
688
|
+
// succeed without inheriting the previous segment's failure flags.
|
|
689
|
+
hadError = false;
|
|
690
|
+
streamingText = "";
|
|
691
|
+
// Drop thinking accumulated by the failed segment so the live sheet
|
|
692
|
+
// shows the resumed segment's reasoning, not stale text.
|
|
693
|
+
thinkingText = "";
|
|
694
|
+
thinkingMetadata = undefined;
|
|
695
|
+
currentResult.stopReason = undefined;
|
|
696
|
+
currentResult.errorMessage = undefined;
|
|
697
|
+
currentResult.stderr = "";
|
|
698
|
+
// Resume (segments > 1) continues the child with its existing
|
|
699
|
+
// transcript through `agentLoopContinue` — the same mechanism the
|
|
700
|
+
// main session uses (Agent.continue → runAgentLoopContinue) — so
|
|
701
|
+
// already-executed tools are NOT re-run. Only when there is nothing
|
|
702
|
+
// to resume from (empty transcript) do we fall back to a fresh
|
|
703
|
+
// `agentLoop` call, which then re-emits the initial prompt.
|
|
704
|
+
const canResume = collectedMessages.length > 0 &&
|
|
705
|
+
collectedMessages[collectedMessages.length - 1].role !== "assistant";
|
|
706
|
+
const stream = segments === 1 || !canResume
|
|
707
|
+
? agentLoop(initialPrompt, baseContext, config, signal, subagentStreamFn)
|
|
708
|
+
: agentLoopContinue({ ...baseContext, messages: [...collectedMessages] }, config, signal, subagentStreamFn);
|
|
709
|
+
await consumeStream(stream);
|
|
710
|
+
// Drain the stream result (already consumed by iteration, but ensure cleanup)
|
|
711
|
+
try {
|
|
712
|
+
await stream.result();
|
|
588
713
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
currentResult.errorMessage = event.error;
|
|
592
|
-
currentResult.stderr = event.error;
|
|
714
|
+
catch {
|
|
715
|
+
// Stream result error — already handled by message events
|
|
593
716
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
//
|
|
717
|
+
// ── Run-level retry decision (mirrors AgentSession._prepareRetry) ──
|
|
718
|
+
if (segments >= maxSegments)
|
|
719
|
+
break;
|
|
720
|
+
if (currentResult.stopReason !== "error")
|
|
721
|
+
break;
|
|
722
|
+
// Abort after a completed segment (parent stop or budget timeout):
|
|
723
|
+
// never retry, and never sleep into a phantom extra segment.
|
|
724
|
+
if (signal?.aborted)
|
|
725
|
+
break;
|
|
726
|
+
const errorMessage = currentResult.errorMessage || "";
|
|
727
|
+
// Payment required (402) is a billing problem — retries cannot help.
|
|
728
|
+
// Explicit for parity with AgentSession._isRetryableError.
|
|
729
|
+
if (isPaymentRequiredError(errorMessage))
|
|
730
|
+
break;
|
|
731
|
+
// Context overflow is a hard failure, not transient. It must be
|
|
732
|
+
// checked before the classifiers because overflow messages can
|
|
733
|
+
// contain bare status-code-like numerals (e.g. "501234 tokens >
|
|
734
|
+
// 500000 maximum").
|
|
735
|
+
if (isContextOverflow(buildOverflowProbe(errorMessage), model.contextWindow)) {
|
|
736
|
+
break;
|
|
737
|
+
}
|
|
738
|
+
if (!isRateLimitError(errorMessage) && !isTransientApiError(errorMessage)) {
|
|
739
|
+
break;
|
|
740
|
+
}
|
|
741
|
+
// Drop the failed assistant message(s) so the child resumes AT the
|
|
742
|
+
// failed turn instead of re-running it. Several trailing assistant
|
|
743
|
+
// messages are possible: the in-loop retry emits a failed assistant
|
|
744
|
+
// message before popping it from the loop context.
|
|
745
|
+
while (collectedMessages.length > 0 &&
|
|
746
|
+
collectedMessages[collectedMessages.length - 1].role === "assistant") {
|
|
747
|
+
collectedMessages.pop();
|
|
748
|
+
}
|
|
749
|
+
currentResult.messages = [...collectedMessages];
|
|
750
|
+
try {
|
|
751
|
+
await retrySleep(baseDelayMs * 2 ** (segments - 1), signal);
|
|
752
|
+
}
|
|
753
|
+
catch {
|
|
754
|
+
// Aborted during the backoff window (the sleep rejects on abort):
|
|
755
|
+
// fall through to the check below instead of throwing out of the
|
|
756
|
+
// child run.
|
|
757
|
+
}
|
|
758
|
+
// Abort during the backoff window: break so the attempt annotation
|
|
759
|
+
// counts only completed segments.
|
|
760
|
+
if (signal?.aborted)
|
|
761
|
+
break;
|
|
601
762
|
}
|
|
602
763
|
currentResult.exitCode = hadError ? 1 : 0;
|
|
603
764
|
if (signal?.aborted) {
|
|
@@ -605,6 +766,12 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, si
|
|
|
605
766
|
currentResult.stopReason = "aborted";
|
|
606
767
|
currentResult.errorMessage = "Subagent was aborted";
|
|
607
768
|
}
|
|
769
|
+
if (segments > 1) {
|
|
770
|
+
currentResult.attempts = segments;
|
|
771
|
+
if (currentResult.exitCode !== 0 && currentResult.errorMessage) {
|
|
772
|
+
currentResult.errorMessage = `${currentResult.errorMessage} (failed after ${segments} attempts)`;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
608
775
|
return currentResult;
|
|
609
776
|
}
|
|
610
777
|
catch (err) {
|
|
@@ -652,6 +819,11 @@ const SubagentParams = Type.Object({
|
|
|
652
819
|
cwd: Type.Optional(Type.String({
|
|
653
820
|
description: "Working directory for the agent process (single mode)",
|
|
654
821
|
})),
|
|
822
|
+
maxRetries: Type.Optional(Type.Integer({
|
|
823
|
+
description: "Max automatic resume retries after a transient provider failure: the child resumes at the failed turn instead of restarting, so its tools are not re-executed (overrides the configured retry count and a disabled retry switch; 0-10).",
|
|
824
|
+
minimum: 0,
|
|
825
|
+
maximum: 10,
|
|
826
|
+
})),
|
|
655
827
|
});
|
|
656
828
|
// ---------------------------------------------------------------------------
|
|
657
829
|
// Extension entry point
|
|
@@ -834,7 +1006,7 @@ export default function subagentExtension(ext, opts) {
|
|
|
834
1006
|
}
|
|
835
1007
|
}
|
|
836
1008
|
: undefined;
|
|
837
|
-
const result = await runSingleAgentWithTimeout(ctx.cwd, agents, step.agent, taskWithContext, step.cwd, i + 1, signal, chainUpdate, makeDetails("chain"), _toolCallId, emitSubagentToolProgress, ext, ctx);
|
|
1009
|
+
const result = await runSingleAgentWithTimeout(ctx.cwd, agents, step.agent, taskWithContext, step.cwd, i + 1, signal, chainUpdate, makeDetails("chain"), _toolCallId, emitSubagentToolProgress, ext, ctx, params.maxRetries);
|
|
838
1010
|
results.push(result);
|
|
839
1011
|
const isError = result.exitCode !== 0 ||
|
|
840
1012
|
result.stopReason === "error" ||
|
|
@@ -861,7 +1033,9 @@ export default function subagentExtension(ext, opts) {
|
|
|
861
1033
|
const output = truncateOutput(getFinalOutput(r.messages) || "(no output)", SUBAGENT_OUTPUT_LIMIT);
|
|
862
1034
|
const status = r.exitCode === 0
|
|
863
1035
|
? "completed"
|
|
864
|
-
:
|
|
1036
|
+
: r.attempts && r.attempts > 1
|
|
1037
|
+
? `failed (after ${r.attempts} attempts)`
|
|
1038
|
+
: "failed";
|
|
865
1039
|
return `[step ${idx + 1} - ${r.agent}] ${status}:\n${output}`;
|
|
866
1040
|
});
|
|
867
1041
|
return {
|
|
@@ -919,7 +1093,7 @@ export default function subagentExtension(ext, opts) {
|
|
|
919
1093
|
allResults[index] = partial.details.results[0];
|
|
920
1094
|
emitParallelUpdate();
|
|
921
1095
|
}
|
|
922
|
-
}, makeDetails("parallel"), _toolCallId, emitSubagentToolProgress, ext, ctx);
|
|
1096
|
+
}, makeDetails("parallel"), _toolCallId, emitSubagentToolProgress, ext, ctx, params.maxRetries);
|
|
923
1097
|
allResults[index] = result;
|
|
924
1098
|
emitParallelUpdate();
|
|
925
1099
|
return result;
|
|
@@ -927,7 +1101,12 @@ export default function subagentExtension(ext, opts) {
|
|
|
927
1101
|
const successCount = results.filter((r) => r.exitCode === 0).length;
|
|
928
1102
|
const summaries = results.map((r) => {
|
|
929
1103
|
const output = truncateOutput(getFinalOutput(r.messages), SUBAGENT_OUTPUT_LIMIT);
|
|
930
|
-
|
|
1104
|
+
const status = r.exitCode === 0
|
|
1105
|
+
? "completed"
|
|
1106
|
+
: r.attempts && r.attempts > 1
|
|
1107
|
+
? `failed (after ${r.attempts} attempts)`
|
|
1108
|
+
: "failed";
|
|
1109
|
+
return `[${r.agent}] ${status}:\n${output || "(no output)"}`;
|
|
931
1110
|
});
|
|
932
1111
|
return {
|
|
933
1112
|
content: [
|
|
@@ -941,7 +1120,7 @@ export default function subagentExtension(ext, opts) {
|
|
|
941
1120
|
}
|
|
942
1121
|
// ── Single mode ─────────────────────────────────────────────
|
|
943
1122
|
if (params.agent && params.task) {
|
|
944
|
-
const result = await runSingleAgentWithTimeout(ctx.cwd, agents, params.agent, params.task, params.cwd, undefined, signal, onUpdate, makeDetails("single"), _toolCallId, emitSubagentToolProgress, ext, ctx);
|
|
1123
|
+
const result = await runSingleAgentWithTimeout(ctx.cwd, agents, params.agent, params.task, params.cwd, undefined, signal, onUpdate, makeDetails("single"), _toolCallId, emitSubagentToolProgress, ext, ctx, params.maxRetries);
|
|
945
1124
|
const isError = result.exitCode !== 0 ||
|
|
946
1125
|
result.stopReason === "error" ||
|
|
947
1126
|
result.stopReason === "aborted";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-loop.d.ts","sourceRoot":"","sources":["../../../src/sdk/agent-core/agent-loop.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAGL,WAAW,
|
|
1
|
+
{"version":3,"file":"agent-loop.d.ts","sourceRoot":"","sources":["../../../src/sdk/agent-core/agent-loop.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAGL,WAAW,EAWZ,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EACV,YAAY,EACZ,UAAU,EACV,eAAe,EACf,YAAY,EACZ,SAAS,EACT,aAAa,EAEb,QAAQ,EACT,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,cAAc,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAiCzE;;;GAGG;AACH,wBAAgB,SAAS,CACvB,OAAO,EAAE,YAAY,EAAE,EACvB,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,MAAM,CAAC,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,QAAQ,GAClB,WAAW,CAAC,UAAU,EAAE,YAAY,EAAE,CAAC,CA2BzC;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,MAAM,CAAC,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,QAAQ,GAClB,WAAW,CAAC,UAAU,EAAE,YAAY,EAAE,CAAC,CAkCzC;AAED,wBAAsB,YAAY,CAChC,OAAO,EAAE,YAAY,EAAE,EACvB,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE,cAAc,EACpB,MAAM,CAAC,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,QAAQ,GAClB,OAAO,CAAC,YAAY,EAAE,CAAC,CAgBzB;AAED,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE,cAAc,EACpB,MAAM,CAAC,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,QAAQ,GAClB,OAAO,CAAC,YAAY,EAAE,CAAC,CAiBzB;AAsxBD;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,aAAa,GAAG,OAAO,CAYlG"}
|
|
@@ -2,15 +2,13 @@
|
|
|
2
2
|
* Agent loop that works with AgentMessage throughout.
|
|
3
3
|
* Transforms to Message[] only at the LLM call boundary.
|
|
4
4
|
*/
|
|
5
|
-
import { EventStream, formatPaymentRequiredMessage, formatRateLimitRetryMessage, getRateLimitRetryDelayMs, isPaymentRequiredError, MAX_RATE_LIMIT_RETRIES, streamSimple, isRateLimitError, validateToolArguments, } from "../ai/index.js";
|
|
5
|
+
import { EventStream, formatPaymentRequiredMessage, formatRateLimitRetryMessage, getRateLimitRetryDelayMs, isPaymentRequiredError, MAX_RATE_LIMIT_RETRIES, streamSimple, isRateLimitError, isTransientApiError, validateToolArguments, } from "../ai/index.js";
|
|
6
6
|
/** Maximum consecutive tool call errors before terminating the loop. */
|
|
7
7
|
const MAX_CONSECUTIVE_TOOL_ERRORS = 10;
|
|
8
8
|
/** Maximum retries for transient provider API errors. */
|
|
9
9
|
const MAX_API_RETRIES = 3;
|
|
10
10
|
/** Base backoff (ms) for transient API error retries; doubles per attempt. */
|
|
11
11
|
const API_RETRY_BASE_MS = 1000;
|
|
12
|
-
/** Shared transient-error classifier (excluding rate limits, which get their own schedule). */
|
|
13
|
-
const TRANSIENT_API_ERROR_RE = /overloaded|529|timeout|network|econnreset|fetch failed|socket connection was closed|connection.?closed|connection.?reset|socket hang up|other side closed|reset before headers|upstream.?connect|http2 request did not get a response/i;
|
|
14
12
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
15
13
|
/** Interval between keepalive ticks during long retry waits. */
|
|
16
14
|
const RETRY_WAIT_TICK_MS = 30_000;
|
|
@@ -211,7 +209,7 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
|
|
|
211
209
|
const isTransientError = !signal?.aborted &&
|
|
212
210
|
!isPaymentRequiredError(errorMsg) &&
|
|
213
211
|
!isRateLimitError(errorMsg) &&
|
|
214
|
-
|
|
212
|
+
isTransientApiError(errorMsg);
|
|
215
213
|
if (isTransientError && apiRetryCount < MAX_API_RETRIES) {
|
|
216
214
|
apiRetryCount++;
|
|
217
215
|
const backoffMs = API_RETRY_BASE_MS * Math.pow(2, apiRetryCount - 1); // 1000, 2000, 4000
|
package/dist/sdk/ai/index.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export * from "./utils/json-parse.js";
|
|
|
17
17
|
export type { OAuthAuthInfo, OAuthCredentials, OAuthDeviceCodeInfo, OAuthLoginCallbacks, OAuthPrompt, OAuthProvider, OAuthProviderId, OAuthProviderInfo, OAuthProviderInterface, OAuthSelectOption, OAuthSelectPrompt, } from "./utils/oauth/types.js";
|
|
18
18
|
export * from "./utils/overflow.js";
|
|
19
19
|
export * from "./utils/rate-limit-retry.js";
|
|
20
|
+
export * from "./utils/transient-error.js";
|
|
20
21
|
export * from "./utils/typebox-helpers.js";
|
|
21
22
|
export * from "./utils/validation.js";
|
|
22
23
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/sdk/ai/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAE/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC;AAC5B,cAAc,0BAA0B,CAAC;AACzC,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC;AACpC,cAAc,kCAAkC,CAAC;AACjD,cAAc,wBAAwB,CAAC;AACvC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC;AACtC,YAAY,EACX,aAAa,EACb,gBAAgB,EAChB,mBAAmB,EACnB,mBAAmB,EACnB,WAAW,EACX,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,sBAAsB,EACtB,iBAAiB,EACjB,iBAAiB,GACjB,MAAM,wBAAwB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uBAAuB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/sdk/ai/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAE/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC;AAC5B,cAAc,0BAA0B,CAAC;AACzC,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC;AACpC,cAAc,kCAAkC,CAAC;AACjD,cAAc,wBAAwB,CAAC;AACvC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC;AACtC,YAAY,EACX,aAAa,EACb,gBAAgB,EAChB,mBAAmB,EACnB,mBAAmB,EACnB,WAAW,EACX,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,sBAAsB,EACtB,iBAAiB,EACjB,iBAAiB,GACjB,MAAM,wBAAwB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uBAAuB,CAAC"}
|
package/dist/sdk/ai/index.js
CHANGED
|
@@ -15,5 +15,6 @@ export * from "./utils/event-stream.js";
|
|
|
15
15
|
export * from "./utils/json-parse.js";
|
|
16
16
|
export * from "./utils/overflow.js";
|
|
17
17
|
export * from "./utils/rate-limit-retry.js";
|
|
18
|
+
export * from "./utils/transient-error.js";
|
|
18
19
|
export * from "./utils/typebox-helpers.js";
|
|
19
20
|
export * from "./utils/validation.js";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transient provider-error classification shared by the agent-core agent
|
|
3
|
+
* loop and the coding-agent auto-retry.
|
|
4
|
+
*
|
|
5
|
+
* Companion module to rate-limit-retry.ts, which is re-exported here so
|
|
6
|
+
* retry call sites can classify an error from a single import. Rate limits
|
|
7
|
+
* (HTTP 429) always take priority over this classifier — they follow their
|
|
8
|
+
* own long 25-attempt backoff schedule — while every other transient
|
|
9
|
+
* signal (overload, HTTP 5xx, "temporarily unavailable", network /
|
|
10
|
+
* connection / stream failures) retries with a short backoff.
|
|
11
|
+
*
|
|
12
|
+
* Payment required (HTTP 402) and context-overflow errors are also not
|
|
13
|
+
* covered here; callers exclude them before consulting this classifier
|
|
14
|
+
* because retrying cannot help in those cases.
|
|
15
|
+
*
|
|
16
|
+
* @module @spectral/ai/utils/transient-error
|
|
17
|
+
*/
|
|
18
|
+
export { isRateLimitError } from "./rate-limit-retry.js";
|
|
19
|
+
/**
|
|
20
|
+
* Check whether an error message (which may embed a raw provider JSON blob)
|
|
21
|
+
* describes a transient API failure worth retrying — overload, HTTP 5xx,
|
|
22
|
+
* "temporarily unavailable", or a network/connection/stream failure.
|
|
23
|
+
*
|
|
24
|
+
* Rate limits (429) are intentionally NOT matched here; call sites must
|
|
25
|
+
* check `isRateLimitError` first so they keep their own long schedule.
|
|
26
|
+
*/
|
|
27
|
+
export declare function isTransientApiError(message: string): boolean;
|
|
28
|
+
//# sourceMappingURL=transient-error.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transient-error.d.ts","sourceRoot":"","sources":["../../../../src/sdk/ai/utils/transient-error.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AA8BzD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAE5D"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transient provider-error classification shared by the agent-core agent
|
|
3
|
+
* loop and the coding-agent auto-retry.
|
|
4
|
+
*
|
|
5
|
+
* Companion module to rate-limit-retry.ts, which is re-exported here so
|
|
6
|
+
* retry call sites can classify an error from a single import. Rate limits
|
|
7
|
+
* (HTTP 429) always take priority over this classifier — they follow their
|
|
8
|
+
* own long 25-attempt backoff schedule — while every other transient
|
|
9
|
+
* signal (overload, HTTP 5xx, "temporarily unavailable", network /
|
|
10
|
+
* connection / stream failures) retries with a short backoff.
|
|
11
|
+
*
|
|
12
|
+
* Payment required (HTTP 402) and context-overflow errors are also not
|
|
13
|
+
* covered here; callers exclude them before consulting this classifier
|
|
14
|
+
* because retrying cannot help in those cases.
|
|
15
|
+
*
|
|
16
|
+
* @module @spectral/ai/utils/transient-error
|
|
17
|
+
*/
|
|
18
|
+
export { isRateLimitError } from "./rate-limit-retry.js";
|
|
19
|
+
/**
|
|
20
|
+
* One classifier for every transient provider-API failure signal:
|
|
21
|
+
*
|
|
22
|
+
* - overload: `overloaded`, `529` as a standalone token (e.g. Anthropic
|
|
23
|
+
* `overloaded_error`); word-bounded so token counts like `529000` never
|
|
24
|
+
* match
|
|
25
|
+
* - server errors: `500`, `502`, `503`, `504`, `unavailable`,
|
|
26
|
+
* `internal error`, `server error`
|
|
27
|
+
* - temporary provider hiccups: `temporarily` (e.g. "model temporarily
|
|
28
|
+
* unavailable")
|
|
29
|
+
* - timeouts and network/connection failures: `timeout`, `timed?.?out`,
|
|
30
|
+
* `network`, `econnreset`, `fetch failed`, `connection.*closed`,
|
|
31
|
+
* `connection.*reset`, `connection.*error`, `connection.*refused`,
|
|
32
|
+
* `connection.*lost`, `socket hang up`, `websocket.*closed`,
|
|
33
|
+
* `websocket.*error`, `other side closed`, `socket connection was
|
|
34
|
+
* closed`, `reset before headers`, `upstream.*connect`, `http2 request
|
|
35
|
+
* did not get a response`
|
|
36
|
+
* - provider wrapper errors and premature stream endings:
|
|
37
|
+
* `provider.*returned.*error`, `ended without`,
|
|
38
|
+
* `stream ended before message_stop`, `terminated`, `retry delay`
|
|
39
|
+
*
|
|
40
|
+
* Consumers must check `isRateLimitError` first: rate limits have their own
|
|
41
|
+
* retry schedule, and this classifier deliberately does not match 429 /
|
|
42
|
+
* "rate limit" / "too many requests".
|
|
43
|
+
*/
|
|
44
|
+
const TRANSIENT_API_ERROR_RE = /overloaded|\b529\b|timeout|timed?.?out|network|econnreset|fetch failed|socket connection was closed|connection.?closed|connection.?reset|connection.?error|connection.?refused|connection.?lost|socket hang up|websocket.?closed|websocket.?error|other side closed|reset before headers|upstream.?connect|http2 request did not get a response|provider.?returned.?error|ended without|stream ended before message_stop|terminated|retry delay|\b5(?:00|02|03|04)\b|unavailable|temporarily|internal.?error|server.?error/i;
|
|
45
|
+
/**
|
|
46
|
+
* Check whether an error message (which may embed a raw provider JSON blob)
|
|
47
|
+
* describes a transient API failure worth retrying — overload, HTTP 5xx,
|
|
48
|
+
* "temporarily unavailable", or a network/connection/stream failure.
|
|
49
|
+
*
|
|
50
|
+
* Rate limits (429) are intentionally NOT matched here; call sites must
|
|
51
|
+
* check `isRateLimitError` first so they keep their own long schedule.
|
|
52
|
+
*/
|
|
53
|
+
export function isTransientApiError(message) {
|
|
54
|
+
return TRANSIENT_API_ERROR_RE.test(message);
|
|
55
|
+
}
|