Skip to main content
loop.run() yields a LoopEvent union — observability only, never the loop’s state. Every journaled event is self-describing: it carries { seq, round, phase } in its envelope, so any slice of the journal is interpretable alone. seq is the monotonic replay key.
type Phase = "execute" | "handoff" | "verify"
type EventEnvelope = { seq: number; round: number; phase: Phase }

The LoopEvent union

GroupEventWhenPayload (beyond the envelope)
Lifecyclephase-starta phase begins, before its first token
Lifecycleverdictthe Verify agent returns{ ok, impossible, reason }
Lifecycleexitterminal — the stream completes after it{ exit, rounds, usd }
Contenttextone coalesced text event per step{ text, partial? }
Contenttext-deltacharacter-level, live{ text } — stream-only, never journaled
Contentreasoningthinking, per step{ text }
Contenttool-callthe agent invokes a tool{ toolCallId, toolName, input }
Contenttool-resulta tool returns{ toolCallId, output }
Metacostper-step spend increment{ inputTokens, outputTokens, cachedInputTokens, usd }
There is no unknown member: a consumer ignores types it does not know, and raw executor output surfaces only as diagnostics under run({ debug: true }).

Ordering: liveness first, durability for transitions

Two rules by kind:
  • Observations (text, text-delta, reasoning, tool-call, tool-result) fan out first and are persisted alongside — liveness wins.
  • State transitions (verdict, exit) commit to the Record first, then emit — a consumer never sees a verdict that is not yet durable.

Crash fidelity: partial: true

text-delta is mirrored to a per-step scratch sidecar. On a clean step the coalesced text is journaled and the sidecar cleared; on a crash the partial is folded into the journal as text { partial: true } — whatever the agent actually produced is recorded, down to half a sentence — and the Round replays.

Iterating never throws

The iterator is a view, not a driver: the engine self-drives whether or not you iterate, and breaking out unsubscribes — it does not cancel. Once the handle exists, every termination — API outage, budget cutoff, even cancellation — resolves the stream to a final exit event; run.done() resolves, never rejects. Only startup fails by throwing (LoopBusy, missing goal, malformed config) — see definition.run.

AgentEvent

An Agent run’s stream is its own clean type: the content and cost events above over a bare { seq } envelope, plus a terminal exit carrying an AgentExit. No phase-start, no verdict.