@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.
Files changed (44) hide show
  1. package/dist/agent/index.d.ts +13 -0
  2. package/dist/agent/index.d.ts.map +1 -1
  3. package/dist/agent/index.js +251 -72
  4. package/dist/sdk/agent-core/agent-loop.d.ts.map +1 -1
  5. package/dist/sdk/agent-core/agent-loop.js +2 -4
  6. package/dist/sdk/ai/index.d.ts +1 -0
  7. package/dist/sdk/ai/index.d.ts.map +1 -1
  8. package/dist/sdk/ai/index.js +1 -0
  9. package/dist/sdk/ai/utils/transient-error.d.ts +28 -0
  10. package/dist/sdk/ai/utils/transient-error.d.ts.map +1 -0
  11. package/dist/sdk/ai/utils/transient-error.js +55 -0
  12. package/dist/sdk/coding-agent/core/agent-session.d.ts.map +1 -1
  13. package/dist/sdk/coding-agent/core/agent-session.js +7 -3
  14. package/dist/server/agent-bridge/pricing.d.ts +17 -0
  15. package/dist/server/agent-bridge/pricing.d.ts.map +1 -1
  16. package/dist/server/agent-bridge/pricing.js +24 -0
  17. package/dist/server/agent-bridge/tps-metrics.d.ts +38 -0
  18. package/dist/server/agent-bridge/tps-metrics.d.ts.map +1 -0
  19. package/dist/server/agent-bridge/tps-metrics.js +50 -0
  20. package/dist/server/agent-bridge.d.ts +6 -0
  21. package/dist/server/agent-bridge.d.ts.map +1 -1
  22. package/dist/server/agent-bridge.js +68 -20
  23. package/dist/server/error-humanizer.d.ts +11 -3
  24. package/dist/server/error-humanizer.d.ts.map +1 -1
  25. package/dist/server/error-humanizer.js +11 -3
  26. package/dist/server/session-stream/turn-events.d.ts +15 -0
  27. package/dist/server/session-stream/turn-events.d.ts.map +1 -1
  28. package/dist/server/session-stream/turn-events.js +31 -0
  29. package/dist/server/session-stream.d.ts +1 -1
  30. package/dist/server/session-stream.d.ts.map +1 -1
  31. package/dist/server/session-stream.js +39 -2
  32. package/dist/server/storage/rows.d.ts +4 -0
  33. package/dist/server/storage/rows.d.ts.map +1 -1
  34. package/dist/server/storage/schema.d.ts +1 -1
  35. package/dist/server/storage/schema.d.ts.map +1 -1
  36. package/dist/server/storage/schema.js +4 -0
  37. package/dist/server/storage/types.d.ts +8 -0
  38. package/dist/server/storage/types.d.ts.map +1 -1
  39. package/dist/server/storage.d.ts +15 -0
  40. package/dist/server/storage.d.ts.map +1 -1
  41. package/dist/server/storage.js +66 -18
  42. package/dist/server/wire.d.ts +39 -0
  43. package/dist/server/wire.d.ts.map +1 -1
  44. package/package.json +1 -1
@@ -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;AAUpC,OAAO,EACN,KAAK,YAAY,EACjB,KAAK,gBAAgB,EAErB,MAAM,8BAA8B,CAAC;AAGtC,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;AA0JD;;;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;AAycD,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,CAgaN"}
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"}
@@ -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
- * Wraps `runSingleAgent` with an independent per-subagent timeout.
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 stream = agentLoop([{ role: "user", content: [{ type: "text", text: task }], timestamp: Date.now() }], {
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
- }, config, signal);
613
+ };
531
614
  // Collect messages from the stream
532
615
  const collectedMessages = [];
533
616
  let hadError = false;
534
- for await (const event of stream) {
535
- if (signal?.aborted) {
536
- hadError = true;
537
- break;
538
- }
539
- onInternalEvent?.(event, parentToolCallId);
540
- if (event.type === "message_update" && event.message?.role === "assistant") {
541
- // Forward streaming text/thinking deltas so the UI can show the
542
- // subagent process as it's generated, not just the final result.
543
- const inner = event.assistantMessageEvent;
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
- else if (inner?.type === "thinking_delta" && inner.delta) {
549
- thinkingText += inner.delta;
550
- if (inner.metadata) {
551
- thinkingMetadata = { ...(thinkingMetadata ?? {}), ...inner.metadata };
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
- emitUpdate();
554
- }
555
- continue;
556
- }
557
- if (event.type === "message_end" && event.message) {
558
- const msg = event.message;
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
- if (!currentResult.model && msg.model)
574
- currentResult.model = msg.model;
575
- if (msg.stopReason)
576
- currentResult.stopReason = msg.stopReason;
577
- if (msg.errorMessage)
578
- currentResult.errorMessage = msg.errorMessage;
579
- if (msg.stopReason === "error" || msg.stopReason === "aborted") {
580
- hadError = true;
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
- if (event.type === "tool_execution_end" && event.isError) {
587
- emitUpdate();
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
- if (event.type === "agent_end" && event.error) {
590
- hadError = true;
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
- // Drain the stream result (already consumed by iteration, but ensure cleanup)
596
- try {
597
- await stream.result();
598
- }
599
- catch {
600
- // Stream result error already handled by message events
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
- : "failed";
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
- return `[${r.agent}] ${r.exitCode === 0 ? "completed" : "failed"}:\n${output || "(no output)"}`;
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,EAUZ,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;AAoCzE;;;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"}
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
- TRANSIENT_API_ERROR_RE.test(errorMsg);
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
@@ -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"}
@@ -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
+ }