Skip to content

Commit a1a3db1

Browse files
author
Anatolii
committed
feat(exceptions): introduce Layer-1 structured exception hierarchy
Every public SDK exception now inherits from NullRunError and carries four actionable fields (error_code, user_action, retryable, docs_url) plus an optional chained cause. Users get a stable, grep-able error code (NR-A001, NR-B002, NR-R001, ...) and a short imperative next-step hint instead of a free-form message string. New specialized classes (back-compat subclasses of existing user-facing classes, so existing except clauses keep matching): * NullRunConfigError — config/initialization failures * NullRunAuthError — invalid/missing API key (subclass of NullRunAuthenticationError) * NullRunBackendError — gateway 5xx (subclass of NullRunTransportError, retryable=True) * NullRunBudgetError — budget exhausted (subclass of NullRunBlockedException) * NullRunToolBlockedError — tool blocked by policy (subclass of NullRunBlockedException) Existing except handlers keep working: every new class is a subclass of an existing one, so e.g. 'except NullRunBlockedException' still catches NullRunBudgetError and NullRunToolBlockedError. Tests: tests/test_exception_hierarchy.py pins the hierarchy shape (class roots), the structured fields on every public class, and the five back-compat invariants (subclass matching for the user-facing exception trees, BaseException isolation for WorkflowKilledInterrupt). Verified locally: pytest 880 passed / 13 skipped, ruff check src/ clean, mypy src/ clean.
1 parent 4610ba9 commit a1a3db1

6 files changed

Lines changed: 816 additions & 54 deletions

File tree

src/nullrun/__init__.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,27 @@ def my_agent():
9595
# caught at startup rather than producing silent allow-all decisions.
9696
resolved_key = api_key or os.getenv("NULLRUN_API_KEY")
9797
if not resolved_key:
98+
# Layer 1: raise the legacy type (``NullRunAuthenticationError``)
99+
# so user code with ``except NullRunAuthenticationError:`` still
100+
# catches this case, but stamp the structured ``error_code`` /
101+
# ``user_action`` so a Layer-2 on_error hook (or a
102+
# ``except NullRunError:`` clause) can branch on the catalog
103+
# value ``NR-C001`` ("configuration: no api_key") without
104+
# parsing the message.
98105
from nullrun.breaker.exceptions import NullRunAuthenticationError
99106

100107
raise NullRunAuthenticationError(
101108
"nullrun.init() requires an api_key. Pass api_key='nr_live_...' "
102109
"explicitly or set the NULLRUN_API_KEY environment variable. "
103-
"(Silent no-op fallback was removed in 0.3.0 — see CHANGELOG.)"
110+
"(Silent no-op fallback was removed in 0.3.0 — see CHANGELOG.)",
111+
error_code="NR-C001",
112+
user_action=(
113+
"Get an API key at https://app.nullrun.io/settings/api-keys, "
114+
"then either pass api_key='nr_live_...' to nullrun.init() or "
115+
"set the NULLRUN_API_KEY environment variable. The SDK cannot "
116+
"operate without credentials — the silent no-op fallback was "
117+
"removed in 0.3.0 because it bypassed every backend gate."
118+
),
104119
)
105120

106121
# Imported lazily so we don't pull the runtime into the namespace
@@ -146,6 +161,7 @@ def my_agent():
146161
# unconditional — we always have a remote LLM traffic source if
147162
# auto-instrumentation libraries are installed.
148163
from nullrun.instrumentation.auto import auto_instrument
164+
149165
auto_instrument(runtime)
150166

151167
# Start the coverage reporter so the backend gets a coverage_report
@@ -179,7 +195,6 @@ def my_agent():
179195
"get_trace_id": ("nullrun.context", "get_trace_id"),
180196
"get_span_id": ("nullrun.context", "get_span_id"),
181197
"get_agent_id": ("nullrun.context", "get_agent_id"),
182-
183198
# Instrumentation
184199
"NullRunCallback": ("nullrun.instrumentation", "NullRunCallback"),
185200
# NOTE (Sprint 1.2 / B11-B12): `patch_openai` and `unpatch_openai`
@@ -191,14 +206,12 @@ def my_agent():
191206
# a worse failure mode than a clean `ImportError` from
192207
# `from nullrun import patch_openai` failing because the symbol
193208
# is no longer in the lazy table.
194-
195209
# Toolbox — framework-specific wrappers (Phase 1 Commit 6).
196210
# The previous `instrument()` helper lived at
197211
# `nullrun.instrumentation.langgraph.instrument`; it is now
198212
# `nullrun.toolbox.langgraph.wrapper`. Reachable as
199213
# `from nullrun import wrapper` for one-line import.
200214
"wrapper": ("nullrun.toolbox.langgraph", "wrapper"),
201-
202215
# Span / trace context (Phase 2 Commit 3).
203216
# `tracing.py` is the structured replacement for the loose `_trace_id`
204217
# / `_span_id` contextvars in `nullrun.context`. `SpanContext` is a
@@ -211,10 +224,8 @@ def my_agent():
211224
"create_child_span": ("nullrun.tracing", "create_child_span"),
212225
"set_span": ("nullrun.tracing", "set_span"),
213226
"reset_span": ("nullrun.tracing", "reset_span"),
214-
215227
# Decorators
216228
"sensitive": ("nullrun.decorators", "sensitive"),
217-
218229
# Actions (Phase 3)
219230
"ActionHandler": ("nullrun.actions", "ActionHandler"),
220231
"ActionType": ("nullrun.actions", "ActionType"),
@@ -223,7 +234,6 @@ def my_agent():
223234
"handle_action": ("nullrun.actions", "handle_action"),
224235
"register_action_handler": ("nullrun.actions", "register_action_handler"),
225236
"get_action_handler": ("nullrun.actions", "get_action_handler"),
226-
227237
# Exceptions (Phase 3)
228238
"NullRunBlockedException": ("nullrun.breaker.exceptions", "NullRunBlockedException"),
229239
"NullRunAuthenticationError": ("nullrun.breaker.exceptions", "NullRunAuthenticationError"),
@@ -265,13 +275,12 @@ def __dir__() -> list[str]:
265275
__all__ = [
266276
# Version (single value, always public)
267277
"__version__",
268-
269278
# Phase 3.4: the curated public surface — six symbols.
270279
# Everything else stays importable as `from nullrun import X` for
271280
# backward compatibility, but does NOT appear in `dir(nullrun)`
272281
# until the user actually accesses it.
273282
"init",
274-
"protect", # gate decorator
283+
"protect", # gate decorator
275284
"track_llm",
276285
"track_tool",
277286
"track_event",

0 commit comments

Comments
 (0)