mirror of
https://github.com/JackChen-me/open-multi-agent.git
synced 2026-10-22 11:02:59 -05:00
Error-aware task retry (opt-in via maxRetries > 0): - Skip retries on provably-terminal failures (4xx client errors except 408/409/429, token-budget, invalid-message, aborted calls). - Equal jitter on backoff to avoid lockstep retries against a rate-limited provider. - Honor abortSignal between attempts (no full-backoff sleep or extra attempt after cancel). - Non-streaming path preserves the structured error on AgentRunResult.error so classification works off the default runTeam path; test-covered. Off by default; behavior changes only when retry is enabled. Closes #337
60 lines
1.9 KiB
TypeScript
60 lines
1.9 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import {
|
|
isRetryableError,
|
|
TokenBudgetExceededError,
|
|
InvalidMessageError,
|
|
LLMCallTimeoutError,
|
|
} from '../src/errors.js'
|
|
|
|
describe('isRetryableError', () => {
|
|
it('treats terminal 4xx client errors as non-retryable', () => {
|
|
for (const status of [400, 401, 403, 404, 422]) {
|
|
expect(isRetryableError({ status })).toBe(false)
|
|
}
|
|
})
|
|
|
|
it('treats 408 / 409 / 429 as retryable', () => {
|
|
for (const status of [408, 409, 429]) {
|
|
expect(isRetryableError({ status })).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('treats 5xx server errors as retryable', () => {
|
|
for (const status of [500, 502, 503, 504]) {
|
|
expect(isRetryableError({ status })).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('treats network / no-status errors as retryable', () => {
|
|
expect(isRetryableError(new Error('ECONNRESET'))).toBe(true)
|
|
expect(isRetryableError(new Error('socket hang up'))).toBe(true)
|
|
expect(isRetryableError('some string')).toBe(true)
|
|
expect(isRetryableError(undefined)).toBe(true)
|
|
})
|
|
|
|
it('reads status from `.statusCode` as well as `.status`', () => {
|
|
expect(isRetryableError({ statusCode: 401 })).toBe(false)
|
|
expect(isRetryableError({ statusCode: 503 })).toBe(true)
|
|
})
|
|
|
|
it('classifies budget and invalid-message framework errors as terminal', () => {
|
|
expect(isRetryableError(new TokenBudgetExceededError('a', 100, 50))).toBe(false)
|
|
expect(isRetryableError(new InvalidMessageError('bad'))).toBe(false)
|
|
})
|
|
|
|
it('classifies a per-call timeout as retryable', () => {
|
|
expect(isRetryableError(new LLMCallTimeoutError(1000, 'agent'))).toBe(true)
|
|
})
|
|
|
|
it('classifies an AbortError as terminal', () => {
|
|
const err = new Error('aborted')
|
|
err.name = 'AbortError'
|
|
expect(isRetryableError(err)).toBe(false)
|
|
})
|
|
|
|
it('ignores a non-numeric status (defaults to retryable)', () => {
|
|
expect(isRetryableError({ status: 'nope' })).toBe(true)
|
|
expect(isRetryableError({ status: NaN })).toBe(true)
|
|
})
|
|
})
|