runbound 0.3.0__tar.gz
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.
- runbound-0.3.0/LICENSE +21 -0
- runbound-0.3.0/PKG-INFO +198 -0
- runbound-0.3.0/README.md +168 -0
- runbound-0.3.0/pyproject.toml +48 -0
- runbound-0.3.0/runbound/__init__.py +89 -0
- runbound-0.3.0/runbound/_coverage.py +612 -0
- runbound-0.3.0/runbound/alerts.py +149 -0
- runbound-0.3.0/runbound/api.py +3016 -0
- runbound-0.3.0/runbound/autowrap.py +159 -0
- runbound-0.3.0/runbound/circuit.py +464 -0
- runbound-0.3.0/runbound/config.py +927 -0
- runbound-0.3.0/runbound/detectors.py +1118 -0
- runbound-0.3.0/runbound/engine.py +1657 -0
- runbound-0.3.0/runbound/events.py +152 -0
- runbound-0.3.0/runbound/exceptions.py +67 -0
- runbound-0.3.0/runbound/export.py +456 -0
- runbound-0.3.0/runbound/integrations/__init__.py +6 -0
- runbound-0.3.0/runbound/integrations/langchain.py +237 -0
- runbound-0.3.0/runbound/ladder.py +242 -0
- runbound-0.3.0/runbound/plane.py +329 -0
- runbound-0.3.0/runbound/plane_types.py +582 -0
- runbound-0.3.0/runbound/policy.py +684 -0
- runbound-0.3.0/runbound/pricing.py +425 -0
- runbound-0.3.0/runbound/quota.py +392 -0
- runbound-0.3.0/runbound/responses.py +381 -0
- runbound-0.3.0/runbound/shared.py +1370 -0
- runbound-0.3.0/runbound/state.py +383 -0
- runbound-0.3.0/runbound/wrappers/__init__.py +1385 -0
- runbound-0.3.0/runbound/wrappers/anthropic_wrapper.py +458 -0
- runbound-0.3.0/runbound/wrappers/openai_wrapper.py +690 -0
- runbound-0.3.0/runbound.egg-info/PKG-INFO +198 -0
- runbound-0.3.0/runbound.egg-info/SOURCES.txt +108 -0
- runbound-0.3.0/runbound.egg-info/dependency_links.txt +1 -0
- runbound-0.3.0/runbound.egg-info/requires.txt +9 -0
- runbound-0.3.0/runbound.egg-info/top_level.txt +1 -0
- runbound-0.3.0/setup.cfg +4 -0
- runbound-0.3.0/tests/test_admission.py +605 -0
- runbound-0.3.0/tests/test_alert_lifecycle.py +504 -0
- runbound-0.3.0/tests/test_anomaly_identity.py +206 -0
- runbound-0.3.0/tests/test_anthropic_thinking.py +96 -0
- runbound-0.3.0/tests/test_api.py +362 -0
- runbound-0.3.0/tests/test_async_throttle.py +182 -0
- runbound-0.3.0/tests/test_async_tool.py +218 -0
- runbound-0.3.0/tests/test_async_wrappers.py +288 -0
- runbound-0.3.0/tests/test_autowrap.py +481 -0
- runbound-0.3.0/tests/test_circuit.py +315 -0
- runbound-0.3.0/tests/test_circuit_force.py +297 -0
- runbound-0.3.0/tests/test_circuit_quota.py +691 -0
- runbound-0.3.0/tests/test_config.py +196 -0
- runbound-0.3.0/tests/test_config_plane.py +229 -0
- runbound-0.3.0/tests/test_coverage.py +429 -0
- runbound-0.3.0/tests/test_coverage_warnings.py +640 -0
- runbound-0.3.0/tests/test_detector_priority.py +158 -0
- runbound-0.3.0/tests/test_detectors.py +740 -0
- runbound-0.3.0/tests/test_docs_examples.py +233 -0
- runbound-0.3.0/tests/test_engine.py +217 -0
- runbound-0.3.0/tests/test_engine_observers.py +533 -0
- runbound-0.3.0/tests/test_error_storm.py +512 -0
- runbound-0.3.0/tests/test_export.py +491 -0
- runbound-0.3.0/tests/test_export_requeue.py +330 -0
- runbound-0.3.0/tests/test_fanout.py +352 -0
- runbound-0.3.0/tests/test_fleet_fixes.py +370 -0
- runbound-0.3.0/tests/test_hash_salt.py +121 -0
- runbound-0.3.0/tests/test_ladder.py +295 -0
- runbound-0.3.0/tests/test_ladder_api.py +471 -0
- runbound-0.3.0/tests/test_ladder_baseline.py +231 -0
- runbound-0.3.0/tests/test_ladder_detector.py +607 -0
- runbound-0.3.0/tests/test_ladder_why.py +438 -0
- runbound-0.3.0/tests/test_langchain.py +404 -0
- runbound-0.3.0/tests/test_latch.py +584 -0
- runbound-0.3.0/tests/test_latch_ttl.py +458 -0
- runbound-0.3.0/tests/test_loop_policy.py +459 -0
- runbound-0.3.0/tests/test_loop_repeatable.py +235 -0
- runbound-0.3.0/tests/test_on_trip.py +91 -0
- runbound-0.3.0/tests/test_openai_read_tool_requests.py +45 -0
- runbound-0.3.0/tests/test_plane_client.py +521 -0
- runbound-0.3.0/tests/test_plane_loss.py +519 -0
- runbound-0.3.0/tests/test_plane_modes.py +263 -0
- runbound-0.3.0/tests/test_plane_refusal_entry.py +164 -0
- runbound-0.3.0/tests/test_plane_types.py +668 -0
- runbound-0.3.0/tests/test_policy_core.py +553 -0
- runbound-0.3.0/tests/test_policy_hook.py +498 -0
- runbound-0.3.0/tests/test_policy_merge.py +408 -0
- runbound-0.3.0/tests/test_policy_merge_properties.py +258 -0
- runbound-0.3.0/tests/test_pricing.py +480 -0
- runbound-0.3.0/tests/test_privacy_wire.py +397 -0
- runbound-0.3.0/tests/test_quota.py +468 -0
- runbound-0.3.0/tests/test_real_sdk.py +733 -0
- runbound-0.3.0/tests/test_responses.py +635 -0
- runbound-0.3.0/tests/test_review_fixes_t59.py +267 -0
- runbound-0.3.0/tests/test_selfhosted.py +928 -0
- runbound-0.3.0/tests/test_session_sync.py +609 -0
- runbound-0.3.0/tests/test_sessions.py +722 -0
- runbound-0.3.0/tests/test_shared_state.py +855 -0
- runbound-0.3.0/tests/test_spike.py +604 -0
- runbound-0.3.0/tests/test_spike_tuning.py +358 -0
- runbound-0.3.0/tests/test_state.py +185 -0
- runbound-0.3.0/tests/test_streaming.py +592 -0
- runbound-0.3.0/tests/test_streams_abandoned.py +709 -0
- runbound-0.3.0/tests/test_time_scopes.py +147 -0
- runbound-0.3.0/tests/test_timeout_costcap.py +352 -0
- runbound-0.3.0/tests/test_token_and_delivery.py +349 -0
- runbound-0.3.0/tests/test_tool_report.py +482 -0
- runbound-0.3.0/tests/test_tool_requests.py +711 -0
- runbound-0.3.0/tests/test_tool_rules.py +703 -0
- runbound-0.3.0/tests/test_unpriced.py +462 -0
- runbound-0.3.0/tests/test_wall_sdk.py +555 -0
- runbound-0.3.0/tests/test_wrapper_errors.py +476 -0
- runbound-0.3.0/tests/test_wrapper_timing.py +743 -0
- runbound-0.3.0/tests/test_wrappers.py +282 -0
runbound-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Amanjot Singh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
runbound-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: runbound
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Deterministic, LLM-free runaway detection and circuit breaking for AI agents.
|
|
5
|
+
Author: Amanjot Singh
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://runbound.co
|
|
8
|
+
Project-URL: Documentation, https://runbound.co/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/runboundai/runbound
|
|
10
|
+
Project-URL: Changelog, https://github.com/runboundai/runbound/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: ai agents,observability,circuit breaker,runaway,cost control,llm
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: System :: Monitoring
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Provides-Extra: test
|
|
24
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
27
|
+
Provides-Extra: langchain
|
|
28
|
+
Requires-Dist: langchain-core; extra == "langchain"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# runbound
|
|
32
|
+
|
|
33
|
+
**Runtime controls for autonomous AI agents.** runbound runs inside your
|
|
34
|
+
agent's process and stops runaway cost, loops, tool abuse and provider
|
|
35
|
+
failures deterministically, by counting, timing and hashing, never by asking a
|
|
36
|
+
model whether something looks wrong. It reads no prompts and no replies, it
|
|
37
|
+
puts no gateway in your hot path, and it never speaks to your users: when it
|
|
38
|
+
stops something it hands control back to you and you answer in your own voice.
|
|
39
|
+
|
|
40
|
+
This is for the platform team that owns AI agents in production, the people
|
|
41
|
+
who get paged when one runs away, not the people who wrote its prompt.
|
|
42
|
+
|
|
43
|
+
One agent burned **$2,847 in four hours** on a refactoring loop while every
|
|
44
|
+
monitoring dashboard stayed green. Nobody was watching it at 3am, and nothing
|
|
45
|
+
in the stack had the authority to pull the plug. runbound is the layer with
|
|
46
|
+
the authority to pull the plug, and the proof that it did — the whole story is
|
|
47
|
+
in [Why this exists](docs/concepts/why.md).
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install runbound
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
There are no runtime dependencies — stdlib only. LangChain support is the one
|
|
56
|
+
extra: `pip install "runbound[langchain]"`.
|
|
57
|
+
|
|
58
|
+
## Quick start
|
|
59
|
+
|
|
60
|
+
Guard every model call in the process:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
import runbound
|
|
64
|
+
runbound.init(budget_usd=5.0, on_anomaly="raise") # everything below is now guarded
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Scope every control to one unit of work:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
with runbound.session(f"run:{run_id}"):
|
|
71
|
+
answer = client.chat.completions.create(...)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Refuse an action before its function body runs:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
@runbound.tool(max_calls=1) # the rule lives on the tool, in the same diff
|
|
78
|
+
def issue_refund(user: str, amount: float): ...
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The full five-minute version — wiring all three sensors, checking what is
|
|
82
|
+
actually guarded, catching the trip, and the offline demos that need no API
|
|
83
|
+
key — is in [Getting started](docs/getting-started.md).
|
|
84
|
+
|
|
85
|
+
## Three levels of protection
|
|
86
|
+
|
|
87
|
+
You choose how far in to go. Each level is a small, separate step, and most of
|
|
88
|
+
the value arrives at the first one. The full text is in
|
|
89
|
+
[Three levels of protection](docs/concepts/three-levels.md).
|
|
90
|
+
|
|
91
|
+
**Level 1: guard the model calls.** Two lines. `runbound.init()` wraps the
|
|
92
|
+
OpenAI and Anthropic clients for you (or you call `runbound.wrap(client)` on
|
|
93
|
+
any client with that shape). From then on every model call is counted, timed
|
|
94
|
+
and priced, and you get: dollar and token budgets, per-call caps, velocity
|
|
95
|
+
limits, step and wall-clock limits, the spike ladder, error-storm detection
|
|
96
|
+
with a circuit per provider, and refusal profiles that carry your own status
|
|
97
|
+
and sentence. All of that is what the free SDK does on its own, with no
|
|
98
|
+
network call and no account. Nothing is decorated, no policy is written. See
|
|
99
|
+
[What it controls](docs/concepts/what-it-controls.md).
|
|
100
|
+
|
|
101
|
+
**Level 2: one session per run.** One line per unit of work. Wrap each run in
|
|
102
|
+
`runbound.session(key)` — the key is any id you already have: a run id, a job,
|
|
103
|
+
a tenant, a customer — and every control above becomes per key instead of per
|
|
104
|
+
process: this run's budget, this run's ladder, this run stopped, every other
|
|
105
|
+
caller untouched. See [Runs keyed by any id](docs/guides/runs.md).
|
|
106
|
+
|
|
107
|
+
**Level 3: refuse actions before they run.** Only for the tools that matter.
|
|
108
|
+
Decorate the two or three functions that are irreversible or reach the outside
|
|
109
|
+
world with `@runbound.tool`, write a policy (deny this tool, at most once per
|
|
110
|
+
run, needs approval, or a predicate of your own over the arguments, run in
|
|
111
|
+
your process), and the call is refused *before the function body runs*. The
|
|
112
|
+
rules are yours; runbound enforces and records them, it never judges the
|
|
113
|
+
action. See [Action policy](docs/guides/policy.md).
|
|
114
|
+
|
|
115
|
+
Nothing here protects against a *bad answer*: hallucinations, prompt injection
|
|
116
|
+
and output quality are out of scope by design. "The agent said something
|
|
117
|
+
wrong" is a different product. "The agent would not stop" is this one.
|
|
118
|
+
|
|
119
|
+
## What it never sees
|
|
120
|
+
|
|
121
|
+
- **Prompts and model replies are never read, stored, or sent** — except the
|
|
122
|
+
one narrow case of `estimate_tokens=True`, which counts characters when the
|
|
123
|
+
server reported no usage.
|
|
124
|
+
- **Tool arguments are sha256-hashed before storage, salted per process.** The
|
|
125
|
+
loop detector compares digests of `(tool_name, args, sorted kwargs)` mixed
|
|
126
|
+
with a random salt generated once at import, never the arguments themselves.
|
|
127
|
+
- **Raw arguments never leave your process.** They are not stored, not logged,
|
|
128
|
+
and not included in any alert payload.
|
|
129
|
+
- **A session key reaches a connected plane as a hash.** The plane gets
|
|
130
|
+
`sha256(key)` and nothing else, unless you opt in with
|
|
131
|
+
`send_session_keys=True`.
|
|
132
|
+
- **Failures are recorded as the exception's class name.** Only the
|
|
133
|
+
exception's class name, never its message, is what reaches the plane.
|
|
134
|
+
- **No network calls except the ones you configure.** No telemetry, no
|
|
135
|
+
phone-home, no hosted backend you did not point us at. With no `token` and
|
|
136
|
+
no `control_plane_url` set, runbound opens no sockets at all.
|
|
137
|
+
|
|
138
|
+
The full accounting — every number, where it comes from, and what goes blind
|
|
139
|
+
without each sensor — is in
|
|
140
|
+
[What the SDK actually sees](docs/concepts/what-it-sees.md) and
|
|
141
|
+
[Privacy](docs/reference/privacy.md).
|
|
142
|
+
|
|
143
|
+
## Documentation
|
|
144
|
+
|
|
145
|
+
The manual lives in [`docs/`](docs/README.md); it is also published, with
|
|
146
|
+
executed snippets, at [runbound.co/docs](https://runbound.co/docs).
|
|
147
|
+
|
|
148
|
+
- **[Getting started](docs/getting-started.md)** — install, wire the three
|
|
149
|
+
sensors around one agent run, and check what is actually guarded.
|
|
150
|
+
- **[Concepts](docs/README.md#concepts)** — the three levels, why this exists,
|
|
151
|
+
how it works, what it controls, and exactly what the SDK sees and never
|
|
152
|
+
sees.
|
|
153
|
+
- **[Guides](docs/README.md#guides)** — keyed runs, fleet mode, spike
|
|
154
|
+
detection, action policy, self-hosted models, LangChain, async and
|
|
155
|
+
streaming.
|
|
156
|
+
- **[Reference](docs/README.md#reference)** — every configuration field, the
|
|
157
|
+
detectors, what happens when something trips, the circuit breaker, limits,
|
|
158
|
+
the compatibility matrix, privacy, and the guarantees.
|
|
159
|
+
|
|
160
|
+
[Roadmap](docs/roadmap.md) is what is shipped, what is still open, and what is
|
|
161
|
+
explicitly out of scope.
|
|
162
|
+
|
|
163
|
+
## Getting help
|
|
164
|
+
|
|
165
|
+
**Something not working, or a question?** Open an issue at
|
|
166
|
+
[github.com/runboundai/runbound/issues](https://github.com/runboundai/runbound/issues),
|
|
167
|
+
or email **runboundai@gmail.com** if it is not something you can post in
|
|
168
|
+
public.
|
|
169
|
+
|
|
170
|
+
The two things that let us answer on the first reply instead of the third:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
import runbound
|
|
174
|
+
print(runbound.__version__)
|
|
175
|
+
print(runbound.coverage()) # after init() and your first guarded call
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`coverage()` is plain numbers and names — which provider classes were
|
|
179
|
+
patched, how many calls and tools runbound saw, which imported providers
|
|
180
|
+
nothing is guarding, and any silent-zero warning — so it shows at a glance
|
|
181
|
+
whether runbound can see your traffic at all, which is the most common
|
|
182
|
+
cause of "it isn't catching anything". It lists your decorated tools'
|
|
183
|
+
names; remove any you would rather not post. Please also say your Python
|
|
184
|
+
version, your `openai` / `anthropic` versions, and whether you run with a
|
|
185
|
+
control plane (`token` / `control_plane_url`) or locally.
|
|
186
|
+
|
|
187
|
+
**Never paste** an API key, a control-plane token, a prompt, a reply, or a
|
|
188
|
+
real session key into an issue.
|
|
189
|
+
|
|
190
|
+
**Security issues go to email, not to an issue** — see
|
|
191
|
+
[SECURITY.md](SECURITY.md) for what to send and the disclosure window.
|
|
192
|
+
|
|
193
|
+
## License
|
|
194
|
+
|
|
195
|
+
MIT — see [LICENSE](LICENSE). Release history is in
|
|
196
|
+
[CHANGELOG.md](CHANGELOG.md). The promises above, stated as bounds with the
|
|
197
|
+
tests that assert them (and where nothing does yet), are in
|
|
198
|
+
[INVARIANTS.md](INVARIANTS.md).
|
runbound-0.3.0/README.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# runbound
|
|
2
|
+
|
|
3
|
+
**Runtime controls for autonomous AI agents.** runbound runs inside your
|
|
4
|
+
agent's process and stops runaway cost, loops, tool abuse and provider
|
|
5
|
+
failures deterministically, by counting, timing and hashing, never by asking a
|
|
6
|
+
model whether something looks wrong. It reads no prompts and no replies, it
|
|
7
|
+
puts no gateway in your hot path, and it never speaks to your users: when it
|
|
8
|
+
stops something it hands control back to you and you answer in your own voice.
|
|
9
|
+
|
|
10
|
+
This is for the platform team that owns AI agents in production, the people
|
|
11
|
+
who get paged when one runs away, not the people who wrote its prompt.
|
|
12
|
+
|
|
13
|
+
One agent burned **$2,847 in four hours** on a refactoring loop while every
|
|
14
|
+
monitoring dashboard stayed green. Nobody was watching it at 3am, and nothing
|
|
15
|
+
in the stack had the authority to pull the plug. runbound is the layer with
|
|
16
|
+
the authority to pull the plug, and the proof that it did — the whole story is
|
|
17
|
+
in [Why this exists](docs/concepts/why.md).
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install runbound
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
There are no runtime dependencies — stdlib only. LangChain support is the one
|
|
26
|
+
extra: `pip install "runbound[langchain]"`.
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
Guard every model call in the process:
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
import runbound
|
|
34
|
+
runbound.init(budget_usd=5.0, on_anomaly="raise") # everything below is now guarded
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Scope every control to one unit of work:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
with runbound.session(f"run:{run_id}"):
|
|
41
|
+
answer = client.chat.completions.create(...)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Refuse an action before its function body runs:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
@runbound.tool(max_calls=1) # the rule lives on the tool, in the same diff
|
|
48
|
+
def issue_refund(user: str, amount: float): ...
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The full five-minute version — wiring all three sensors, checking what is
|
|
52
|
+
actually guarded, catching the trip, and the offline demos that need no API
|
|
53
|
+
key — is in [Getting started](docs/getting-started.md).
|
|
54
|
+
|
|
55
|
+
## Three levels of protection
|
|
56
|
+
|
|
57
|
+
You choose how far in to go. Each level is a small, separate step, and most of
|
|
58
|
+
the value arrives at the first one. The full text is in
|
|
59
|
+
[Three levels of protection](docs/concepts/three-levels.md).
|
|
60
|
+
|
|
61
|
+
**Level 1: guard the model calls.** Two lines. `runbound.init()` wraps the
|
|
62
|
+
OpenAI and Anthropic clients for you (or you call `runbound.wrap(client)` on
|
|
63
|
+
any client with that shape). From then on every model call is counted, timed
|
|
64
|
+
and priced, and you get: dollar and token budgets, per-call caps, velocity
|
|
65
|
+
limits, step and wall-clock limits, the spike ladder, error-storm detection
|
|
66
|
+
with a circuit per provider, and refusal profiles that carry your own status
|
|
67
|
+
and sentence. All of that is what the free SDK does on its own, with no
|
|
68
|
+
network call and no account. Nothing is decorated, no policy is written. See
|
|
69
|
+
[What it controls](docs/concepts/what-it-controls.md).
|
|
70
|
+
|
|
71
|
+
**Level 2: one session per run.** One line per unit of work. Wrap each run in
|
|
72
|
+
`runbound.session(key)` — the key is any id you already have: a run id, a job,
|
|
73
|
+
a tenant, a customer — and every control above becomes per key instead of per
|
|
74
|
+
process: this run's budget, this run's ladder, this run stopped, every other
|
|
75
|
+
caller untouched. See [Runs keyed by any id](docs/guides/runs.md).
|
|
76
|
+
|
|
77
|
+
**Level 3: refuse actions before they run.** Only for the tools that matter.
|
|
78
|
+
Decorate the two or three functions that are irreversible or reach the outside
|
|
79
|
+
world with `@runbound.tool`, write a policy (deny this tool, at most once per
|
|
80
|
+
run, needs approval, or a predicate of your own over the arguments, run in
|
|
81
|
+
your process), and the call is refused *before the function body runs*. The
|
|
82
|
+
rules are yours; runbound enforces and records them, it never judges the
|
|
83
|
+
action. See [Action policy](docs/guides/policy.md).
|
|
84
|
+
|
|
85
|
+
Nothing here protects against a *bad answer*: hallucinations, prompt injection
|
|
86
|
+
and output quality are out of scope by design. "The agent said something
|
|
87
|
+
wrong" is a different product. "The agent would not stop" is this one.
|
|
88
|
+
|
|
89
|
+
## What it never sees
|
|
90
|
+
|
|
91
|
+
- **Prompts and model replies are never read, stored, or sent** — except the
|
|
92
|
+
one narrow case of `estimate_tokens=True`, which counts characters when the
|
|
93
|
+
server reported no usage.
|
|
94
|
+
- **Tool arguments are sha256-hashed before storage, salted per process.** The
|
|
95
|
+
loop detector compares digests of `(tool_name, args, sorted kwargs)` mixed
|
|
96
|
+
with a random salt generated once at import, never the arguments themselves.
|
|
97
|
+
- **Raw arguments never leave your process.** They are not stored, not logged,
|
|
98
|
+
and not included in any alert payload.
|
|
99
|
+
- **A session key reaches a connected plane as a hash.** The plane gets
|
|
100
|
+
`sha256(key)` and nothing else, unless you opt in with
|
|
101
|
+
`send_session_keys=True`.
|
|
102
|
+
- **Failures are recorded as the exception's class name.** Only the
|
|
103
|
+
exception's class name, never its message, is what reaches the plane.
|
|
104
|
+
- **No network calls except the ones you configure.** No telemetry, no
|
|
105
|
+
phone-home, no hosted backend you did not point us at. With no `token` and
|
|
106
|
+
no `control_plane_url` set, runbound opens no sockets at all.
|
|
107
|
+
|
|
108
|
+
The full accounting — every number, where it comes from, and what goes blind
|
|
109
|
+
without each sensor — is in
|
|
110
|
+
[What the SDK actually sees](docs/concepts/what-it-sees.md) and
|
|
111
|
+
[Privacy](docs/reference/privacy.md).
|
|
112
|
+
|
|
113
|
+
## Documentation
|
|
114
|
+
|
|
115
|
+
The manual lives in [`docs/`](docs/README.md); it is also published, with
|
|
116
|
+
executed snippets, at [runbound.co/docs](https://runbound.co/docs).
|
|
117
|
+
|
|
118
|
+
- **[Getting started](docs/getting-started.md)** — install, wire the three
|
|
119
|
+
sensors around one agent run, and check what is actually guarded.
|
|
120
|
+
- **[Concepts](docs/README.md#concepts)** — the three levels, why this exists,
|
|
121
|
+
how it works, what it controls, and exactly what the SDK sees and never
|
|
122
|
+
sees.
|
|
123
|
+
- **[Guides](docs/README.md#guides)** — keyed runs, fleet mode, spike
|
|
124
|
+
detection, action policy, self-hosted models, LangChain, async and
|
|
125
|
+
streaming.
|
|
126
|
+
- **[Reference](docs/README.md#reference)** — every configuration field, the
|
|
127
|
+
detectors, what happens when something trips, the circuit breaker, limits,
|
|
128
|
+
the compatibility matrix, privacy, and the guarantees.
|
|
129
|
+
|
|
130
|
+
[Roadmap](docs/roadmap.md) is what is shipped, what is still open, and what is
|
|
131
|
+
explicitly out of scope.
|
|
132
|
+
|
|
133
|
+
## Getting help
|
|
134
|
+
|
|
135
|
+
**Something not working, or a question?** Open an issue at
|
|
136
|
+
[github.com/runboundai/runbound/issues](https://github.com/runboundai/runbound/issues),
|
|
137
|
+
or email **runboundai@gmail.com** if it is not something you can post in
|
|
138
|
+
public.
|
|
139
|
+
|
|
140
|
+
The two things that let us answer on the first reply instead of the third:
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
import runbound
|
|
144
|
+
print(runbound.__version__)
|
|
145
|
+
print(runbound.coverage()) # after init() and your first guarded call
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`coverage()` is plain numbers and names — which provider classes were
|
|
149
|
+
patched, how many calls and tools runbound saw, which imported providers
|
|
150
|
+
nothing is guarding, and any silent-zero warning — so it shows at a glance
|
|
151
|
+
whether runbound can see your traffic at all, which is the most common
|
|
152
|
+
cause of "it isn't catching anything". It lists your decorated tools'
|
|
153
|
+
names; remove any you would rather not post. Please also say your Python
|
|
154
|
+
version, your `openai` / `anthropic` versions, and whether you run with a
|
|
155
|
+
control plane (`token` / `control_plane_url`) or locally.
|
|
156
|
+
|
|
157
|
+
**Never paste** an API key, a control-plane token, a prompt, a reply, or a
|
|
158
|
+
real session key into an issue.
|
|
159
|
+
|
|
160
|
+
**Security issues go to email, not to an issue** — see
|
|
161
|
+
[SECURITY.md](SECURITY.md) for what to send and the disclosure window.
|
|
162
|
+
|
|
163
|
+
## License
|
|
164
|
+
|
|
165
|
+
MIT — see [LICENSE](LICENSE). Release history is in
|
|
166
|
+
[CHANGELOG.md](CHANGELOG.md). The promises above, stated as bounds with the
|
|
167
|
+
tests that assert them (and where nothing does yet), are in
|
|
168
|
+
[INVARIANTS.md](INVARIANTS.md).
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "runbound"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Deterministic, LLM-free runaway detection and circuit breaking for AI agents."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Amanjot Singh" }]
|
|
13
|
+
keywords = [
|
|
14
|
+
"ai agents",
|
|
15
|
+
"observability",
|
|
16
|
+
"circuit breaker",
|
|
17
|
+
"runaway",
|
|
18
|
+
"cost control",
|
|
19
|
+
"llm",
|
|
20
|
+
]
|
|
21
|
+
classifiers = [
|
|
22
|
+
"Development Status :: 4 - Beta",
|
|
23
|
+
"Intended Audience :: Developers",
|
|
24
|
+
"License :: OSI Approved :: MIT License",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"Programming Language :: Python :: 3.13",
|
|
29
|
+
"Topic :: System :: Monitoring",
|
|
30
|
+
]
|
|
31
|
+
dependencies = []
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://runbound.co"
|
|
35
|
+
Documentation = "https://runbound.co/docs"
|
|
36
|
+
Repository = "https://github.com/runboundai/runbound"
|
|
37
|
+
Changelog = "https://github.com/runboundai/runbound/blob/main/CHANGELOG.md"
|
|
38
|
+
|
|
39
|
+
[project.optional-dependencies]
|
|
40
|
+
test = ["pytest>=7.0"]
|
|
41
|
+
dev = ["pytest>=7.0"]
|
|
42
|
+
langchain = ["langchain-core"]
|
|
43
|
+
|
|
44
|
+
[tool.setuptools]
|
|
45
|
+
packages = ["runbound", "runbound.wrappers", "runbound.integrations"]
|
|
46
|
+
|
|
47
|
+
[tool.pytest.ini_options]
|
|
48
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""runbound — deterministic, LLM-free runaway detection for AI agents.
|
|
2
|
+
|
|
3
|
+
Three lines to guard an agent::
|
|
4
|
+
|
|
5
|
+
import runbound
|
|
6
|
+
|
|
7
|
+
runbound.init(budget_usd=5.0, max_steps=50, on_anomaly="raise")
|
|
8
|
+
client = runbound.wrap(client) # OpenAI- or Anthropic-shaped
|
|
9
|
+
|
|
10
|
+
@runbound.tool # every tool call is recorded
|
|
11
|
+
def search(query): ...
|
|
12
|
+
|
|
13
|
+
Fail-open by design: runbound's own bugs are logged and swallowed, and the
|
|
14
|
+
only exception it raises on purpose is :class:`GuardrailTripped`.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from .alerts import verify_webhook_signature
|
|
18
|
+
from .api import (
|
|
19
|
+
active_sessions,
|
|
20
|
+
assert_guarded,
|
|
21
|
+
circuit_state,
|
|
22
|
+
clear,
|
|
23
|
+
coverage,
|
|
24
|
+
current_session,
|
|
25
|
+
fleet_status,
|
|
26
|
+
inflight_calls,
|
|
27
|
+
init,
|
|
28
|
+
is_tripped,
|
|
29
|
+
key_hash,
|
|
30
|
+
llm,
|
|
31
|
+
plane_status,
|
|
32
|
+
record_call,
|
|
33
|
+
reset,
|
|
34
|
+
session,
|
|
35
|
+
session_status,
|
|
36
|
+
tool,
|
|
37
|
+
tool_calls,
|
|
38
|
+
tools,
|
|
39
|
+
unpatch,
|
|
40
|
+
wrap,
|
|
41
|
+
)
|
|
42
|
+
from .config import GuardrailConfig
|
|
43
|
+
from .events import Anomaly, Event
|
|
44
|
+
from .exceptions import CircuitOpen, GuardrailTripped, PolicyViolation
|
|
45
|
+
from .plane_types import PlaneStatus
|
|
46
|
+
from .policy import ToolCall, ToolPolicy, Violation
|
|
47
|
+
from .responses import Refusal
|
|
48
|
+
from .state import SessionState
|
|
49
|
+
|
|
50
|
+
__version__ = "0.3.0"
|
|
51
|
+
|
|
52
|
+
__all__ = [
|
|
53
|
+
"Anomaly",
|
|
54
|
+
"CircuitOpen",
|
|
55
|
+
"Event",
|
|
56
|
+
"GuardrailConfig",
|
|
57
|
+
"GuardrailTripped",
|
|
58
|
+
"PlaneStatus",
|
|
59
|
+
"PolicyViolation",
|
|
60
|
+
"Refusal",
|
|
61
|
+
"SessionState",
|
|
62
|
+
"ToolCall",
|
|
63
|
+
"ToolPolicy",
|
|
64
|
+
"Violation",
|
|
65
|
+
"__version__",
|
|
66
|
+
"active_sessions",
|
|
67
|
+
"assert_guarded",
|
|
68
|
+
"circuit_state",
|
|
69
|
+
"clear",
|
|
70
|
+
"coverage",
|
|
71
|
+
"current_session",
|
|
72
|
+
"fleet_status",
|
|
73
|
+
"inflight_calls",
|
|
74
|
+
"init",
|
|
75
|
+
"is_tripped",
|
|
76
|
+
"key_hash",
|
|
77
|
+
"llm",
|
|
78
|
+
"plane_status",
|
|
79
|
+
"record_call",
|
|
80
|
+
"reset",
|
|
81
|
+
"session",
|
|
82
|
+
"session_status",
|
|
83
|
+
"tool",
|
|
84
|
+
"tool_calls",
|
|
85
|
+
"tools",
|
|
86
|
+
"unpatch",
|
|
87
|
+
"verify_webhook_signature",
|
|
88
|
+
"wrap",
|
|
89
|
+
]
|