Metadata-Version: 2.4
Name: runfuse
Version: 0.1.0
Summary: Runtime circuit breakers for AI agents: trip fuses on runaway cost, loops, and retry storms.
Keywords: ai,agents,llm,circuit-breaker,cost-control,safety
Author: Akshat Agrawal
Author-email: Akshat Agrawal <akshatagrawal.work@gmail.com>
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries
Requires-Dist: pydantic>=2.5
Requires-Dist: anthropic>=0.40 ; extra == 'anthropic'
Requires-Dist: fastapi>=0.115 ; extra == 'dashboard'
Requires-Dist: uvicorn>=0.32 ; extra == 'dashboard'
Requires-Dist: langchain-core>=0.3 ; extra == 'langgraph'
Requires-Dist: openai>=1.50 ; extra == 'openai'
Requires-Python: >=3.11
Project-URL: Homepage, https://github.com/akshat333-debug/RunFuse
Project-URL: Repository, https://github.com/akshat333-debug/RunFuse
Project-URL: Issues, https://github.com/akshat333-debug/RunFuse/issues
Project-URL: Changelog, https://github.com/akshat333-debug/RunFuse/blob/master/CHANGELOG.md
Provides-Extra: anthropic
Provides-Extra: dashboard
Provides-Extra: langgraph
Provides-Extra: openai
Description-Content-Type: text/markdown

# RunFuse ⚡

[![CI](https://github.com/akshat333-debug/RunFuse/actions/workflows/ci.yml/badge.svg)](https://github.com/akshat333-debug/RunFuse/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](pyproject.toml)

**Runtime circuit breakers for AI agents.** Trip fuses on runaway cost, infinite loops, and retry storms — before the invoice does it for you.

```python
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

```bash
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:

```python
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:

```python
@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.

```python
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:

```python
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:

```bash
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

- [x] Anthropic + OpenAI SDK wrappers
- [x] Cost / step / time / error / loop fuses, soft thresholds
- [x] SQLite event store + CLI report
- [x] Burn-rate fuse (trailing-window $/min cap)
- [x] Fuzzy loop detection (near-identical args)
- [x] `runfuse demo` one-command demo
- [x] Live dashboard (FastAPI + HTMX) with abort — a warned run continues unless you kill it
- [x] LangGraph/LangChain callback integration
- [x] `guard_tool` decorator — loop/abort enforcement for MCP handlers and framework-executed tools
- [ ] PyPI release

## Development

```bash
uv sync --all-extras
uv run pytest
uv run mypy src
uv run ruff check src tests
```

## License

MIT
