diff --git a/docs/internal/events-strategy.md b/docs/internal/events-strategy.md index ad7a63823..5d036ed05 100644 --- a/docs/internal/events-strategy.md +++ b/docs/internal/events-strategy.md @@ -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. diff --git a/docs/internal/events.md b/docs/internal/events.md index 7192ab40c..2e0ed43e7 100644 --- a/docs/internal/events.md +++ b/docs/internal/events.md @@ -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` diff --git a/docs/internal/fabro-event-schema-v2-concrete-shape.md b/docs/internal/fabro-event-schema-v2-concrete-shape.md index 81d3eab15..89b8ee806 100644 --- a/docs/internal/fabro-event-schema-v2-concrete-shape.md +++ b/docs/internal/fabro-event-schema-v2-concrete-shape.md @@ -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