Skip to article
Browse chapters

The event stream

One frame type, one journal, two pure reducers, and every surface a client of them.

One vocabulary

bingo_sdk::Event is the only output vocabulary in the system. The terminal surface, --print, JSON-RPC and IM channels all consume it and derive their views at render time. No surface defines a private mirror enum, and scripts/check_discipline.sh refuses any surface crate that declares a type whose name ends in Event (ADR-0002).

The frame

pub struct Frame {
    pub seq: Seq,
    pub ts: Timestamp,
    pub session: SessionId,
    /// The client intent this frame answers or results from, when there is one.
    pub cause: Option<IntentId>,
    pub event: Event,
}

seq is minted by the session actor under one lock and is gapless for durable frames. Durable frames are appended to the store before they are published.

The events

groupvariants
sessionSessionUpdated, SessionClosed
turnTurnStarted, TurnRetrying, TurnUsage, TurnCompleted
itemsItemStarted, ItemDelta, ItemUpdated, ItemCompleted
queueQueueChanged
questionsInteractionOpened, InteractionResolved, InteractionCancelled
intentsIntentAck
historyCompacted, Rewound
configConfigChanged, CatalogChanged
pluginsNotice, Extension, Signal
transportLagged

Four of them are ephemeral: ItemDelta, Notice, Signal and Lagged take a live seq but are never written to the journal. A replay is therefore a subsequence of the live stream, and clients require monotonic sequences rather than gapless ones.

The items

An Item is a durable unit of the transcript.

pub struct Item {
    pub id: ItemId,
    pub turn: Option<TurnId>,
    pub round: u32,
    pub status: ItemStatus,
    pub started_at: Timestamp,
    pub completed_at: Option<Timestamp>,
    pub intent: Option<IntentId>,
    pub body: ItemBody,
    pub meta: Map<String, Value>,
}

ItemBody is what an item is: User, Assistant, Reasoning, ToolCall, Action, Compaction, Rewind, Interruption, Notice, QuestionAnswer, PermissionReceipt and a few more. A ToolCall carries its call_id, its input, its ToolOutput when it has one, its progress tail while it runs, and its duration.

Two reducers over one journal

  • SessionState::apply(&Frame) produces the client view. It is the same reducer the kernel runs for its own snapshot, so any client’s view equals apply(snapshot, frames since snapshot.seq).
  • ContextView::fold(frames) produces the provider messages. It is the most load-bearing function in the system and gets a golden test per journal version.

Neither ever rewrites history. Compaction and rewind are events — Compacted { boundary, kept, summary }, Rewound { to_turn, dropped } — and the journal header carries a version, so a format change is a migrator, never an in-place edit.

SessionState holds the transcript in order, the live turn, the queue, the open interactions, the context usage, the config view, and two maps of plugin-owned state — extensions (durable, the latest Extension payload per kind) and signals (ephemeral, gone after a resume).

Writes return nothing

fn submit(&self, intent: IntentId, input: Input);
fn interrupt(&self, intent: IntentId, scope: InterruptScope);
fn answer(&self, intent: IntentId, interaction: InteractionId, answer: Answer, activation: Activation);

All three take a client-minted IntentId, which is also the idempotency key, and return (). The outcome arrives as Event::IntentAck { intent, outcome }. A synchronous key handler can never wait for a receipt, because none exists.

An intent that waited is acknowledged twice — Queued when it joined the queue, TurnStarted when its turn opened — so a client learns which turn is its own without matching items.

Ids

SessionId, TurnId, ItemId and InteractionId are ULIDs minted once by the actor and persisted; they are never re-minted on restart. IntentId is the client’s to mint.

Backpressure is the kernel’s to announce

Each subscriber has a bounded channel. On overflow the kernel sends Lagged { from, to } and the client re-reads with events_since(seq). The kernel never blocks on a client.

The model stream never leaves the loop

ModelEvent mirrors the provider stream-part algebra — per-block ids, start/delta/end for text, reasoning and tool input, a Finish carrying usage and both a unified and a raw finish reason, and provider metadata keyed by provider id. The accumulator folds it into Items inside the turn loop. Only Items and Events are ever published, so no client parses a provider’s dialect.

Sessions, sub-agents and rooms

A sub-agent is a session with a parent link; a room is a session without a model. There is no second noun and no second reducer.

Opening a session with children: true makes the attachment carry every live descendant’s frames from its head, descendants created later included, each frame stamped with its own session. A lag anywhere in the tree is healed in the kernel — re-subscribed from the last seq forwarded — so a tree stream never carries a Lagged marker. On the wire an event notified under a tree attachment also carries root, the session it was opened through, which is what a client routes by (ADR-0010).

The child’s SessionSummary.parent.item names the tool call that spawned it. There is no second copy of that link on the tool call itself: a client derives it from the frames it already holds.

Plugin state travels as data

A plugin’s own resources — a roster, a room, a task list — travel as Event::Extension { plugin, kind, payload }, journaled, where the latest payload is the whole of that kind’s state (ADR-0011). Live state that should cost the journal nothing travels as Event::Signal instead: never written, folded into SessionState.signals as the latest payload per (plugin, kind), removed by a Null payload, absent after a resume. A progress bar updated ten times a second is free.

The kernel enumerates neither. What a surface does with them is UI as data.

Reading it yourself

  • bingo --print --output-format json writes one Frame per line on stdout.
  • bingo serve --stdio notifies each frame verbatim as an event — see schemas and protocols.

Both are the same frames the terminal surface folds.