State the agent event contract in the docs

Pebble's CodingAgentEvent stream is the agent event contract: every event
except streaming deltas is stored verbatim under its derived name and
folded into StageProjection.agent with pebble's SessionProjection. The
events doc, the events strategy, and the v2 shape doc say so, list the
agent events fabro still emits for facts pebble cannot know, and tell
consumers to read the fold rather than fold the events again.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-09-13 08:28:32 -06:00
parent 7a6eea0399
commit 2c3c2d8b7f
No known key found for this signature in database
3 changed files with 79 additions and 26 deletions

View file

@ -175,11 +175,32 @@ Check:
- store validation
- tests or fixtures that inspect event names or fields
## Agent Events
Pebble's `CodingAgentEvent` stream is the agent event contract. The worker's
event sink stores every event the coding agent publishes for a stage, except
streaming deltas, verbatim as `EventBody::Agent` under a name derived from
its variant (`fabro_types::coding_event_name`), and the store folds those
events into `StageProjection.agent` with pebble's `SessionProjection`. Do not
add a fabro event that restates a pebble event, and do not add a second fold
of the stream: read `StageProjection.agent`, or the stored pebble event
itself, instead.
Fabro emits an agent event of its own only for a fact pebble cannot know.
Today those are `agent.session.activated`, `agent.session.deactivated`,
`agent.tools.available`, `agent.pair.user_message`,
`agent.pair.system_message`, `agent.interrupt.injected`,
`agent.steer.buffered`, `agent.steer.dropped`, the `agent.acp.*` family, and
`prompt.failover` for a one-shot prompt stage that walks its fallback plan
without pebble. A new fabro agent event needs the same justification: name
the fact pebble does not have.
## Consumer Guidance
When writing Rust consumers (listeners, store projections, CLI progress):
- Match on `event.body` using `EventBody::*` variants. This gives you typed access to event-specific fields.
- Match on `event.body` using `EventBody::*` variants. This gives you typed access to event-specific fields. For a pebble event, match `EventBody::Agent(props)` and then `props.coding_event()`.
- For a stage's agent facts (usage, route, MCP servers, skills, todos, subagents, files, failovers, compactions), read `StageProjection.agent` rather than folding the events again.
- 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.

View file

@ -883,7 +883,36 @@ Emitted when execution loops back to an earlier node.
## Agent events
Most agent activity events are stage-scoped and carry `node_id` (the workflow stage), `node_label`, `stage_id`, `session_id`, and `parent_session_id` in the envelope. Session object lifecycle events are the exception: `agent.session.started` and `agent.session.ended` are not stage-scoped and intentionally omit `node_id`, `node_label`, `stage_id`, and `visit`.
Pebble's `CodingAgentEvent` stream is the agent event contract. Every event
the coding agent publishes for a stage, except streaming deltas, is stored
verbatim as an `EventBody::Agent` under a name derived from its variant
(`agent.message`, `agent.tool.started`, `agent.route.failover`,
`agent.mcp.server.ready`, `todo.created`, and so on; the full list is
`CODING_EVENT_NAMES`). Its `properties` are pebble's own envelope, so
pebble's event types are part of fabro's stored format, and the store folds
the same events into `StageProjection.agent` with pebble's
`SessionProjection`, the one fold of that stream.
Fabro emits an agent event of its own only for a fact pebble cannot know:
- `agent.session.activated` and `agent.session.deactivated`: the stage's
route, controls, permission level, and steering capabilities, as fabro
resolved them.
- `agent.tools.available`: the tool catalog fabro handed the agent.
- `agent.pair.user_message` and `agent.pair.system_message`: pair mode.
- `agent.interrupt.injected`, `agent.steer.buffered`, `agent.steer.dropped`:
run-level steering as it reaches, waits for, or misses a session.
- `agent.acp.started`, `agent.acp.completed`, `agent.acp.cancelled`,
`agent.acp.timed_out`: an external ACP agent process, which pebble does
not run.
- `prompt.failover`: a one-shot prompt stage moving to a fallback route,
which it does without pebble.
Every agent activity event is stage-scoped and carries `node_id` (the
workflow stage), `node_label`, `stage_id`, `session_id`, and
`parent_session_id` in the envelope. Pebble's session lifecycle events
(`agent.session.started`, `agent.session.ended`) are stored with the stage
that ran the session like the rest.
### `agent.session.started`

View file

@ -364,30 +364,33 @@ V2 keeps the current durable family surface broadly intact.
### Agent Durable Events
- `agent.session.started`
- `agent.session.ended`
- `agent.processing.end`
- `agent.input`
- `agent.message`
- `agent.tool.started`
- `agent.tool.completed`
- `agent.error`
- `agent.warning`
- `agent.loop.detected`
- `agent.steering.injected`
- `agent.compaction.started`
- `agent.compaction.completed`
- `agent.llm.started`
- `agent.llm.first_output`
- `agent.llm.retry`
- `agent.sub.spawned`
- `agent.sub.completed`
- `agent.sub.failed`
- `agent.sub.closed`
- `agent.mcp.ready`
- `agent.mcp.failed`
- `agent.mcp.disconnected`
- `agent.failover`
Pebble's events, stored verbatim under the names `fabro_types::CODING_EVENT_NAMES`
lists (every `CodingEvent` variant except the streaming deltas):
- `agent.session.started`, `agent.session.ended`, `agent.processing.end`
- `agent.input`, `agent.message`
- `agent.llm.started`, `agent.llm.first_output`, `agent.llm.retry`
- `agent.tool.started`, `agent.tool.completed`, `agent.tool.process.completed`, `agent.tool.rounds.exhausted`
- `agent.error`, `agent.warning`, `agent.loop.detected`
- `agent.steering.injected`, `agent.round.interrupted`
- `agent.compaction.started`, `agent.compaction.completed`, `agent.compaction.failed`, `agent.compaction.cancelled`
- `agent.route.failover`, `agent.route.failover.stopped`
- `agent.mcp.server.ready`, `agent.mcp.server.failed`, `agent.mcp.server.disconnected`
- `agent.sub.spawned`, `agent.sub.turn.started`, `agent.sub.completed`, `agent.sub.failed`, `agent.sub.closed`
- `agent.memory.loaded`, `agent.skills.discovered`, `agent.skill.activated`
- `todo.created`, `todo.updated`, `todo.deleted`
Fabro's own, for facts pebble cannot know:
- `agent.session.activated`, `agent.session.deactivated`, `agent.tools.available`
- `agent.pair.user_message`, `agent.pair.system_message`
- `agent.interrupt.injected`, `agent.steer.buffered`, `agent.steer.dropped`
- `agent.acp.started`, `agent.acp.completed`, `agent.acp.cancelled`, `agent.acp.timed_out`
- `prompt.failover` (a one-shot prompt stage's move to a fallback route)
The former mirrors `agent.mcp.ready`, `agent.mcp.failed`,
`agent.mcp.disconnected`, and `agent.failover` are no longer emitted; runs
recorded with them read them back as generic events.
### Git