starling

Event schema

Event kinds, payloads, the canonical CBOR wire format, and the invariants the runtime enforces.

Wire-format reference for the event log.

Envelope

Every event is wrapped in the same struct:

type Event struct {
    RunID     string             `cbor:"run_id"`     // ULID, per-run identifier
    Seq       uint64             `cbor:"seq"`        // monotonic per run, starts at 1
    PrevHash  []byte             `cbor:"prev_hash"`  // BLAKE3 of canonical CBOR of prev event
    Timestamp int64              `cbor:"ts"`         // unix nanoseconds (from step.Now)
    Kind      Kind               `cbor:"kind"`       // discriminator
    Payload   cborenc.RawMessage `cbor:"payload"`    // kind-specific struct, CBOR-encoded
}

Hash chain: ev.PrevHash = BLAKE3(CanonicalCBOR(prevEvent)). The first event has empty PrevHash. Canonical CBOR follows RFC 8949 §4.2: shortest integer form, sorted map keys, no indefinite-length items, shortest float that round-trips.

Event kinds

Closed set. Adding a kind is a schema-version bump.

Emitted by the runtime today

#KindEmitted by
1RunStartedFirst event of every run.
2UserMessageAppendedResume injecting an extra message.
3TurnStartedstep.LLMCall pre-call.
4ReasoningEmittedProvider reasoning blocks (optional).
5AssistantMessageCompletedstep.LLMCall on ChunkEnd.
6ToolCallScheduledstep.CallTool / CallTools pre-dispatch.
7ToolCallCompletedTool returned successfully.
8ToolCallFailedTool returned an error.
9SideEffectRecordedstep.Now, Random, SideEffect.
10BudgetExceededBudget axis tripped.
12RunCompletedTerminal: successful run.
13RunFailedTerminal: error path.
14RunCancelledTerminal: ctx cancelled.
15RunResumedNon-terminal seam from Resume.

Reserved (defined in schema, not emitted by core)

#KindNotes
11ContextTruncatedReserved for context-window trim strategies. Validate accepts; core does not emit.
16TurnFailedReserved for mid-turn streaming failure with retry. Validate accepts; core does not emit.

Payloads

Payloads are CBOR-encoded structs. The highlights:

RunStarted (kind 1)

Pins the entire deterministic surface of the run.

type RunStarted struct {
    SchemaVersion    uint32
    Goal             string
    ProviderID       string
    ModelID          string
    APIVersion       string
    ParamsHash       []byte
    Params           cborenc.RawMessage
    SystemPromptHash []byte
    SystemPrompt     string
    ToolRegistryHash []byte
    ToolSchemas      []ToolSchemaRef
    Budget           *BudgetLimits
    StarlingVersion  string  // linked module version
    AppVersion       string  // caller-supplied
}

TurnStarted / AssistantMessageCompleted (kinds 3, 5)

type TurnStarted struct {
    TurnID      string
    PromptHash  []byte
    InputTokens int64
}

type AssistantMessageCompleted struct {
    TurnID            string
    Text              string
    ToolUses          []PlannedToolUse
    StopReason        string
    InputTokens       int64
    OutputTokens      int64
    CacheReadTokens   int64
    CacheCreateTokens int64
    CostUSD           float64
    RawResponseHash   []byte  // BLAKE3 of canonicalized provider response
    ProviderRequestID string
}

ReasoningEmitted (kind 4)

Optional. Anthropic-only fields are populated only when the provider returned them; OpenAI reasoning summaries arrive without a signature.

type ReasoningEmitted struct {
    TurnID    string
    Content   string
    Sensitive bool
    Signature []byte  // Anthropic per-block integrity signature
    Redacted  bool    // true when Content is the opaque redacted_thinking payload
}

ToolCallScheduled / Completed / Failed (kinds 6, 7, 8)

type ToolCallScheduled struct {
    CallID   string
    TurnID   string
    ToolName string
    Args     cborenc.RawMessage
    Attempt  uint32
    IdempKey string
}

type ToolCallCompleted struct {
    CallID     string
    Result     cborenc.RawMessage
    DurationMs int64
    Attempt    uint32
}

type ToolCallFailed struct {
    CallID     string
    Error      string
    ErrorType  string  // "timeout" | "panic" | "tool" | "cancelled"
    DurationMs int64
    Attempt    uint32
}

Retries share CallID with incrementing Attempt.

SideEffectRecorded (kind 9)

Captures any non-deterministic value the agent loop consumed. step.Now emits one with Name: "now"; step.Random with "rand"; user calls to step.SideEffect set their own name.

type SideEffectRecorded struct {
    Name  string
    Value cborenc.RawMessage
}

BudgetExceeded (kind 10)

type BudgetExceeded struct {
    Limit         string  // "input_tokens" | "output_tokens" | "usd" | "wall_clock"
    Cap           float64
    Actual        float64
    Where         string  // "pre_call" | "mid_stream" | "post_call"
    TurnID        string
    CallID        string
    PartialText   string
    PartialTokens int64
}

RunCompleted / Failed / Cancelled (kinds 12, 13, 14)

All three terminals carry a MerkleRoot []byte over the BLAKE3 hashes of every prior event. RunCompleted adds totals (turn count, tool-call count, USD, tokens, duration). RunFailed adds an error string and classification. RunCancelled adds a reason.

RunResumed (kind 15)

type RunResumed struct {
    AtSeq        uint64  // last seq from the prior process
    ExtraMessage string
    ReissueTools bool
    PendingCalls int
}

A seam, not a terminal. Marks the boundary between a crashed run's prior process and its resuming process. Validation pairing rules treat it as a reset point: orphaned tool-call schedules from before the seam don't need outcomes after it.

Invariants

eventlog.Validate enforces:

  1. Slice non-empty, events[0].Seq == 1, monotonic seq with no gaps.
  2. RunID consistent and non-empty across all events.
  3. Hash chain unbroken: each PrevHash equals BLAKE3 of canonical CBOR of the previous event.
  4. Exactly one terminal event, and it's the last event.
  5. First event is RunStarted with SchemaVersion ∈ [1, current].
  6. Turn pairing. Every TurnStarted is closed by a same-TurnID AssistantMessageCompleted or BudgetExceeded before the next TurnStarted. An open turn at the terminal is allowed only when the terminal is RunFailed or RunCancelled.
  7. Call pairing. Every ToolCallScheduled has exactly one matching ToolCallCompleted or ToolCallFailed with the same (CallID, Attempt). Outcomes without a prior schedule, or duplicate outcomes for the same key, are rejected. A RunResumed seam clears pending pairing state.
  8. Merkle root in the terminal payload matches the recomputed BLAKE3 Merkle root over every pre-terminal event.

Invalid log → ErrLogCorrupt with a diagnostic locating the violation.

Worked example

A run with one LLM turn, two parallel tool calls, then a final summary turn:

seq=1  RunStarted                  prev=nil
seq=2  TurnStarted                 prev=H(seq1)  turn=T1
seq=3  AssistantMessageCompleted   prev=H(seq2)  turn=T1, tool_uses=[C1, C2]
seq=4  ToolCallScheduled           prev=H(seq3)  call=C1, attempt=1
seq=5  ToolCallScheduled           prev=H(seq4)  call=C2, attempt=1
seq=6  ToolCallCompleted           prev=H(seq5)  call=C2, attempt=1
seq=7  ToolCallCompleted           prev=H(seq6)  call=C1, attempt=1
seq=8  TurnStarted                 prev=H(seq7)  turn=T2
seq=9  AssistantMessageCompleted   prev=H(seq8)  turn=T2, no tool_uses
seq=10 RunCompleted                prev=H(seq9)  merkle_root=M(seq1..seq9)

Parallel tool completions land in arrival order: replay reproduces that ordering deterministically because results are read from the log, not re-executed.

Size expectations

Order of magnitude per event:

KindTypical size
RunStarted1–10 KB
TurnStartedunder 1 KB
AssistantMessageCompleted2–50 KB
ToolCallScheduled/Completed1–20 KB each
Terminalsunder 1 KB

A typical 5-turn run with 10 tool calls is ~100–500 KB. The retention patterns are documented under Operations.

Schema evolution

Pre-1.0: any change permitted, schema-version bump on breaks. After 1.0:

  • Additive (new optional fields, new kinds) → minor bump.
  • Breaking → major bump.
  • Replayer refuses logs whose schema version is newer than its own major.
  • Old logs remain replayable forever: pin the binary version, or use the migration tools shipped at the major bump.

On this page