SPB Git

spb/ultra-sharp-agent-skills Public

Ultra-Sharp Agent Skills — a research-first skill-authoring system + 72 production-ready skills for AI agents.

Python 100%
4.2 KB · 117 lines markdown
Rendered Raw Blame History
1<!--2Author: Simon-Pierre Boucher3Contact: contact@spboucher.ai4-->56# Patterns — Handling Errors78## Contents9- Error taxonomy skeleton (Python)10- Boundary handler → problem+json11- Retry with exponential backoff + full jitter12- Circuit breaker13- Fail-fast startup14- Gotchas1516## Error taxonomy skeleton (Python)1718```python19class AppError(Exception):20    status = 50021    title = "Internal error"22    def __init__(self, detail="", **ctx):23        super().__init__(detail)24        self.detail, self.ctx = detail, ctx2526class ValidationError(AppError): status, title = 400, "Invalid request"27class AuthError(AppError):       status, title = 401, "Authentication required"28class Forbidden(AppError):       status, title = 403, "Not allowed"29class NotFound(AppError):        status, title = 404, "Resource not found"30class Conflict(AppError):        status, title = 409, "Conflict"31class RateLimited(AppError):     status, title = 429, "Too many requests"32```3334## Boundary handler → problem+json3536One handler at the HTTP edge; nothing below it formats responses.3738```python39@app.exception_handler(AppError)40def app_error(request, exc):41    # Exactly one log line per failure, with context, at the boundary.42    logger.warning("request_failed", status=exc.status,43                   error=type(exc).__name__, **exc.ctx)44    return JSONResponse(status_code=exc.status, media_type="application/problem+json",45        content={"type": f"https://api.example.com/errors/{type(exc).__name__}",46                 "title": exc.title, "status": exc.status,47                 "detail": exc.detail, "instance": str(request.url.path)})4849@app.exception_handler(Exception)50def unexpected(request, exc):51    logger.exception("unhandled_error")          # full trace to logs only52    return JSONResponse(status_code=500, media_type="application/problem+json",53        content={"title": "Internal error", "status": 500,54                 "detail": "Unexpected error. Retry or contact support."})55```5657## Retry with exponential backoff + full jitter5859```python60import random, time6162# base 0.2s, factor 2, cap 30s, 5 attempts: worst-case wait ~ <60s total.63def retry(fn, retryable=(TimeoutError, ConnectionError),64          attempts=5, base=0.2, cap=30.0):65    for n in range(attempts):66        try:67            return fn()68        except retryable:69            if n == attempts - 1:70                raise71            time.sleep(random.uniform(0, min(cap, base * 2 ** n)))  # full jitter72```7374Honor server hints: if the response carries `Retry-After: N`, sleep `N` seconds instead of the computed backoff.7576## Circuit breaker7778```python79class Breaker:80    # 5 consecutive failures opens; probe after 30s (half-open).81    def __init__(self, threshold=5, reset_after=30.0):82        self.fail, self.threshold, self.reset_after = 0, threshold, reset_after83        self.opened_at = None84    def call(self, fn):85        if self.opened_at is not None:86            if time.monotonic() - self.opened_at < self.reset_after:87                raise DependencyDown("circuit open")88            self.opened_at = None            # half-open: allow one probe89        try:90            out = fn()91        except Exception:92            self.fail += 193            if self.fail >= self.threshold:94                self.opened_at = time.monotonic()95            raise96        self.fail = 097        return out98```99100## Fail-fast startup101102```python103def main():104    cfg = load_config()          # raises with the missing key named105    db.ping(cfg.database_url)    # unreachable DB -> crash now, not at first request106    run_pending_migration_check(cfg)107    serve(cfg)108```109110## Gotchas111- `except Exception: pass` hides bugs for months — if a failure is truly ignorable, log it at DEBUG with a reason string.112- Re-raising with `raise NewError(...) from e` preserves the chain; bare `raise NewError(...)` destroys the original traceback.113- Retrying a POST without an idempotency key can double-charge/double-create — timeouts are *unknown outcome*, not failure.114- Breakers per dependency, not global — one dead cache must not open the DB breaker.115- problem+json `type` URLs should be stable identifiers; they don't have to resolve, but never reuse one for a different meaning.116- 500 bodies must be static — rendering them from the exception risks leaking internals and can itself fail.117