> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rkat.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Events

> Unified agent and module envelopes, structural mob events, lossy WorkGraph wakes, ordering, and runtime lifecycle stages.

*Mirrored from Meerkat Meta main at [26f5ae9da1fd](https://github.com/lukacf/meerkat/commit/26f5ae9da1fd3caac378f1cd847ce50f9ec5ac92) 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

```rust theme={null}
pub struct EventEnvelope<T> {
    pub event_id: String,
    pub source: String,
    pub timestamp_ms: u64,
    pub event: T,
}

#[serde(tag = "kind", rename_all = "snake_case")]
pub enum UnifiedEvent {
    Agent {
        agent_id: String,
        event_type: String,
        payload: Option<serde_json::Value>,
    },
    Module(ModuleEvent),
}

pub struct ModuleEvent {
    pub module: String,
    pub event_type: String,
    pub payload: serde_json::Value,
}
```

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:

```json theme={null}
{
  "type": "run_failed",
  "session_id": "01a068a0-f911-7331-8187-d436145ac895",
  "error_report": {
    "class": "llm",
    "reason": { "reason_type": "llm_auth_error" },
    "message": "LLM error: authentication failed (401)"
  },
  "error": "LLM error: authentication failed (401)",
  "reason": "llm_auth_error"
}
```

| Key | Meaning |
| - | - |
| `error_report.class` | Meerkat's `AgentErrorClass` (`llm`, `tool`, `budget`, `auth`, `internal`, ...). |
| `error_report.reason` | Optional typed reason. `reason_type` is the stable discriminator (`llm_auth_error`, `llm_rate_limited`, `llm_provider_error`, `llm_context_exceeded`, `hook_denied`, ...); the remaining fields are variant data (`retry_after_ms`, `provider_error`, ...). Absent when Meerkat attached no typed reason. |
| `error_report.message` | Human-readable failure text (the `Display` of the failing error). |
| `error` | Derived: `error_report.message`. |
| `reason` | Derived: `error_report.reason.reason_type`. Absent when there is no typed reason. |

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.

```json theme={null}
[
  { "type": "tool_execution_completed", "id": "toolu_1", "tool_call_id": "toolu_1", "name": "search", "is_error": false, "duration_ms": 12 },
  { "type": "tool_result_received", "id": "toolu_1", "tool_call_id": "toolu_1", "name": "search", "is_error": false, "content": [{ "type": "text", "text": "done" }], "result": "done" }
]
```

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:

| `module` | `event_type` | Payload |
| - | - | - |
| Configured module ID | `mcp.ready` | `tools` returned by the startup probe |
| Configured module ID | `supervisor.warning` | Startup or runtime boundary `error` |
| `router` | `resolved` | Full `RoutingResolution` |
| `delivery` | `send` | `delivery_id`, `route_id`, `recipient`, `sink`, `status`, and `attempts` |

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`.

| `module` | `event_type` | Payload |
| - | - | - |
| `meta.workgraph` | `workgraph.fact` | Lossy WorkGraph fact wake payload |
| `meta.workgraph` | `workgraph.resync_required` | Resynchronization reason and optional skipped count |

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:

```rust theme={null}
pub struct MobStructuralEventEnvelope {
    pub event_id: String,
    pub cursor: u64,
    pub mob_id: String,
    pub timestamp_ms: u64,
    pub kind: String,
    pub run_id: Option<String>,
    pub step_id: Option<String>,
    pub agent_identity: Option<String>,
    pub mob_labels: BTreeMap<String, String>,
    pub run_labels: BTreeMap<String, String>,
    pub data: serde_json::Value,
}
```

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](https://docs.rkat.ai/reference/session-contracts.md#member-creation-provenance-and-host-activation).

### 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](https://docs.rkat.ai/meta/guides/deployment.md#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

| Surface | Purpose |
| - | - |
| `meta/mob_events/query` JSON-RPC | Query a structural snapshot or batch |
| `meta/mob_events/subscribe` JSON-RPC | Create a subscription and receive its SSE URL |
| `GET /meta/mob_events/stream` SSE | Tail structural events with cursor and filter resume |

See [SSE API](https://docs.rkat.ai/meta/api/sse.md) for stream parameters and [JSON-RPC API](https://docs.rkat.ai/meta/api/rpc.md) for request and response shapes.

## Lifecycle events

`LifecycleStage` has these exact variants:

| Stage | Meaning |
| - | - |
| `MobStarted` | Runtime bootstrap began |
| `ModulesStarted` | All configured module startup attempts completed |
| `MergedStreamStarted` | The initial agent and module envelopes were merged |
| `ShutdownRequested` | Runtime shutdown began |
| `ShutdownComplete` | Runtime shutdown completed |

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](https://docs.rkat.ai/meta/concepts/modules.md) - module startup and event origins
* [Delivery](https://docs.rkat.ai/meta/concepts/delivery.md) - `resolved` and `send` payloads
* [SSE API](https://docs.rkat.ai/meta/api/sse.md) - unified, WorkGraph wake, and structural streams
* [Architecture](https://docs.rkat.ai/meta/reference/architecture.md) - event transport and ownership


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.