Part 5 · Harness
A browser agent you can leave alone
Autonomy is the easy part. The hard part is what happens when an agent that can do anything is about to do the wrong thing, gets stuck, or hits an outage at 3am. Here is every step a tool call goes through in Slapify, and why.
Every failure in Part 2 had the same shape: the agent was trusted to do something the harness should have guaranteed. The pieces below are the result. Several are adapted from ideas in vercel-labs/fx, a coding agent with an unusually careful harness, carried over to the browser, where the stakes include posting, sending and buying.
The life of a tool call
Before anything touches the page, a call goes through five stages. Pick a scenario and step through it.
The scenarios are illustrative; the results are the exact messages the harness hands back to the model.
1. Contracts: know a scroll from a purchase
Each call is classified before it runs as read, navigate, mutate, irreversible or control. Some tools are fixed: get_page_text reads, upload_file is irreversible because it sends a local file to a website. Clicks and key presses depend on what they touch. A click on a control labelled Post, Send, Buy, Delete, Confirm and so on is irreversible; a click on a tab is not. Meta+Enter, the submit shortcut on most composers, is irreversible, while a plain Enter is not. The word list is deliberately broad: a false positive costs one review, a false negative can post or pay without one.
2. Admission: guide, don't block
The original loop guard blocked a call after it repeated. In practice it blocked legitimate work: pressing the same submit shortcut on ten different posts is correct, not a loop. And once tripped, it walled off every tool. Admission keeps the model in charge:
- Calls are keyed by tool, arguments and the page they run on, so the same action on a different post is a different call.
- Nothing is blocked for repeating. After two identical failures, the result gains a
guidancefield saying to change approach; after three identical reads, one saying nothing is changing. - One hold: an irreversible action that already succeeded on this page isn't run again. That's the difference between a stuck agent and one that posts twice or pays twice.
3. Review: check irreversible actions against your policy
With review on, every irreversible action is judged just before it runs, together with the page it's on and the exact text it's about to publish: clear or caution. A caution holds the action and tells the agent why, so it can revise or skip.
review:
enabled: true
policy: |
Replies must be on-topic, friendly and technically accurate.
No self-promotion, no links, nothing political.
on_unavailable: hold # the default
The most important line is the last one. If no reviewer can answer, the action is held. Failing open would mean the safety check quietly disappears exactly when something is already going wrong. Review uses Jev when it's configured, which takes about a second per check, and the main model otherwise.
Recovery: never die of something temporary
A long run will hit a rate limit, a quota, a dropped connection, a malformed response. None of those should end it. Errors are classified: bad tool arguments are handed back for correction, timeouts are retried, and provider refusals rotate to the next model in your llm list. When every model has refused, the agent waits, patiently, and tells you once.
An outage, minute by minute
Every model in the chain is refusing (a usage limit, say). Drag to set how long it lasts.
Before this, one usage limit turned into 272 process restarts overnight: the process exited, the supervisor restarted it instantly, and it failed instantly again. Now the process never exits for a provider error, and the supervisor script in the README backs off on its own as a second line of defence.
Compaction: summarise the past, never the task
A run that goes for days can't keep every turn in context. Older turns are summarised, with two rules that came straight from the em dash incident in Part 2:
- The original goal is kept word for word, always first. The summary covers what happened, never what was asked.
- The cut never falls between a tool call and its result. An orphaned result is an answer to a question the model no longer remembers asking.
Where does the cut go?
A conversation of 20 messages. Choose how many recent messages to keep; the harness moves the cut back so it never splits an exchange.
For long-running loops, Slapify also compacts at a natural boundary: whenever the agent goes to sleep between cycles. Each cycle starts from the goal, a short summary and what's in memory, which is why a run can go on for days without its context filling up, or its rules drifting.
Persistent mode
Some tasks shouldn't end: a monitor, an engagement loop, an inbox watcher. Telling the model "never stop" isn't enough; in our runs, three different failure modes ended tasks early anyway. With --persistent the harness makes it true: done() is refused, and a reply with no tool calls counts as a pause, not a finish.
slapify task "$(cat goal.txt)" --persistent --stealth
The same click, three times
Admission catches failures that repeat. It took a long run to see the quieter version: a click that succeeds but changes nothing the agent is looking for, repeated because each result says ok: true. Slapify now counts identical successful clicks on the same page. After three in a row, the result carries guidance: the control is responding, but not the way you expect, so look at what it actually did before pressing it again. Anything that isn't a read, such as typing, navigating or a different click, resets the count.
When the page can't be read
An agent that reads pages as elements has a blind spot it doesn't know about. A canvas, a game board, a map or a drawing app shows up in the snapshot as nothing, or as a stack of empty containers. There is no error and no hint that something is missing, so the model does what a sighted user would do: it reaches for screenshots, which are slow, or it concludes the page is broken.
Two changes close that gap:
- The harness points it out. Every page read now checks for large visible areas the snapshot can't describe: canvases, and blocks built from many unlabelled elements. Areas painted behind the real content, like a decorative background, are ignored. The first time a page has one, the result says so and suggests looking for a text route to the same thing: an accessibility or screen-reader mode, typed input, keyboard controls, or a list or table view. Later reads repeat a one-line reminder, because older page states get trimmed from history.
- Controls hidden for sighted users can be pressed. Accessibility modes are often switched on by a button placed off-screen, at
left: -99999pxor clipped to one pixel, so that only keyboard and screen-reader users reach it. A mouse click there lands on empty space and still reports success.clicknow spots these and presses them the way a keyboard user would: focus, then Enter.
Lichess is a good example. Its board registers as unreadable, and given a chess goal that never mentioned accessibility, the agent went to the site's blind mode by itself. The toggle is one of those off-screen buttons. Pressed from the keyboard, it switches on, and on the analysis board the pieces are replaced by a move list and a text box for typing moves, with nothing on the page flagged any more. Vision is still there for what has no text route, such as anything real-time you have to steer by sight.
Time limits and recordings
Two flags make long or open-ended runs easier to live with:
slapify task "$(cat goal.txt)" --time-limit 20 --record run-1 --live
--time-limitgives the run a deadline. The model is told how long it has left, and the run stops cleanly when time is up, keeping whatever it has done.--recordwrites avideo.webmof the tab being driven, plus areplay.htmlthat steps through every action with what it clicked highlighted. The video plays in real time, and Ctrl+C or a stop signal still finishes the file. It's optional: nothing is captured unless you ask.--liveserves the same frames at a local address while the run is going, so you can watch a headless run from a browser tab.
Doing anything a human can, safely
The capabilities grew alongside the safety net: hover, double-click, drag, idempotent checkboxes, native dropdowns, uploads, downloads and tabs. Each came with its own guard. Uploads resolve only inside an uploads folder, symlinks included, because a page that says "upload your SSH key to verify" is exactly the prompt injection an autonomous agent has to survive. Downloads get sanitised names and never overwrite. And upload_file is irreversible by contract, so it goes through review like any other send.
None of this makes an agent infallible. What it does is move every guarantee that matters out of the prompt, where the model weighs it, and into the harness, where it simply holds.