How Magnitude Implements Event Sourcing for Agent Behavior: An Effect-TS Deep Dive

Magnitude implements event sourcing for agent behavior through an Effect-TS-native architecture that captures every state change as an immutable event, processes updates synchronously through a two-phase model, and guarantees full replayability from a persistent event log.

Magnitude is an open-source agent framework designed around deterministic execution guarantees. The magnitudedev/magnitude repository implements event sourcing for agent behavior in its event-core package, ensuring that every tool invocation, message, and lifecycle transition is captured as a durable, replayable event. This architecture enables features like time-travel debugging, session reconstruction, and deterministic testing that are critical for reliable AI agent systems.

Event Publication and the Central Bus

At the heart of Magnitude's implementation is the Event Bus, defined in packages/event-core/src/core/event-bus-core.ts. Workers—such as tool executors, turn initiators, and lifecycle coordinators—emit domain events using the publish() method exposed through the EventBusTag service.

import { EventBusTag } from '@magnitudedev/event-core';

Effect.run(
  Effect.flatMap(
    EventBusTag,
    bus => bus.publish({ type: 'user_message', message: 'Hello' })
  )
);

When publish() is invoked, the event is immediately forwarded to the Projection Bus via the internal processEvent function. This happens synchronously: the method blocks until all registered handlers complete, ensuring that workers always see a consistent view of state before continuing execution.

The Two-Phase Processing Model

Magnitude separates state updates into two distinct phases to prevent race conditions and infinite signal loops. As documented in ARCHITECTURE.md, this two-phase model ensures that complex state transitions remain predictable and deterministic.

Phase 1: Projection State Updates

During the first phase, the bus invokes all registered projection handlers (state reducers) for the specific event type. These handlers update their local mutable state snapshots, called fork instances. The processing is strictly sequential—no handler runs in parallel, and the entire chain must finish before publish() returns.

import { defineProjection } from '@magnitudedev/event-core';

export const ChatProjection = defineProjection({
  eventHandlers: {
    user_message: ({ event, fork }) => ({
      messages: [...fork.messages, event.message],
    }),
  },
});

Phase 2: Signal Cascade Processing

Once all projections have updated their state, Phase 2 begins. Signal handlers—derived from projection state changes—execute and may emit further signals. These secondary events are processed within the same flush cycle, creating a controlled cascade rather than an uncontrolled re-entrant loop. This separation ensures that state updates complete before any side effects trigger additional events.

Event Persistence and the EventSink

After both processing phases complete, the event is handed to the EventSink (packages/event-core/src/core/event-sink.ts). This component maintains an in-memory buffer of pending events and exposes a claim/acknowledge API for durable storage.

import { makeEventSinkLayer } from '@magnitudedev/event-core';

const eventSinkLayer = makeEventSinkLayer();

The sink batches events until a claim is explicitly acknowledged, at which point the batch flushes to durable storage such as SQLite. This design decouples the latency-sensitive projection processing from the slower I/O of persistent storage while maintaining durability guarantees.

Replay, Hydration, and Deterministic Guarantees

The Event Engine (packages/event-core/src/event-engine/make.ts ) handles system initialization and crash recovery. When an agent process starts, the engine replays all persisted events through the same projection pipeline used during live operation.

import { makeEventEngineLayer } from '@magnitudedev/event-core';

const engine = makeEventEngineLayer({
  projections: [ChatProjection],
  sink: eventSinkLayer,
});

Because every publish call blocks until all projections and signal cascades finish, there is no eventual-consistency window within a single publish cycle. Workers cannot observe partial states or stale data. This deterministic guarantee means that given the same sequence of events, the system will always reconstruct identical state, enabling reliable session replay and time-travel debugging.

Summary

  • Immutable event log: Every agent action is captured as a durable event in event-sink.ts, creating a complete audit trail of the session.
  • Synchronous processing: The publish() method in event-bus-core.ts blocks until all projections update, eliminating race conditions within the event loop.
  • Two-phase safety: Separating state updates (Phase 1) from signal handling (Phase 2) prevents infinite loops and ensures consistent state transitions.
  • Full replay capability: The Event Engine can reconstruct agent state exactly by replaying events through makeEventEngineLayer, enabling crash recovery and deterministic testing.

Frequently Asked Questions

What makes Magnitude's event sourcing deterministic?

Magnitude guarantees determinism by blocking the publish() method until all projection handlers and signal cascades complete synchronously. As implemented in packages/event-core/src/core/event-bus-core.ts, no worker can proceed until the entire event processing chain finishes, ensuring that state is always consistent and replaying the same event log always produces identical results.

How does the two-phase model prevent infinite loops?

The architecture separates projection handlers (Phase 1) from signal handlers (Phase 2). Projections update state first, then signals derived from those state changes may emit new events. Because signals are processed in a distinct phase after all state updates complete, the system avoids re-entrant cycles where a signal triggers an event that immediately triggers another signal, as documented in ARCHITECTURE.md.

Can agent state be reconstructed after a process crash?

Yes. The Event Engine (packages/event-core/src/event-engine/make.ts ) replays all persisted events from the EventSink through the projection pipeline on startup. Since projections are pure reducers over the event log, the system rebuilds the exact state that existed before the crash, enabling reliable recovery and debugging.

What is the relationship between EventBus and EventSink?

The EventBus (event-bus-core.ts) coordinates the synchronous processing of events through projections, while the EventSink (event-sink.ts) handles the asynchronous durability layer. The bus ensures immediate consistency for in-memory state, then delegates persistence to the sink, which batches events and flushes them to storage after processing completes.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →