Skip to content

Latest commit

 

History

History
89 lines (74 loc) · 7.26 KB

File metadata and controls

89 lines (74 loc) · 7.26 KB
concept EventSerialBus
status stable
tracked_sources
ampere-core/src/commonMain/kotlin/link/socket/ampere/agents/events/bus/**
ampere-core/src/commonMain/kotlin/link/socket/ampere/agents/events/api/**
ampere-core/src/commonMain/kotlin/link/socket/ampere/agents/events/relay/**
ampere-core/src/commonMain/kotlin/link/socket/ampere/agents/events/utils/EventLogger.kt
ampere-core/src/commonMain/kotlin/link/socket/ampere/agents/events/utils/SignificanceAwareEventLogger.kt
ampere-core/src/commonMain/kotlin/link/socket/ampere/agents/events/subscription/**
ampere-core/src/commonMain/kotlin/link/socket/ampere/agents/domain/event/**
ampere-core/src/commonMain/sqldelight/link/socket/ampere/db/events/**
related
PropelLoop
AgentSurface
CognitionTrace
MemoryProvenance
last_verified 2026-06-04

EventSerialBus

What it is

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).

Why it exists

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:

  1. 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.
  2. Late binding. A new agent can subscribe to existing events without any publisher being aware of it. Coordination is additive.
  3. Replay & time-travel. Persisted events form an append-only log keyed by run_id; ArcTraceProjection can reconstruct what happened in any past Arc run from this log alone.
  4. Cross-platform. ESB lives in commonMain and targets JVM, Android, iOS, and Desktop with a single contract. There is no platform-specific coordination layer.

Where it lives

  • 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.kt and the event/ package — the sealed Event hierarchy.
  • agents/domain/event/CognitivePhaseEvent.kt — phase transition events emitted by PhaseSparkManager when a bus is wired.
  • ampere-core/src/commonMain/sqldelight/link/socket/ampere/db/events/EventStore.sq — persistence schema (with run_id indexes for trace queries).

Invariants

  • 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 AgentReasoning injecting fakes are fine; an agent calling another agent's handleX(...) 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.publish write to a database directly violates the layering — persistence belongs to EventStore invoked by an event-aware logger or projector.
  • run_id is propagated through the event chain. Events emitted within an Arc run carry the originating run_id so trace projection can find them. Lossy event handlers that strip run_id break time-travel.
  • No mutex held across handler invocation. The bus snapshots handlers under the mutex and releases before launching coroutines. A change that holds mutex while running handlers would serialize the entire system.

Common operations

  • Publish an eventagentEventApi.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. Prefer AgentEventApi.publish elsewhere.
  • Subscribebus.subscribe(eventType, scope) { event, subscription -> ... }. Hold onto the returned EventSubscription so you can unsubscribe on shutdown.
  • Add a new event type — extend Event (or the appropriate sub-sealed family in agents/domain/event/), register a @Serializable subclass with a stable @SerialName, add a CLI display handler, and add a logger summary in Event.getSummary if relevant.
  • Observe phase transitions — subscribe to CognitivePhaseEvent.PhaseEntered.EVENT_TYPE and/or PhaseExited.EVENT_TYPE; use nestingDepth == 0 for outermost phase changes only.
  • Publish an agent milestone — call AgentEventApi.reachMilestone(...) or, for observable agents, agent.reachMilestone(...). Milestones emit MemoryEvent.MilestoneReached, a low-volume sibling event to routine memory writes.
  • Persist for trace — events flow into EventStore via the configured logger. New event types are picked up automatically; just verify run_id is set on the emitter.

Anti-patterns

  • "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 runBlocking inside a handler. Handlers run on the bus's CoroutineScope. Blocking that scope blocks the next dispatch loop. Suspend functions only.
  • Emitting events outside an agent's AgentEventApi. Direct bus.publish calls 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.