# Retry Decorator (sync + async)

Fuente conceptual: MiroFish `backend/app/utils/retry.py` (AGPL-3.0)
Reimplementar desde cero. No copiar codigo.

Nota: OpenViking spec 09 documenta retry como funciones. Este es complementario: retry como **decorador** con API mas ergonomica para decorar funciones existentes.

## Funciones a implementar

### 1. retry_with_backoff (decorator)

- **Dominio:** core
- **Lang:** Python
- **Purity:** impure (decorator que modifica comportamiento)
- **Signature:**
  ```python
  def retry_with_backoff(
      max_retries: int = 3,
      initial_delay: float = 1.0,
      max_delay: float = 30.0,
      backoff_factor: float = 2.0,
      jitter: bool = True,
      exceptions: tuple[type[Exception], ...] = (Exception,),
      on_retry: Callable[[Exception, int], None] | None = None
  ) -> Callable:
  ```
- **Descripcion:** Decorador que reintenta una funcion sincrona con exponential backoff cuando lanza excepciones del tipo especificado.
- **Uso:**
  ```python
  @retry_with_backoff(max_retries=3, exceptions=(ConnectionError, TimeoutError))
  def call_api():
      ...
  ```
- **Algoritmo:**
  1. Wrapper con `functools.wraps`
  2. Loop: intentar func(*args, **kwargs)
  3. Si excepcion en `exceptions`:
     - Si ultimo intento: re-raise
     - Calcular delay: `min(delay, max_delay)`, si jitter: `*= (0.5 + random())`
     - Llamar `on_retry(error, attempt)` si definido
     - `time.sleep(delay)`, `delay *= backoff_factor`
- **Deps:** `functools`, `time`, `random`

### 2. retry_with_backoff_async (decorator)

- **Dominio:** core
- **Lang:** Python
- **Purity:** impure
- **Signature:** Misma firma que `retry_with_backoff` pero retorna async wrapper
- **Descripcion:** Version async. Usa `asyncio.sleep` en vez de `time.sleep`.
- **Deps:** `asyncio`, `functools`, `random`
- **Tests:** funcion que falla 2 veces y luego exito (3 retries), funcion que siempre falla (agota retries y lanza), on_retry callback se llama, jitter produce delays variables, solo reintenta excepciones especificadas
