https://github.com/runcycles/cycles-runaway-demo
Demo: How Cycles prevents runaway AI agent spend ($6 in 30s → hard stop at $1)
https://github.com/runcycles/cycles-runaway-demo
ai-agents budget cost-control demo llm runaway-agent
Last synced: 4 months ago
JSON representation
Demo: How Cycles prevents runaway AI agent spend ($6 in 30s → hard stop at $1)
- Host: GitHub
- URL: https://github.com/runcycles/cycles-runaway-demo
- Owner: runcycles
- License: apache-2.0
- Created: 2026-03-14T10:54:57.000Z (4 months ago)
- Default Branch: main
- Last Pushed: 2026-03-28T13:21:26.000Z (4 months ago)
- Last Synced: 2026-04-04T10:58:52.001Z (4 months ago)
- Topics: ai-agents, budget, cost-control, demo, llm, runaway-agent
- Language: Python
- Homepage: https://runcycles.io
- Size: 5.58 MB
- Stars: 2
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Cycles Runaway Demo
A runaway agent burns $6 in 30 seconds. Cycles stops it at $1.
Same agent. Same bug. Two outcomes.
## The scenario
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.
## Run it
Prerequisites: Docker Compose v2+, Python 3.10+, `curl`
```bash
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.sh
```
That'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:
```bash
./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:
```bash
./teardown.sh
```
### Windows (WSL)
The demo runs on Windows 11 via WSL. Install [Docker Desktop for Windows](https://docs.docker.com/get-docker/) with the WSL 2 backend enabled (the default), then inside your WSL terminal:
```bash
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.sh
```
Docker Desktop shares the daemon between Windows and WSL automatically — no extra configuration needed.
> **Note:** Ubuntu 23.04+ requires `python3-full` (not just `python3`) so that venvs get their own pip. Without it, even `pip` inside a venv hits the PEP 668 "externally-managed-environment" error.
### First run notes
The first run pulls three Docker images (~200MB total). You'll see Docker's pull progress. Subsequent runs start in seconds.
## What you'll see

### Without Cycles
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 (~$3.60/hr per stuck ticket)
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.
### With Cycles (budget: $1.00)
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."*
### Expected output
```
⚡ 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 code change
The diff between `agent/unguarded.py` and `agent/guarded.py` is:
```python
# --- 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 cleanly
```
Three decorators. One except. That is the entire integration.
## Why this matters
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.
## Next steps
After running the demo, explore how to add Cycles to your own application:
- [What is Cycles?](https://runcycles.io/quickstart/what-is-cycles) — understand the problem and the solution
- [End-to-End Tutorial](https://runcycles.io/quickstart/end-to-end-tutorial) — zero to a working budget-guarded app in 10 minutes
- [Choose a First Rollout](https://runcycles.io/quickstart/how-to-choose-a-first-cycles-rollout-tenant-budgets-run-budgets-or-model-call-guardrails) — decide your adoption strategy
- [Adding Cycles to an Existing App](https://runcycles.io/how-to/adding-cycles-to-an-existing-application) — incremental adoption guide
- [Full Documentation](https://runcycles.io) — complete docs at runcycles.io
## Links
- 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`