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.