Agent loops
An agent loop calls a model, does what the model asked for, feeds the result back, and repeats until the model says it is finished. Written as a Hyperspool workflow, every turn is a checkpoint: the loop survives the process dying, the tool calls it already made are not repeated, and the tokens it already paid for are not paid for twice.
A note on the word. Everywhere else in these docs an agent is a worker machine that claims jobs - see deploying agents. On this page “agent loop” names a shape you write, and “the model” is the thing being called.
The shape
Section titled “The shape”#!/usr/bin/env bashset -e
goal="$1"turn=1while [ "$turn" -le 20 ]; do reply="$(spool step model -- ./ask-model.sh "$goal")" [ "$reply" = "done" ] && break spool step tool -- ./run-tool.sh "$reply" turn=$((turn + 1))doneTwo things make this durable, and both come straight from the authoring model.
A step name says which program point, not which time through. model
is one point in the script, and reaching it twenty times records twenty
checkpoints under that one name — turn seven’s answer is turn seven’s,
and on replay each returns what it got the first time. You do not put
the turn index in the name, and spool steps groups the turns because
they share one.
The loop condition reads a value captured in a step. reply came out of
spool step, so it is the same on every pass and the loop takes the same
path. A condition that read the wall clock, fresh randomness, or an
unwrapped API call would diverge on replay and fail with a determinism
error.
Everything else follows: kill the agent halfway through turn seven and
the run resumes, races through turns one to six out of the log, and
picks up where it stopped. The upper bound on turn is yours - there is
no duration cap and no step budget.
Calling a model
Section titled “Calling a model”Two ways, and they compose.
A connector node, for the common case. anthropic calls the Claude
Messages API and openai-compatible calls the /chat/completions API
that OpenAI, Mistral, Ollama, vLLM, LM Studio and LiteLLM all speak. A
script reaches either one through spool call node:
reply="$(spool call node anthropic.messages --config "$(jq -n --arg p "$prompt" \ '{ with: { model: "claude-sonnet-5" }, input: { prompt: $p }, credential: "anthropic" }')")"text="$(jq -r '.body.content[0].text' <<<"$reply")"The credential resolves to auth headers at dispatch and never appears in the definition, the queue, or the run log. See connectors for the field and action of each built-in.
Your own client, when you want streaming, tool-calling, or an SDK’s ergonomics. Name a credential on submit and it arrives as an environment variable in the run:
spool submit research.py --as script --credential ANTHROPIC_KEY=anthropicThe script then uses whatever library you like. Wrap each model call in
a spool step and it checkpoints the same way a connector call does -
the engine does not care which one made the request.
A model on your own hardware
Section titled “A model on your own hardware”openai-compatible takes its base_url as a field and its credential
is optional, so a node pointed at http://localhost:11434/v1 with no
credential at all runs against a model on the machine the job landed
on. Combined with capability-matched dispatch, that means the turn can
be routed to the box with the GPU while the rest of the run goes
anywhere.
A human in the loop
Section titled “A human in the loop”An approval gate is a durable primitive, so it can sit inside the loop rather than around it:
if [ "$(spool step risk -- ./classify.sh "$reply")" = "high" ]; then spool wait-signal approvefispool step tool -- ./run-tool.sh "$reply"The run parks holding no machine until someone answers, on the run page
or through spool signal. A loop that waits three days for a person
costs the same as one that waits three seconds.
Bound it if nobody answering is itself an outcome you want to handle.
spool wait-signal approve --timeout 3d exits 124 on expiry, so the loop
can take the cautious branch instead of parking forever:
if ! spool wait-signal approve --timeout 3d; then spool step skip -- ./log-unreviewed.sh "$reply" breakfiThe same name every turn. A signal name is a queue, so each wait takes
the next answer that arrived, and the reviewer sends spool signal <run> approve without knowing which turn the loop is on. When the answer
needs to say what it approves, put that in the payload - the sender
can see a payload, and cannot see a loop counter.
What this doesn’t do yet
Section titled “What this doesn’t do yet”- The connectors are single-shot.
anthropic.messagesandopenai-compatible.chattake a prompt and hand back the reply. There is notoolsparameter and notool_usehandling, so a loop that needs real tool-calling drives it from your own client. The canvas cannot express an agent loop today; a script can. - No streaming through the connectors. A turn’s output arrives when the request completes.
- No token or cost accounting. Usage is in the response body if the API returns it, and nothing aggregates it across a run.
- No eval harness, prompt store, or model routing. Hyperspool runs the loop; choosing and measuring the model is yours.
A worked example
Section titled “A worked example”examples/release-watch is a single run that deploys, sleeps through a
soak, waits for a human, searches Hacker News a week later, sends the
comments through a model, and posts the summary to Slack. The model call
is one line in the middle of a workflow that is mostly not about models,
which is the point.