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¶
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¶
tulip.core.errors— every class in the hierarchy.