runfence
Cancel, deadline and budget limits for agent runs — that actually stop the work.
Every agent framework hands you an async generator of events. When you stop consuming
it, the work it started keeps running. Model calls keep streaming, tools keep
executing, and you keep paying. break does not mean stop.
from runfence import run_scope, Cancelled
scope = run_scope(deadline=30, tokens=50_000, usd=0.25)
async with scope:
try:
async for event in scope.stream(runner.run_async(...)):
handle(event)
except Cancelled:
... # scope.cancel() was called, from anywhere
scope.stream() takes any async iterator, so it works with whatever framework produced
it. There is no adapter to install and nothing to register.
What it costs to get this wrong
Cancelling a real streaming run against llama-3.3-70b, two runs on different days:
| Wall clock | Events | |
|---|---|---|
| Run to completion | 262.3s / 42.6s | 1369 / 1366 |
scope.cancel() at 1.5s |
1.8s / 1.5s | 1 |
The completion time swings with provider load, which is the point: you cannot predict how long a run will take, so the ceiling has to be enforced rather than assumed. No framework tasks were left alive after cancelling in either run.
Reproduce it with examples/live_cancel.py, or see the mechanism with no API key at all:
python examples/stop_means_stop.py
break + aclose() -> tools that still finished: ['search', 'summarise', 'draft']
inside a run_scope -> tools that still finished: none
anything left behind? no
Limits
scope = run_scope(
deadline=30, # seconds of wall clock for the whole run
tokens=50_000, # stop once this many tokens are spent
usd=0.25, # stop once this much money is spent
)
Usage has to come from somewhere, so tell the scope how to read it off an event:
async for event in scope.stream(source, usage=lambda e: {"tokens": e.usage.total_tokens}):
...
Stopping raises, and the exception carries what was spent:
except BudgetExceeded as stopped:
log.warning("stopped after %.1fs and %d tokens", stopped.elapsed, stopped.tokens)
Cancelled, DeadlineExceeded and BudgetExceeded all derive from RunStopped.
Work started inside the scope
Anything spawned through the scope is cancelled with it:
async with run_scope(deadline=10) as scope:
scope.spawn(background_tool())
async for event in scope.stream(source):
...
Anything spawned outside it cannot be cancelled by it — but it is reported rather than ignored:
print(scope.leaked) # names of tasks still running when the scope closed
That list is the honest answer to "did my framework clean up?", and it is usually the first thing you want to know when a run refuses to die.
Why this exists
Stopping an agent is unsolved across the ecosystem, not in one framework:
- google/adk-python — 52 reactions across its three top cancellation issues, the oldest open since August 2025, with three community PRs unmerged
- langchain-ai/langgraph — 25 open issues mentioning cancel, interrupt or abort; the most discussed is about cancellation losing state that was not yet checkpointed
- strands-agents — 21 open issues on the same theme
What it does not do
- It cannot cancel work a framework spawned as an orphan task. Nothing outside that
framework can. It detects and reports those instead, in
scope.leaked. - It does not price tokens. Pass
usdyourself, from your provider's numbers or a library liketokencost. - Verified against the OpenAI Agents SDK on real streaming traffic. Google ADK exposes the same async-generator shape and is expected to work, but is not yet tested.
Install
pip install runfence
No dependencies. Python 3.11+ (it uses asyncio.timeout semantics and modern task APIs).
Development
pip install -e ".[dev]"
pytest
ruff check .
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 runfence-0.1.2.tar.gz.
File metadata
- Download URL: runfence-0.1.2.tar.gz
- Upload date:
- Size: 8.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7f9ba8815d2a209a2b5a38e9ded2d2adbeb057006d8a25591b6d7292528d2911
|
|
| MD5 |
c22143fead64bbf681936fa4314e9b15
|
|
| BLAKE2b-256 |
9a5929e30224d812091f72df96bd53ca3bd38c9634632d26efd1f60d2d802640
|
File details
Details for the file runfence-0.1.2-py3-none-any.whl.
File metadata
- Download URL: runfence-0.1.2-py3-none-any.whl
- Upload date:
- Size: 7.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e5d11bac93446e9e87f3aba8b7ec8e67c51e70785d597ab26862ae2d42deda16
|
|
| MD5 |
56d8a457df4b7febc6a802171c8bfca0
|
|
| BLAKE2b-256 |
932c1da8508a1425d7425a3c170d277f7b3c9f89b4821270894f578601611671
|