Skip to content

Errors

Every exception raised from inside locus subclasses a single root — LocusError. One handler catches any locus-originated failure; a stable kind attribute on each subclass keeps your structured logs and metrics dashboards portable across releases.

from locus.core.errors import LocusError

try:
    result = agent.run_sync(prompt, thread_id=thread_id)
except LocusError as exc:
    logger.exception(
        "agent run failed",
        extra={"kind": exc.kind, "thread_id": thread_id},
    )
    raise

When you'll catch which

Situation Catch
Anything from locus — single sweep handler at your service boundary LocusError
A specific tool blew up; want to retry / skip / re-route ToolError (or one of its three subtypes)
Provider auth or quota issue; want to escalate or back off ModelError (or ModelAuthError / ModelThrottledError)
Checkpoint resume failed; thread is corrupt or missing CheckpointError
Vector store / embeddings call failed RAGError
Bad config or invalid input at the public-API boundary ConfigError / ValidationError

Outside this hierarchy, nothing locus emits will leak through — unwrapped third-party exceptions are wrapped at the boundary.

Hierarchy

LocusError                       kind="locus_error"
├── ToolError                    kind="tool_error"
│   ├── ToolNotFoundError        kind="tool_not_found"
│   ├── ToolValidationError      kind="tool_validation"
│   └── ToolExecutionError       kind="tool_execution"
├── ModelError                   kind="model_error"
│   ├── ModelAuthError           kind="model_auth"
│   ├── ModelThrottledError      kind="model_throttled"
│   └── ModelResponseError       kind="model_response"
├── CheckpointError              kind="checkpoint_error"
│   ├── CheckpointNotFoundError  kind="checkpoint_not_found"
│   └── CheckpointSerializationError  kind="checkpoint_serialization"
├── RAGError                     kind="rag_error"
│   ├── EmbeddingError           kind="embedding"
│   └── VectorStoreError         kind="vector_store"
├── ValidationError              kind="validation"     (public-API input)
└── ConfigError                  kind="config"         (invalid/missing config)

Class names may evolve; kind strings are part of the stable contract. Key your dashboards on kind.

Idiomatic patterns

One handler, structured logs

import logging

logger = logging.getLogger(__name__)

try:
    result = agent.run_sync(prompt)
except LocusError as exc:
    logger.exception("agent failed", extra={"kind": exc.kind})
    return error_response(exc.kind)

Metric on kind

from locus.core.errors import LocusError

try:
    result = agent.run_sync(prompt)
except LocusError as exc:
    metrics.counter("agent.errors", tags={"kind": exc.kind}).increment()
    raise

Use kind instead of the class name — the string never changes; the class name might.

Differentiated retry policy

from locus.core.errors import (
    ModelThrottledError, ModelAuthError, ToolExecutionError, LocusError,
)

for attempt in range(3):
    try:
        return agent.run_sync(prompt)
    except ModelThrottledError:
        time.sleep(2 ** attempt)         # 429 — exponential back-off
    except ModelAuthError:
        raise                            # auth issues never recover with retry
    except ToolExecutionError:
        return fallback_path(prompt)     # tool went south — degrade gracefully
    except LocusError:
        raise                            # everything else: no retry

Chained causes

Every constructor accepts a cause= keyword so the original exception is preserved as __cause__:

from locus.core.errors import CheckpointSerializationError

try:
    blob = json.dumps(state)
except (TypeError, ValueError) as exc:
    raise CheckpointSerializationError(
        f"failed to serialize state for {thread_id}",
        cause=exc,
    )

The full chain shows up in traceback.format_exc() and structured- log adapters — you don't lose context.

Common gotchas

Symptom Likely cause
Catching Exception instead of LocusError You'll silently swallow KeyboardInterrupt and provider SDK bugs. Catch the concrete locus base.
ModelThrottledError retries forever Cap the loop with a max attempt count or a deadline; don't rely on the provider giving up.
ToolValidationError keeps firing for the same call The model isn't reading the schema error. Tighten the system prompt or reduce the tool's surface.
Cause chain lost in logs Use logger.exception(...), not logger.error(str(exc)).

Source

See also

  • Retry — built-in retry hook keyed on ModelThrottledError.
  • HooksAfterToolCallEvent carries any exception raised by the body.
  • Tools — when ToolValidationError and ToolExecutionError fire.