Skip to main content
Mirrored from Meerkat Meta main at 26f5ae9da1fd on 2026-11-03; Meta version 0.9.9 at that commit. Meta exposes three related event surfaces:
  • EventEnvelope<UnifiedEvent> merges projected Meerkat agent events with Meta module events.
  • MobStructuralEventEnvelope projects structural events from the Meerkat mob-event ledger.
  • The WorkGraph wake stream emits bare UnifiedEvent::Module values at GET /meta/workgraph/facts/stream as lossy notifications.
They have different transport shapes and durability properties.

Unified event envelope

For UnifiedEvent::Agent, source must be "agent". For UnifiedEvent::Module, it must be "module". The optional agent payload is part of the wire contract and must not be dropped when matching or forwarding the event.

Agent event names

Agent event names come from Meerkat’s exhaustive event mapper. Current examples include:
  • Run and turn: run_started, retrying, run_completed, run_failed, turn_started, turn_completed
  • Model output: reasoning_delta, reasoning_complete, text_delta, text_complete
  • Tools: tool_call_requested, tool_result_received, tool_execution_started, tool_execution_completed, tool_execution_timed_out
  • Hooks: hook_started, hook_completed, hook_failed, hook_denied
  • Compaction: compaction_started, compaction_completed, compaction_failed
  • Interaction: interaction_complete, interaction_callback_pending, interaction_failed. Only the first and last are terminals; interaction_callback_pending means the interaction is paused at an external callback boundary waiting for tool results, and it resumes and closes under the same interaction_id
These strings are projections of Meerkat AgentEvent variants. Meta does not synthesize them as module events.

Run failure payload

run_failed carries Meerkat’s typed failure fact as error_report; since Meerkat 0.7 there is no separately carried error string on the wire. Meta’s projection (console_agent_event_payload, the single site every SSE stream, event-log row and console frame goes through) keeps the report and derives two flat keys from it, so consumers read one shape:
The console interaction_failed frame Meta projects from run_failed carries the same payload, so wire consumers read frame.payload.error and frame.payload.reason on REST/RPC/SSE timeline frames. Frames Meta mints itself (reason: "superseded_by_later_run", or error set from a delivery failure) carry only the flat keys. Meerkat’s own interaction_failed agent event carries a typed reason object (kind discriminator) whose Display is mirrored into error. The Python SDK exposes the report as RunFailed.error_report (with error_class and reason_type accessors) and derives RunFailed.error from it; the TypeScript SDK exposes RunFailedEvent.errorReport; UnifiedAgentEvent.payload carries it for query_events consumers. A retried LLM call (a 429, a network timeout) surfaces as retrying events before either terminal; a non-retryable failure (a 401 on a bad key) reaches run_failed in one round trip. The user_input frame of a console send terminates at delivered by design (the send was admitted); the turn’s outcome is the sibling interaction_complete or interaction_failed frame, not a status change on the input frame. A turn still running when the gateway shuts down gracefully (meta/shutdown, or SIGTERM to the gateway process) on a member that is not durable also gets its terminal frame. The shutdown ends the run, and it drains the run’s terminal into the console log before the gateway exits: interaction_failed when the shutdown cancelled the run, interaction_complete when the run finished first. A durable member’s run (one whose session lives on a persistent session service) is not ended there: the shutdown suspends it without a terminal, and the run continues at the member’s next start, where its terminal frame comes when it ends. A crash or SIGKILL mid-turn can leave the turn without a terminal frame. After a restart Meerkat settles some interrupted runs without publishing a terminal, by design, so the caller’s own timeout is the outcome it sees. Bound any wait on an interaction_id with a timeout, or check the member’s status.

Tool result payload

A tool call’s output travels once, on tool_result_received: its typed content blocks, and a flat result string Meta’s projection derives from their text. tool_execution_completed, published just before it for the same id, is the execution fact: whether the call failed and how long it took, with no content and no result. Both events mirror id into tool_call_id, so consumers join them on it.
The Python SDK exposes the output as ToolResultReceived.result and the TypeScript SDK as ToolResultReceivedEvent.result; neither SDK’s completion type has a result. Console timeline frames rebuilt from session history keep the tool_execution_completed kind and carry the output, because a transcript records only the result.

Merged module event names

Meta itself generates these module event types in the merged envelope log: Generic modules may supply additional event names through their own valid envelopes. Gating audit entries, memory operations, and supervisor health transitions are not currently generated as unified module events by Meta.

WorkGraph wake events

The separate WorkGraph wake stream uses the UnifiedEvent::Module shape but serializes that value directly as SSE data. It does not wrap the value in EventEnvelope, write it to merged_events, or include it in meta/events/subscribe. Every connection begins with workgraph.resync_required and reason = "initial_sync". The stream then sends workgraph.fact wakes and can send workgraph.resync_required with reason = "lagged" after broadcast lag. It has no SSE ID, Last-Event-ID, catch-up, or replay contract. Consumers obtain authoritative graph state through the WorkGraph get, list, ready, and snapshot pull methods.

Ordering and normalization

merge_unified_events orders envelopes by:
  1. timestamp_ms
  2. event_id
  3. source
There is no special agent-before-module tie rule. The module boundary accepts direct EventEnvelope<UnifiedEvent> JSON and a supported flat event form. Normalization verifies that the source agrees with the UnifiedEvent variant and rejects malformed input. It does not mint missing IDs, enforce ID uniqueness, deduplicate events, or rewrite timestamps. insert_event_sorted uses the same ordering when runtime operations add later events. The merged envelopes are a recent-event buffer, not a history: the runtime keeps at least the newest 4,096 and drops older ones in batches as new ones arrive. Every forwarded member event lands there, text deltas included. For history, read the event log (meta/query_events, when one is configured) or the console timeline.

Structural mob events

The structural surface preserves a Meerkat mob-event cursor and the serialized MobEventKind payload:
The projection uses event_id: "mob-evt-{cursor}", preserves the upstream cursor and timestamp, maps the event kind to its snake-case name, and places the full serialized kind payload in data. Current kinds include mob_created, member_spawned, flow_started, step_dispatched, members_wired, and supervisor_escalation. The enum can grow, so clients should not depend on a fixed variant count.

Member creation facts

The member_spawned structural payload includes creation, a MemberCreationRecord from the canonical Meerkat journal. Its creation_id is a runtime-issued token; its provenance.kind is root, spawn, fork, successor, unproven or legacy_unknown. Spawn provenance records the exact source session and member binding. Fork provenance refers to the event’s existing fork_source endpoint instead of introducing a second endpoint. Older journals that omit creation deserialize with creation_id: null and provenance.kind: "legacy_unknown". Projection never fabricates a token or turns missing ancestry into a proven root. New creations whose ancestry cannot be proved use unproven with a new token; ordinary execution can still proceed. These facts establish runtime ancestry and contain no tool policy. Resource ACLs and current native policy remain separate checks. See Meerkat’s member creation contract.

Cursor persistence and replay

The structural subscriber reads from MobEventsView. With a saved cursor it uses subscribe_after(cursor); without one it starts from the latest cursor. It writes the last projected cursor to PersistentMetadataStore after projection. Cursor persistence is not event persistence:
  • If a persistent-state path is configured and no explicit metadata store is supplied, Meta creates <path>/meta_metadata.sqlite3 for projection metadata.
  • That metadata stores the resume cursor, not the mob-event ledger. On an identity-first runtime it also keeps the lifecycle projection’s cursor, what each lifecycle step owes and the side effects that failed: a host that supplies its own store (UnifiedRuntimeBuilder::persistent_metadata) keeps them durably, or a restart skips the side effects a stopped process still owed (see Lifecycle side effects across a restart).
  • .persistent_state(path) keeps Mob storage durable in event_log.sqlite3 under the state directory; a build without it keeps Mob storage in memory.
  • Replaying events written while Meta was down requires an upstream Meerkat mob-event store that retained those events.
If a saved cursor is stale for the available ledger, the subscriber falls back to the latest cursor.

Access surfaces

See SSE API for stream parameters and JSON-RPC API for request and response shapes.

Lifecycle events

LifecycleStage has these exact variants: Each LifecycleEvent contains a monotonic seq and one stage. Read the retained entries through MetaRuntimeHandle::lifecycle_events(). ModulesStarted is not proof that every module is healthy; inspect SupervisorReport for module transitions.

See also

  • Modules - module startup and event origins
  • Delivery - resolved and send payloads
  • SSE API - unified, WorkGraph wake, and structural streams
  • Architecture - event transport and ownership