Manifest declares the kind
Every kind you emit appears in emitted_event_kinds. Namespace prefix matches your pack slug.
A pack emits events to the cloud so remote surfaces can observe and
react to what the pack is doing. This page is the contract pack
authors sign: what fields must appear in the manifest, what shape
.emit() calls must have, how to pick a backpressure policy, and what
idempotency guarantees a subscriber handler must meet.
For live agent-to-agent coordination on a single workspace, see Agent addressing. That session-level memory traffic complements, rather than replaces, the pack event contract described on this page.
This page is grounded in ADR-013 §2 (delivery guarantee), §3 (ordering), §4 (backpressure), and the “Event Namespacing” + “Manifest Extension Fields” sections. Every rule here is load-bearing for the cross-pack event registry; if you think a rule is arbitrary, read the ADR first.
Every pack manifest that emits or consumes events declares four additional fields (ADR-013 “Manifest Extension Fields”):
Field semantics:
| Field | Meaning |
|---|---|
emitted_event_kinds | Canonical kinds this pack may emit. Every .emit() call is validated against this list at runtime; unknown kinds are rejected. |
subscribed_event_kinds | Kinds this pack handles. The runtime wires a dispatcher that routes incoming events of these kinds to pack handlers. |
required_approvals | Approval surface identifiers this pack may request. Checked against installed approval providers at workspace load. |
surfaces_required | Transport surfaces this pack needs (http, mcp, sidekick-channel). Runtime refuses to activate a pack whose required surfaces are unavailable. |
All four fields are additive. A manifest that omits them gets the
default emitted: [], subscribed: [], approvals: [], surfaces: ['http'].
Every emitted kind must match <pack-slug>:<event_name_snake_case>.
Kernel-intrinsic kinds stay unprefixed (task_created, task_claimed,
task_completed from ADR-011 §2) for backward compatibility; everything
else carries the pack slug as the prefix, colon-separated.
A boundary lint rule (INIT-060 WU-1) rejects manifest entries that
violate the rule. Runtime assertion at pack load also rejects emit
calls with a kind not in the pack’s emitted_event_kinds list.
buildKernelEventV2Every pack emission flowing to the cloud goes through
buildKernelEventV2 from the kernel-event-sync path. You don’t construct
the wire envelope by hand; the builder populates schema_version,
timestamp, and event_id for you.
Field contract for the payload:
kind — must match one of the pack’s emitted_event_kinds. Checked at
emit time.from — the workspace-identity (or phone-device) subject from the
enrollment token. Not a free-text field. ADR-013 §5.@lumenflow/conductor-sdk’s
tagged union for the allowed shape per kind.event_id and idempotency keysbuildKernelEventV2 accepts an idempotencyKey option. When provided,
event_id is derived by content-hashing {idempotencyKey, kind, timestamp, payload}.
Two emitter calls with the same key in the same window produce the same
event_id. Cloud’s dedup layer then drops the replay.
When idempotencyKey is omitted, event_id is a random UUID — every
call is distinct, replay behaviour depends entirely on the cloud’s
content-aware dedup. Prefer idempotency keys for every emission where
“the same thing happened twice” is a real scenario (network retry, local
restart mid-turn, subscriber re-drain). Omit it only when the event
describes a genuinely unique occurrence (turn_started with a unique
turn id in the payload is already unique).
ADR-013 §4 splits events into two classes: ephemeral (dropped on disconnect) and queue + replay (buffered in the outbox and drained FIFO on reconnect). The split rule is declared per kind in the manifest, not decided per emitter call:
Picking a policy:
| Kind describes… | Policy |
|---|---|
| Observation the cloud can recompute from later events | ephemeral |
| Operator action the cloud must act on (approval request) | queue |
| Inbound command targeting the agent | queue |
| State transition that terminates a longer-running process | ephemeral (the turn_completed will re-sync state; the intermediate turn_started is recomputable) |
| Anything whose loss would cause a user-visible “I clicked but nothing happened” bug | queue |
If you are tempted to make everything queue “just to be safe,” read the
ADR-013 §4 “Uniform backpressure (everything queues)” rejection:
ephemeral telemetry queued for a long disconnect fills the outbox and
possibly the disk, for no observational benefit. Fail-silent is the
right default for telemetry.
A manifest validator (INIT-060 WU-11) rejects adding a new
emitted_event_kind without a matching backpressure_policy entry. You
cannot forget.
required_approvals — the approval contractA kind in required_approvals maps to an approval provider the runtime
checks before the pack may act on an incoming command or before the pack
may call a governed tool. The manifest field declares the surface
identifier; the runtime wires in a provider at workspace load.
If a declared provider is missing at load time, the workspace surfaces a diagnostic — not a silent skip. A pack that declares an approval it cannot satisfy refuses to activate, rather than running un-gated.
Every subscribed_event_kinds handler must be idempotent under
replay (ADR-013 §2, “Delivery Guarantee” — at-least-once).
Rules:
event_id. Every event carries a content-hashed
event_id (per-emitter idempotency key) or a random UUID. Store
the key once the handler’s effect has committed; subsequent
invocations short-circuit.event_id, or use set-semantics (INSERT … ON CONFLICT DO NOTHING).@idempotent or replay_key. The conformance test
(INIT-060 WU-11) requires every registered handler to either carry
the @idempotent static flag or expose a replay_key(event) function
returning a deterministic dedup string. A handler without either
fails the boot check.A repo-wide test (INIT-060 WU-11) fires every subscribed kind twice through a replay harness and asserts equal post-state. If your handler has non-obvious non-idempotency (a timer increment, a clock read, a random-id generator), the test catches it before the pack ships.
Writing a handler that passes the test is simpler than retrofitting one. Follow the rules above from the first commit.
software-delivery packTrimmed excerpt of the real manifest (packages/@lumenflow/packs/software-delivery/manifest.ts):
Emit site (simplified):
Note the .catch(() => {}) on the push — it matches the
ephemeral policy declared for this kind. For a queue kind, the
emitter would hand the event to the outbox, which retries until drained.
The pack author doesn’t write the retry loop; they pick the policy.
Manifest declares the kind
Every kind you emit appears in emitted_event_kinds. Namespace prefix matches your pack slug.
Backpressure picked
Each kind has an entry in backpressure_policy — ephemeral or queue.
Uses buildKernelEventV2
No hand-rolled envelope. The builder owns schema_version, timestamp, event_id.
Handlers are idempotent
Every subscribed handler dedupes by event_id and uses upsert semantics. Decorated with
@idempotent or replay_key.