Loop engineering: do/while for AI agents
Unattended agent loops need hard ceilings, a graceful landing, and a written definition of done. rysh makes the loop a reviewable file.
My most-used automation is the one that publishes my articles. I write markdown locally; an agent opens Medium in a browser it owns, types the story in, verifies the rendering, publishes, and saves the URL to a results file. I don't watch it do this.
That last sentence is the whole problem with agent loops, isn't it? I don't watch it. Every horror story about autonomous agents starts with someone walking away. So this post is about the part of my stack that makes walking away boring: loop engineering — the discipline of giving a loop hard ceilings, a graceful landing, and a written definition of done.
Budgets you can say out loud
First, vocabulary. rysh counts agent spend the way you count reading:
- 1 page = 1,000 tokens
- 1 book = 200 pages (200k tokens)
- 1 shelf = 20 books
"This run gets four books" is a sentence you can say in a meeting. "This run gets 800,000 tokens" is not. Every ceiling below uses these units, and they are enforced by the runtime — a run cannot outspend them. (The full case for the vocabulary is its own post: https://rysh.ai/blog/pages-books-shelves.)
The loop is a file
Here is the real recipe, from .rysh/automations/webs/halilagin-medium.md. It's a
markdown file with YAML frontmatter: the frontmatter is the control envelope, the
body is the task prompt.
{% raw %}
---
description: Publish a local markdown article to medium.com on halilagin's account.
web_profile: halilagin-medium # persistent, pre-authenticated browser profile
url: https://medium.com/new-story
args: [article_path] # run as: ##auto web run halilagin-medium /path/to/article.md
output_dir: halilagin-medium/results
loop:
do: # one publishing session
max_iterations: 150
max_duration: 15m
budget:
size: 4b # 4 "books" = 800k tokens, hard ceiling
watch:
takeover_when: 90 # at 90% consumed, switch to the wrap-up leg
takeover_prompt: >
The publishing budget is used up - stop now. Medium autosaves as you
type, so the story should exist as a DRAFT: report the draft URL,
what remains to be done, and save that status to the results file.
Do NOT publish a half-written article.
while: # the OUTER loop - retry sessions until published
enabled: true
max_iterations: 5
max_duration: 15m
budget: 20b # TOTAL across passes
prompts:
until: >
The results file for this run contains a PUBLISHED story URL for the
article at {{args}} - the story is live on the profile, not a draft.
iterate_with: >
Continue the interrupted publish of {{args}}. Check the profile's
stories and drafts FIRST: if the draft already exists, open it, finish
the remaining formatting, and publish THAT draft - never create a
duplicate story.
---
{% endraw %}
Two loops, two jobs:
dois one working session. Its budget is what a single attempt may burn.whileis the retry envelope. After each pass, an LLM judge reads the outcome against theuntil:text. Goal met → the loop ends. Not met → the budget re-arms anditerate_with:seeds the next pass with the previous result. (A judge is an LLM reading your criteria — a strong signal, not a proof. Vague goals get vague verdicts; writing a crispuntil:is the skill.)
Because the loop is a file, the safety envelope is code-reviewable. The until
condition shows up in a diff. Your automations live in git, not in a vendor's
dashboard.
The takeover leg: land the plane
The two default failure modes of a budgeted loop are both bad: die mid-thought at
100%, or never die because there is no ceiling. takeover_when: 90 adds a third
option — at 90% spent, the task prompt loses the wheel and the takeover prompt
inherits the reserved 10% to wrap up.
For the publishing recipe, the wrap-up is domain-specific and it matters: do not publish a half-written article — report the autosaved draft URL and what's left. The agent lands the plane instead of hitting a wall. (If your reserved share is too thin to land anything, rysh floors it: a takeover leg always gets at least ~5 minutes / 100 steps / 1 book to finish. The takeover mechanism gets its own deep-dive: https://rysh.ai/blog/takeover-when.)
The budget algebra — and when the engine says no
Here's the part I find genuinely interesting: the while totals and the do values
interact through a small, opinionated algebra. Per axis (tokens, wall-clock):
while.X > do.X→ redistribute. The total is authoritative and gets split evenly:do.X := while.X / max_iterations, with the total kept as the outer ceiling.while.X ≤ do.X→ disable. A total that can't fund even one pass is ignored — no outer constraint on that axis.
This isn't documentation-ware; you can watch it resolve. For the recipe above:
loop enabled=true maxIter=5
perPass: tokens=800000 duration=15m0s takeover=90
outer: tokensTotal=4000000 duration=0s
The token axis redistributed: 20 books total ÷ 5 passes = 4 books per pass, with
the 20-book total held as a hard outer ceiling. The duration axis disabled itself: I
set while.max_duration: 15m, but one pass already gets 15m — a total that can't
cover even one pass would be a lie, so the engine drops it rather than pretending. If
I want a real outer clock, it has to exceed a single pass. Budgets that can say "no"
to their author are budgets you can trust.
No goal, no loop
One design rule in rysh that I've come to appreciate the hard way: a loop only arms
if until: is written. No flag combination will loop a recipe that lacks a goal — a
judge with nothing to judge is just repetition with extra steps. If you take one idea
from this post into whatever stack you use, take that one: make "done" a written,
reviewable artifact, not a vibe.
Operating the loop
Everything is a first-class command in the terminal:
##auto web run halilagin-medium ./drafts/loop-engineering.md
##auto web status # live loop table: pass 2/5, state, deadline, last verdict
##auto web stop halilagin-medium # graceful: in-flight pass finishes, no judge, no iterate
##auto web continue halilagin-medium # resume from the last checkpoint, budget re-armed
##auto web resume halilagin-medium # fresh budget + latest result seeded into context
And because the browser is CLI-owned and can run headless, the whole thing schedules:
##cron add publish-queue "0 7 * * *" ##auto web run --headless halilagin-medium ./queue/next.md
Log in once with a headed browser (##web headless login halilagin-medium); every
headless run reuses the profile. It genuinely works while you sleep — which is only an
acceptable sentence because of everything above it.
The honest part
rysh runs on Claude — bring your own Anthropic key — and it's self-hostable: the recipes, the results, and the budget envelopes are files on your infra, not rows in my database. The loop machinery doesn't make an agent capable; it makes an agent bounded. Those are different products, and most stacks only ship the first one.
rysh is in private beta. If you have one workflow you want to run overnight on a leash, that's exactly the conversation we want — we're onboarding design partners: https://rysh.ai/design-partner
Poke holes in the budget algebra in the comments — especially the redistribute/disable policy. That's the part I most want adversarial eyes on.
Related: Pages, books, and shelves: budget your agents like a
library · takeover_when: the 90%
threshold where a human steps in