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. In a Hyperspool workflow every turn is a checkpoint: the loop survives the process dying, and tool calls and tokens already paid for are not repeated.
A note on the word. Everywhere else in these docs an agent is the process that runs steps (see deploying agents). Here “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 from the authoring model make this durable.
A step name can be used again. model is one place 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. Leave the turn index out of the name;
spool run 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.
So kill the agent process 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.
Calling a model
Section titled “Calling a model”Calling a model covers the providers, credentials and where the reply lands. From a loop there are two routes:
-
A connector, through
spool call --node anthropic.messagesoropenai-compatible.chat. The call is a durable checkpoint, and the credential is resolved to auth headers when the request is made, so it never appears in the script or the run log. On the service, connector work runs in a managed environment and needs no agent of yours. -
Your own client, when you want streaming, tool calling, or an SDK. Name a credential on submit and it arrives as an environment variable:
Terminal window spool submit research.py --as script --credential ANTHROPIC_KEY=anthropicWrap each model call in a
spool stepand it checkpoints the same way a connector call does.
A model on your own hardware is an openai-compatible step whose
base_url points at it, run by an agent that can reach it: start one on
the GPU machine with a capability of its own
(spool agent run ... --cap connector --cap gpu, with a token an org
owner issued for those same capabilities) and give the model step
requires: [gpu]. The rest of the run goes anywhere.
Connectors shows the setup.
A human in the loop
Section titled “A human in the loop”A wait on a signal 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 waits for the approve signal holding no agent, and the console
shows it as “waiting for approve”. Someone answers with
spool run signal <run> approve, or from the run page, which offers to send
the signal by hand. A waiting run holds no agent.
A plain signal has no form and no reject button. For those, put an
approval step in a graph and wait for it from the script with
spool call (see Composition).
If nobody answering is an outcome you want to handle, bound the wait.
spool wait signal approve --timeout 3d exits 124 on expiry, so the
loop can take the cautious branch instead of waiting forever:
if ! spool wait signal approve --timeout 3d; then spool step skip -- ./log-unreviewed.sh "$reply" breakfiUse the same name every turn. A signal name is a queue, so each wait
takes the next answer that arrived, and the reviewer sends
spool run 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, but not a loop counter.
A worked example
Section titled “A worked example”A release watcher is one run that deploys, sleeps through a soak, waits for a person, lets the release settle for a week, sends what people said through a model, and posts the summary. The model call is one step in a workflow that is mostly about something else:
version=1.4.0spool step deploy -- ./deploy.sh "$version"spool sleep 2h # soakspool wait signal ship # a person decidesspool step promote -- ./promote.sh "$version"spool sleep 7d # let it settleprompt="$(spool step mentions -- ./mentions-as-prompt.sh "$version")" # {"prompt": "..."}reply="$(spool call --node anthropic.messages \ --config '{"credential":"anthropic","with":{"model":"claude-sonnet-4-6"}, "input":{"prompt":{"$expr":"input.prompt"}}}' \ --input "$prompt")"spool step post -- ./post-summary.sh "$reply"The connector reads its prompt from the input in --config, and the
$expr there takes it from --input. The call’s output is the HTTP
response, so post-summary.sh reads the text at
.body.content[0].text.
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, in a script. - 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.