Skip to content

The event log

The event log is one ordered, append-only stream per session. Every entry is a wire envelope carrying a typed payload. This page is the log at mechanism level: the shape of an envelope, the families of payload it can carry, the single component allowed to write it, the one thing that deliberately never appears on it, and how drivers and the console read it back.

Every event is an Envelope with a small, fixed outer shape — an id, the agent it is about, a timestamp, and exactly one typed payload:

  • id — the correlation key. There is no separate correlation_id field; the envelope id is it. A fresh id is minted once per dispatch, and the worker echoes that same id on every lifecycle event it emits for that dispatch — so an agent’s Created, Running, and terminal events for one dispatch all share one id and correlate cleanly. The id is per-dispatch, not per-agent-lifetime: when an agent is re-dispatched (a resume), a new id is minted. What ties an agent’s whole history together across dispatches is its AgentRef, the opaque, tenant-scoped identity the substrate issues — not the envelope id.
  • agent_ref — the subject agent. Which agent the event is about. A few control-plane envelopes carry none (a bare cancel, a parse failure before any agent identity exists). Parent linkage is not an envelope field — it rides inside the lifecycle payload’s parent_ref, so a consumer building the spawn tree reads it from there.
  • timestamp. The orchestrator stamps lifecycle envelopes at append time, not the worker at emit time, so the log’s clock is the one authority that ordered the event.
  • Scope rides context, not fields. An envelope has no session_id or tenant_id of its own. Session scope is the stream it lives in; tenant scope rides inside agent_ref and is enforced at the state-store boundary. A consumer supplies the SessionId when it subscribes; a cross-tenant append fails loudly rather than crossing silently. See Identity & authorization.

The payload is a closed enum — a finite, exhaustively-known set of variant families the substrate guarantees for a major version. Closed is deliberate: a match over the payload must handle every case with no catch-all, so adding a variant is a compile-time event on every surface that reads it, never a silent runtime gap. At reader altitude the families are:

FamilyWhat it carries
LifecycleAn agent’s state machine — CreatedRunning → a terminal Done/Failed/Cancelled, plus Spawned, AgentResuming/AgentResumed, and the spawn/affinity control events. The birth-time config snapshot rides here too, so reconstruction can rebuild an agent exactly.
StreamingLive model output as it arrives — text deltas, thinking deltas, and tool-call deltas — for a real-time tail. Terminal usage and cost do not ride here; they land on cost and lifecycle events.
CostOne record per billable action — model call or priced tool — carrying the tenant, agent, provider, and the dollar figures the cost rollup folds over.
HITLThe human-in-the-loop pause and its response. The pause carries the trigger and the proposed action; the response carries the decision. Actor identity is not on the wire — it is resolved server-side from auth and stamped on the log row, never trusted from a caller.
MemoryThe procedural-memory (AGENT.md) propose/approve/reject flow, and per-dispatch confer / per-completion contribute records. The tiers themselves are the memory plane.
ContextHow each call was composed — PromptComposed (the compiled prompt, with a content digest for replay), CompactionApplied, and ToolResolution. The mechanism is the context plane.
VerificationThinner than you might expect: there is no dedicated “validator verdict” event. A validator agent’s output rides an ordinary lifecycle event tagged with the sub-task it grades. The one dedicated verification signal is the preflight self-check outcome that gates a side-effecting tool.
ExtensionThe one open-vocabulary escape hatch (below).

Alongside these observable families the payload also carries cross-process plumbing — per-session tool-registry push/drop, orchestrator-mediated tool delegation, async-job transport, and the durable SessionStarted bootstrap and CoordinatorCheckpoint that make recovery possible.

A closed vocabulary is correct for everything the substrate interprets — but a consumer or vertical sometimes needs to record a signal the substrate has no reason to understand. That is the single Extension variant: a vendor-namespaced, self-describing payload the substrate transports and projects but never interprets. It lands inert at every control-flow site — the substrate routes the bytes to a consumer-owned handler and renders a structural view; only that handler reads them. The variant stays closed and singular (the compile-time canary still fires on it), while the vocabulary inside it is open — any consumer ships a new signal without a substrate change. The debate loop uses it to record per-round audit detail; the same open-vocabulary seam runs through the context plane’s prompt pipeline, where the fuller story is told.

The orchestrator is the sole writer of the log. This is structural, not a convention: a worker holds no durable state and never appends. It streams its envelopes back over its dial-in connection, and the orchestrator’s drain loop reads that stream and appends each one through the single low-level append primitive — the one audited write path. Bypassing it is a contract violation.

Many producers stream back; the orchestrator is the only writer; many consumers derive their view by tailing the one log

The log is persistence-backed — a state-store tier, the same layer as agent and working memory, with a Postgres backing and a Redis hot-replay buffer in the cluster shape. Whatever the backing, every read and write routes through the state store.

One thing is deliberately kept off the log: a resolved tool credential. When the orchestrator forwards a secret to a worker, it rides the transient dispatch work-request only — never appended, never checkpointed, never projected to a tail or a reconstruction. Forwarding is fail-closed: a secret crosses only over a proven-secure hop, and a plaintext hop forwards nothing. This is the one substrate-only channel that must not be event-sourced; the full contract is the credential plane.

Everything downstream is a tail of the same log, subscribed from a position:

  • gRPC streaming — the TailEvents RPC, consumed by the console and the SDKs. Each streamed item pairs the envelope with its log position.
  • REST server-sent eventsGET /api/v1/events, the same live stream over HTTP, with an optional agent filter. Each frame’s id: is that same position. This is the REST & SSE surface.
  • Historical read. A terminated session is read as one bounded snapshot rather than a live subscription — the primitive behind browsing completed sessions.

One subscription primitive, and what it guarantees

Section titled “One subscription primitive, and what it guarantees”

Both live surfaces are thin adapters over a single subscription primitive; they differ in framing, never in behaviour. That is why the guarantees below hold identically whichever transport you pick, and it is worth designing against:

  • Lossless. The primitive advances a subscriber’s position only over entries it has actually read back from the durable log. A slow consumer therefore makes its own tail slower, never gappy — there is no dropped-event case to detect or reconcile. (A broadcast-style fanout would have the opposite property: once a subscriber falls behind its buffer, entries are simply gone.)
  • Resumable from any position. Persist the position you last handled and reconnect with it — SSE via Last-Event-ID, gRPC via from_position — to receive exactly the remainder. No gap, no duplicate. This is also what makes a late subscriber correct: a tail opened mid-session backfills the history it missed before going live.
  • Clean end. The stream closes when the session reaches its terminal event, so a tail consumer gets an end rather than a connection that hangs open forever.

Because a subscription is a poll over the durable log rather than a side channel, these properties are consequences of the log being the single source of truth — not features layered on top of it.

Because a tail is just a subscriber, writing one is small: deploy/local/smoke.sh starts a session and tails GET /api/v1/events with curl, and the out-of-tree samples/tail-consumer-prometheus folds the same stream into metrics. Nothing in the substrate is special-cased for a particular consumer — they all read the one log.

  • Operate: The operator console is a live tail of the log; Monitoring and the audit view are tail consumers.
  • Build: REST & SSE and The Python SDK expose the tail; the SDK presents it as an async iterator of events.
  • Reference: the event-bus and protocol crates under Reference own the envelope and the Payload vocabulary.