docs: align events strategy with RunEvent simplification

Update terminology (RunEventEnvelope → RunEvent), consumer guidance
(match on body instead of .event/.properties), and "Adding A New Event"
steps to reflect the direct Event → EventBody construction.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-04-04 13:26:08 -04:00
parent 76e1da8b35
commit c32705f12f
No known key found for this signature in database

View file

@ -11,24 +11,25 @@ Detached runs rely on this distinction. If something needs to be visible after r
```text
Engine/Handler -> Event -> Emitter::emit()
|- trace(raw event)
|- canonicalize -> RunEventEnvelope
`- on_event(&RunEventEnvelope)
|- canonicalize -> RunEvent
`- on_event(&RunEvent)
|- progress.jsonl + live.json
|- run store
|- SSE
`- CLI / tests / metrics listeners
```
The canonical envelope is built exactly once in `fabro-workflow/src/event.rs`.
The canonical `RunEvent` is built exactly once in `fabro-workflow/src/event.rs`.
- `Event` remains the internal typed source of truth.
- `Emitter` owns an immutable `run_id` and converts typed events into `RunEventEnvelope`.
- Every listener receives `&RunEventEnvelope`, not `&Event`.
- Bypass paths that cannot go through the emitter must call `canonicalize_event()` once and reuse the same envelope for every sink.
- `Event` (in `fabro-workflow`) is the internal typed event emitted by engine and handlers.
- `Emitter` owns an immutable `run_id` and converts `Event` into `RunEvent` via `to_run_event_at()`.
- `RunEvent` (in `fabro-types`) holds envelope metadata plus a typed `body: EventBody`. It has no cached JSON fields; the wire format is produced only during serialization.
- Every listener receives `&RunEvent`, not `&Event`.
- Bypass paths that cannot go through the emitter must call `to_run_event()` once and reuse the same `RunEvent` for every sink.
## Canonical Envelope
Each line in `progress.jsonl` is a `RunEventEnvelope`:
Each line in `progress.jsonl` is a serialized `RunEvent`:
```json
{
@ -102,11 +103,11 @@ Agent events now use explicit session links:
Most events flow through `Emitter::emit()`. The remaining direct-write paths must use:
1. `canonicalize_event(run_id, event)`
1. `to_run_event(run_id, event)`
2. Serialize and redact once
3. Reuse that exact serialized envelope for every sink
3. Reuse that exact `RunEvent` for every sink
Never canonicalize the same logical event twice if multiple sinks receive it.
Never build the same `RunEvent` twice if multiple sinks receive it.
## Adding A New Event
@ -122,21 +123,26 @@ Extend `Event::trace()` so the raw event is observable in tracing output.
Extend `event_name()` with the new lowercase dot-notation string.
### 4. Map envelope fields
### 4. Add the `EventBody` variant
Update `extract_envelope_fields()`:
Add a variant to `EventBody` in `fabro-types/src/run_event/mod.rs` with a corresponding props struct. Use `#[serde(rename = "dotted.name")]` matching the external name from step 3.
### 5. Map envelope fields and construct `EventBody`
Update `stored_event_fields()` and `event_body_from_event()` in `fabro-workflow/src/event.rs`:
- Move `node_id`, `node_label`, `session_id`, and `parent_session_id` into the envelope when appropriate.
- Keep event-specific data in `properties`.
- Flatten structured failure details into explicit property keys when needed.
- Construct the `EventBody` variant directly from the `Event` fields.
- For `Event::Agent` sub-variants, merge `visit` into the inner props and lift `stage` to `node_id`.
- For `Event::Sandbox` sub-variants, unwrap and flatten into the corresponding `EventBody` variant.
### 5. Emit it
### 6. Emit it
Prefer `Emitter::emit(&Event::...)`.
Use `canonicalize_event()` only for true bypass paths.
Use `to_run_event()` only for true bypass paths.
### 6. Update consumers
### 7. Update consumers
Check:
@ -148,14 +154,20 @@ Check:
## Consumer Guidance
When writing listeners:
When writing Rust consumers (listeners, store projections, CLI progress):
- Match on `envelope.event`, not Rust variant names.
- Read event payload from `envelope.properties`.
- Read stage/branch identity from `node_id` and `node_label`.
- Read agent hierarchy from `session_id` and `parent_session_id`.
- Match on `event.body` using `EventBody::*` variants. This gives you typed access to event-specific fields.
- Use `event.node_id`, `event.node_label`, `event.session_id`, and `event.parent_session_id` for envelope metadata.
- Only use `event.event_name()` or `event.properties()` for generic/display purposes (logging, forwarding). These involve serialization and should not be used on hot paths.
Do not rebuild or mutate the envelope in downstream listeners.
When writing external JSON consumers (SSE clients, JSONL parsers):
- Match on the `"event"` field for the dot-notation event name.
- Read event-specific data from `"properties"`.
- Read stage/branch identity from `"node_id"` and `"node_label"`.
- Read agent hierarchy from `"session_id"` and `"parent_session_id"`.
Do not rebuild or mutate the `RunEvent` in downstream listeners.
## Bypass And Persistence Guarantees