report aborted retry attempts as unsuccessful

This commit is contained in:
David Brailovsky
2026-07-21 15:34:31 +00:00
parent ceac988635
commit 243f64be59
2 changed files with 27 additions and 5 deletions
+12 -5
View File
@@ -110,7 +110,7 @@ export interface RetryCallbacks {
) => void | Promise<void>; ) => void | Promise<void>;
/** Emitted after the backoff sleep, immediately before the retried call starts. */ /** Emitted after the backoff sleep, immediately before the retried call starts. */
onRetryAttemptStart?: () => void | Promise<void>; onRetryAttemptStart?: () => void | Promise<void>;
/** Emitted once when the loop ends: success if a later call returned a non-error message. */ /** Emitted once when the loop ends: success if a later call completed normally. */
onRetryFinished?: (success: boolean, attempt: number, finalError?: string) => void | Promise<void>; onRetryFinished?: (success: boolean, attempt: number, finalError?: string) => void | Promise<void>;
} }
@@ -142,9 +142,10 @@ function sleep(ms: number, signal?: AbortSignal): Promise<void> {
* Run a single assistant-producing call with bounded retry on transient errors. * Run a single assistant-producing call with bounded retry on transient errors.
* *
* Behavior: * Behavior:
* - A non-error response (success or aborted) is returned immediately; aborts are * - A successful response is returned immediately. Aborts are terminal and never
* never retried. Aborts during the backoff sleep are normalized to an aborted * retried, but reported as unsuccessful if they happen after a retry was scheduled.
* `AssistantMessage` too, so callers do not need to care when cancellation happened. * Aborts during the backoff sleep are normalized to an aborted `AssistantMessage`
* too, so callers do not need to care when cancellation happened.
* - A non-retryable error (per {@link isRetryableAssistantError}, including quota/ * - A non-retryable error (per {@link isRetryableAssistantError}, including quota/
* billing exhaustion) is returned immediately so deterministic errors fail fast. * billing exhaustion) is returned immediately so deterministic errors fail fast.
* - Otherwise retries up to `maxRetries` times with exponential backoff, emitting * - Otherwise retries up to `maxRetries` times with exponential backoff, emitting
@@ -168,7 +169,13 @@ export async function retryAssistantCall(
for (;;) { for (;;) {
const response = await produce(); const response = await produce();
// Success or abort: never retry an aborted message; non-error returns as-is. // Abort: terminal but not successful. Never retry an aborted message.
if (response.stopReason === "aborted") {
if (lastRetry) await callbacks?.onRetryFinished?.(false, lastRetry.attempt);
return response;
}
// Success: non-error, non-abort responses return as-is.
if (response.stopReason !== "error") { if (response.stopReason !== "error") {
if (lastRetry) await callbacks?.onRetryFinished?.(true, lastRetry.attempt); if (lastRetry) await callbacks?.onRetryFinished?.(true, lastRetry.attempt);
return response; return response;
+15
View File
@@ -126,6 +126,21 @@ describe("retryAssistantCall", () => {
expect(onRetryFinished).toHaveBeenCalledWith(true, 2); expect(onRetryFinished).toHaveBeenCalledWith(true, 2);
}); });
it("reports an aborted retried call as unsuccessful", async () => {
let n = 0;
const produce = vi.fn(async () => {
n++;
return n === 1
? fauxAssistantMessage("", { stopReason: "error", errorMessage: "terminated" })
: fauxAssistantMessage("", { stopReason: "aborted" });
});
const onRetryFinished = vi.fn();
const res = await retryAssistantCall(produce, enabled, undefined, { onRetryFinished });
expect(res.stopReason).toBe("aborted");
expect(produce).toHaveBeenCalledTimes(2);
expect(onRetryFinished).toHaveBeenCalledWith(false, 1);
});
it("does not retry when policy is disabled", async () => { it("does not retry when policy is disabled", async () => {
const produce = vi.fn(async () => fauxAssistantMessage("", { stopReason: "error", errorMessage: "terminated" })); const produce = vi.fn(async () => fauxAssistantMessage("", { stopReason: "error", errorMessage: "terminated" }));
const onRetryScheduled = vi.fn(); const onRetryScheduled = vi.fn();