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 real runs against a 70B model:
| Framework | To completion | Cancelled early |
|---|---|---|
| OpenAI Agents SDK | 262.3s / 42.6s | 1.8s / 1.5s |
| Google ADK | 123.8s | 2.0s |
ADK logs Root node writer was cancelled when this happens, so the cancellation reaches
the framework's own machinery rather than just ending your loop.
One caveat worth knowing, measured while
discussing this upstream: cancelling
stops an async def tool at its next await, but a plain def tool keeps running to
completion in its thread. The caller is freed either way, so a synchronous tool can go on
spending after you think the run has stopped. scope.leaked does not see that, because
the thread is not a task.
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 and Google ADK on real traffic. Other frameworks exposing an async generator of events should work, but are untested.
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 .
Metadata
Release files for runfence 0.1.4
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| runfence-0.1.4.tar.gz | 8.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| runfence-0.1.4-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 16.0 kB
Release files / runfence-0.1.4.tar.gz
| Download URL | runfence-0.1.4.tar.gz |
|---|---|
| Size | 8.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b7f804f42ce5b594bb2073ddb9cc64b36917b040e32f907b4ed29275cad97478
|
|
BLAKE2b-256 checksum How to use checksums |
3e690a977d5c19cd6850e7eb37e81bba71500b283b162b60a4353f5f3ea35728
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 27, 2026.
Transparency logRelease files / runfence-0.1.4-py3-none-any.whl
| Download URL | runfence-0.1.4-py3-none-any.whl |
|---|---|
| Size | 7.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
953a5d50f85cf7461a28f78d85baf3598c07f91a2c6cb87c6a7c94ba2f3e4103
|
|
BLAKE2b-256 checksum How to use checksums |
21258b3492c4fd6f5010dd9c3b0f9b2dc99ad38af76ae0904a3c4d063cbdb8c6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 27, 2026.
Transparency log