Repository navigation
[coordination] Platform-provided durable agent loop — the Loop is an agent-level primitive, not a browser poll #158
Description
Re-scoped 2026-08-04 — un-deferred, and now a precondition for #184
Originally filed as "converge the client-driven Loop into the durable Pilot" — framed as Coder cleanup. It is bigger than that: the autonomous loop is a general agent capability the platform should provide, and today it is half-built in the wrong place.
What is actually true in the code
The platform already owns the thinking:
POST /v1/instances/:id/loop-decidelives inroutes/instances.ts— notcoding.ts— with a pure, testedlib/loop-decide.ts. It is instance-scoped and agent-generic (BYOK Claude decides continue/done/escalate/failed).
The browser owns the persistence:
store/console/src/pages/InstanceDetail.tsxpolls/loop-decideand sends the next instruction. Close the tab, lock the phone, sleep the laptop — the loop dies mid-objective.
Meanwhile the Pilot (CodingSessionWorkflow) is a durable server-side loop, but Coder-bound and tied to the workflow closed enum (#160).
So there are three overlapping things: a generic decide endpoint, a client-driven driver, and a durable-but-hardcoded workflow. The decide step is already right. What is missing is a durable, generic loop runner that any agent can use, with the Pilot as one configuration of it.
Why this is now blocking, not deferred
You cannot budget a loop you do not own. #184 requires bounding spend on unattended work — but today the Loop runs in the user's browser, so the platform cannot stop it, cannot reliably count its iterations, and cannot reserve against a pool. Every enforcement point #184 needs is on the server side of a loop the server does not drive.
Making the loop platform-owned is therefore a precondition for #184, not parallel work.
It is also a straight UX win
Today closing the tab kills an in-flight objective. A durable loop survives tab close, phone lock, and laptop sleep, and can be reattached from another device. That is an improvement to the current Coder experience, not just plumbing — and it is what makes max iterations (already in the Loop UI) meaningful rather than best-effort.
Scope
- A durable, generic loop runner at instance level: objective → snapshot →
loop-decide→ act → repeat, until done / escalate / budget exhausted. - The console Loop becomes a thin UI over it — start, watch, interject, stop — rather than a parallel client-driven brain.
- The Pilot (
CodingSessionWorkflow) becomes one configuration of the primitive; Coder keeps its behaviour. - Works for non-coding agents too (the Chat-tab Loop already targets these).
- Budget hooks ([coordination] Autonomous work budget — bound spend across every unattended entry point #184) enforced by the runner, once, for every agent.
- Interject / stop / resume survive disconnect and are addressable from another device.
Acceptance
- An objective started in the console continues after the tab is closed and can be reattached.
- One autonomous brain, not two — the client-driven driver is removed.
- Coder's Loop and Pilot behaviour are preserved from the user's point of view.
- Non-coding agents can run a durable objective.
- Iterations and spend are countable and stoppable server-side (what [coordination] Autonomous work budget — bound spend across every unattended entry point #184 depends on).
Part of #154. Pairs with #160 (retiring the workflow enum makes the loop fully declarative). Design: docs/supervision.md.
Activity
docs: coordination-primitives design (#154) — delegate goals, not one…
[-][coordination][deferred] Converge the client-driven Loop into the durable Pilot[/-][+][coordination] Platform-provided durable agent loop — the Loop is an agent-level primitive, not a browser poll[/+]feat(coordination): delegation budget — shared root pool, reserved be…
feat(coordination): platform-provided durable agent loop (#158)
serge-ivo commentedon Aug 4, 2026
Server side landed in
60b59fc(migration 0062, workflow bindingAGENT_LOOP).Shipped:
AgentLoopWorkflow·lib/agent-loop.ts(pure policy) ·lib/agent-loop-store.ts·lib/loop-orchestrator.ts·POST /v1/instances/:id/loop,GET …/loop,GET …/loop/:runId,POST …/loop/:runId/cancel.Each iteration: cancel? → reserve → act (the instance's own brain, with ITS tools and guardrails per #185) → decide → apply the pure policy.
Two policy rules worth recording:
done, not the misleadingmax_iterations.no_progressguard stops a loop repeating the same instruction.parseLoopDecisionalready infers "failed" from obvious self-repetition in prose, but that depends on the model noticing; this does not.This closes #184's open half. Reserve before the model runs, settle the actual cost measured as an
ai_usagedelta across the iteration. My first pass settled the reserved amount — which means the refund never happens and the pool drains at the worst case every iteration, defeating reserve-and-refund entirely. Caught and fixed before commit. Settled even on a failed step, since the tokens were spent either way. Cancellation is cooperative (checked at the top of an iteration) so a stop cannot strand a reservation and leak headroom.One regression I introduced and fixed: refactoring
/loop-decideonto the shared orchestrator swallowed the missing-API-key 402 into a silentescalate. Credential errors are now rethrown and handled explicitly in both callers — they fail identically on every retry, so neither the route nor the workflow should burn iterations on them.51 new tests; full suite 1639 passing.
Not included: the console migration. The browser Loop in
InstanceDetail.tsxstill polls/loop-decidedirectly, so until that is repointed at these routes the durability is available but unused by the UI. That is the remaining work on this ticket.feat(coordination): turn on agent-to-agent delegation; console rides …
serge-ivo commentedon Aug 4, 2026
Console migration done in
67eb915— this ticket is now complete.InstanceDetail.tsxno longer is the loop. It POSTs/loop, watches the run, and cancels cooperatively. Removed: the client-side continuation and the effect that kicked the next iteration after a human interjected — a kick from the client would now race the workflow and double-send.Two details worth recording:
Net effect: an objective now survives closing the tab, locking a phone, or sleeping a laptop, and can be watched from another device.
feat(coder2): declare the Coder as data — two templates, no bespoke c…
feat(mcp): make composing a multi-agent system self-serve (#182, #183, …
feat(console): autonomous-runs history (#158, #182)
fix(loop): read the AgentDO chat response correctly — it returns an o…
serge-ivo commentedon Aug 4, 2026
Done. Server side in
60b59fc(AgentLoopWorkflow, migration 0062); console migrated off the browser poll in67eb915; autonomous-runs history in8042f7a. An objective now survives closing the tab, and the platform drives the loop — which is what made #184 enforceable.fix(coder): the Coding tab's Loop stops being the loop, and starts on…
feat(loop): per-instance objective cap, default 8,000, with a message…