Skip to content

Liadshiran/spinloop

Repository files navigation

spinloop

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


Why not just write the while loop?

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."


60-second quickstart

pip install spinloop          # zero required dependencies
# or:  uv add spinloop

Bring 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.py

Then read the full guide: docs/quickstart.md — write your first agent loop in 5 minutes.


The gate: three ways to verify=

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 goals

When 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.


Works with your key or your subscription

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 tokenbudget= 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.


What you get back

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 / eval

See it loop for real

Two 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

Animated spinloop trace: an agent triaging an inbox step by step, the count of untriaged items falling 3 → 2 → 1 → 0, until verified done

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.


When to use spinloop (and when not to)

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."


More

  • 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 by max_steps/budget/timeouts. Found an issue? Open a GitHub security advisory.
  • Roadmap & changesBACKLOG.md (with an explicit out of scope list) and CHANGELOG.md.

Credits

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.

Contributing

PRs welcome — especially new backends and non-coding examples. See CONTRIBUTING.md. MIT licensed.

About

The agent loop you can actually see — write your first reliable AI agent loop in ~5 lines: a goal, tools, and a verifier that stops when the goal is verified done. Zero deps, Python 3.10+.

Resources

License

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages