# Retry with Error Classification

Fuente conceptual: OpenViking `openviking/utils/model_retry.py` (AGPL-3.0)
Reimplementar desde cero. No copiar codigo.

## Funciones a implementar

### 1. classify_api_error

- **Dominio:** core
- **Lang:** Python
- **Purity:** pure
- **Signature:** `classify_api_error(error: Exception) -> str`
- **Retorno:** `"permanent"` | `"transient"` | `"unknown"`
- **Descripcion:** Clasifica un error de API como permanente (no reintentar), transitorio (reintentar) o desconocido.
- **Algoritmo:**
  1. Obtener textos: `str(error)` + `str(error.__cause__)` si existe
  2. Buscar patrones permanentes: `"400"`, `"401"`, `"403"`, `"Forbidden"`, `"Unauthorized"`
  3. Buscar patrones transitorios: `"429"`, `"500"`, `"502"`, `"503"`, `"504"`, `"TooManyRequests"`, `"RateLimit"`, `"timeout"`, `"Timeout"`, `"ConnectionError"`, `"Connection refused"`, `"Connection reset"`
  4. Permanente tiene prioridad sobre transitorio
- **Deps:** ninguna
- **Tests:** error 429, error 401, error timeout, error desconocido, error con __cause__

### 2. compute_backoff_delay

- **Dominio:** core
- **Lang:** Python
- **Purity:** pure (con jitter usa random pero es aceptable)
- **Signature:** `compute_backoff_delay(attempt: int, base_delay: float = 0.5, max_delay: float = 8.0, jitter: bool = True) -> float`
- **Descripcion:** Calcula el delay para exponential backoff con jitter opcional.
- **Algoritmo:**
  1. `delay = min(base_delay * 2^attempt, max_delay)`
  2. Si jitter: `delay += random.uniform(0, min(base_delay, delay))`
- **Deps:** `random`
- **Tests:** attempt 0 (base_delay), attempt alto (capped a max_delay), sin jitter (determinista)

### 3. retry_sync

- **Dominio:** core
- **Lang:** Python
- **Purity:** impure (ejecuta funcion, hace sleep)
- **Signature:** `retry_sync(func: Callable[[], T], max_retries: int, base_delay: float = 0.5, max_delay: float = 8.0, jitter: bool = True, is_retryable: Callable[[Exception], bool] | None = None) -> T`
- **Descripcion:** Reintenta una funcion sincrona en errores transitorios con exponential backoff.
- **Algoritmo:**
  1. Loop: llamar func()
  2. Si excepcion Y es retryable Y no agotamos intentos: dormir delay, incrementar attempt
  3. Si no retryable o agotamos intentos: re-raise
- **Deps:** `time`

### 4. retry_async

- **Dominio:** core
- **Lang:** Python
- **Purity:** impure
- **Signature:** `async retry_async(func: Callable[[], Awaitable[T]], max_retries: int, base_delay: float = 0.5, max_delay: float = 8.0, jitter: bool = True, is_retryable: Callable[[Exception], bool] | None = None) -> T`
- **Descripcion:** Version async de retry_sync. Usa `asyncio.sleep` en vez de `time.sleep`.
- **Deps:** `asyncio`
