- 4 agent roles
- 1 state machine
- turns survive disconnects
The problem
A coding agent is not a chat reply. One request ("add 2FA to the settings page") can mean minutes of planning, editing files, running checks and fixing what broke. The first version I worked with ran that whole loop inside the request that started it. That works in a demo. It does not work for real people, who close laptops, switch networks and reload tabs.
Every dropped connection killed the turn halfway through. The files were left half-edited, the user saw a spinner that never ended, and nobody could tell what the agent had actually done.
What a turn needed to be
I wrote down what had to be true before writing any code:
- A turn outlives its connection. The browser is a window onto the work, not the thing doing it.
- A turn can be resumed. If the process running it restarts, it picks up from the last finished step, not from scratch.
- A turn has one owner at a time. Two workers must never edit the same workspace at once.
- Anyone can reattach. Reopen the tab and you see the live state and everything that happened while you were away.
The design
Each turn became a durable job with its own record: its input, its current step and an append-only log of events. The browser no longer runs the turn. It subscribes to the turn's events, and when it reconnects it asks for everything after the last event it saw.
browser ──(start turn)──▶ API ──▶ turn record + job queue
▲ │
└────(events since #n)◀── worker ◀──┘
runs one step, saves it, emits eventsThe worker runs one step at a time and saves the result before starting the next. A crash costs at most the step in flight, and steps are written so they are safe to run again.
Four roles instead of one prompt
One giant prompt that plans, codes and checks its own work tends to mark its own homework as correct. So the agent became a small state machine with four roles:
- Planner turns the request into concrete steps.
- Implementer makes the changes for one step.
- Reviewer checks the change against the plan and the project's rules.
- Fixer handles what the reviewer rejected, then hands back for another review.
The transitions are explicit and stored with the turn, so a resumed turn knows exactly which role runs next. The tools each role can use are exposed through MCP, which keeps them the same whichever model or client is driving.
What I would do differently
- Design the event log first. Reattaching, progress bars and debugging all came from the same log. I added it after the job system, and it should have come first.
- Put a limit on review loops from day one. Reviewer → Fixer → Reviewer can loop forever on a vague rule. A cap plus a "needs a human" exit is cheaper than tuning prompts.
- Test disconnects on purpose. Killing the connection and the worker at random points found more bugs than any happy-path test.
Takeaway
If a task takes longer than a request, make it a durable job with a log, not a long request. The UI gets simpler, the agent gets safer to retry, and "what did it do while I was gone?" finally has an answer.