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

runfence-0.1.2.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.2-py3-none-any.whl (7.5 kB view details)

Uploaded Python 3

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

Hashes for runfence-0.1.2.tar.gz
Algorithm Hash digest
SHA256 7f9ba8815d2a209a2b5a38e9ded2d2adbeb057006d8a25591b6d7292528d2911
MD5 c22143fead64bbf681936fa4314e9b15
BLAKE2b-256 9a5929e30224d812091f72df96bd53ca3bd38c9634632d26efd1f60d2d802640

See more details on using hashes here.

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

Hashes for runfence-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 e5d11bac93446e9e87f3aba8b7ec8e67c51e70785d597ab26862ae2d42deda16
MD5 56d8a457df4b7febc6a802171c8bfca0
BLAKE2b-256 932c1da8508a1425d7425a3c170d277f7b3c9f89b4821270894f578601611671

See more details on using hashes here.

Release history Release notifications | RSS feed

0.1.3

2 files

This release

0.1.2 This release

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