mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(ai): Add jitter to agent retries
Spread concurrent retries, including at the delay ceiling, without changing retry limits. Avoids thundering herd issues in workflows with many subagents.
This commit is contained in:
parent
55a5c69145
commit
2e58814842
2 changed files with 27 additions and 3 deletions
|
|
@ -125,7 +125,7 @@ const RETRYABLE_PROVIDER_ERROR_PATTERN = buildProviderErrorPattern([
|
|||
]);
|
||||
|
||||
/**
|
||||
* Retry policy: bounded attempts with exponential backoff (`baseDelayMs * 2^(attempt-1)`).
|
||||
* Retry policy: bounded attempts with exponential backoff and up to 25% downward jitter.
|
||||
* `maxAgentDelayMs` caps each computed delay and defaults to 60 seconds.
|
||||
* Matches `settings.retry` (`enabled`, `maxRetries`, `baseDelayMs`, `maxAgentDelayMs`) in coding-agent; kept
|
||||
* here so the classifier and the policy-driven retry loop live together and stay reusable
|
||||
|
|
@ -146,7 +146,8 @@ export const DEFAULT_MAX_AGENT_RETRY_DELAY_MS = 60_000;
|
|||
export function retryDelayMs(policy: Pick<RetryPolicy, "baseDelayMs" | "maxAgentDelayMs">, attempt: number): number {
|
||||
const delay = policy.baseDelayMs * 2 ** Math.max(0, attempt - 1);
|
||||
const safeDelay = Number.isSafeInteger(delay) ? delay : Number.MAX_SAFE_INTEGER;
|
||||
return Math.min(safeDelay, policy.maxAgentDelayMs ?? DEFAULT_MAX_AGENT_RETRY_DELAY_MS);
|
||||
const cappedDelay = Math.min(safeDelay, policy.maxAgentDelayMs ?? DEFAULT_MAX_AGENT_RETRY_DELAY_MS);
|
||||
return cappedDelay * (1 - Math.random() * 0.25);
|
||||
}
|
||||
|
||||
/** Optional callbacks emitted by {@link retryAssistantCall} around each retry. */
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { fauxAssistantMessage } from "../src/providers/faux.ts";
|
||||
import { isRetryableAssistantError, type RetryPolicy, retryAssistantCall, retryDelayMs } from "../src/utils/retry.ts";
|
||||
|
||||
|
|
@ -166,8 +166,30 @@ describe("provider retry classification", () => {
|
|||
});
|
||||
});
|
||||
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
describe("retryDelayMs", () => {
|
||||
it.each([
|
||||
[0, 2000],
|
||||
[0.5, 1750],
|
||||
[0.999, 1500.5],
|
||||
])("jitters exponential backoff with random value %s", (random, expected) => {
|
||||
vi.spyOn(Math, "random").mockReturnValue(random);
|
||||
|
||||
expect(retryDelayMs({ baseDelayMs: 2000 }, 1)).toBeCloseTo(expected);
|
||||
expect(retryDelayMs({ baseDelayMs: 2000 }, 2)).toBeCloseTo(expected * 2);
|
||||
});
|
||||
|
||||
it("jitters capped delays so retries stay spread at the ceiling", () => {
|
||||
vi.spyOn(Math, "random").mockReturnValue(0.5);
|
||||
|
||||
expect(retryDelayMs({ baseDelayMs: 2000 }, 6)).toBe(52500);
|
||||
expect(retryDelayMs({ baseDelayMs: 2000, maxAgentDelayMs: 5000 }, 5)).toBe(4375);
|
||||
expect(retryDelayMs({ baseDelayMs: 2000, maxAgentDelayMs: 0 }, 5)).toBe(0);
|
||||
});
|
||||
|
||||
it("caps agent retry delay", () => {
|
||||
vi.spyOn(Math, "random").mockReturnValue(0);
|
||||
// Regression for #8826.
|
||||
expect(retryDelayMs({ baseDelayMs: 2000 }, 6)).toBe(60000);
|
||||
expect(retryDelayMs({ baseDelayMs: 2000, maxAgentDelayMs: 5000 }, 5)).toBe(5000);
|
||||
|
|
@ -221,6 +243,7 @@ describe("retryAssistantCall", () => {
|
|||
|
||||
it("reports capped retry delays", async () => {
|
||||
// Regression for #8826.
|
||||
vi.spyOn(Math, "random").mockReturnValue(0);
|
||||
let n = 0;
|
||||
const policy: RetryPolicy = { enabled: true, maxRetries: 4, baseDelayMs: 10, maxAgentDelayMs: 15 };
|
||||
const produce = vi.fn(async () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue