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 cryosleep 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="$(cryo step model -- ./ask-model.sh "$goal")" [ "$reply" = "done" ] && break cryo 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 cryo steps groups the turns because
they share one.
The loop condition reads a value captured in a step. reply came out of
cryo 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 cryo call node:
reply="$(cryo 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:
cryo submit research.py --as script --credential ANTHROPIC_KEY=anthropicThe script then uses whatever library you like. Wrap each model call in
a cryo 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 [ "$(cryo step risk -- ./classify.sh "$reply")" = "high" ]; then cryo wait-signal approveficryo step tool -- ./run-tool.sh "$reply"The run parks holding no machine until someone answers, on the run page
or through cryo 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.
cryo wait-signal approve --timeout 3d exits 124 on expiry, so the loop
can take the cautious branch instead of parking forever:
if ! cryo wait-signal approve --timeout 3d; then cryo 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 cryo 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. Cryosleep 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.