Harness Design Patterns for Long-Running Agents
Long tasks often fail because a new session cannot reconstruct what the previous session changed. Context compaction helps preserve conversation history, but it is not a substitute for project state, acceptance criteria, or recoverable checkpoints. A harness lets every session resume quickly and deliver one verified increment.
Separate the model, agent loop, and harness
User outcome
↓
Harness: startup, state, budget, access, retries, handoff, evaluation
↓
Agent loop: observe → plan → call tools → record result
↓
Model: reason and generate within the current context
↓
Environment: repository, browser, database, and external APIs
| Layer | Owns | Does not own |
|---|---|---|
| Model | Reasoning in the current context | Remembering an unpersisted decision from days ago |
| Agent loop | Tool orchestration and response handling | Treating a completion claim as evidence |
| Harness | State, recovery, gates, and budgets | Implementing all business logic for the model |
| Environment | Durable facts and outcomes | Acting as state storage through temporary console output |
Four failure modes a harness should prevent
| Failure | What it looks like | Harness control |
|---|---|---|
| Attempting the whole product in one session | Context ends with a half-built system | Claim one feature per session |
| Reinvestigating from zero | Repeated code reading and guessed progress | progress.md, Git history, and an init script |
| Declaring success too early | Unit tests pass, user flow still fails | An immutable end-to-end feature list |
| Building on a broken environment | New code hides an existing failure | Smoke test at the start of every session |
Anthropic's long-running agent experiment used distinct initializer and coding-agent prompts. Those are roles, not necessarily separate services or different models.
A minimal project layout
agent-harness/
├── init.sh
├── feature_list.json
├── progress.md
├── runs/
│ └── 2026-08-25T10-00-00Z.json
└── workspace/
Make the feature list the acceptance source of truth
[
{
"id": "checkout-card-payment",
"priority": 1,
"description": "A signed-in user can complete a card payment",
"steps": [
"Open checkout with one item",
"Enter valid test card details",
"Submit payment",
"Verify order status is paid",
"Verify receipt remains visible after refresh"
],
"passes": false,
"evidence": []
}
]
The coding session may change passes from false to true only after appending evidence. It must not delete a test, weaken an assertion, or rewrite the user outcome merely to obtain a pass.
The initializer session builds the runway
The initializer should not implement ten features. Its job is to create a reproducible working environment:
- Break the outcome into end-to-end features.
- Write an
init.shthat starts the environment deterministically. - Define progress and run-record formats.
- Run one smoke test to prove that the baseline works.
- Create the first clean checkpoint.
#!/usr/bin/env bash
set -euo pipefail
test -f package.json
npm ci
npm run build
npm run dev
A production init script also checks ports, health endpoints, timeouts, and log locations. Do not use a fixed sleep as proof that a service is ready.
Every coding session follows the same protocol
1. Confirm the working directory
2. Read progress.md, feature_list.json, and recent Git history
3. Start the environment and run the smoke test
4. Select the highest-priority failing feature
5. Implement the smallest vertical slice
6. Run unit, integration, and actual UI checks
7. Save evidence, then set passes=true
8. Update progress and create a clean checkpoint
progress.md should answer operational questions, not tell a story:
## Current state
- Active feature: checkout-card-payment
- Last verified commit: 1a2b3c4
- Dev command: npm run dev
- Smoke test: passed at 2026-08-25T10:22:00+10:00
## Evidence
- Unit: payment.service.test.ts
- E2E: runs/checkout-card-payment.webm
- Database: order ord_123 has status paid
## Next safe action
- Verify the receipt survives a hard refresh
Evidence has levels
| Level | Evidence | What it proves |
|---|---|---|
| Code | Diff and typecheck | The implementation exists and compiles |
| Unit | Deterministic tests | Local rules behave correctly |
| Integration | API and database assertions | Services work together |
| User flow | Browser or real client | The requested outcome is reachable |
| External state | Provider read-back | A payment, publication, or message truly happened |
If the outcome is “publish a video,” a local file or successful upload is not the finish line. The harness should require the public provider URL and status read-back.
Recovery and idempotency
Give each task a stable ID and a small state machine such as pending, running, and verified. After a crash:
- Read the latest append-only run record.
- Check whether the external side effect already happened.
- If it happened, attach evidence instead of repeating it.
- If it did not happen, resume from the latest checkpoint.
- If the state cannot be determined, record
unknown; do not guess.
Payments, email, publishing, and deletion require idempotency keys or provider request IDs.
Budgets and stop conditions
Before execution, define:
- Maximum model calls and token budget.
- Tool-call and session timeouts.
- A threshold for repeated identical errors.
- Operations that need human approval.
- Conditions for
blockedand permitted fallbacks.
A long-running task without stop conditions is an uncontrolled loop, not a dependable autonomous agent.
Practice task
Design a harness for “upload a CSV and generate an analysis report”:
- Write at least 12 feature cases, including empty files, invalid encodings, and duplicate uploads.
- Add the init script and health check.
- Limit each session to one feature.
- Preserve both the database record and the downloadable file as evidence.
- Simulate a crash after writing the file but before updating the database, then verify recovery creates no duplicate report.
Self-check
- A new session does not need to guess what happened previously.
- The coding agent cannot delete or weaken feature criteria.
- Passing includes evidence from the user flow.
- External side effects can be recovered through idempotency.
- The harness has explicit budgets, timeouts, and approval boundaries.
Related reading
Official references
📚 Related resources
❓ Common questions
Open a question to review the practical answer.
What is an agent harness, and how is it different from the model itself?
A harness is the execution framework around the model: state storage, session handoff, task tracking, and environment setup. The model handles reasoning and code generation; the harness ensures each new session starts with a complete picture of project state. Without one, every session starts from scratch.
Why use JSON instead of Markdown for the feature list?
Anthropic found the model is less likely to inappropriately change or overwrite JSON files. Combined with hard rules — only the passes field may flip to true, and removing or editing test items is forbidden — a JSON list reliably serves as the cross-session source of truth.
The agent's unit tests and API calls all pass — why isn't the feature done?
Code-level correctness is not end-to-end correctness. Unit tests and curl can succeed while the actual UI is broken. The feature only passes after walking through it with browser automation (e.g. Puppeteer MCP) the way a real user would.
Does the two-agent pattern require the Claude Agent SDK?
No. The init.sh + features.json + claude-progress.txt + git discipline structure is framework-agnostic — a hand-rolled agent loop or a cron job relaunching Claude Code works too. What matters is persisting state outside the context window.