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 llama-3.3-70b:

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.

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 .

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

runfence-0.1.3.tar.gz (8.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

runfence-0.1.3-py3-none-any.whl (7.5 kB view details)

Uploaded Python 3

File details

Details for the file runfence-0.1.3.tar.gz.

File metadata

  • Download URL: runfence-0.1.3.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

Hashes for runfence-0.1.3.tar.gz
Algorithm Hash digest
SHA256 21f9ee5bf9218d9f7334410c9d43d0346463d5f86bb335ccc2a41aed4fcc0cd6
MD5 40688fc4b6a5198e5042999b099c580f
BLAKE2b-256 c345cfda8761f192c827eae8644182f5098bea139fde93efdc4845fcf88fbd83

See more details on using hashes here.

File details

Details for the file runfence-0.1.3-py3-none-any.whl.

File metadata

  • Download URL: runfence-0.1.3-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

Hashes for runfence-0.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 77554b9ee4adead4becbc8d30b99def9da5858afe0818f99948043aebe240970
MD5 15b5f66bdae87f7b1fa1c3c4ac68a216
BLAKE2b-256 28c90d99794508378082d1c1f43b0e6ab8b69d4464faee73c292a524a89f7b81

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.3 This release

2 files

0.1.2

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page