Skip to content

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.

#!/usr/bin/env bash
set -e
goal="$1"
turn=1
while [ "$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))
done

Two 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 covers the providers, credentials and where the reply lands. From a loop there are two routes:

  • A connector, through spool call --node anthropic.messages or openai-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=anthropic

    Wrap each model call in a spool step and 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 wait on a signal can sit inside the loop rather than around it:

Terminal window
if [ "$(spool step risk -- ./classify.sh "$reply")" = "high" ]; then
spool wait signal approve
fi
spool 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:

Terminal window
if ! spool wait signal approve --timeout 3d; then
spool step skip -- ./log-unreviewed.sh "$reply"
break
fi

Use 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 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:

Terminal window
version=1.4.0
spool step deploy -- ./deploy.sh "$version"
spool sleep 2h # soak
spool wait signal ship # a person decides
spool step promote -- ./promote.sh "$version"
spool sleep 7d # let it settle
prompt="$(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.

  • The connectors are single-shot. anthropic.messages and openai-compatible.chat take a prompt and hand back the reply. There is no tools parameter and no tool_use handling, 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.