nullrun 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nullrun/__init__.py +282 -0
- nullrun/__version__.py +4 -0
- nullrun/actions.py +455 -0
- nullrun/breaker/__init__.py +27 -0
- nullrun/breaker/circuit_breaker.py +402 -0
- nullrun/breaker/exceptions.py +319 -0
- nullrun/context.py +208 -0
- nullrun/decorators.py +649 -0
- nullrun/instrumentation/__init__.py +23 -0
- nullrun/instrumentation/_safe_patch.py +99 -0
- nullrun/instrumentation/auto.py +1095 -0
- nullrun/instrumentation/auto_requests.py +257 -0
- nullrun/instrumentation/autogen.py +163 -0
- nullrun/instrumentation/crewai.py +140 -0
- nullrun/instrumentation/langgraph.py +412 -0
- nullrun/instrumentation/llama_index.py +110 -0
- nullrun/observability.py +160 -0
- nullrun/py.typed +0 -0
- nullrun/runtime.py +1806 -0
- nullrun/toolbox/__init__.py +20 -0
- nullrun/toolbox/langgraph.py +94 -0
- nullrun/tracing.py +155 -0
- nullrun/transport.py +1509 -0
- nullrun/transport_websocket.py +627 -0
- nullrun-0.4.0.dist-info/METADATA +194 -0
- nullrun-0.4.0.dist-info/RECORD +28 -0
- nullrun-0.4.0.dist-info/WHEEL +4 -0
- nullrun-0.4.0.dist-info/licenses/LICENSE +201 -0
nullrun/__init__.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NullRun Platform SDK.
|
|
3
|
+
|
|
4
|
+
Enforcement gateway client for AI agents. Curated 6-symbol surface:
|
|
5
|
+
`init`, `protect`, `track_llm`, `track_tool`, `track_event`. Everything
|
|
6
|
+
else is reachable on demand via `from nullrun import X` but does NOT
|
|
7
|
+
appear in `dir(nullrun)`.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
import nullrun
|
|
11
|
+
nullrun.init(api_key="nr_live_...")
|
|
12
|
+
|
|
13
|
+
@nullrun.protect
|
|
14
|
+
def my_agent(query):
|
|
15
|
+
return call_llm(query)
|
|
16
|
+
|
|
17
|
+
See README.md for LangGraph, OpenAI Agents, llama-index, crewai, autogen
|
|
18
|
+
auto-instrumentation; CHANGELOG.md for breaking changes between versions.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import threading as _threading
|
|
24
|
+
|
|
25
|
+
# Use lazy import inside __getattr__ instead of `import importlib` at
|
|
26
|
+
# module top-level — keeps `dir(nullrun)` focused on the curated surface.
|
|
27
|
+
from nullrun.__version__ import __version__
|
|
28
|
+
|
|
29
|
+
# Module-level lock that serialises the three singleton-slot writes
|
|
30
|
+
# inside `init()`. See plan item B3.
|
|
31
|
+
_init_lock = _threading.Lock()
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# Curated public surface (Phase 3.4)
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# These six names are imported eagerly so they show up in `dir(nullrun)` and
|
|
37
|
+
# in tab-completion — that's the "track AI cost in 5 minutes" surface. All
|
|
38
|
+
# other names (legacy Breaker exports, instrumentation, exceptions, …) live
|
|
39
|
+
# in `_LAZY_EXPORTS` below and are loaded on first access via __getattr__.
|
|
40
|
+
from nullrun.decorators import protect # the gate decorator
|
|
41
|
+
from nullrun.runtime import track_event, track_llm, track_tool
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def init(
|
|
45
|
+
api_key: str | None = None,
|
|
46
|
+
api_url: str | None = None,
|
|
47
|
+
debug: bool = False,
|
|
48
|
+
):
|
|
49
|
+
"""
|
|
50
|
+
Initialize the NullRun SDK. Call once at application startup.
|
|
51
|
+
|
|
52
|
+
`api_key` is **required** as of 0.3.0. The previous silent fallback to
|
|
53
|
+
"local mode" (a NullRunNoop stub) was removed because it hid policy
|
|
54
|
+
violations and bypassed every backend gate — a real safety hole. Pass
|
|
55
|
+
`api_key=...` explicitly or set the `NULLRUN_API_KEY` environment
|
|
56
|
+
variable before calling `init()`. If neither is set, `init()` raises
|
|
57
|
+
`NullRunAuthenticationError`.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
api_key: NullRun API key (or NULLRUN_API_KEY env var). Required.
|
|
61
|
+
api_url: Gateway URL (or NULLRUN_API_URL env var)
|
|
62
|
+
debug: Enable debug logging
|
|
63
|
+
|
|
64
|
+
Note: the background control-plane listener (WebSocket + HTTP poll) is
|
|
65
|
+
always started on `init()`. To disable it, construct `NullRunRuntime`
|
|
66
|
+
directly with `polling=False` — this is an internal/test-only knob.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
NullRunRuntime singleton instance.
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
NullRunAuthenticationError: if neither `api_key` nor
|
|
73
|
+
`NULLRUN_API_KEY` is set.
|
|
74
|
+
|
|
75
|
+
Example:
|
|
76
|
+
import nullrun
|
|
77
|
+
|
|
78
|
+
nullrun.init(api_key="your-key")
|
|
79
|
+
|
|
80
|
+
@nullrun.protect
|
|
81
|
+
def my_agent():
|
|
82
|
+
return agent.run()
|
|
83
|
+
"""
|
|
84
|
+
import logging
|
|
85
|
+
import os
|
|
86
|
+
|
|
87
|
+
if debug:
|
|
88
|
+
logging.getLogger("nullrun").setLevel(logging.DEBUG)
|
|
89
|
+
|
|
90
|
+
# T3-S2 (0.3.0): api_key is now required. Previous versions fell back
|
|
91
|
+
# to a NullRunNoop stub in `local_mode`, which silently bypassed every
|
|
92
|
+
# backend gate (budget, policy, control plane). That was a real
|
|
93
|
+
# safety hole — production callers were unaware their policies were
|
|
94
|
+
# not being enforced. We raise instead so the misconfiguration is
|
|
95
|
+
# caught at startup rather than producing silent allow-all decisions.
|
|
96
|
+
resolved_key = api_key or os.getenv("NULLRUN_API_KEY")
|
|
97
|
+
if not resolved_key:
|
|
98
|
+
from nullrun.breaker.exceptions import NullRunAuthenticationError
|
|
99
|
+
|
|
100
|
+
raise NullRunAuthenticationError(
|
|
101
|
+
"nullrun.init() requires an api_key. Pass api_key='nr_live_...' "
|
|
102
|
+
"explicitly or set the NULLRUN_API_KEY environment variable. "
|
|
103
|
+
"(Silent no-op fallback was removed in 0.3.0 — see CHANGELOG.)"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Imported lazily so we don't pull the runtime into the namespace
|
|
107
|
+
# when the user only wants the static helpers.
|
|
108
|
+
import threading as _threading
|
|
109
|
+
|
|
110
|
+
import nullrun.decorators as _dec_mod
|
|
111
|
+
import nullrun.runtime as _rt_mod
|
|
112
|
+
from nullrun.runtime import NullRunRuntime
|
|
113
|
+
|
|
114
|
+
# Phase 0.3.1: the three singleton slots (NullRunRuntime._instance,
|
|
115
|
+
# _rt_mod._runtime, _dec_mod._runtime) must all be assigned
|
|
116
|
+
# atomically. Without a lock, concurrent init() calls from
|
|
117
|
+
# multiple threads can leave the three slots pointing at two
|
|
118
|
+
# different runtimes. The failure mode is silent — the
|
|
119
|
+
# decorator's @protect wrapper reads _dec._runtime once and
|
|
120
|
+
# never re-resolves, so a missed assignment drops every
|
|
121
|
+
# span_start/span_end event for that runtime.
|
|
122
|
+
with _init_lock:
|
|
123
|
+
runtime = NullRunRuntime(
|
|
124
|
+
api_key=api_key,
|
|
125
|
+
api_url=api_url,
|
|
126
|
+
debug=debug,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Register as the module-level singleton so `nullrun.track_llm` /
|
|
130
|
+
# `nullrun.track_tool` (which resolve via `get_runtime()`) and any
|
|
131
|
+
# other consumers reading the cached instance find *this* runtime —
|
|
132
|
+
# not whatever a previous test or stale env would otherwise produce.
|
|
133
|
+
_rt_mod._runtime = runtime
|
|
134
|
+
NullRunRuntime._instance = runtime
|
|
135
|
+
|
|
136
|
+
# Wire the @protect decorator's own module-level cache to this
|
|
137
|
+
# runtime too. The decorator short-circuits on its local `_runtime`
|
|
138
|
+
# slot and never re-resolves via `get_instance()`, so without this
|
|
139
|
+
# assignment a re-init cycle (init → shutdown → init) leaves the
|
|
140
|
+
# decorator pointing at the dead previous runtime and silently
|
|
141
|
+
# drops span_start/span_end events.
|
|
142
|
+
_dec_mod._runtime = runtime
|
|
143
|
+
|
|
144
|
+
# Phase D6: wire auto-instrumentation AFTER the runtime is fully
|
|
145
|
+
# constructed. In 0.3.0 api_key is required, so this branch is
|
|
146
|
+
# unconditional — we always have a remote LLM traffic source if
|
|
147
|
+
# auto-instrumentation libraries are installed.
|
|
148
|
+
from nullrun.instrumentation.auto import auto_instrument
|
|
149
|
+
auto_instrument(runtime)
|
|
150
|
+
|
|
151
|
+
return runtime
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# ---------------------------------------------------------------------------
|
|
155
|
+
# Lazy exports (PEP 562) — backward compat without bloating dir()
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
# Each entry maps an attribute name on `nullrun` to (module_path, attr_name)
|
|
158
|
+
# inside that module. They are loaded on first attribute access and cached
|
|
159
|
+
# in `globals()` so subsequent lookups are O(1) and not visible in
|
|
160
|
+
# `vars(nullrun)` until then. This is the same pattern used by pandas /
|
|
161
|
+
# sqlalchemy / etc. to keep the top-level namespace discoverable.
|
|
162
|
+
_LAZY_EXPORTS: dict[str, tuple[str, str]] = {
|
|
163
|
+
# Runtime + context (advanced)
|
|
164
|
+
"NullRunRuntime": ("nullrun.runtime", "NullRunRuntime"),
|
|
165
|
+
"get_runtime": ("nullrun.runtime", "get_runtime"),
|
|
166
|
+
"get_protected_runtime": ("nullrun.decorators", "get_protected_runtime"),
|
|
167
|
+
"track": ("nullrun.runtime", "track"),
|
|
168
|
+
"reset": ("nullrun.decorators", "reset"),
|
|
169
|
+
"workflow": ("nullrun.context", "workflow"),
|
|
170
|
+
"span": ("nullrun.context", "span"),
|
|
171
|
+
"agent": ("nullrun.context", "agent"),
|
|
172
|
+
"get_workflow_id": ("nullrun.context", "get_workflow_id"),
|
|
173
|
+
"get_trace_id": ("nullrun.context", "get_trace_id"),
|
|
174
|
+
"get_span_id": ("nullrun.context", "get_span_id"),
|
|
175
|
+
"get_agent_id": ("nullrun.context", "get_agent_id"),
|
|
176
|
+
|
|
177
|
+
# Instrumentation
|
|
178
|
+
"NullRunCallback": ("nullrun.instrumentation", "NullRunCallback"),
|
|
179
|
+
# NOTE (Sprint 1.2 / B11-B12): `patch_openai` and `unpatch_openai`
|
|
180
|
+
# were removed from `_LAZY_EXPORTS` because they pointed at
|
|
181
|
+
# non-existent attributes on `nullrun.instrumentation` (the actual
|
|
182
|
+
# function is `patch_openai_agents`, with different semantics —
|
|
183
|
+
# it patches `agents.Runner`, not the `openai` SDK). The pre-fix
|
|
184
|
+
# lazy entries caused `AttributeError` on first access, which is
|
|
185
|
+
# a worse failure mode than a clean `ImportError` from
|
|
186
|
+
# `from nullrun import patch_openai` failing because the symbol
|
|
187
|
+
# is no longer in the lazy table.
|
|
188
|
+
|
|
189
|
+
# Toolbox — framework-specific wrappers (Phase 1 Commit 6).
|
|
190
|
+
# The previous `instrument()` helper lived at
|
|
191
|
+
# `nullrun.instrumentation.langgraph.instrument`; it is now
|
|
192
|
+
# `nullrun.toolbox.langgraph.wrapper`. Reachable as
|
|
193
|
+
# `from nullrun import wrapper` for one-line import.
|
|
194
|
+
"wrapper": ("nullrun.toolbox.langgraph", "wrapper"),
|
|
195
|
+
|
|
196
|
+
# Span / trace context (Phase 2 Commit 3).
|
|
197
|
+
# `tracing.py` is the structured replacement for the loose `_trace_id`
|
|
198
|
+
# / `_span_id` contextvars in `nullrun.context`. `SpanContext` is a
|
|
199
|
+
# single value (parent + children derive from it); `set_span` /
|
|
200
|
+
# `reset_span` are the token-based API the runtime and `@protect`
|
|
201
|
+
# use to push/pop the active span.
|
|
202
|
+
"SpanContext": ("nullrun.tracing", "SpanContext"),
|
|
203
|
+
"get_current_span": ("nullrun.tracing", "get_current_span"),
|
|
204
|
+
"create_root_span": ("nullrun.tracing", "create_root_span"),
|
|
205
|
+
"create_child_span": ("nullrun.tracing", "create_child_span"),
|
|
206
|
+
"set_span": ("nullrun.tracing", "set_span"),
|
|
207
|
+
"reset_span": ("nullrun.tracing", "reset_span"),
|
|
208
|
+
|
|
209
|
+
# Decorators
|
|
210
|
+
"sensitive": ("nullrun.decorators", "sensitive"),
|
|
211
|
+
|
|
212
|
+
# Actions (Phase 3)
|
|
213
|
+
"ActionHandler": ("nullrun.actions", "ActionHandler"),
|
|
214
|
+
"ActionType": ("nullrun.actions", "ActionType"),
|
|
215
|
+
"ActionEvent": ("nullrun.actions", "ActionEvent"),
|
|
216
|
+
"WebhookConfig": ("nullrun.actions", "WebhookConfig"),
|
|
217
|
+
"handle_action": ("nullrun.actions", "handle_action"),
|
|
218
|
+
"register_action_handler": ("nullrun.actions", "register_action_handler"),
|
|
219
|
+
"get_action_handler": ("nullrun.actions", "get_action_handler"),
|
|
220
|
+
|
|
221
|
+
# Exceptions (Phase 3)
|
|
222
|
+
"NullRunBlockedException": ("nullrun.breaker.exceptions", "NullRunBlockedException"),
|
|
223
|
+
"NullRunAuthenticationError": ("nullrun.breaker.exceptions", "NullRunAuthenticationError"),
|
|
224
|
+
# Sprint 2.2: zombie exception classes removed. See the
|
|
225
|
+
# NOTE block in breaker/exceptions.py for the list.
|
|
226
|
+
"WorkflowPausedException": ("nullrun.breaker.exceptions", "WorkflowPausedException"),
|
|
227
|
+
"WorkflowKilledException": ("nullrun.breaker.exceptions", "WorkflowKilledException"),
|
|
228
|
+
"WorkflowKilledInterrupt": ("nullrun.breaker.exceptions", "WorkflowKilledInterrupt"),
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def __getattr__(name: str):
|
|
233
|
+
"""PEP 562 — lazy attribute access for backward-compatible symbols."""
|
|
234
|
+
if name in _LAZY_EXPORTS:
|
|
235
|
+
module_path, attr_name = _LAZY_EXPORTS[name]
|
|
236
|
+
module = __import__(module_path, fromlist=[attr_name])
|
|
237
|
+
value = getattr(module, attr_name)
|
|
238
|
+
# Cache on the module so subsequent lookups are O(1) and
|
|
239
|
+
# dir(nullrun) still reports the curated public surface until
|
|
240
|
+
# the legacy name is actually accessed.
|
|
241
|
+
globals()[name] = value
|
|
242
|
+
return value
|
|
243
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def __dir__() -> list[str]:
|
|
247
|
+
"""PEP 562 — `dir(nullrun)` only shows the curated public surface.
|
|
248
|
+
|
|
249
|
+
We deliberately ignore `globals()` here so that auto-imported
|
|
250
|
+
submodules (`nullrun.decorators`, `nullrun.runtime`, etc.) and any
|
|
251
|
+
side-effect imports do NOT leak into the public namespace. Users
|
|
252
|
+
who want internals can still reach them via `from nullrun import X`
|
|
253
|
+
(see `_LAZY_EXPORTS` in `__getattr__`) — `dir()` is for discovery,
|
|
254
|
+
not for reachability.
|
|
255
|
+
"""
|
|
256
|
+
return sorted(__all__)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
__all__ = [
|
|
260
|
+
# Version (single value, always public)
|
|
261
|
+
"__version__",
|
|
262
|
+
|
|
263
|
+
# Phase 3.4: the curated public surface — six symbols.
|
|
264
|
+
# Everything else stays importable as `from nullrun import X` for
|
|
265
|
+
# backward compatibility, but does NOT appear in `dir(nullrun)`
|
|
266
|
+
# until the user actually accesses it.
|
|
267
|
+
"init",
|
|
268
|
+
"protect", # gate decorator
|
|
269
|
+
"track_llm",
|
|
270
|
+
"track_tool",
|
|
271
|
+
"track_event",
|
|
272
|
+
]
|
|
273
|
+
|
|
274
|
+
# Sprint 2.1: the SDK-side ``decision_history`` module was deleted.
|
|
275
|
+
# Decision history is a backend + dashboard surface only — the SDK
|
|
276
|
+
# does not (and cannot) replay LLM calls because NULLRUN does not
|
|
277
|
+
# store request/response payloads or hold client LLM keys. The
|
|
278
|
+
# orphan ``start_recording`` / ``stop_recording`` methods on
|
|
279
|
+
# ``NullRunRuntime`` are kept as no-op stubs for one minor version
|
|
280
|
+
# for backward compatibility; they will be removed in 0.5.0.
|
|
281
|
+
# Do NOT re-export ReplayManager / ReplaySession / ReplayEvent /
|
|
282
|
+
# EventRecorder.
|
nullrun/__version__.py
ADDED