The agent loop you can actually see. Write your first reliable AI loop in ~5 lines: a goal, some tools, and a verifier that stops when the goal is verified done — not when the model runs out of patience.
from spinloop import loop, tool, shell
@tool
def edit_file(path: str, content: str) -> str:
"Overwrite a file with new content."
open(path, "w").write(content); return f"wrote {path}"
result = loop(
goal="Make the tests in ./tests pass.",
tools=[edit_file],
model="openai:gpt-4o-mini", # or anthropic:, gemini:, ollama: (free), cli:opencode
verify=shell("pytest -q"), # THE GATE — exit 0 means done
until=["goal_met", "max_steps:25", "budget:$0.50"], # honest off switch
trace=True, # watch it work, live ↓
)
print(result.done, result.reason, f"${result.cost:.4f}")...and trace=True streams every step so you actually see the loop:
● step 1 · $0.0021 | 410+88 tok
│ 🧠 The tests are failing — let me read the error and fix the function.
│ 🔧 edit_file(path='calc.py', content='def add(a, b):\n return a + b')
│ 👁 wrote calc.py
│ ⚖️ not yet: 1 test still failing — test_subtract expected 2, got 8
╰─ continue
● step 2 · $0.0024 | 470+96 tok
│ 🧠 Now fix subtract too.
│ 🔧 edit_file(path='calc.py', content='...')
│ 👁 wrote calc.py
│ ⚖️ verified ✓ pytest exit 0
╰─ continue
✓ done reason=goal_met $0.0045 | 1.4k+184 tok | 2 steps
pip install spinloop · zero required dependencies · the core loop is ~9 lines · Python 3.10+ · MIT
Because the loop is the easy part. Here's the naive version everyone writes first, and what goes wrong:
# ❌ The loop everyone writes first
while True:
out = model(messages)
if out.tool_calls:
messages += run(out.tool_calls) # ⚠️ tool results before the assistant msg → API error
else:
break # ⚠️ stops when the model goes quiet — NOT when the goal is done
# ⚠️ no budget → "$50 burned overnight"
# ⚠️ no verifier → the model edits the tests to make them pass, and you never know# ✅ spinloop: the hard parts are first-class
result = loop(
goal="Make the tests pass.",
tools=[edit_file],
verify=shell("pytest -q"), # done = VERIFIED, not "model stopped talking"
until=["goal_met", "max_steps:25", "budget:$0.50"], # a real off switch + a HARD cost cap
trace=True, # see every step
)The whole industry is shifting from writing prompts to writing loops — what Addy Osmani (Google) calls "Loop Engineering". But the loop is famously ~9 lines; what makes loops reliable is everything around it. spinloop packages exactly that, and nothing more:
| The hard part | spinloop's answer |
|---|---|
| Knowing when you're actually done | verify= — a function, a shell/test exit code, or an LLM judge. The gate. |
| An honest, reusable stop model | until=["goal_met", "max_steps:25", "budget:$0.50", "needs_human"] |
| Runaway cost ("$50 overnight") | budget="$0.50" — a hard cap; refuses to start a step it can't afford. |
| Seeing what the loop is doing | trace=True — thought → action → observation → verdict, every step. |
| Self-correction | tools return error strings (never raise), so the model reads the error and fixes itself. |
spinloop is not a framework — no graphs, no DAGs, no multi-agent orchestration. It's the primitive + the canonical teaching: "requests for the agent loop."
pip install spinloop # zero required dependencies
# or: uv add spinloopBring an API key and write your first loop:
from spinloop import loop, tool
inbox = ["INVOICE OVERDUE", "50% OFF SALE!!!", "Can you send the Q3 numbers?"]
labels = {}
@tool
def label(i: int, tag: str) -> str:
"Label email i as one of: urgent | spam | normal."
labels[i] = tag
return f"labeled #{i} {tag}"
result = loop(
goal=f"Label every email by urgency. Emails: {dict(enumerate(inbox))}",
tools=[label],
model="openai:gpt-4o-mini",
verify=lambda: len(labels) == len(inbox), # the GATE: done when all are labeled
until=["goal_met", "max_steps:10", "budget:$0.10"],
trace=True,
)
print(result.done, labels)No API key? You can also use free, local Ollama:
ollama pull llama3
python -m spinloop # interactive picker (recommends Ollama)
# …or run a demo directly:
SPINLOOP_MODEL=ollama:llama3 python examples/iterate_code.pyThen read the full guide: docs/quickstart.md — write your first agent loop in 5 minutes.
A loop should end because the goal is verified, not because the model went quiet. Pick the cheapest one that proves "done":
from spinloop import loop, shell, judge
loop(..., verify=lambda: my_tests_pass()) # a function — deterministic, free
loop(..., verify=shell("pytest -q")) # a shell/test exit code — output is fed back on failure
loop(..., verify=judge("Covers cost, risks, and a recommendation.")) # an LLM judge — for fuzzy goalsWhen a gate fails, spinloop feeds the reason back into the conversation ("NOT done yet — 2 tests still failing"), so the next turn fixes the real problem instead of repeating itself. The model cannot talk its way past a deterministic gate — that's what stops reward-hacking.
The same loop(...) drives two adapter types; switching is one argument. No other loop library cleanly does both.
| Model API (bring your API key) | CLI runner (bring your subscription) | |
|---|---|---|
model= |
openai:, anthropic:, gemini:, ollama: (free/local), litellm:, openrouter: |
cli:opencode, cli:claude, cli:codex, cli:copilot |
| How it works | spinloop calls the LLM directly, one turn at a time | spinloop drives an installed agent CLI as the step executor |
| You pay | per token → budget= caps the dollars |
your flat-rate plan → metered cost is $0 |
| The loop | fully visible — you watch every step | the CLI runs its own loop inside one step (coarser) |
loop(goal="…", model="ollama:llama3", verify=…) # local, free, shows the loop
loop(goal="…", model="gemini:gemini-1.5-pro", verify=…) # native Google Gemini, no extra deps
loop(goal="…", model="cli:opencode", verify=…) # your subscription (incl. Copilot models)Native OpenAI + Anthropic + Gemini + Ollama + a LiteLLM/OpenRouter passthrough, plus four CLI runners. Adding a backend is ~30 lines — see docs/adding-a-backend.md.
result = loop(...)
result.done # bool — was the goal VERIFIED done?
result.reason # goal_met | max_steps | budget | needs_human | stalled | model_finished | error
result.cost # total $ spent (0.0 for local / subscription)
result.steps # full trace as data: list[Step] (thought, tool_calls, observations, verdict, usage)
result.summary # the final answer
result.to_dict() # JSON-serializable, for logging / evalTwo demos that show a genuine multi-step loop (not scripted) against a strict, un-fakeable gate. Run the picker — it recommends free Ollama if you have no key:
python -m spinloop| Demo | What it proves |
|---|---|
iterate_code.py |
fixes 3 problems revealed one at a time → it must loop until ruff + mypy + pytest all pass |
iterate_writing.py |
a non-coding loop: draft → a strict LLM judge rejects with reasons → revise → … → pass |
The loop is visible with an API-key or local model (spinloop drives each turn). A CLI runner does its own iterating inside one step, so it finishes fast but you won't see the turns — use an API/Ollama model to watch the loop.
| Reach for | Because | |
|---|---|---|
| One honest loop you can track | spinloop | 5 lines, verify+budget+trace built in, zero deps |
| Stateful graphs / durable execution | LangGraph | checkpoints, resumability, branching |
| Multi-agent handoffs / crews | OpenAI Agents SDK · CrewAI | orchestrating many agents |
The big frameworks all hide the loop — spinloop shows it. As Anthropic puts it in Building Effective Agents: "the most successful implementations use simple, composable patterns rather than complex frameworks."
- Teach your AI agent to use it — spinloop ships an Agent Skill so opencode/Claude Code write correct spinloop loops (with a verifier + budget, not a naive
while) when you ask.cp -R skills/spinloop ~/.config/opencode/skills/. - How it works & the reliability details (message ordering, retries/backoff, stall detection, cost accounting) — see docs/quickstart.md and the source; the body of
loop()reads top-to-bottom like the canonical pseudocode. - Security — LLM-directed actions, built to fail safe: CLI auto-approval is opt-in (
autonomous=True), HTTPS-only requests (SSRF guard), secrets scrubbed from errors, trace output sanitized, and everything bounded bymax_steps/budget/timeouts. Found an issue? Open a GitHub security advisory. - Roadmap & changes — BACKLOG.md (with an explicit out of scope list) and CHANGELOG.md.
spinloop stands on the shoulders of: Addy Osmani's Loop Engineering · Anthropic's Building Effective Agents · Simon Willison on agents-as-tools-in-a-loop · Steve Kinney's Anatomy of an Agent Loop · the viral 9-line loop · Andrew Ng's The Batch (the 48%→95% HumanEval-in-a-loop result).
A note on hype: the viral "nobody writes prompts anymore" lines are not verified to a primary source — we don't repeat them as fact. The honest claim spinloop is built on: a loop with a verifier and a budget beats a one-shot prompt by turning one big leap into many small checked steps.
PRs welcome — especially new backends and non-coding examples. See CONTRIBUTING.md. MIT licensed.