Repository navigation
Internals
The driver was one file until v0.3.0, when it was split into the files below as pure moves; what each file holds is what the loop does at that stage. Since v0.4.0 the sequence the loop runs is written once, over a Session, and both the Driver and a Gate run it: a Driver is a gate with an agent.
| File | Contents |
|---|---|
agentrt.go |
every public type: run, step, decision, observation, tool, policy, approval, agent, reconciliation, event, limits |
driver.go |
Config, NewDriver, Start, StartWithID, the per-driver claim on runs it is executing |
loop.go |
the Driver's loop over a session, and the code it shares with the gate: starting a step with its limit checks, proposing a decision, validation, the one dispatch over policy outcomes (apply) shared with resume, and tool execution |
gate.go |
GateConfig, Gate, Begin and Attach, Session, OpenStep, Verdict, ErrSessionClosed, and Session.Input; see The Gate
|
step.go |
starting, recording, and ending a step; settle, which ends a step and its run in one transaction; the late step.tool_finished after an operator's cancel |
approval.go |
the approval hash, the pause, Approve, Reject, ApproveShown, RejectShown, Cancel, expiry and ErrApprovalExpired, and Operator, the driverless decision path on a chosen clock |
resume.go |
Resume, and the attach a gate's Attach shares with it: the approved path, the interrupted path, reconciliation, and the interrupted_side_effect pause |
lease.go |
the run lease: DefaultLeaseTTL, the heartbeat, and release |
cache.go |
the step cache: the loop's kept steps and approvals, and the mirrors handed to the agent and the policy |
store.go |
OpenStore, OpenExisting, migrations (the sixth adds the chain, tool_specs, and the step columns), file-mode checks, queries, page reads, Store.Lease, Store.ListEventsAfter, and Store.ApprovalReason, the lease statements, and the transaction helper that writes state and events together, reading the chain's head and hashing each event inside the same transaction |
store_mode_unix.go, store_mode_other.go
|
whether the file-mode rules apply on this platform, and how to read a file's owner |
model.go |
model types, the accounting caller, reservations, retries and Retry-After, usage checks, the price table, token estimation |
policy.go |
PolicyFunc, SideEffectPolicy, DefaultPolicy, NeedApproval, DecodePresentation
|
schema.go |
JSON Schema compilation, with no loader beyond the document and the standard metaschemas, and validation; CompileTool, ToolSchema.Check, and CheckArgs, the runtime's acceptance of a tool's arguments without a driver, which NewDriver and the loop go through too |
hash.go |
the canonical JSON encoder, written out and pinned to the stored hashes; content hashes; the boundary check on consumer JSON (checkJSON, checkDepth, and since v0.4.0 checkNumbers, which refuses a number past the bound); ids |
canonical.go |
CanonicalJSON, the exported form of that encoder, which refuses what the runtime refuses where consumer JSON enters, and, since v0.5.0, PolicyJSON, the same form without HTML escaping, in which a policy is handed a request's arguments and its tool's schema |
record.go |
what a step records so its decision can be checked again: the tool spec stored once by its hash in tool_specs, the policy's identity (IdentifiedPolicy), and the view a step.policy event carries. Since v0.5.0 |
chain.go |
the event hash chain: eventHash, Store.VerifyEvents, Store.ChainHead, Store.EventHash, and the backfill migration 6 runs. Since v0.5.0 |
recheck.go |
Recheck, RecheckOptions, and RecheckReport: it rebuilds each recorded policy evaluation from the events, steps, approvals, and specs, asks a policy, and compares. Since v0.5.0; see Re-checking a Run
|
replay/ |
scripted, recording, and replaying models |
scripted/ |
the scripted agent |
providers/ |
the dependency-free helpers every provider adapter shares |
render/ |
recorded steps to model messages, and a response back to a decision |
trace/ |
the aligned and JSON Lines event observers, Sanitize, and WriteRecheck, which renders a re-check report as text |
view/ |
the run, step, and approval read models, full and paged |
cmd/agentrt/ |
the operator command |
testkit/ |
the interruption harness, which also runs over a caller's own loop on a gate, the policy conformance table, schema fuzzing, the render equivalence check, and, since v0.5.0, the re-check helpers a consumer's tests use; see Testing a Consumer |
export/ |
the read-only follower of the events table, its cursors, and the JSON Lines sink; see Exporting Events |
approver/ |
the interface an operator surface decides approvals through, and its store-backed implementation |
approver/webhook/ |
the reference signed HTTP approval channel over approver, and its plain-text rendering; see Approval Channels
|
api_test.go |
tests of the v0.3.1 core additions: Store.Lease, Store.ListEventsAfter, ErrApprovalExpired, Operator, Store.ApprovalReason, CheckArgs
|
schema_internal_test.go |
the test that CheckArgs compiles a spec once and keeps its cache bounded, which reads the compile counter and so lives inside the package |
chain_test.go, recheck_test.go, recheck_run_test.go, recheck_internal_test.go, policyjson_test.go
|
the tests of the chain, the record, Recheck, and the policy form. recheck_internal_test.go re-checks a database written by v0.4.0 |
examples/scripted/ |
the runnable example with no model |
examples/recheck/ |
a scripted run re-checked under a stricter policy, and an altered event found by the chain check; see Re-checking a Run |
examples/live/ |
the same tools against a local model, pausing for an operator |
examples/mcp/ |
the MCP filesystem server behind the policy |
examples/approver/ |
a scripted run decided through the webhook as a chat bot would, then resumed; a plain core example with no go.mod of its own |
Nested modules, each with its own go.mod so its dependencies stay out of the core: providers/ollama, providers/openai, providers/anthropic, and mcp, plus bench, which has no dependencies at all, and export/otel with its example/ program, which brings OpenTelemetry (see Measuring Agents and Exporting Events), plus proxy, the MCP proxy agentrt-proxy with its fake payment server and its example/ demonstration (see MCP Proxy), plus the two model examples. ./... in the root module does not reach them; see Provider Adapters for the workspace that does.
The loop below is what a Driver does. A Gate runs the same steps with the caller standing where Decide does. Gate.Begin and Driver.StartWithID share one begin, Gate.Attach and Driver.Resume share one attach, and a session's Step, Propose, and Execute are the loop's start-a-step, validate-and-apply, and execute stages, called by the caller's code instead of by the loop. TestGate_ExternalLoopMatchesTheDriver is the test that keeps the two the same: rows, events, payloads, transaction boundaries, and clock readings.
loop runs until the run leaves RUNNING. Each iteration:
- read the run row, which model calls and an operator change; stop if it is no longer
RUNNING, and stop withErrLeaseLostif the lease is gone; - on the first iteration, or when the cache is stale, load the run's steps and approvals; refuse to continue from a row that would not decode;
- check the step, failure, active-time, elapsed-time, and loop limits;
- start a step: claim the next index on the run (compare-and-set on the step count, status, and lease owner) and write
step.started; - call
Decidewith a fresh copy of the steps and approvals; - record the decision verbatim, with unusable JSON moved to the
Invalidfields, and writestep.decided; - validate; an invalid decision ends the step as
invalid_decisionand loops; - on
completeorfail, settle the step and the run together; - evaluate the policy on a copy of the view, handing it the arguments and the schema in
PolicyJSONform, and apply its outcome: on allow, check the lease is held and unexpired, mark the step executing, and writestep.policyandstep.tool_startedin one transaction; execute under the tool's timeout with panic recovery; record the observation with its hash andstep.tool_finished; a terminal tool's success or a tool abort settles the run; on deny, end the step; on abort, settle the run aspolicy_abort; on require_approval, write the approval and pause.
Every write goes through Store.tx, which opens an immediate transaction, runs the mutation, appends the events, commits, and only then delivers the events to the observer. Appending an event reads the chain's head and writes the event's hash in the same transaction (see The record and the chain). An observer never sees an event whose state did not commit. A panic inside the mutation rolls the transaction back and propagates.
Every write the loop makes to a RUNNING run is guarded inside its transaction: the run must still be RUNNING and this driver must hold its lease, or the write fails with ErrRunState or ErrRunLeased, and the loop answers through lost, which returns the run as it now is if an operator cancelled it and ErrLeaseLost otherwise.
The loop loads its run's steps and approvals once, when Start or Resume enters it, and keeps them in a runCache. After each write commits, it decodes the columns that write bound, with the same code ListSteps and ListApprovals use, so what it keeps equals a fresh load. A write that fails applies nothing; a row that cannot be applied marks the cache stale and the next iteration reloads. Nothing is kept between loops.
The agent and the policy each get a mirror of the cache: bytes of their own, copied only for a step that changed since the last handout, and fresh structs over them at every handout. Neither can reach the driver's copy or the other's. Performance has the measurements and the equivalence tests; Driver.reload, reachable only from tests, runs the old reread-every-step path for comparison.
A step that names a registered tool records the hash of its spec when its decision is recorded, and the spec is stored once under that hash in tool_specs. A step the policy evaluated records the policy's identity. Its step.policy event carries policy_id, spec_hash, the spec handed to the policy, and view, the fields of the run that no row keeps. None of it reads a clock, and TestDriver_ClockReadingsAreStable still pins the order of readings.
The request a policy is handed is made in one place for every path. The loop and a gate's Propose make PolicyJSON from the agent's arguments and the registered schema; Resume and Attach make it from the approval's stored request, which is compacted and HTML-escaped, and from the schema registered now; Recheck makes it from the stored decision and the stored spec. All give the same bytes for one request (TestPolicy_LoopAndResumeHandedTheSameBytes, TestPolicy_GateProposeAndAttachHandedTheSameBytes). The approval hash, the content hash, loop detection, and the arguments a tool is called with are over or from the unchanged forms.
An event's hash is the SHA-256 of seven netstrings over the previous event's hash and the event's own fields as stored. Store.tx reads the head after it has the write lock and writes the hash with the event, so the chain follows commit order across every run. Event has no hash field. VerifyEvents recomputes it a page at a time, hashing a long field in chunks of MaxPageText as it reads it.
Migration 6 adds tool_specs, steps.spec_hash, steps.policy_id, and events.hash, and computes the hash of every event already stored, in seq order, in the migration's transaction. TestStore_ReleasedMigrationsAreUnchanged pins the released migrations by a hash of their text, and a migration is folded into that pin when the release that carries it is tagged, as 5 and 6 were (Releasing).
Recheck is read-only. It pages the events and steps, reads the approvals and specs it needs whole, and rebuilds for each step.policy carrying a view the request, the run, the first steps, and the first approvals with the status each had by the seq of its approval.decided. The rules and the limits are on Re-checking a Run.
Two clocks are read. Config.Now stamps what is recorded and judges approval expiry and the time limits, one reading per recorded state change, in an order pinned by TestDriver_ClockReadingsAreStable: a consumer with a deterministic clock keys its own records on that sequence. The wall clock stamps leases and nothing else, and is never Now. See Testing.
Run and step ids come from Config.NewID, defaulting to random 128-bit hex ids; StartWithID lets a consumer supply a run id it has already recorded elsewhere. The lease owner is Config.LeaseOwner plus a random suffix per driver. Config.Now supplies time. All are overridable for tests.
Resume dispatches on the run's state, after refusing a run this driver is already executing.
-
WAITING_FOR_APPROVAL: take the awaiting step's latest approval; expire it if itsApprovalTTLorGrantTTLhas passed; require it approved and its hash intact; check the recorded arguments against the tool's current schema; evaluate the policy while the run is still waiting; and apply the outcome through the sameapplythe loop uses, so the move toRUNNING, the lease,step.policy, and the tool start commit together. -
RUNNING: refuse withErrRunLeasedwhile another owner's lease is live; otherwise take the lease and mark the steps in flight interrupted in one transaction, then reconcile, or pause an interrupted side effect for an operator, or continue. See Interruption and Reconciliation. -
Terminal: refuse with
ErrRunState.
The store uses one SQLite connection in WAL mode, plus, for a file-backed store opened for writing, a second connection used only by the lease heartbeat. The loop's seven hot statements are prepared on the main connection at open. Queries must not be issued inside an open transaction on the same connection, or the process deadlocks: load what you need before tx. A consumer transaction on DB() holds the main connection and stalls the loop, but not the heartbeat.
Only when a consumer demonstrates the need. The runtime stays small on purpose; an abstraction without a consumer is refused.
Using it
- Getting Started
- Upgrading to v0.5
- Upgrading to v0.4
- Upgrading to v0.3
- Concepts
- Writing an Agent
- The Gate
- Writing Tools
- MCP Tools
- MCP Proxy
- Writing a Policy
- Models and Replay
- Provider Adapters
- Rendering Model Context
- Approvals and Resume
- Interruption and Reconciliation
- Run Lease
- Limits
- Testing a Consumer
- Re-checking a Run
- Measuring Agents
Inside it
- Internals
- Persistence and Inspection
- Performance
- The Operator Command
- Approval Channels
- Exporting Events
- Testing
Deciding on it
- Safety Properties
- Security Model
- Comparison with Frameworks
- Status and Roadmap
- Roadmap
- Design Decisions
- Essays
- Contributing
- Releasing
Reference