Runtime circuit breakers for AI agents: trip fuses on runaway cost, loops, and retry storms.
Project description
RunFuse ⚡
Runtime circuit breakers for AI agents. Trip fuses on runaway cost, infinite loops, and retry storms — before the invoice does it for you.
from runfuse import Fuse, FusePolicy
from anthropic import Anthropic
fuse = Fuse(FusePolicy(
max_cost_usd=5.00, # hard spend ceiling for this run
max_steps=50, # max LLM calls
max_identical_tool_calls=10, # loop detector
max_llm_errors=5, # retry-storm detector
))
client = fuse.wrap(Anthropic()) # AsyncAnthropic / (Async)OpenAI work too
with fuse.run("research-task"):
... # your agent loop — trips raise FuseTripped, run stops
with fuse.run("untrusted-task", policy=FusePolicy(max_cost_usd=0.50)):
... # per-run override: tighter budget for this run only
Why
Agents aren't chat. One user request fans out into dozens–hundreds of autonomous LLM calls. When an agent enters a retry loop or a planning spiral, nothing stops it — it burns tokens until someone notices the bill.
Existing tools don't cover this:
- LLM gateways (LiteLLM, Portkey, Helicone) enforce budgets per API key. They see isolated HTTP requests — they can't tell "this run is looping" or kill one sick agent without killing all of them.
- Observability platforms (LangSmith, Langfuse) are passive — they show you the fire afterwards.
RunFuse enforces at the run level, actively, mid-flight. The with fuse.run(...) scope is the identity gateways don't have.
Install
pip install runfuse # core (pydantic only)
pip install runfuse[anthropic] # + Anthropic SDK
pip install runfuse[openai] # + OpenAI SDK
pip install runfuse[langgraph] # + LangGraph/LangChain callback
pip install runfuse[dashboard] # + live web dashboard
Using LangGraph instead of a raw SDK client? Same fuse, one callback:
from runfuse.interceptors.langgraph import RunFuseCallback
with fuse.run("graph-task"):
graph.invoke(inputs, config={"callbacks": [RunFuseCallback(fuse)]})
Tools the interceptor can't see (MCP server handlers, framework-executed tools)? Guard them directly — trips fire before the tool body runs:
@fuse.guard_tool
def web_search(query: str) -> str: ...
Fuses
| Limit | Catches |
|---|---|
max_cost_usd |
runaway spend |
max_total_tokens |
token blowout |
max_steps |
unbounded agent loops |
max_wall_time_s |
hung runs |
max_llm_errors |
retry storms |
max_identical_tool_calls |
the agent calling the same tool with the same args, forever |
max_burn_rate_usd_per_min |
spend spikes — $/min over a trailing window (burn_rate_window_s, default 60s) trips long before the total cap would |
max_similar_tool_calls |
fuzzy loops — retries with slightly varied args counted together (similarity_threshold, default 0.9) |
Every hard limit also has a soft threshold (default 80%): a warning callback fires before the fuse blows, once per fuse per run.
Costs come from a built-in pricing table (override via Fuse(pricing={...})). Unknown models count as $0 with a logged warning — set strict_pricing=True on the policy to hard-trip instead of running cost-blind.
fuse = Fuse(
policy,
on_soft_trip=lambda verdict, state: notify_slack(verdict),
)
Trip semantics — honest by design
A trip raises FuseTripped at the next interception point (the next LLM call). It cannot stop a tool mid-execution — nothing can, and tools that claim otherwise are lying. The same honesty applies to blind spots: streaming calls are refused loudly (stream=True raises) rather than silently recorded as $0, until true streaming accounting ships. The exception carries the full verdict and run state:
try:
with fuse.run("job-42") as state:
agent_loop(client)
except FuseTripped as trip:
print(trip.verdict) # [hard] max_cost_usd: spent $5.0012 >= $5.0000
print(state.steps) # 37
print(state.cost_usd) # 5.0012
See it trip
No API key needed:
git clone https://github.com/akshat333-debug/RunFuse && cd RunFuse
uv sync --all-extras
uv run runfuse demo # scripted runaway, fuse trips
uv run runfuse report # per-run cost/step/trip summary
uv run runfuse dashboard # live web UI (localhost:8321)
The dashboard's abort button writes a flag the fuse checks at the run's next interception point — same honest semantics as any other trip. Approve is implicit: a soft-warned run keeps going unless you kill it.
How it works
fuse.wrap(client) → every LLM call emits an Event
│
▼
RunState (cost, steps, errors, tool-call counts)
│
▼
PolicyEngine → ok | soft trip (warn) | hard trip (raise)
│
▼
SQLite event store → `runfuse report` / dashboard
Tool calls are parsed from LLM response payloads (tool_use blocks / tool_calls), so loop detection works from the SDK wrapper alone — no framework hooks required. Runs are scoped via contextvars, so concurrent async runs stay independent.
Roadmap
- Anthropic + OpenAI SDK wrappers
- Cost / step / time / error / loop fuses, soft thresholds
- SQLite event store + CLI report
- Burn-rate fuse (trailing-window $/min cap)
- Fuzzy loop detection (near-identical args)
-
runfuse demoone-command demo - Live dashboard (FastAPI + HTMX) with abort — a warned run continues unless you kill it
- LangGraph/LangChain callback integration
-
guard_tooldecorator — loop/abort enforcement for MCP handlers and framework-executed tools - PyPI release
Development
uv sync --all-extras
uv run pytest
uv run mypy src
uv run ruff check src tests
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file runfuse-0.1.0.tar.gz.
File metadata
- Download URL: runfuse-0.1.0.tar.gz
- Upload date:
- Size: 21.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1ab5276054b6f4c3fd65af1c8d001fb36d4f31e028352c15aa82165b769c33f5
|
|
| MD5 |
f41d49e5829f2ad664129540605d0954
|
|
| BLAKE2b-256 |
a5accd820382b8a6006e0071365dd8eb1f74b6f0b140e4e94a48f3be2121cfc6
|
Provenance
The following attestation bundles were made for runfuse-0.1.0.tar.gz:
Publisher:
release.yml on akshat333-debug/RunFuse
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
runfuse-0.1.0.tar.gz -
Subject digest:
1ab5276054b6f4c3fd65af1c8d001fb36d4f31e028352c15aa82165b769c33f5 - Sigstore transparency entry: 2136685814
- Sigstore integration time:
-
Permalink:
akshat333-debug/RunFuse@c433d8cdaf8d1c909e27b59ccdcf18b47713e31a -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/akshat333-debug
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@c433d8cdaf8d1c909e27b59ccdcf18b47713e31a -
Trigger Event:
push
-
Statement type:
File details
Details for the file runfuse-0.1.0-py3-none-any.whl.
File metadata
- Download URL: runfuse-0.1.0-py3-none-any.whl
- Upload date:
- Size: 30.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f079fe5341e2c24f1768ee1c6da5deb05281fccf208d59813bcbd601931176d3
|
|
| MD5 |
71e35d21c5d3e67493d55b6651c825fd
|
|
| BLAKE2b-256 |
016dcf42d82461a4faf3f6164b015a7a7f1d9f82f6d3697cd26aa2a92e1dc9eb
|
Provenance
The following attestation bundles were made for runfuse-0.1.0-py3-none-any.whl:
Publisher:
release.yml on akshat333-debug/RunFuse
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
runfuse-0.1.0-py3-none-any.whl -
Subject digest:
f079fe5341e2c24f1768ee1c6da5deb05281fccf208d59813bcbd601931176d3 - Sigstore transparency entry: 2136685935
- Sigstore integration time:
-
Permalink:
akshat333-debug/RunFuse@c433d8cdaf8d1c909e27b59ccdcf18b47713e31a -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/akshat333-debug
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@c433d8cdaf8d1c909e27b59ccdcf18b47713e31a -
Trigger Event:
push
-
Statement type: