Skip to content

Commit c424a20

Browse files
author
Anatolii
committed
release: 0.6.1 — Layer-1/2/3 ('give the user a chance')
Bump version 0.6.0 → 0.6.1. This release lands all three layers of the 'give the user a chance' design on top of the 0.6.0 P0 hardening pass: * Layer 1 — structured exception hierarchy. Every public SDK exception inherits from NullRunError and carries error_code / user_action / retryable / docs_url / cause. Five new typed classes (NullRunConfigError, NullRunAuthError, NullRunBackendError, NullRunBudgetError, NullRunToolBlockedError) are subclasses of the existing user-facing classes, so every 'except' clause from 0.6.0 keeps matching. * Layer 2 — nullrun.on_error() global error hook. Fires for every structured NullRunError before the exception propagates. Skipped for WorkflowKilledInterrupt (BaseException subclass — kill is a signal, not an error). Multiple hooks fire in registration order; hook exceptions are caught and logged at DEBUG. has_hooks() short-circuit keeps the hot path zero-cost when no hook is registered. * Layer 3 — nullrun.status() introspection. Synchronous, thread-safe, side-effect-free snapshot of runtime state. Returns a frozen NullRunStatus dataclass with one of four headline states (ok / degraded / offline / misconfigured). Raises NullRunConfigError (NR-C004) if no runtime has been init()'d — never lazily creates a runtime as a side effect. Per-code docs in docs/errors/ (15 pages + README index). New tests pin the hierarchy, the hook semantics, the snapshot fields, and the recent-errors ring buffer. TestPyPI: the previous 0.6.0 (uploaded 2026-06-23, before #31 and #32 landed) is yanked separately so the new 0.6.1 wheel can be uploaded. The yank is a TestPyPI-side action; it does not change the source tree.
1 parent f9cc345 commit c424a20

3 files changed

Lines changed: 149 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,153 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
77

88
---
99

10+
## [0.6.1] — 2026-06-24
11+
12+
Additive release — Layers 1, 2, and 3 of the "give the user a chance"
13+
design land together. Structured exceptions, a global error hook,
14+
and a synchronous runtime snapshot. No breaking changes.
15+
16+
### Layer 1 — structured exception hierarchy
17+
18+
Every public SDK exception now carries a stable, grep-able
19+
`error_code` (e.g. `NR-A001`, `NR-B002`, `NR-R001`) plus a short
20+
imperative `user_action` and a `retryable` flag, so cookbook
21+
examples and Sentry integrations can branch on the code instead
22+
of parsing the message string.
23+
24+
- **`NullRunError` — structured base for every user-facing SDK
25+
exception.** Carries four actionable fields:
26+
- `error_code` — stable `NR-LETTERNNN` identifier
27+
(documented per-code in `docs/errors/<code>.md`).
28+
- `user_action` — short imperative next-step hint
29+
("Set NULLRUN_API_KEY", "Verify API key at …", "Retry in 30s
30+
— backend is down", …). Empty when there is no actionable
31+
step.
32+
- `retryable``True` only for transient failures (5xx,
33+
network blip, transient auth); `False` for config,
34+
permission, and budget-exhausted (retrying without
35+
changing something will just hit the same wall).
36+
- `docs_url` — per-code docs page (falls back to the
37+
`https://docs.nullrun.io/errors` index when the per-code
38+
page does not exist yet).
39+
- `cause` — optional chained `BaseException`.
40+
41+
- **New specialized exception classes** (each is a subclass of
42+
the existing user-facing class, so existing `except` clauses
43+
keep matching):
44+
45+
| Class | Subclass of | `error_code` | `retryable` |
46+
|---|---|---|---|
47+
| `NullRunConfigError` | `NullRunError` | `NR-C001` | False |
48+
| `NullRunAuthError` | `NullRunAuthenticationError` | `NR-A001` | False |
49+
| `NullRunBackendError` | `NullRunTransportError` | `NR-B002` | **True** |
50+
| `NullRunBudgetError` | `NullRunBlockedException` | `NR-X001` | False |
51+
| `NullRunToolBlockedError` | `NullRunBlockedException` | `NR-T001` | False |
52+
53+
- **Public re-exports**`nullrun.NullRunError`,
54+
`nullrun.NullRunAuthError`, `nullrun.NullRunConfigError`,
55+
`nullrun.NullRunBackendError`, `nullrun.NullRunBudgetError`,
56+
`nullrun.NullRunToolBlockedError`,
57+
`nullrun.WorkflowKilledInterrupt` are now in
58+
`nullrun.__all__` and show up in `dir(nullrun)` for
59+
discoverability. The legacy types (`NullRunBlockedException`,
60+
`NullRunAuthenticationError`, `WorkflowKilledException`,
61+
`WorkflowPausedException`) stay importable via the lazy-export
62+
table for back-compat — adding them here would change
63+
`dir(nullrun)` for existing users.
64+
65+
### Layer 2 — `nullrun.on_error()` global hook
66+
67+
- **`nullrun.on_error(hook)` — global error hook.** Fires for
68+
every structured `NullRunError` *before* the exception
69+
propagates so the call stack is still live. Returns an
70+
idempotent `unregister` callable.
71+
- **Skipped** for `WorkflowKilledInterrupt` (BaseException
72+
subclass — kill is a signal, not an error) and for
73+
non-`NullRunError` exceptions.
74+
- **Multiple hooks** fire in registration order.
75+
- **Hook exceptions** are caught and logged at DEBUG — a
76+
misbehaving hook cannot break the SDK.
77+
- **Zero-cost fast path** when no hook is registered
78+
(`has_hooks()` short-circuit before any allocation).
79+
- **Backed by** `nullrun.observability.error_hooks`
80+
`register_hook`, `unregister_hook`, `emit_error`, `clear_hooks`,
81+
`STAGES`, `ErrorContext`.
82+
83+
### Layer 3 — `nullrun.status()` introspection
84+
85+
- **`nullrun.status()` — synchronous runtime snapshot.** Returns
86+
a frozen `NullRunStatus` dataclass (state, version, reason,
87+
auth state, policy state, connectivity, workflow state,
88+
bounded recent-errors ring buffer).
89+
- **Four headline states** derived automatically: `ok`,
90+
`degraded`, `offline`, `misconfigured`.
91+
- **Raises** `NullRunConfigError` (`NR-C004`) if no runtime
92+
has been `init()`'d — never lazily creates a runtime as a
93+
side effect.
94+
- **Thread-safe** — safe to call from the agent loop, the
95+
transport flush thread, or a debug console.
96+
- **Backed by** `nullrun.observability.status`
97+
`NullRunStatus`, `RecentError`, `WorkflowState`,
98+
`_RecentErrorRing`.
99+
100+
### Docs
101+
102+
- **`docs/errors/`** — 15 per-code pages (`NR-A001..A003`,
103+
`NR-B001..B005`, `NR-C001/C003`, `NR-L001`, `NR-R001`,
104+
`NR-T001`, `NR-W002/W003`) plus a `README.md` index. Each
105+
page documents the trigger conditions, the `user_action`,
106+
the `retryable` hint, and a small reproducer / fix snippet.
107+
- **`docs/integration-baseline-2026-06-19.md`** — pinned
108+
baseline for the next integration run.
109+
110+
### Tests
111+
112+
- **`tests/test_exception_hierarchy.py`** — pins the
113+
hierarchy shape (class roots), the structured fields on every
114+
public class, and the five back-compat invariants (`except`
115+
clauses keep matching across the new subclasses;
116+
`WorkflowKilledInterrupt` is the only public class not
117+
catchable by `except Exception`).
118+
- **`tests/test_error_hooks.py`** — registry basics, `emit_error`
119+
semantics (fires with both args, swallows hook exceptions,
120+
one-bad-hook-isolated, unregister-mid-dispatch is safe),
121+
`ErrorContext` validation, the `WorkflowKilledInterrupt` and
122+
`WorkflowKilledException` bypass rules, and that the global
123+
`nullrun.on_error` shim is wired through.
124+
- **`tests/test_status.py`** — no-runtime raises `NR-C004`,
125+
with-runtime snapshot is frozen / equality-stable, key prefix
126+
is truncated to 10 chars, state derivation (ok / degraded /
127+
misconfigured), recent-errors ring buffer (capacity 10, fed
128+
by `_emit_sdk_error`).
129+
- **`tests/test_integration_contract.py`**`track_event`
130+
`setdefault` race pinned against the locked helper.
131+
- **`tests/test_dead_code_removed.py::test_dir_size_unchanged`**
132+
rewritten to key off `nullrun.__all__` (source of truth for
133+
the curated surface) instead of a hardcoded symbol count, so
134+
the curated-surface contract is still pinned without
135+
blocking legitimate additions.
136+
137+
### Release plumbing
138+
139+
- The previous `0.6.0` on TestPyPI is **yanked** (visible but
140+
not installable via `pip install nullrun`) — it predates
141+
the Layer-1 / Layer-2 / Layer-3 work merged in this release,
142+
so users who pinned `0.6.0` on TestPyPI should upgrade to
143+
`0.6.1` to pick up the new structured exceptions and
144+
observability APIs.
145+
146+
### Back-compat
147+
148+
- Every existing `except` clause keeps matching — the new
149+
exception classes are subclasses of the existing ones.
150+
- `from nullrun.breaker.exceptions import X` keeps working
151+
unchanged.
152+
- `pip install nullrun==0.6.1` is a drop-in replacement for
153+
`0.6.0`.
154+
155+
---
156+
10157
## [0.6.0] — 2026-06-23
11158

12159
Hardening pass driven by the 2026-06-22 SDK↔backend integration audit.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "nullrun"
7-
version = "0.6.0"
7+
version = "0.6.1"
88
description = "NullRun Python SDK — Enforcement gateway for AI agents."
99
readme = "README.md"
1010
license = { text = "Apache-2.0" }

src/nullrun/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
"""NullRun Platform SDK."""
22

3-
__version__ = "0.6.0"
3+
__version__ = "0.6.1"
44
__platform_version__ = "1.0.0"

0 commit comments

Comments
 (0)