Skip to main content

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 usd yourself, from your provider's numbers or a library like tokencost.
  • 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)

Source distribution for runfence 0.1.4
File Size Uploaded
runfence-0.1.4.tar.gz 8.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for runfence 0.1.4
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.1.4 This release

2 release files

0.1.3

2 release files

0.1.2

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page