From 2c3c2d8b7f852ba7971e0c1b2cc286117c27aa95 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Sun, 13 Sep 2026 08:28:32 -0600 Subject: [PATCH] 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 --- docs/internal/events-strategy.md | 23 ++++++++- docs/internal/events.md | 31 ++++++++++- .../fabro-event-schema-v2-concrete-shape.md | 51 ++++++++++--------- 3 files changed, 79 insertions(+), 26 deletions(-) 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