Observed since 20 Oct, 22:15 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
agentic-exception-sdk
Domain-agnostic exception management SDK for agentic systems
A domain-agnostic exception management SDK for agentic systems. Classifies failures into three tiers, routes them through pluggable handlers, wraps tool boundaries with resilient call wrappers, and provides safe no-op defaults for zero-config adoption.
Designed as a standalone Python package installed and wired into host projects via constructor injection. Contains no host-specific imports, no global state, and no module-level singletons.
Python 3.11+ — no exceptiongroup backport required.
Every failure is assigned to exactly one class:
| Class | Meaning | Action |
|---|---|---|
EXCEPTION | Gracefully resolvable | Automatic retry or fallback — no human involved |
ISSUE | Requires intervention | Agent pauses; checkpoint handoff or human escalation |
HARD_KILL | Not resolvable | Agent terminates immediately; envelope written to DLQ |
HARD_KILL raises AgentHardKillError(BaseException) — it is never caught by
except Exception anywhere in the call stack.
# Core only (pydantic + RFC 8785 JCS canonicalization)
pip install agentic-exception-sdk
# With OpenTelemetry export
pip install agentic-exception-sdk[otel]
# With Redis circuit-breaker state
pip install agentic-exception-sdk[redis]
# With Prometheus metrics
pip install agentic-exception-sdk[prometheus]
# With structlog JSON renderer
pip install agentic-exception-sdk[structlog]
# Everything
pip install agentic-exception-sdk[all]
ReDoS protection: Install with
pip install "agentic-exception-sdk[re2]"to enable linear-time google-re2 redaction. Without it the SDK falls back to stdlibrewith patterns designed to avoid catastrophic backtracking.
from agentic_exception_sdk import (
AgentHardKillError,
NoOpResilienceBundle,
SafeContextSnapshot,
resilient,
)
# Zero-config: every component is a safe no-op
bundle = NoOpResilienceBundle()
# Wrap any tool boundary with the curried factory:
# resilient(bundle, ...)(fn)(*args, **kwargs)
result = resilient(
bundle,
tool_name="search_flights",
agent_id="booking-agent",
correlation_id="req-abc123",
context_snapshot=SafeContextSnapshot({"booking_id": "BK-001"}),
fallback_value=[], # returned on EXCEPTION or ISSUE if set
)(flight_client.search)(origin, destination, dates)
# Catch HARD_KILL only at the outermost executor boundary
try:
executor.run(booking_request)
except AgentHardKillError as hk:
audit.emit(hk.envelope.exception_id)
raise
Wire a real ResilienceBundle in the composition root when you need retries,
circuit breaking, budget enforcement, or escalation routing:
from agentic_exception_sdk import (
AgentBudget,
BudgetWatchdog,
ExponentialBackoffRetry,
InMemoryCircuitBreaker,
InMemoryMetricsCollector,
ResilienceBundle,
)
bundle = ResilienceBundle(
retry_policy=ExponentialBackoffRetry(
max_attempts=3,
base_delay_seconds=0.5,
jitter=True,
retryable_exceptions=(TimeoutError, ConnectionError),
),
circuit_breaker=InMemoryCircuitBreaker(
failure_threshold=5,
cooldown_seconds=30,
),
agent_budget=BudgetWatchdog(AgentBudget(
max_seconds=45.0,
max_tool_calls=10,
failure_budget=3,
max_total_cost_micros_usd=2_000_000, # $2.00
)),
metrics_collector=InMemoryMetricsCollector(), # swap for PrometheusMetricsCollector
)
ResilienceBundle — the single injection object. Every field defaults to
a safe no-op. Inject only what you need.NoOpResilienceBundle() — returns a fully defaulted bundle. Use as the
default when no bundle is injected.resilient() / async_resilient() — curried factory wrappers. Preserve
wrapped callable parameter types via ParamSpec. Use async_resilient() with
asyncio.timeout() for async tool calls and LLM calls with timeouts.AgentHardKillError(BaseException) — raised on HARD_KILL. Propagates
past all except Exception handlers. Catch only at the outermost executor
boundary.SafeContextSnapshot — frozen Pydantic RootModel for agent state.
Enforces JSON-safe values, max depth 5, max 100 keys per dict, and rejects
non-finite floats.extend_lineage() — validated factory for multi-agent envelope
propagation. Caps lineage at 64 hops; exceeding the cap auto-promotes to
HARD_KILL.TrustBoundaryValidator — runs pattern-based redaction (secrets, tokens,
PANs, bidi controls) over exception messages and over string values in
context snapshots. Snapshot handling redacts values under sensitively-named
keys (password, token, secret, …) outright, and pattern-scans every
other string value — including nested and list elements — so a secret embedded
in an otherwise-benign field ({"note": "token is sk-…"}) is still redacted,
not merely length-bounded. A single ReDoS budget spans the walk and fails
closed. Redaction remains best-effort — see Limitations & Scope.ResilienceBundle.metrics_collector — optional MetricsCollector
(protocol). PrometheusMetricsCollector emits agent_exceptions_total,
agent_hard_kills_total, agent_retries_total, agent_budget_exhausted_total.ResilienceBundle.meter_provider — optional OTel MeterProvider. When
set, creates an agent_tool_call_duration_seconds histogram per call.ResilienceBundle.dlq — DeadLetterQueue for HARD_KILL envelopes.
Defaults to InMemoryDLQ (ring buffer, drop-oldest). Swap for AsyncInMemoryDLQ
in async contexts, or a satellite-package adapter (SQS, Pub/Sub, Redis).ResilienceBundle.recovery_policy — optional post-classification recovery
hook called before the default fallback/escalation path on EXCEPTION and ISSUE
tiers. Satellite package agentic-exception-retry supplies AdaptiveRetryPolicy
and CircuitAwareRetryPolicy.ResilienceBundle.async_circuit_breaker — optional async circuit breaker
used by async_resilient(). Falls back to sync circuit_breaker when not set.allow_sync_llm_timeout — opt-in flag on resilient() that runs fn in
a bounded ThreadPoolExecutor with the given timeout_seconds. The leaked
thread cannot be cancelled and may continue spending tokens after the wrapper
returns; prefer async_resilient() with asyncio.timeout() for LLM calls.PersistenceProvider / NullProvider — the audit-persistence boundary.
Every envelope carries a full-envelope integrity leaf hash (envelope_leaf_hash()),
distinct from the envelope_canonical_sha256() dedup key. The default in-process
NullProvider computes real leaf hashes and in-memory Merkle roots but emits
unsigned, non-durable checkpoints. sentirock.attestation() reports the active
provider's capabilities (durable, checkpoint_signing, worm).Every governed call emits a leaf hash over the redacted envelope and participates
in an in-memory Merkle log. In the default (in-process) provider these checkpoints
are unsigned and non-durable — the SDK's attestation API and span attributes
(sentirock.provider.durable, sentirock.audit.degraded) report this explicitly.
Durable, signed, WORM-retained audit trails are delivered by the persistence
substrate on our enterprise roadmap; the PersistenceProvider interface it plugs
into is already published in this repo (the conformance suite that will gate full
attestation is planned).
from agentic_exception_sdk import AgentBudget, BudgetWatchdog
from agentic_exception_sdk.budget import StreamingBudgetGuard
watchdog = BudgetWatchdog(AgentBudget(max_output_tokens=100))
stream = [{"text": "hi", "tokens": 1}, {"text": " there", "tokens": 2}]
for chunk in StreamingBudgetGuard.wrap_sync_stream(
stream,
watchdog,
tool_name="openai-chat",
agent_id="agent-1",
token_extractor=lambda c: c["tokens"],
):
print(chunk["text"])
The SDK's controls are mitigations, not guarantees, and are not a substitute for defense-in-depth. Please read this before relying on the SDK for a security- or compliance-critical outcome:
HARD_KILL is in-execution termination, not an operator kill switch. AgentHardKillError stops the agent's own run when a tripwire fires. It is not an out-of-band control that deactivates a running agent, agent type, or fleet on demand — that capability is on the roadmap. Do not represent it as a fleet-wide or human-pullable deactivation switch.AgentHardKillError extends BaseException — mind your runtime. This is deliberate (it survives except Exception), but runtimes that treat BaseException as fatal (Celery workers, some ASGI/thread-pool executors) may tear down the worker when it propagates. Catch it at your outermost executor boundary and translate it to your runtime's shutdown/abort convention; do not let it escape uncaught into a shared worker pool.sentirock.attestation() (durable=false). Durable, signed, WORM-retained audit is delivered by the enterprise substrate on the roadmap.MIT License — see LICENSE. The SDK is provided "AS IS", without warranty of any kind; the authors are not liable for any claim or damages arising from its use. See Limitations & Scope above.
Download counts and popularity will appear here as the collector receives them.
| Package / target | Requirement | Relationship |
|---|---|---|
jcsEnvironment{
"extras": "",
"marker": null,
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | jcs>=0.2.1 | runtime |
pydanticEnvironment{
"extras": "",
"marker": null,
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | pydantic>=2.0.0 | runtime |
uuid-utilsEnvironment{
"extras": "",
"marker": null,
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | uuid-utils>=0.10.0 | runtime |
opentelemetry-apiEnvironment{
"extras": "",
"marker": "extra == \"otel\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | opentelemetry-api>=1.29.0; extra == "otel" | runtime |
opentelemetry-sdkEnvironment{
"extras": "",
"marker": "extra == \"otel\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | opentelemetry-sdk>=1.29.0; extra == "otel" | runtime |
redisEnvironment{
"extras": "",
"marker": "extra == \"redis\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | redis>=5.0.0; extra == "redis" | runtime |
prometheus-clientEnvironment{
"extras": "",
"marker": "extra == \"prometheus\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | prometheus-client>=0.20.0; extra == "prometheus" | runtime |
structlogEnvironment{
"extras": "",
"marker": "extra == \"structlog\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | structlog>=24.0.0; extra == "structlog" | runtime |
google-re2Environment{
"extras": "",
"marker": "extra == \"re2\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | google-re2>=1.1; extra == "re2" | runtime |
opentelemetry-apiEnvironment{
"extras": "",
"marker": "extra == \"all\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | opentelemetry-api>=1.29.0; extra == "all" | runtime |
opentelemetry-sdkEnvironment{
"extras": "",
"marker": "extra == \"all\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | opentelemetry-sdk>=1.29.0; extra == "all" | runtime |
redisEnvironment{
"extras": "",
"marker": "extra == \"all\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | redis>=5.0.0; extra == "all" | runtime |
prometheus-clientEnvironment{
"extras": "",
"marker": "extra == \"all\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | prometheus-client>=0.20.0; extra == "all" | runtime |
structlogEnvironment{
"extras": "",
"marker": "extra == \"all\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | structlog>=24.0.0; extra == "all" | runtime |
google-re2Environment{
"extras": "",
"marker": "extra == \"all\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | google-re2>=1.1; extra == "all" | runtime |
setuptoolsEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | setuptools>=68; extra == "dev" | runtime |
wheelEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | wheel; extra == "dev" | runtime |
pytestEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | pytest>=8.0.0; extra == "dev" | runtime |
pytest-asyncioEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | pytest-asyncio>=0.23.0; extra == "dev" | runtime |
pytest-mockEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | pytest-mock>=3.14.0; extra == "dev" | runtime |
pytest-covEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | pytest-cov>=5.0.0; extra == "dev" | runtime |
pytest-randomlyEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | pytest-randomly>=3.15.0; extra == "dev" | runtime |
pytest-timeoutEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | pytest-timeout>=2.3.0; extra == "dev" | runtime |
pytest-benchmarkEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | pytest-benchmark>=4.0.0; extra == "dev" | runtime |
freezegunEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | freezegun>=1.5.0; extra == "dev" | runtime |
hypothesisEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | hypothesis>=6.100.0; extra == "dev" | runtime |
mypyEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | mypy>=1.10.0; extra == "dev" | runtime |
ruffEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | ruff>=0.4.0; extra == "dev" | runtime |
uvEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | uv>=0.5.0; extra == "dev" | runtime |
buildEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | build>=1.2.0; extra == "dev" | runtime |
banditEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | bandit>=1.7.0; extra == "dev" | runtime |
mutmutEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | mutmut>=2.0; extra == "dev" | runtime |
pip-auditEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | pip-audit>=2.7.0; extra == "dev" | runtime |
types-redisEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | types-redis; extra == "dev" | runtime |
bdist_wheel · 111.44 KB · Python py3
sdist · 78.45 KB · Python source
Shared repository data · GitHub
Observed since 20 Oct, 22:15 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package_version:bd14efdeabbdbe20c348526ce40ac719a3d712daa8db567ccace92cfe76e7224