Changelog
Reference

Event schema

The canonical timeline event — envelope, the fifteen event types, token deltas, and the helpers exported from @teaspill/schema.

The canonical event is the single vocabulary everything in teaspill speaks: both harnesses emit it, the platform appends it to an entity's timeline, and the frontend SDK materializes it. This page is the field-by-field specification; for the mental model, read Timelines & events.

The schema is defined in @teaspill/schema (events.ts and deltas.ts).

The v1 event schema is stable: additive-only within v1. New optional fields and new event types may be added; a breaking change to an existing field bumps the envelope v and ships a migration.

Envelope

Every timeline event is { v, entityId, seq, ts, type, payload }.

FieldTypeMeaning
v1 (literal)Schema version. Bumps only on a breaking change.
entityIdstringCanonical entity URL (/t/<tenant>/a/<type>/<id> — see Addressing).
seqint ≥ 00-based, gapless, monotonic per entity. The ordering authority.
tsISO 8601 (with offset)Informational timestamp. Ordering authority is seq, not ts.
typeone of 15Discriminant (the table below).
payloadper-typeShape depends on type.

The seq guarantees

seq is the backbone of the timeline. Three rules hold for every entity:

  • The first event has seq === 0 and is always entity_spawned.
  • Every event occupies a slot: a state_snapshot at seq N consumes N like any other event. Nothing skips a number.
  • seq is assigned server-side by the one component that owns the entity's history, so numbering is gapless by construction. Producers never pick their own seq.

Because seq is 0-based and gapless, a reader can always answer "did I miss anything?" — a gap is drift, never normal. A snapshot at seq N lets a reader join late and consume from N+1 without replaying earlier events.

Token deltas are not canonical events and carry no seq. They ride a sibling stream (see Token deltas).

The fifteen event types

TypePurpose
entity_spawnedAlways seq 0. The entity's first event.
run_startedA wake began. Carries runId, wake source, harness kind, model.
messageA finalized conversation message (user / assistant / system_note).
tool_callThe model invoked a tool.
tool_resultA tool returned or errored.
reasoningFinalized thinking. Display-only — never re-projected into provider context.
state_snapshotThe complete entity state as of this event's seq (inclusive).
summarizationA context-truncation boundary.
controlA control verb was applied.
errorAn error occurred.
run_finishedA wake ended. Carries outcome and token usage.
child_spawnedThis entity spawned a child.
child_finishedA child reported completion back to this entity.
archivedEpisode-terminal archive marker. Not globally terminal.
opaqueA lossless carrier for foreign records with no canonical home.

Each subsection below gives the payload shape and the notes you need to consume it.

entity_spawned

{ entityType: string, parentId: string | null, spawnArgs?: Json, workspaceRef?: string }

Always seq 0. parentId is the parent entity URL, or null for a root entity. workspaceRef is the workspace key (<tenant>/<name>) chosen at spawn and never switched afterward.

run_started

{
  runId: string,
  wake: { source: WakeSource, from?: string },
  harness: "native" | "casdk",
  model?: string,
  detail?: Json,
}

runId is unique per wake; every event the run produces carries it. wake.source is one of spawn, message, steer_degraded, cron, control, system. wake.from is the sender entity URL when the wake came from another entity. detail carries harness-specific extras (informational only).

message

{ id: string, runId?: string, role: "user" | "assistant" | "system_note", content: ContentBlock[], from?: string }

id is the stable message id that token deltas reference. system_note is a platform-authored annotation (for example, "child x finished") — it is context-bearing but rendered to providers as a marked user message, never the API-level system prompt. from is the sender entity URL for inter-agent messages.

tool_call

{ runId: string, toolUseId: string, name: string, input: Json }

toolUseId is the provider tool-use id — the third component of the tool idempotency key (entityId, runId, toolUseId) that makes tool execution exactly-once.

tool_result

{ runId: string, toolUseId: string, name?: string, content: ContentBlock[], detail?: Json, isError: boolean }

detail is structured result data (diff, exit code, a stream reference, …) for rich renderers; it is populated by the in-process tool layer.

reasoning

{ id: string, runId?: string, text: string, encrypted?: string }

Display-only history. Reasoning is never projected back into provider context (thinking signatures are unforgeable), and context assembly skips it. encrypted carries an opaque redacted-thinking payload when the provider supplies one.

state_snapshot

{ state: Json, reason: "periodic" | "pre_archive" | "recovery", historyHole?: boolean }

A snapshot at seq N asserts: state is the complete materialization of this entity after consuming every event with seq <= N. To fast-join, initialize from state and consume from N+1. historyHole: true marks a recovery snapshot after catastrophic stream loss — a marked gap where earlier events may be missing; consumers must not gap-check across it.

summarization

{ runId?: string, summary: string, replacesThroughSeq: number, detail?: Json }

A context-truncation boundary. For context assembly, context-bearing events with seq <= replacesThroughSeq are superseded by summary. replacesThroughSeq is always less than the event's own seq. It does not delete or compact the stream — history stays intact for UIs; only the model-facing projection folds.

control

{ verb: "interrupt" | "pause" | "resume" | "archive", reason?: string, from?: string }

Records that a control verb was applied. from is the requesting principal or entity when known.

error

{ runId?: string, code?: string, message: string, source: "harness" | "tool" | "platform" | "provider" | "projection", detail?: Json }

code is a stable machine-readable code when available; message is always present.

run_finished

{ runId: string, outcome: "success" | "error" | "interrupted", usage: RunUsage, durationMs?: number, detail?: Json }

Ends a wake. usage (below) is the authoritative token accounting for the run.

child_spawned / child_finished

// child_spawned
{ childId: string, childType: string, runId?: string, toolUseId?: string }
// child_finished
{ childId: string, outcome: "success" | "error" | "interrupted" | "archived", result?: Json }

child_finished arrives on a later wake of the parent — the golden rule of multi-agent coordination. result is whatever the child reported back.

archived

{ reason: "idle" | "requested", snapshotSeq?: number }

The episode-terminal marker of an archive. A state_snapshot with reason: "pre_archive" immediately precedes it. Not globally terminal: sending to an archived entity resurrects it — the same identity continues the same seq counter, and the next event is the resurrecting wake's run_started.

opaque

{ origin: string, kind: string, data: Json }

A lossless carrier for foreign records with no clean canonical home — unknown provider artifacts, records from another agent runtime. data round-trips verbatim; every consumer that does not recognize the origin may ignore it.

Shared fragments

ContentBlock

Message and tool-result content is deliberately small — text and image only for v1. Anything richer a tool wants to convey goes in tool_result.detail.

{ type: "text", text: string }
{ type: "image", mimeType: string, data: string /* base64 */ }

RunUsage

{
  inputTokens: number,        // uncached input: fresh prompt + cache writes; cache reads excluded
  cacheReadTokens?: number,   // tokens read from the prompt cache
  outputTokens: number,       // completion tokens
  contextTokens?: number,     // cache-inclusive prompt size of the last step ("% of context used")
  steps?: number,
  costUsd?: number,
  attempt?: number,           // retry attempt; keep the latest attempt's numbers only
}

Token deltas

Token deltas are the live, streaming fragments of a message while the model is still producing it. They ride a sibling /deltas stream, never the timeline, and are always superseded by the finalized event.

// common fields
{ v: 1, entityId: string, runId: string, ref: string, idx: number, ts: string, attempt?: number, kind, ... }

Contract:

  • Deltas carry no seq. Each references the canonical event it streams toward via ref — the message id, the reasoning id, or the toolUseId the finalized event will carry.
  • idx is a per-ref chunk counter for UI assembly. Gaps are allowed — dropped chunks are normal, not drift.
  • The finalized event always wins. Once the timeline carries the finalized event with id == ref, buffered deltas for that ref are discarded. Deltas are never used to reconstruct state.
  • attempt distinguishes retry attempts of the same run; render the highest attempt and drop the rest.

The four delta kinds (discriminated on kind):

Kindref isExtra fields
textmessage idtext
reasoningreasoning idtext
tool_inputtoolUseIdtext (partial JSON)
usagerunIdusage (partial RunUsage) — best-effort mid-run counters; the authoritative figure is run_finished.usage.

Helpers

Exported from @teaspill/schema.

FunctionPurpose
parseTimelineEvent / safeParseTimelineEventParse an unknown value into a canonical event (throwing / non-throwing).
parseTimelineEventJsonParse one JSON-encoded stream record.
isTimelineEventType guard.
finalizeEvent(init, { entityId, seq })Stamp v/entityId/seq onto a produced event and validate it.
checkSeqContiguity(events, { expectedFirstSeq })The gap detector. A reader joining from a snapshot at seq N passes N + 1.
checkTimelineInvariants(events)Structural checks beyond per-event shape (first event is entity_spawned, summarization bound).
parseDeltaRecord / safeParseDeltaRecordParse a delta record.
EVENT_TYPES / DELTA_KINDSThe vocabularies as string arrays.