toolbreaker
Why this exists
AI agents don't fail gracefully. When an upstream tool returns truncated JSON, a 429 Too Many Requests, or a 502 Bad Gateway, the agent doesn't back off — it retries. And retries. And retries. Burning tokens, burning time, and burning your budget on infinite retry loops that never change their arguments.
We call this "tool rot": the silent degradation of agent reliability when external tools fail in realistic ways. Unit tests with 200 OK responses don't catch it. Integration tests against a live MCP server don't catch it either, because they pass until they don't.
toolbreaker is a chaos-engineering harness and circuit breaker for AI agent tools. You wrap your tool functions (sync or async), inject realistic faults, and watch your agent fail — before it fails in production.
What it does
- Injects faults — truncated JSON, HTTP 429s, latency spikes, stack-trace bombs, empty
{"data": null}responses, and server errors - Detects retry loops — trips
AgentRetryLoopErrorwhen an agent calls the same failing tool with the same arguments too many times - Preserves signatures —
inspect.signature(fn)still works on wrapped functions, so LangChain, MCP, CrewAI, and OpenAI function-calling schemas keep working - Deterministic chaos — a seeded
random.Randommakes CI runs reproducible
Install
pip install toolbreaker
Quickstart
toolbreaker demo
This runs a zero-config simulation comparing a naive retry agent against a resilient one:
┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓
┃ Tool ┃ Injected Fault ┃ Calls Made ┃ Loop Status ┃ Verdict ┃
┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩
│ Naive Retry Agent │ server_fault │ 3 │ Caught │ LOOP CAUGHT │
│ Resilient Agent │ server_fault │ 3 │ Caught │ LOOP CAUGHT │
└───────────────────┴────────────────┴────────────┴─────────────┴─────────────┘
Pytest plugin
Add --toolbreaker-seed to any test run:
pytest --toolbreaker-seed=42
Use the toolbreaker fixture in your tests:
def test_agent_handles_truncation(toolbreaker):
harness = toolbreaker # RotHarness, seeded from --toolbreaker-seed (default 42)
wrapped_tool = harness.wrap(my_mcp_tool)
result = wrapped_tool({"query": "users"})
assert "error" in result or result is not None
If a test fails with AgentRetryLoopError, pytest appends a reproduction line:
Reproduce with: pytest --toolbreaker-seed=42
MCP wrapper example
from toolbreaker import RotHarness, truncate_json, rate_limit
def call_tool(name: str, arguments: dict):
# your MCP dispatch logic
...
harness = RotHarness(
faults=[truncate_json(ratio=0.5), rate_limit(retry_after=30)],
seed=42,
)
wrapped = harness.wrap_mcp(call_tool)
wrap_mcp accepts a single call_tool(name, arguments) function or a dict of {tool_name: callable}.
CLI: toolbreaker run
Inspect how your function's output mutates under every built-in fault:
toolbreaker run my_module:my_function
API
from toolbreaker import (
RotHarness,
CircuitBreaker,
AgentRetryLoopError,
truncate_json,
rate_limit,
payload_bomb,
latency_spike,
empty_lie,
server_fault,
)
| Fault | What it simulates |
|---|---|
truncate_json(ratio=0.5) |
Mid-stream socket drop — output cut in half |
rate_limit(retry_after=30) |
MCP/HTTP 429 — return dict or raise |
payload_bomb(size_kb=128) |
Stack-trace + memory dump — context window overflow |
latency_spike(seconds=2.0) |
Upstream latency — blocking sleep before return |
empty_lie() |
{"status": "success", "data": None} — hallucination bait |
server_fault(code=502) |
Bad Gateway — upstream process crashed |
CI
GitHub Actions runs ruff check, ruff format --check, and pytest on Python 3.11, 3.12, and 3.13. Publishing to PyPI happens automatically on GitHub Release via OIDC Trusted Publishing.
License
MIT.# toolbreaker
Metadata
Release files for toolbreaker 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| toolbreaker-0.1.0.tar.gz | 9.1 kB | Without provenance | Details for toolbreaker-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| toolbreaker-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for toolbreaker-0.1.0-py3-none-any.whl |
Total release size: 19.2 kB
Release files / toolbreaker-0.1.0.tar.gz
| Download URL | toolbreaker-0.1.0.tar.gz |
|---|---|
| Size | 9.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9507b1769dc5be54ef3a7d489f3c61b8973fb4f98ee6ecded7c34d37041b5bc6
|
|
BLAKE2b-256 checksum How to use checksums |
270b88e4b9727ba66b0f6eae41635674394ad367c234aaceef7ac1c135cb3ae3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.1
|
Release files / toolbreaker-0.1.0-py3-none-any.whl
| Download URL | toolbreaker-0.1.0-py3-none-any.whl |
|---|---|
| Size | 10.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8ae7baee1e419e2f90faddce1829ba9d7f3747bad0cb753506ad39f06ee7d510
|
|
BLAKE2b-256 checksum How to use checksums |
9c74e34d3d4f430953b1619f56c3fabb50f34ff319ea9c36458c67a638075ec2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.1
|
Security for toolbreaker 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.