Skip to content

How It Works

Daniel Boles edited this page Nov 6, 2026 · 1 revision

How It Works

Why unattended runs need these checks

The characteristic failure of an unattended agent is continuation rather than collapse. A process that crashes stops spending and announces the problem, whereas an agent that has lost the thread goes on reading files, repeating searches, and calling a model, and every one of those actions looks like work from the outside. The difficulty is that the agent is the worst witness to its own condition, because a model that has stopped advancing can still produce a confident account of what it will do next. Therefore the controls that matter sit outside the model, in ordinary code that reads counters, timestamps, and files. They act at four moments in a run.

Moment Control Question it answers Without it
Before work starts Admission Is this request complete enough to finish, and small enough to afford? Work begins with no definition of done, so nothing can say when to stop.
While work runs Budget How much of the allowance has gone? The limit arrives as a provider error, at a moment nobody chose.
While work runs Watchdog Has anything been proved since the last check? Activity passes for progress.
After work stops Checkpoint Can another model continue from here? The next attempt starts from nothing.

Admission: refuse the task that cannot finish

Admission is the cheapest control because it acts before anything has been spent. admit() in governance/admission.py runs its checks in a fixed order: schema, acceptance criteria, stop condition, cache hit, fresh-token limit, total-token limit, and finally the choice of model. The order carries the reasoning. A request with no stop condition goes back as REQUIRE_REPAIR before any budget question arises, because an agent with no definition of done has no honest way to finish, and a budget cannot repair that; it can only end the run later, at greater cost. A request that is complete but too large receives REJECT, which is a different fact and calls for a different response from the caller. Bounded work requested of the primary model receives DOWNROUTE to the worker role, so the expensive model stays reserved for work that declares requires_deep_reasoning. Admission decides what may begin; however, it can say nothing about what happens next, and that is the job of the budget.

Budgets: make the limit arrive in stages

A budget with one hard limit fails at the worst possible moment, because the run reaches the wall with no allowance left to save its state. evaluate_budget() in governance/budget.py therefore compares five counters (fresh input tokens, output tokens, turns, tool calls, and wall seconds) against their limits and reports the worst ratio as a level. At 70 percent the level is SOFT, with the instruction to compact context and reduce work. At 88 percent it is HARD, with the instruction to checkpoint before additional expensive work. At the limit itself it is EMERGENCY, and allowed becomes false. The consequence of that staging is that a checkpoint happens while the run can still afford one. Even so, a budget measures spending and not result, and a run can stay comfortably inside its allowance while achieving nothing at all.

Watchdogs: separate motion from progress

The watchdog exists because a busy agent and a productive agent look identical to a budget. evaluate_watchdog() in governance/watchdog.py counts time, actions, and tokens since the last proof of progress, not since the run began. With the default WatchdogPolicy, the run receives WARN after 60 seconds and CHECKPOINT_AND_STOP after 120 seconds, or as soon as it passes 5 actions or 4,000 tokens with no proof. Loop fingerprints cover the narrower case of exact repetition: fingerprint() hashes the tool name, the arguments, and the task state with SHA-256, and it ignores timestamp, request_id, and nonce, so a retry that changes only a timestamp still counts as the same call. What qualifies as proof remains the host's decision. The gateway contributes one such signal, a capability_completed event with a progress weight of 1, while a refusal records a weight of 0. Stopping a stalled run is only half the problem, because a stop without a record throws away everything the run has learned.

Checkpoints: make stopping safe

A stop is useful only when something survives it. write_checkpoint() writes a temporary file and then replaces the target, so a crash mid-write leaves the old checkpoint or the new one and never half of either. choose_orchestrator() then ties recovery to that file: a switch to the fallback model raises RuntimeError when no checkpoint exists, because the fallback has nothing else to continue from. The switch is also one-way. Once fallback is active, every later call returns the fallback with the reason one-way fallback lock, which prevents a run from bouncing between two models while each undoes the other. That is the whole chain: admission defines done, the budget warns early enough to save, the watchdog notices when nothing is happening, and the checkpoint makes the stop recoverable. Each control depends on the one before it, and the next section shows how they fit into one flow.

Architecture

flowchart TD
    A[Orchestrator] -->|context capsule| B{Admission}
    B -->|needs repair or too large| A
    B -->|allowed or downrouted| C[Agent works]
    C --> D[Hooks: loop check and ledger]
    D --> E{Budget and watchdog}
    E -->|continue| C
    E -->|stop| F[Checkpoint]
    F --> G{Fallback router}
    G -->|checkpoint exists| A
    A -->|task packet| H{Gateway}
    H -->|refused| I[Result packet with a reason]
    H -->|schemas and outputs pass| J[Handler]
    J --> K[Result packet with proofs]
    A -.->|advisory task| L[Ollama pool]
    L -.->|needs primary review| A
Loading

The package has no run loop of its own, and that absence is deliberate. These are plain functions and a command line that a host calls at the points in the diagram, so the same checks can serve an agent host's hooks, a shell script, or an orchestrator in another process.

Main parts

Part Code Role
Admission governance/admission.py Sends back or refuses a request before work starts
Budget governance/budget.py Reports NORMAL, SOFT, HARD, or EMERGENCY
Watchdog governance/watchdog.py Reports CONTINUE, WARN, or CHECKPOINT_AND_STOP
Loop fingerprints governance/loop_fingerprinter.py Flags an identical call past a limit
Fallback router governance/router.py One-way switch, gated on a checkpoint
Checkpoints and ledger governance/checkpoint.py, governance/ledger.py Atomic JSON state and an append-only JSONL log
Rule packs governance/rulepack.py, governance/rule_eval.py YAML rules in a closed grammar, with three-valued logic
Capability gateway gateway/dispatch.py Dispatches a task packet and answers with a result packet
Agent pool pool/ Optional advisory work on a local Ollama model, off by default
Agent definitions governance/agent_registry.py Refuses unsafe combinations in agent files

Walkthrough: examples/quickstart.py

The example is a tour rather than a recording of a live agent. Each stage receives fixed inputs and calls the real code, and the pool stage uses a stub in place of Ollama, so the whole script runs offline and writes only to a temporary folder. The order follows the life of a run, which makes the tour a fair picture of how the controls relate. The full output, from a run on 2026-11-06, is below; the rest of this section reads it line by line.

admission: DOWNROUTE to worker: primary model not required for bounded implementation
budget: HARD
loop: deny repeat call: True
watchdog: CHECKPOINT_AND_STOP
fallback: fallback, resume at ['rerun tests']
DECISION ACCEPT [MEDIUM] by RG-004 of RELEASE-GATE
  MATCHED RG-004: change.tests_passed == true and change.review_confidence >= 0.9
    change.review_confidence = 0.95
    change.tests_passed = True
DECISION none: no rule matched in RELEASE-GATE
dispatch: passed [{'name': 'word_count', 'value': 4}]
dispatch: blocked capability_unbuilt
pool: completed, demo:latest, review required: True

1. Admission. The request asks for the primary model, estimates 1,000 fresh tokens against a limit of 5,000, and carries acceptance criteria and a stop condition. It passes every check, but it does not set requires_deep_reasoning, so admission moves the work to the worker role. The decision itself has spent nothing.

2. Budget. The budget allows 1,000 fresh tokens and the run has used 900. A ratio of 0.90 is above the 0.88 line, so the level is HARD while allowed stays true. The run may continue, but the instruction is to checkpoint first, because this is the last moment when a checkpoint is cheap.

3. Loop check. The script records the same grep call three times with a limit of 2. The tracker reports True on the third, since the count now exceeds the limit. The call carries a timestamp argument, and normalization removes it, which is why a real retry with a fresh timestamp would still match.

4. Watchdog. The state says the last proof of progress came 130 seconds ago, and the default policy intervenes at 120. The answer is CHECKPOINT_AND_STOP, regardless of how much budget remains. This is the point of keeping the two controls apart: the budget above would have let the run go on.

5. Fallback. The script writes a checkpoint containing {"next_actions": ["rerun tests"]} and then reports primary_rate_limit. Because that reason is eligible and the checkpoint exists, the router moves the run to the fallback role, and the script reads rerun tests back from the checkpoint as the next action. The same call with no checkpoint raises Fallback blocked because no checkpoint exists.

6. Release rules. The rule pack examples/rules/release-gate.yaml decides whether the finished change may pass. Given tests that passed and a review confidence of 0.95, rule RG-004 accepts it, and the trace shows the value behind each operand. The second decision is the more instructive one. Given an empty context, no rule fires, and the command line shows why:

DECISION none: no rule matched in RELEASE-GATE
  UNKNOWN (4): context did not supply:
    RG-001: change.touches_credentials
    RG-002: change.tests_passed
    RG-003: change.review_confidence
    RG-004: change.review_confidence, change.tests_passed

A check that did not happen is reported as unknown, never as false, so a wiring fault cannot pass for a clean result.

7. Dispatch. run_task_packet() sends examples/task_packet.json to the capability text.word_count. The handler counts "four words of text" and reports the required artifact, so the gateway returns passed. The same packet aimed at text.summarise comes back blocked with capability_unbuilt, because that capability is declared in examples/capabilities.yaml and has no handler yet. Nothing raised; the caller received a valid result packet in both cases.

8. Advisory pool. The last stage delegates a review to a local role, with a stub standing in for the model. The stub reports a confidence of 0.6 and no opinion about review. The envelope still says review required: True, because normalize_result() sets that flag itself and the model cannot clear it.

The tour therefore runs from a request that was narrowed before it started, through a run that was stopped and handed over, to a change that was accepted on stated evidence. Evidence shows how those claims are tested, and Limits and Next Steps sets out what a host must still supply.