Skip to content

Errors

Every exception class in tulip.core.errors subclasses a single root — TulipError. One handler catches the whole hierarchy; a stable kind attribute on each class keeps your structured logs and metrics dashboards independent of class names.

from tulip.core.errors import TulipError

try:
    result = agent.run_sync(prompt, thread_id=thread_id)
except TulipError 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 this hierarchy — single sweep handler at your service boundary TulipError
GSARConfig.fail_on_low_score is set and the GSAR decision for a run_sync / arun result is not proceed GSARValidationError (carries .decision and .score)
Every credential in a CredentialPoolModel is cooling down ModelAuthError (its kind is model_pool_exhausted)
A safety check rejected input — safe_resolve (path), validate_url (URL), or MCPClient.connect() (server URL or launch package) ValidationError
Provider auth, quota or rate-limit failure The provider SDK's own exception — it is not wrapped. classify maps it to a FailoverReason
A tool raised Nothing — the loop returns the error to the model as the tool's result; result.metrics.tool_errors counts them

TulipError does not cover everything a run can throw. Provider exceptions pass through unwrapped, much of the SDK raises built-in exceptions such as ValueError, and several modules define their own exceptions outside this hierarchy — so a boundary handler that must see every failure catches Exception after TulipError. The SDK does not itself raise the tool, checkpoint and RAG families, ConfigError, or the other model errors; they are public, so your own model adapters and checkpointers can raise them under the same root.

Hierarchy

The Tulip error hierarchy TulipError, kind tulip_error, is the root of the hierarchy in tulip.core.errors, so catching it catches every class shown here. Its direct subclasses are ToolError, ModelError, CheckpointError, RAGError, ValidationError, ConfigError and GSARValidationError. ToolError, kind tool_error, has ToolNotFoundError (tool_not_found), ToolValidationError (tool_validation) and ToolExecutionError (tool_execution). ModelError, kind model_error, has ModelAuthError (model_auth), ModelThrottledError (model_throttled) and ModelResponseError (model_response). CheckpointError, kind checkpoint_error, has CheckpointNotFoundError (checkpoint_not_found) and CheckpointSerializationError (checkpoint_serialization). RAGError, kind rag_error, has EmbeddingError (embedding_error) and VectorStoreError (vector_store_error). ValidationError (validation_error), ConfigError (config_error) and GSARValidationError (gsar_validation_error) have no subclasses. Catching a class also catches all of its subclasses. TulipError tulip_error ToolNotFoundError tool_not_found ToolValidationError tool_validation ToolExecutionError tool_execution ToolError tool_error ModelAuthError model_auth ModelThrottledError model_throttled ModelResponseError model_response ModelError model_error CheckpointNotFoundError checkpoint_not_found CheckpointSerializationError checkpoint_serialization CheckpointError checkpoint_error EmbeddingError embedding_error VectorStoreError vector_store_error RAGError rag_error ValidationError validation_error ConfigError config_error GSARValidationError gsar_validation_error Arrows point from a class to its subclasses; catching a class also catches its subclasses. Under each name is its kind, the stable key.

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 TulipError as exc:
    logger.exception("agent failed", extra={"kind": exc.kind})
    return error_response(exc.kind)

Metric on kind

from tulip.core.errors import TulipError

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

Use kind instead of the class name — it is the documented stable key, and it can be finer than the class: a ModelAuthError from an exhausted credential pool carries kind="model_pool_exhausted".

Differentiated retry policy

Provider errors are not TulipErrors, so route them on what classify says rather than on their class:

import time

from tulip.core.errors import GSARValidationError, TulipError
from tulip.models.failover import FailoverReason, classify

for attempt in range(3):
    try:
        return agent.run_sync(prompt)
    except GSARValidationError:
        return fallback_path(prompt)     # answer not grounded — degrade gracefully
    except TulipError:
        raise                            # everything else from the hierarchy: no retry
    except Exception as exc:             # provider errors arrive unwrapped
        if classify(exc).reason is not FailoverReason.RATE_LIMIT:
            raise                        # anything but a rate limit: no retry
        time.sleep(2 ** attempt)         # rate limit — exponential back-off

Chained causes

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

from tulip.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
A provider error slips past except TulipError Provider exceptions are not wrapped. Catch them after TulipError and route them with classify.
Rate-limit retries run forever Cap the loop with a max attempt count or a deadline; don't rely on the provider giving up.
The same tool call keeps failing The error goes back to the model as the tool's result, and the model isn't acting on it. 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 — the opt-in ModelRetryHook (retries empty model responses).
  • Hooks — AfterToolCallEvent carries the error message when a tool body raises.
  • Tools — how a tool that raises becomes an error result for the model.