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

> Magnitude uses Effect TS event sourcing for agent behavior, capturing state changes as immutable events. Experience synchronous updates and full replayability from a persistent log. Explore the architecture.

- Repository: [Magnitude/magnitude](https://github.com/magnitudedev/magnitude)
- Tags: deep-dive
- Published: 2026-09-05

---

**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`](https://github.com/magnitudedev/magnitude/blob/main/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.

```typescript
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`](https://github.com/magnitudedev/magnitude/blob/main/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.

```typescript
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`](https://github.com/magnitudedev/magnitude/blob/main/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.

```typescript
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`](https://github.com/magnitudedev/magnitude/blob/main/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.

```typescript
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`](https://github.com/magnitudedev/magnitude/blob/main/event-sink.ts), creating a complete audit trail of the session.
- **Synchronous processing**: The `publish()` method in [`event-bus-core.ts`](https://github.com/magnitudedev/magnitude/blob/main/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`](https://github.com/magnitudedev/magnitude/blob/main/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`](https://github.com/magnitudedev/magnitude/blob/main/ARCHITECTURE.md).

### Can agent state be reconstructed after a process crash?

Yes. The **Event Engine** ([`packages/event-core/src/event-engine/make.ts`](https://github.com/magnitudedev/magnitude/blob/main/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`](https://github.com/magnitudedev/magnitude/blob/main/event-bus-core.ts)) coordinates the synchronous processing of events through projections, while the **EventSink** ([`event-sink.ts`](https://github.com/magnitudedev/magnitude/blob/main/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.