Architecture
agentsessions is a narrow waist for durable agent sessions: one neutral core with three pluggable
seams. Producers plug in on top, compute plugs in underneath, and the harness is brought by the user —
the core knows about none of them specifically.
flowchart TB
prod[Producer adapter<br/>issue tracker / chat / app] -->|Sessions gRPC| ctrl
subgraph core[Neutral core]
ctrl[Controller<br/>single-writer, event-sourced] --> log[(Event log<br/>CAS + fence + hash chain)]
ctrl --> plc[Placer<br/>CanPlace gate + fence]
end
ctrl -->|Harness.Connect stream| har[Harness<br/>BYOH, out of process]
plc -->|Runtime SPI| rt{{Runtime backend}}
rt --> pod[runtime/local<br/>MemorySnapshot=false]
rt --> sub[runtime/substrate<br/>MemorySnapshot=true]
The three contracts (api/)
Section titled “The three contracts (api/)”One wire form (api/*.proto) + a Go SPI (api/*.go). The Event type is shared by the session log and
the harness stream, so what is journaled is what the harness emitted.
- Sessions (
api/session.proto, served bysession/) — the client-facing lifecycle: create, list, exec, suspend, resume, fork, replay. This is the seam a producer (GitHub, Foundry, a CLI) drives.CreateSessionpersists the session’s metadata, so a session is listable and survives a restart before it has any events or any compute. - Harness (
api/harness.proto) — Bring-Your-Own-Harness.Harness.Run(Start, EventSink)drives one execution (turn): the harness readsStart(inputs, replayHistory, identity) and emits typed events via the sink (MODEL_CALL,OUTPUT,TOOL_CALL,TOOL_RESULT,USAGE). The sink hides the emit → wait-for-host round-trip. - Runtime (
api/runtime.go) — the compute SPI:Create,Snapshot(kind),Restore,Fork,Stop,Status,Capabilities. Any sandbox (pod, Kata, Cloud Hypervisor, agent-substrate) implements it.
The durable core
Section titled “The durable core”- Event log (
eventlog/reference,sqlitelog/persistent) — a single-writer, append-only log with:- CAS on append (
expectedLastSeq) — optimistic concurrency, one writer wins. - a fencing token per incarnation — a resumed incarnation mints a fresh fence; a zombie writer from a dead incarnation is rejected.
- a hash chain — each record chains the prior hash, so tampering is detectable.
- CAS on append (
- Session metadata (
sqlitelog/sessions.go) — the log records what happened; it cannot record what a session is (its project, name, configured harness and model), because none of that is an event. That sits in a row beside the log, written at create time so a session with no events is still listable.compute_stateis a projection advanced in the same transaction that appends an event, since an event body is an opaque blob no query can filter on; being in-transaction is what stops it drifting from the log. - Canonical hash (
canon/) — thecontent_hashis RFC 8785 JCS over proto3-JSON, computed over the proto, not Go’sjson.Marshal. The chain is therefore language-neutral:hack/verify_chain.py(a ~30-line non-Go verifier) reproduces the Go hash for the golden vector, so any implementation or auditor can verify provenance.
The controller (controller/)
Section titled “The controller (controller/)”The single-writer, event-sourced core. It drives one session’s log with one incarnation (fence). Per turn
it appends the input, runs the harness, and mediates every nondeterministic effect through the sink so
the effect is recorded before it is observed. The determinism invariants it enforces (specified in the
companion determinism contract, exercised by conformance/):
- I1 — replay never invokes the model. On
Replay, recorded model results are served from the journal; the model is called zero times. - I2 — reasoning continuity. Opaque provider reasoning parts are recorded verbatim and replayed, so reasoning survives resume/fork on the stateless path.
- I3 — at-most-once effect re-drive. A crash between an effect’s intent and its result is re-driven
at most once; host-executed tools carry an idempotency key so the re-drive dedups. A model call cut
after
MODEL_CALL, when that call is the turn’s last recorded effect, is re-invoked onResumewhen the harness re-issues the same input hash, and its completion is recorded against that call. Each cut costs at most one extra provider call within one host, where the per-session guard serializesExec,SuspendandResume; the bound does not hold across hosts sharing a journal. A recovering call that fails is itself a cut, and the nextResumere-drives it again. A fork at aMODEL_CALLcosts one provider call when the child resumes. This applies to journals with execution IDs; a v0.1.2 journal without them is not re-driven andResumestill fails with “recorded completion missing”. - I4 — memory-restore is not replay. When a memory snapshot is restored the harness already holds its state
in RAM, so
Start.Historyis empty and the journal is not replayed into it (replaying would double-apply). TheREQUIRES_MEMORY_SNAPSHOTharness carries this on its side by never reconstructing state fromHistory. - I5 — byte-identical replay. Replay reproduces the recorded events byte-for-byte (checked against the canonical hash), not merely equivalent output.
Harness transport (harnesswire/)
Section titled “Harness transport (harnesswire/)”The controller drives the in-process api.Harness interface directly, OR — for a harness in a separate
process/sandbox — over the Harness.Connect bidi gRPC stream. harnesswire bridges the two: the same
api.Harness/EventSink contract, tunneled over gRPC, so the determinism guarantees (model mediation,
record-before-effect) hold across the process boundary unchanged. A harness plugs in as a thin adapter
and never touches a provider SDK on the replay path.
Placement (placement/)
Section titled “Placement (placement/)”The Placer wires Sessions to the Runtime SPI and owns the incarnation lifecycle:
- Gate — before provisioning compute,
CanPlace(harness.Capabilities, runtime.Capabilities)refuses a harness the runtime cannot host (e.g. aREQUIRES_MEMORY_SNAPSHOTharness on a filesystem-only backend), surfaced asErrUnplaceable→FailedPrecondition. Honest degradation, before any compute or log write. - Fence — the log is the single fence authority; the Placer mints a fence and binds it to both the
incarnation and the controller. A
Restoremints a fresh fence so a new incarnation supersedes a zombie. - Dial — the harness is reached by dialing
Incarnation.AddresswithIncarnation.CallMetadataon every call. One dial path, two address forms: aunix://socket forruntime/local; theatenet-routerhost:portforruntime/substrate, whoseate-target-actormetadata names the actor. Before a stateful fork checkpoints its parent the Placer closes the parent’s open harness streams, because substrate waits for an actor’s in-flight requests to drain before it snapshots;Suspendinstead refuses a session whose turn is still running. - Lifecycle map — session
Suspend → Runtime.Snapshot,Resume → Runtime.Restore, session-end→ Runtime.Stop. - Fork — capability-driven. A
STATELESS_REPLAYsession replay-forks (the child cold-boots and the copied journal reconstructs it, parent untouched). AREQUIRES_MEMORY_SNAPSHOTsession holds live state the journal cannot rebuild (I4), so the Placer checkpoints the parent viaRuntime.Snapshot, records the ref in a SUSPEND event, and clones every child from it throughRuntime.Fork. An N-way fan-out takes exactly one parent checkpoint, so all children branch from identical state.
Harness registry (session/harness_registry.go)
Section titled “Harness registry (session/harness_registry.go)”placement.Registry maps a harness name to the Placer that runs it. Its entries come from two
sources:
- Static harnesses are built into the host or configured when it starts (the built-in
echoandchat, and eachagentsessionsd --harness name=address). They are never stored, always active, and read-only. Their names, plus any reserved built-in name the host is not serving right now, cannot be registered. - Registered harnesses are added at runtime through the
HarnessRegistryAPI (api/harness_registry.proto) and stored in theharnessestable of the journal database (schema version 2). At startup the host loads each usable stored registration, retired ones included. An unloadable registration stays reserved and visible withunservable_reasoninstead of stopping startup.
Several hosts can share one journal, and their static names can differ. A name must not be static on one and registered on another: the registered harness’s sessions would run on the static one, and retiring it would refuse new sessions on the static one. Four checks keep the two apart:
- At startup a host records its static and reserved names in the
reserved_harness_namestable, in one transaction with a check that none of them is registered.NewHarnessRegistrydoes this, and so doesagentsessionsd, whether or not its operator listener is enabled. Embeddedagentctlreserves its static names withsession.ReserveStaticHarnessNamesbut does not serve the registry. A host refuses to start if one of its names is registered. - The migration to schema version 2 reserves every harness name a version 1 journal uses, in the
transaction that creates the table: each session’s harness and each harness recorded on an
EXECUTION_STARTmarker, since a turn can override the session’s harness and Resume re-runs a pending turn on the name it recorded. All of them were static, and the host that migrates the journal may not serve them. RegisterHarnessrefuses a reserved name withALREADY_EXISTS, checked in its own transaction, so a host that is already running keeps its names.- A host that did not reserve its names fails closed: creating or routing a session on a static name
that has a registration row is
FAILED_PRECONDITION.
Reservations are never released, because the store cannot tell whether another host still serves the name.
A registration is immutable. Its identity is the name plus spec_digest, a SHA-256 over the RFC 8785
form of the spec’s proto3-JSON, the same canonical form the journal hashes.
stateDiagram-v2 [*] --> ACTIVE: Register (CREATED) ACTIVE --> ACTIVE: Register same spec (UNCHANGED) ACTIVE --> RETIRED: Retire RETIRED --> RETIRED: Retire (keeps first time and reason) RETIRED --> ACTIVE: Register same spec (REACTIVATED)
- A different spec under a taken name is
ALREADY_EXISTS; a new spec needs a new name. - Retire refuses new sessions only. The check runs in the same transaction as the session insert,
so once
RetireHarnessreturns no new session can land on the harness. Existing sessions keepExec,Suspend,ResumeandFork; fork children inherit the harness. - Registered harnesses pin their sessions.
ExecRequest.harnessthat differs from the session’s harness isFAILED_PRECONDITIONwhen either harness is registered, whether or not this host has loaded it, so no session can run turns on a registered harness it was not created on, retired or not.Resumeof an interrupted turn recorded on another harness applies the same rule, also when the name was registered after the turn was recorded. Overrides that involve no registered harness keep working as before. - Recovery uses the recorded identity, not the registry’s state. Each turn’s
EXECUTION_STARTrecords the registry name and the harness’s advertisedDescriptor.Version, andResumeof an interrupted turn routes to that name, where the controller requires the recorded version (see Concepts). A registration is never replaced or removed, and retiring one leaves it loaded, so a recorded name keeps resolving to the samespec_digest. The registry adds no version check of its own:spec_digestfixes what the host dials, and the recorded version is the only check on what answers there. - One session guard per registry.
Registry.Addgives a registered harness’s Placer the same session guard and per-session checkpoint state as the static ones, so overlappingExec,SuspendandResumeon one session areABORTEDwhichever of its Placers each call reaches, a statefulForkthrough any of them ends and refuses a turn through another, and aSuspendand a statefulForkof one session refuse each other (ABORTED). APlacerFactoryreturns a new Placer for each call;Addrefuses one that belongs to another registry or has an operation in progress. - How placements are served is the host’s choice. A
PlacerFactoryturns a spec into a Placer, or refuses a placement the host cannot serve (FAILED_PRECONDITION, nothing stored). It must not dial, so a harness that is down does not keep the host from starting. A spec with a field or enum value the host does not understand isINVALID_ARGUMENT. For aRemotePlacement,runtime/remoteis the backend, the same oneagentsessionsd --harnessuses. descriptor_idis checked on every turn. The factory builds the Placer withplacement.WithDescriptorID(spec.descriptor_id), and the registry refuses a Placer that does not expect exactly that id, so a factory that drops it fails closed. The Placer then refusesExec,ResumeandForkwithFAILED_PRECONDITIONwhen the harness’sDescribereports another id, at the same gate that appliesCanPlace: before compute is touched or anything is journaled, and, forruntime/remote, again on the connection that runs the turn. The id is self-reported, so this catches an address that reaches the wrong harness, not an impostor. Empty means not checked.- Known limits. The declared
capabilitiesare stored, not compared with what the harness reports; placement still gates every turn on the liveDescribe.descriptor_idis not checked at registration, only when a call places the harness, and like the capability check it is not bound to the turn’sConnectstream (see Security posture). A registration made by another process sharing the database is served after this host restarts. There is no delete, becauseDeleteSessiondoes not exist yet and a harness with sessions must stay resolvable.
The registry is an admin API. agentsessionsd loads stored registrations on every start, even
without the operator listener: existing sessions on retired harnesses still route to their backend
when this host can serve the registration. Both new and stored remote registrations require
-model; without it, stored remote registrations load as unservable and their existing sessions
fail FAILED_PRECONDITION, whether active or retired.
With -registry-addr 127.0.0.1:8081 it serves HarnessRegistry alone on a separate, literal-loopback
plaintext gRPC listener; the Sessions listener serves only Sessions. Its factory attaches remote
harnesses without dialing during registration; this daemon does not support substrate placements.
See Security posture.
Runtime backends (runtime/)
Section titled “Runtime backends (runtime/)”runtime/local— filesystem-only, serves the harness over a unix socket.MemorySnapshot=false, soCanPlacerefusesREQUIRES_MEMORY_SNAPSHOTharnesses — the honest-degradation counterpart that makes the capability tier meaningful.runtime/remote— attaches to a harness already running at a given address instead of provisioning one, so a harness is registered by configuration rather than compiled into the server. It asks the harness toDescribeitself, soCanPlacegates on the tier the harness declares.MemorySnapshot=false, and structurally so: it does not own that process.runtime/substrate— agent-substrate (actors on pre-warmed workers, RAM+disk memory snapshots).MemorySnapshot=true. It depends only on aControlClientinterface defined in the core; the real ate-api adapter lives in a separate module (integrations/substrate/) so the core imports zero substrate code. See Running on agent-substrate.
Conformance (conformance/)
Section titled “Conformance (conformance/)”The CSI/CRI-style neutral checks any implementation must pass: replay reconstructs a genuinely
nondeterministic output, multi-turn resume-then-continue across incarnations, crash-mid-turn at-most-once
re-drive, fork equivalence (hash-tree), single-writer CAS, the cross-implementation integrity golden
vector, and the mediated-tool idempotency + rejection paths — in-process and over the harnesswire gRPC
transport. Passing this suite is the neutrality/leadership deliverable.