Skip to content

Guardrails and outcomes

The stop model — no-progress checks, parked work, budgets and ceilings, and the twelve run states a Loop can hold.

For people running agent work18 pages in this section

A Loop never runs forever and never lies about how it ended. Its contract carries the stop model: the axes that decide "no progress," the budgets that cap spend, the ceilings the daemon enforces regardless of what a definition asks for, and the fixed set of terminal outcomes. This page is the reference for all of them.

Run states

A run holds exactly one of twelve states — five live, seven terminal. Color carries state, never category; a node's class is never colored.

Live states

StateMeaning
queuedA same-Loop start deferred by concurrency: queue. Promoted FIFO when a sibling run terminates.
runningThe coordinator owns an active generation. The initial state at start.
watchingA dormant run awaiting a watch-source poll tick or a matching watch-events event.
needs-approvalA dormant run parked on a human gate (or a budget.on_exceeded: escalate). Resumed by an approval decision.
pausedAn operator paused the run at a generation boundary.

needs-approval and paused are live pauses, never terminal — the run is still alive and will resume. paused is operator-initiated; needs-approval is system-initiated by a human gate.

Terminal outcomes

OutcomeMeaning
doneThe goal was verified. The only success outcome.
no-opThe run executed but found nothing to do. A clean watch tick is no-op, never a fake done.
blockedAn external dependency made progress impossible — a missing dependency, missing credential, or unreachable resource; a human gate reject; or a loop.gate.pre hook denial. Distinct from stalled.
failedAn unrecoverable node or gate error, or a loop.generation.pre hook denial.
canceledAn operator cancel or kill, with cause operator_cancel or operator_kill.
exhaustedA hard stop limit tripped before the goal — the iteration cap or an authored max_fan_out bound.
stalledNo progress is being made — the no-progress window elapsed, the failure circuit breaker tripped, the blocker-ID signature repeated, or a watched source went silent past its window.

ready and awaiting_child are node-level states only (a node's per-run status inside a generation) — they are never run states. awaiting_child marks a run-loop node in await mode parked until its child Loop terminates.

While that child is live, the parent remains live, dependent nodes remain pending, and a daemon restart restores the same child_loop_run_id without submitting another child. A child ending done or no-op settles the node as succeeded; every other terminal child outcome settles it as failed. mode: detach remains immediate success and does not establish parent wait ownership.

No-progress checks

"Making progress" is measured from committed work, not elapsed wall time.

Generation progress

The blocker-ID stall signature compares the sorted set of blocking_issues[].id a gate emits across contract.no_progress.window. The same unresolved blockers repeating means the Loop is spinning, and the run ends stalled.

Evidence-based liveness

A long-running node stays healthy while the daemon sees stream activity, an in-flight tool, or transport presence. Silence raises an attention flag; duration alone never ends, pauses, or cancels the node. Only a watch-source that exceeds its authored silence window ends the run stalled.

A separate failure circuit breaker ends the run stalled after consecutive node failures reach the daemon limit, so a Loop that only ever fails cannot burn its whole iteration cap.

Parked work suspends work bounds

Paused nodes, durable waits, approval waits, and quarantined nodes are parked. The coordinator uses one classifier to exclude them from scheduling, rerun sets, due scans, and no-progress arithmetic. Their attempt timeout and deadline clocks stop while parked, as does the run's wall-clock work budget. Token usage still counts whenever tokens are spent.

Parked does not mean hidden or terminal. Waiting, quarantined, attention, and retrying inventories remain available through compozy loop nodes --state …; a required quarantined producer raises attention instead of terminating the run. Waits and approvals have no default expiry. Authors may add an expiry and route, while operators may explicitly resume, cancel, kill, or requeue work.

Re-attempt and rejection semantics

Every new generation records why it exists. Node failures use the configured re-attempt strategy; gate rejection uses the gate's explicit route. They are separate controls.

CauseWhat runs nextOrigin
Node failure with failed_only (default)Failed or pending nodes and their transitive dependents re-run; unrelated successful outputs carry forward.reattempt
Node failure with full_bodyThe whole body re-runs.reattempt
In-body gate rejection routed to reviseProducers of every route-causing gate, those gates, and their dependents re-run; unrelated successful outputs carry forward.gate_revise
Metric gate revise with an existing bestThe same producer-scoped repair runs, but carried outputs seed from the best generation.ratchet_restore
In-body gate rejection routed to next_generationA fresh full-body generation starts.gate_next_generation
Definition-of-done rejection routed to next_generationA fresh full-body generation starts instead of terminating the run.dod_retry

revise is targeted repair; next_generation is a fresh pass. Both carry the prior verdicts in previous.verdicts.* and their ordered causes in previous.route_causes, so the next attempt can act on typed failure context. Iteration caps, budgets, gate revision limits, no-progress detection, and the failure circuit breaker apply to every row in the table.

Weak evidence is a route, not a hidden prompt instruction

If evidence quality decides whether work may continue, encode that decision as a gate. A prompt that says “improve this if the evidence feels weak” leaves the transition inside model prose; the runtime cannot audit, bound, or reproduce it. A typed criterion plus on_result makes the same decision visible and deterministic:

- id: evidence
  class: control
  kind: gate
  criteria:
    - id: source_quality
      type: agent-judge
      agent: evidence_reviewer
      rubric: >
        Approve only when every material claim cites a primary source and the cited evidence
        directly supports it. Return stable blocking issue ids for missing or weak support.
  verdict_policy: revise_until_clean
  on_result:
    fail: revise
  max_revisions: 4

The next producer can read previous.verdicts.evidence.blocking_issues under a guarded {{ with .previous }} block. Choose revise when the evidence-producing branch should be repaired, next_generation when the entire body needs a fresh pass, or halt when weak evidence must stop the run. The gate owns the decision; the prompt consumes its recorded evidence.

Limits, budgets, and ceilings

Every stop limit has a per-Loop default (the definition, seeded by [loops.defaults.*]). Some also have a hard daemon ceiling. The fan-out window has no fixed daemon cap; the required positive max_fan_out remains the author's logical-width bound.

LimitContract keyDelivery defaultwatch-source defaultDaemon ceiling
Iteration capcontract.iteration_cap500 (unbounded, )none — 0 means unbounded
Token budgetcontract.budget.tokens0 (off)0 (off)none
Wall-clock budgetcontract.budget.wall_clock_sec0 (off)0 (off)none
No-progress windowcontract.no_progress.window3230
Fan-out windownode max_parallel / fan_out_width42none
Gate max revisionsgate max_revisions1064

A definition containing watch-source defaults iteration_cap: 0 and renders the cap as . watch-events retains the delivery default of 50 unless its contract explicitly sets 0; a 0 budget means unlimited (progress-first). Budgets are opt-in — set a nonzero value to cap spend. Fan-out logical width is bounded by the node's required positive max_fan_out; only the max_parallel window materializes at once.

When a set budget is exceeded during active work, budget.on_exceeded chooses the response:

  • halt (default) — end the run exhausted.
  • escalate — park the run in needs-approval for a human decision.

Cost is a meter, not a cap

The run page shows a live cost meter (tokens × price). It is a visibility signal only. No surface accepts a cost cap — the run form and configure sheet have no USD field, and no daemon ceiling is expressed in dollars. Spend is bounded by the token budget and the iteration cap, not by cost.

Where these show up

  • The catalog shows each Loop's last outcome and success rate.
  • The run page streams the live state, the five meters, and the terminal-outcome banner.
  • The DSL reference defines every contract field named here.

On this page