report aborted retry attempts as unsuccessful
This commit is contained in:
@@ -110,7 +110,7 @@ export interface RetryCallbacks {
|
||||
) => void | Promise<void>;
|
||||
/** Emitted after the backoff sleep, immediately before the retried call starts. */
|
||||
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>;
|
||||
}
|
||||
|
||||
@@ -142,9 +142,10 @@ function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
||||
* Run a single assistant-producing call with bounded retry on transient errors.
|
||||
*
|
||||
* Behavior:
|
||||
* - A non-error response (success or aborted) is returned immediately; aborts are
|
||||
* never retried. Aborts during the backoff sleep are normalized to an aborted
|
||||
* `AssistantMessage` too, so callers do not need to care when cancellation happened.
|
||||
* - A successful response is returned immediately. Aborts are terminal and never
|
||||
* retried, but reported as unsuccessful if they happen after a retry was scheduled.
|
||||
* 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/
|
||||
* billing exhaustion) is returned immediately so deterministic errors fail fast.
|
||||
* - Otherwise retries up to `maxRetries` times with exponential backoff, emitting
|
||||
@@ -168,7 +169,13 @@ export async function retryAssistantCall(
|
||||
for (;;) {
|
||||
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 (lastRetry) await callbacks?.onRetryFinished?.(true, lastRetry.attempt);
|
||||
return response;
|
||||
|
||||
@@ -126,6 +126,21 @@ describe("retryAssistantCall", () => {
|
||||
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 () => {
|
||||
const produce = vi.fn(async () => fauxAssistantMessage("", { stopReason: "error", errorMessage: "terminated" }));
|
||||
const onRetryScheduled = vi.fn();
|
||||
|
||||
Reference in New Issue
Block a user