Design an agent that runs 8 hours unattended, no human in the loop, and produces a report you can trust by morning: a ReAct loop wrapped in five harness mechanisms the model cannot reach.
Preconditions (build autonomy only if all three)
- Reversible actions replace a human’s undo; irreversible mistake at 3am has unbounded cost.
- Machine-checkable success replaces a human’s judgment; a model asked “did you succeed?” just predicts what a successful assistant says.
- Hard budget ceiling replaces a human’s attention; a stuck loop burns 8 hours at full token price.
- If the success predicate can’t be written as code, the task is not autonomy-eligible.
- Human on the loop (watches, can intervene, run never waits) is the better default when the constraint allows.
Five mechanisms (what makes it more than ReAct)
- Verification after every task: machine-checkable, never the model’s opinion.
- Checkpoint before and after every task.
- Drift check against the immutable original goal.
- Dual budgets: one the model sees, one it cannot.
- Termination set where every exit, including failures, is reported.
Termination set (exhaustive + prioritized)
STOP has 7 members; the set is exhaustive (every exit, including a raised exception, maps to exactly one) and prioritized (order of checks is code, not convention). GOAL_MET is checked first so a run that meets the goal on its last dollar reports success. The harness runs the goal predicate, never the agent.
| Stop | Class | Exit | Alert |
|---|---|---|---|
| GOAL_MET | SUCCESS | 0 | — |
| BUDGET_EXHAUSTED | PARTIAL | 2 | alert |
| TIME_LIMIT | PARTIAL | 2 | alert |
| DRIFT_DETECTED | FAILURE | 3 | page |
| NO_PROGRESS | FAILURE | 3 | alert |
| QUEUE_EMPTY_GOAL_UNMET | FAILURE | 3 | alert |
| UNRECOVERABLE_ERROR | FAILURE | 4 | — |
- Only drift pages (wakes someone now); it’s the ending spending money on the wrong problem.
while True+ one stop check at top;tryclassifies exceptions (the likely exit); module-levelassert STOP == set(STOP_CLASS)catches a class with no producer at import.- Empty-queue trap:
while queue:makes “empty queue, goal unmet” the normal exit, so it reports FAILURE ascompleted. A false success is worse than a crash.
Drift detection
- Drift: each self-generated task is locally reasonable but the chain isn’t; it’s a property of the path, so no local check catches it. Compare the queue against the origin.
- Goal must be immutable: held in the harness (no tool reaches it), SHA-256 hashed,
assert_intacton every check. Unreachability is the real lock; the hash is a cheap secondary check. - Judge runs in a fresh context: only
(original_goal, queue), never the agent’s reasoning. reasoningfield emitted beforedrift_score(constrained decoding emits in schema order), so the number is conditioned on analysis.- Cadence
DRIFT_EVERY = 5(full Opus call, slow-moving property). Strict>:> 0.5halt,> 0.3prune off-goal tasks,<= 0.3aligned. - False-negative bias; calibrate on 20 labeled queues (10 aligned, 10 drifted) before trusting a threshold.
Checkpointing (atomic or nothing)
- A torn write that still parses is worse than no checkpoint: silent, confident wrong resume.
- Safe write: temp file on same filesystem →
flush+fsync(fd)→os.replace(atomic rename) →fsyncthe directory. - Frame the payload with a SHA-256; every refusal raises
RuntimeErrornamingUNRECOVERABLE_ERROR.os.replacedefeats a torn file; the checksum covers bit rot after commit. Neither catches a body wrong before serialization. - Store artifacts by path, history as a compacted summary (not raw transcript). Checkpoint per task, not on a timer.
- Resume verifies, never re-executes an
IN_PROGRESStask. Three-way outcome:done→ complete; nothing happened → re-queue;partially_applied→ human. Re-running non-idempotent work (a post, a payment) does damage.
Budgets, cost, evals
- Hard cap: harness ledger, invisible to model,
reserve()refuses the call before it’s made (worst case = prompt +max_tokens), setsblockedsoshould_stopcan reportBUDGET_EXHAUSTED(spend stalls below the cap, e.g. $27.75 of $28). - Task budget (
task_budget, tokens): visible to model, pacing only; a runaway loop sails past it. Run both. recordcounts all four usage fields (cache reads 0.1x, writes 1.25x) or the ceiling is far from where you think.- A night ≈ $22 (40 tasks,
6 calls each); 83% is task execution. Input cost grows with the square of loop length. Set cap at $28; track cost per verified task ($0.73), halt if pass rate drops below ~70%.