Skip to main content
AgentRunner — the bounded loop that terminates on verification (§5). The runner owns all TaskState mutation (§8); tools and the verifier are pure-ish workers. Phase 1 covers the read-only path: tier-0 tools, no permission gate (every tool is tier 0), and the minimal investigate verifier. The loop is deliberately a near-verbatim transcription of the §5 pseudocode.

Classes

AgentRunner

The bounded loop that owns all state mutation and ends on verification (§5, §8). Collaborators are injected explicitly so a run is self-contained and replayable; the runner orchestrates them but mutates TaskState itself. Args: model_client: Proposes each turn’s ModelDecision. registry: The active ToolRegistry. deps: Run-scoped RunDeps (workspace, etc.). context_builder: Assembles the per-iteration context packet (§9). verifier: Disposes of “done” on external evidence (§12). emitter: Observation-only event emitter (§13). config: Budgets and harness settings. policy: The before-tool-call control gate (§11); defaults to the standard tier policy. planner: Resolves the per-session verification plan (ADR-0007); defaults to a VerificationPlanner(config). event_sink: Optional typed-event sink (a Session); absent on the batch/sync path. approval_controller: Optional awaited gate for tier-3 ask calls (a Session). conversational: Terminal-boundary authority (§23.5, ADR-0046). The verifier steers (repair loop) in both strict and conversational modes; this flag only changes the disposition at repair exhaustion — False (default, --auto) pronounces failed; True (interactive) defers to the human, blocking with an open_question. It never makes a failed verdict advisory. advisory: External-grading authority (ADR-0040 option A). True runs the verifier as advisory — it reports but does not steer or gate, and a failed verdict is delivered as success for a held-out grader (the eval probe) to judge. Off by default; set only by external-grading callers (the eval harness), never the REPL. Takes precedence over conversational.

AgentRunner.arun(self, state: TaskState) -> TaskState

Drive the loop to a terminal outcome asynchronously — the real loop (§5). A near-verbatim async transcription of the §5 pseudocode: blocking model/tool/ verifier bodies are offloaded with asyncio.to_thread so the event loop stays responsive (spinners, keystrokes, ESC), and typed lifecycle events are published to the sink as the run progresses. With no sink/controller wired it matches run(). Args: state: The task state to drive; mutated in place. Returns: The final TaskState with a terminal outcome. Raises: TransportError: When a model call fails at the transport layer (timeout / NUL body) past the client’s retries — a system failure surfaced to the caller, never fed back to the model or masked as a soft incomplete (§16, ADR-0028 R4). asyncio.CancelledError: When the run task itself is cancelled (the signal / _run_task.cancel() path, ADR-0030) — re-raised after aborting the in-flight model call so the loop unwinds without orphaning the request.

AgentRunner.run(self, state: TaskState) -> TaskState

Drive the loop to a terminal outcome synchronously (§5). The thin sync wrapper over the async core: arun() is the loop, run() wraps it via asyncio.run() for batch/library callers. Behavior is identical to the async path with no sink/controller wired. Args: state: The task state to drive; mutated in place. Returns: The final TaskState with a terminal outcome.