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

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

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

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:

Terminal window
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:

Terminal window
cryo submit research.py --as script --credential ANTHROPIC_KEY=anthropic

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

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.

An approval gate is a durable primitive, so it can sit inside the loop rather than around it:

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

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

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

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

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.