A runaway agent burns $6 in 30 seconds. Cycles stops it at $1.
Same agent. Same bug. Two outcomes.
A customer support bot drafts a response, evaluates its quality, and refines it in a loop until the quality score exceeds 8.0. The bug: the quality evaluator never returns above 6.9. Without a budget boundary, the agent loops forever — burning tokens with no exit condition. With Cycles, the server returns 409 BUDGET_EXCEEDED before the next call can proceed, and the agent stops cleanly.
No real LLM is used. All calls are simulated at 50ms latency. The cost math is real.
Prerequisites: Docker Compose v2+, Python 3.10+, curl
git clone https://github.com/runcycles/cycles-runaway-demo
cd cycles-runaway-demo
python3 -m venv .venv && source .venv/bin/activate
pip install -r agent/requirements.txt
./demo.shThat's it. The script starts the Cycles stack (Redis + server + admin), provisions a tenant and budget, then runs both modes back to back.
Run a single mode:
./demo.sh unguarded # without Cycles (~30s)
./demo.sh guarded # with Cycles (stops at $1.00)
./demo.sh both # both back to back (default)Re-runs just work — the script resets the stack automatically to ensure a fresh budget.
Stop the stack when done:
./teardown.shThe demo runs on Windows 11 via WSL. Install Docker Desktop for Windows with the WSL 2 backend enabled (the default), then inside your WSL terminal:
sudo apt update && sudo apt install -y python3-full curl
git clone https://github.com/runcycles/cycles-runaway-demo
cd cycles-runaway-demo
python3 -m venv .venv && source .venv/bin/activate
pip install -r agent/requirements.txt
./demo.shDocker Desktop shares the daemon between Windows and WSL automatically — no extra configuration needed.
Note: Ubuntu 23.04+ requires
python3-full(not justpython3) so that venvs get their own pip. Without it, evenpipinside a venv hits the PEP 668 "externally-managed-environment" error.
The first run pulls three Docker images (~200MB total). You'll see Docker's pull progress. Subsequent runs start in seconds.
JedisConnectionException: Failed to create socket / UnknownHostException: redis
You started the stack with docker compose up directly (instead of ./demo.sh) on top of a previous run, and the cycles-server container ended up on a stale network where the redis service no longer exists. Reset and restart:
docker compose down -v && docker compose up -d./demo.sh does this automatically before every run, so the script path doesn't hit this.
The GIF squeezes a $10 vs $1 contrast into ~30 seconds: the unguarded
segment is recorded with simulation latency dropped to 11ms so spend hits
~$10 in 12s. The end card projects per-day / per-week / per-month at
real-LLM economics ($0.03/call · 1s/call, conservative for Claude Opus
4): $2,592/day · $18,144/week · $77,760/month per stuck agent, vs
$1.00 / $1.00 / $1.00 with Cycles. The live demo (./demo.sh) keeps
the documented 50ms latency — you'll see ~$6 of unguarded spend over
the full 30s instead.
For homepage embedding, also available as demo.mp4 (951K, H.264) and
demo.webm (1.4M, VP9). All assets are recorded at 2000×1200 (2× retina
density) so text stays crisp on HiDPI displays even when the browser
scales them down to ~1000×600 for layout. Both videos autoplay inline:
<video autoplay loop muted playsinline poster="demo-runaway-poster.png">
<source src="demo.webm" type="video/webm">
<source src="demo.mp4" type="video/mp4">
<img src="demo.gif" alt="Cycles Runaway Demo">
</video>demo-runaway-poster.png (303K, 2000×1200) is the last-frame summary
card — used as the poster attribute so autoplay-blocked browsers,
slow-network first paint, and social/SEO link previews all show the
visceral $/day vs $1.00 contrast even before the video plays.
A live terminal display (no scroll flood) shows three panels updating in-place:
- Live Counter — call count climbing, spend in dollars, current action with quality score
- Budget Thresholds — the $0.10 threshold crossed in red; $0.50 and $1.00 showing "X% to go"
-
Projection — extrapolated cost rate:
$/min, $ /hr, $/day plus a real-LLM estimate (~$108/hr per stuck ticket at 1s × $0.03/call, conservative for Claude Opus 4)
After 30 seconds the demo auto-terminates. The final red panel reads:
"In production: no hard stop existed. Alert fires AFTER spend."
In 30s at simulation speed, the agent makes ~600 calls and spends ~$6.00. The projection panel shows what happens if you don't catch it — the hourly and daily rates are the scary numbers.
The same counter, the same loop, the same bug. The display is identical — same panels, same structure. But when cumulative spend reaches $1.00 (after ~100 calls), the Cycles server returns 409 BUDGET_EXCEEDED on the next reservation attempt. The @cycles decorator raises BudgetExceededError, the agent catches it, and the loop ends cleanly. The final green panel reads:
"Cycles stopped the agent BEFORE call N+1 could proceed."
⚡ Cycles — Runaway Agent Demo
Resetting stack (clean budget state)...
[Docker compose output]
Waiting for services to be healthy...
Provisioning tenant and budget...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
MODE 1: Without Cycles
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[live panels update in-place for ~30s]
╭──────────────── Final — UNGUARDED ─────────────────╮
│ Result: auto-stop after 30s │
│ Calls: ~595 │
│ Spend: ~$5.95 │
│ Duration: 30.0s │
│ │
│ In production: no hard stop existed. │
│ Alert fires AFTER spend. │
╰────────────────────────────────────────────────────╯
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
MODE 2: With Cycles (budget: $1.00)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[live panels update in-place until budget hit]
╭───────────────── Final — GUARDED ──────────────────╮
│ Result: BUDGET_EXCEEDED — Cycles server │
│ returned 409 │
│ Calls: 100 │
│ Spend: $1.0000 │
│ Duration: ~8s │
│ │
│ Cycles stopped the agent BEFORE call 101 │
│ could proceed. │
╰────────────────────────────────────────────────────╯
Demo complete.
Swagger UI: http://localhost:7878/swagger-ui.html
Admin UI: http://localhost:7979/swagger-ui.html
Re-run: ./demo.sh
Stop stack: ./teardown.sh
The diff between agent/unguarded.py and agent/guarded.py is:
# --- Import the SDK ---
from runcycles import BudgetExceededError, CyclesClient, CyclesConfig, cycles, set_default_client
# --- Initialize the client ---
def _setup():
config = CyclesConfig(
base_url=os.environ["CYCLES_BASE_URL"],
api_key=os.environ["CYCLES_API_KEY"],
tenant=os.environ["CYCLES_TENANT"],
workspace="default",
app="default",
workflow="default",
agent="support-bot",
)
set_default_client(CyclesClient(config))
# --- Add three decorators ---
@cycles(estimate=COST_PER_CALL_MICROCENTS, action_kind="llm.completion", action_name="draft-response")
def draft_response(ticket_text: str) -> str: ...
@cycles(estimate=COST_PER_CALL_MICROCENTS, action_kind="llm.completion", action_name="evaluate-quality")
def evaluate_quality(draft: str) -> float: ...
@cycles(estimate=COST_PER_CALL_MICROCENTS, action_kind="llm.completion", action_name="refine-response")
def refine_response(draft: str, score: float) -> str: ...
# --- Catch the budget exception ---
except BudgetExceededError:
# agent stops cleanlyThree decorators. One except. That is the entire integration.
Rate limits cap velocity, not total exposure. Observability alerts fire after the damage. Provider caps are per-provider and per-key. Cycles enforces a hard ceiling before the next call is made — across providers, tenants, and agents.
After running the demo, explore how to add Cycles to your own application:
- What is Cycles? — understand the problem and the solution
- End-to-End Tutorial — zero to a working budget-guarded app in 10 minutes
- Choose a First Rollout — decide your adoption strategy
- Adding Cycles to an Existing App — incremental adoption guide
- Full Documentation — complete docs at runcycles.io
- Protocol: https://github.com/runcycles/cycles-protocol
- Server: https://github.com/runcycles/cycles-server
- Python:
pip install runcycles - Java:
io.runcycles:cycles-client-java-spring - Node.js:
npm install runcycles
