| concept | EventSerialBus | ||||||||
|---|---|---|---|---|---|---|---|---|---|
| status | stable | ||||||||
| tracked_sources |
|
||||||||
| related |
|
||||||||
| last_verified | 2026-06-04 |
The EventSerialBus (ESB) is AMPERE's nervous system: a thread-safe,
Kotlin Multiplatform-compatible publish/subscribe bus that carries typed
Events between agents, services, and human-facing surfaces. Subscribers
register against an EventType; the bus snapshots its handler list under a
mutex on each publish and dispatches handlers asynchronously on a shared
CoroutineScope. The bus itself only dispatches — persistence is delegated
to higher-level APIs (EventStore, SignificanceAwareEventLogger).
Agents in AMPERE coordinate through convergence, not through RPC. A direct method call between two agents creates a synchronous coupling that: (a) hides the interaction from observers (the glass brain goes opaque), (b) bakes a topology into the code (agent A must know about agent B), and (c) prevents new subscribers from joining without modifying the caller.
By making Event the universal coordination primitive, we get four
properties for free:
- Observability. Every interesting state change passes through the bus
and is recorded by
EventLogger/EventStore. The CLI and trace projection read the same stream the agents write. - Late binding. A new agent can subscribe to existing events without any publisher being aware of it. Coordination is additive.
- Replay & time-travel. Persisted events form an append-only log keyed
by
run_id;ArcTraceProjectioncan reconstruct what happened in any past Arc run from this log alone. - Cross-platform. ESB lives in
commonMainand targets JVM, Android, iOS, and Desktop with a single contract. There is no platform-specific coordination layer.
ampere-core/src/commonMain/kotlin/link/socket/ampere/agents/events/bus/EventSerialBus.kt— the bus itself;publish,publishAsync,subscribe,unsubscribe.agents/events/bus/EventSerialBusFactory.kt— wiring the bus into the application graph.agents/events/api/AgentEventApi.kt— the agent-facing facade for emitting events without touching the bus directly.agents/events/relay/EventRelayService.kt— out-of-process bridging (e.g., CLI streaming).agents/events/utils/EventLogger.kt,SignificanceAwareEventLogger.kt— pluggable logging implementations.Console, significance-filtered.agents/events/subscription/EventSubscription.kt,Subscription.kt— the handle returned to subscribers.agents/domain/event/Event.ktand theevent/package — the sealedEventhierarchy.agents/domain/event/CognitivePhaseEvent.kt— phase transition events emitted byPhaseSparkManagerwhen a bus is wired.ampere-core/src/commonMain/sqldelight/link/socket/ampere/db/events/EventStore.sq— persistence schema (withrun_idindexes for trace queries).
- Direct agent-to-agent method calls are forbidden for coordination. If agent A needs to influence agent B, A publishes; B subscribes. The only direct calls allowed are within an agent's own services or into stateless helpers. (Read: tests around
AgentReasoninginjecting fakes are fine; an agent calling another agent'shandleX(...)is not.) - Every event type has a serializer, a registration in the event hierarchy, and a CLI display handler. New event types must satisfy all three before merge — see the "Agent System Rules" in
AGENTS.md. - Handler exceptions never propagate to the publisher. The bus swallows and logs handler failures. Publishers cannot rely on subscriber success; if a downstream effect is required, it gets its own event.
- The bus does not persist; loggers and stores do. A change that makes
EventSerialBus.publishwrite to a database directly violates the layering — persistence belongs toEventStoreinvoked by an event-aware logger or projector. run_idis propagated through the event chain. Events emitted within an Arc run carry the originatingrun_idso trace projection can find them. Lossy event handlers that striprun_idbreak time-travel.- No mutex held across handler invocation. The bus snapshots handlers under the mutex and releases before launching coroutines. A change that holds
mutexwhile running handlers would serialize the entire system.
- Publish an event —
agentEventApi.publish(SomeEvent(...)). The api wraps the bus and is the agent-facing entry point. - Publish from a synchronous boundary hook — use
EventSerialBus.publishAsync(event)only when the caller cannot suspend, such as phase boundary hooks. PreferAgentEventApi.publishelsewhere. - Subscribe —
bus.subscribe(eventType, scope) { event, subscription -> ... }. Hold onto the returnedEventSubscriptionso you canunsubscribeon shutdown. - Add a new event type — extend
Event(or the appropriate sub-sealed family inagents/domain/event/), register a@Serializablesubclass with a stable@SerialName, add a CLI display handler, and add a logger summary inEvent.getSummaryif relevant. - Observe phase transitions — subscribe to
CognitivePhaseEvent.PhaseEntered.EVENT_TYPEand/orPhaseExited.EVENT_TYPE; usenestingDepth == 0for outermost phase changes only. - Publish an agent milestone — call
AgentEventApi.reachMilestone(...)or, for observable agents,agent.reachMilestone(...). Milestones emitMemoryEvent.MilestoneReached, a low-volume sibling event to routine memory writes. - Persist for trace — events flow into
EventStorevia the configured logger. New event types are picked up automatically; just verifyrun_idis set on the emitter.
- "Just call the other agent's method directly, the event is annoying." This is how AMPERE became opaque the first time. The cost of an event is one serialized struct; the cost of bypassing one is invisibility.
- Catching exceptions inside a handler and silently dropping them. The bus already swallows handler errors and logs them. Adding a second swallow inside the handler hides real failures from the logger.
- Using
runBlockinginside a handler. Handlers run on the bus'sCoroutineScope. Blocking that scope blocks the next dispatch loop. Suspend functions only. - Emitting events outside an agent's
AgentEventApi. Directbus.publishcalls in domain code skip the source-tagging the api adds, which means the trace can't attribute the event to an agent. - Persisting state in the bus. The bus is a router. Anything that needs persistence belongs in a store one layer up.