Magnitude Agent Runtime Projection Types: Regular vs Forked Explained

The Magnitude agent runtime uses two primary projection types—RegularProjection for single-state snapshots and ForkedProjection for branched, parallel state evolution.

Magnitude's event-driven agent architecture relies on projections to model and query state across its runtime. Understanding the different projection types is essential for building complex, branching agent behaviors. In this guide, we'll explore the two core projection families defined in the magnitudedev/magnitude repository and their practical implementations.

What Are Projections in Magnitude?

Projections in Magnitude are read-only, event-sourced state containers that derive their data from a stream of domain events. They provide a typed, reactive interface for agents to observe and respond to state changes without directly mutating shared data.

The projection system lives primarily in packages/event-core/src/surface/index.ts, where the two public projection types are exported for use throughout the agent runtime.

Regular Projection: Single-State Snapshots

RegularProjection is the default projection type for linear, non-branching state. It maintains exactly one state object per projection ID, making it ideal for straightforward agent tracking.

Definition and Implementation

In packages/event-core/src/surface/index.ts at line 91, the RegularProjection type is defined as:

export interface RegularProjection<TId, TStateSchema> {
  readonly id: TId;
  readonly state: TStateSchema;
}

The factory function Projection.define creates these projections. Its implementation resides in packages/event-core/src/projection/define.ts at line 365:

// packages/event-core/src/projection/define.ts#L365
export const define = <TEvent>() => <TDef extends ProjectionDefinition<TEvent>>(
  definition: TDef
): RegularProjection<InferId<TDef>, InferState<TDef>> => {
  // Projection factory implementation
};

Practical Example

Here's a concrete ChatTitleProjection used in the agent runtime:

import { Projection } from '@magnitude/event-core';
import { Schema } from '@effect/schema';
import type { AppEvent } from '../events.js';

export const ChatTitleProjection = Projection.define<AppEvent>()({
  name: 'ChatTitle',
  state: {
    title: Schema.String
  },
  signals: {
    setTitle: (title: string) => ({ title })
  }
});

When to use RegularProjection:

  • Tracking conversation metadata (titles, timestamps, participant lists)
  • Maintaining agent goals or current task status
  • Storing configuration or routing state that doesn't branch

Forked Projection: Parallel State Branches

ForkedProjection extends the projection model with fork semantics, allowing multiple independent branches of state to coexist and evolve separately. This enables speculative execution, parallel plan evaluation, and complex agent reasoning workflows.

Definition and Implementation

Also in packages/event-core/src/surface/index.ts at line 98, the ForkedProjection type adds fork management capabilities:

export interface ForkedProjection<TId, TStateSchema> {
  readonly id: TId;
  readonly forks: ReadonlyMap<ForkId, TStateSchema>;
  createFork(parentForkId?: ForkId): ForkId;
  mergeFork(forkId: ForkId): void;
  abandonFork(forkId: ForkId): void;
}

The factory Projection.defineForked is implemented in packages/event-core/src/projection/defineForked.ts at line 385:

// packages/event-core/src/projection/defineForked.ts#L385
export const defineForked = <TEvent>() => <TDef extends ProjectionDefinition<TEvent>>(
  definition: TDef
): ForkedProjection<InferId<TDef>, InferState<TDef>> => {
  // Fork-aware projection factory with branch management
};

Practical Example

A TaskGraphProjection for parallel task planning:

import { Projection } from '@magnitude/event-core';
import { Schema } from '@effect/schema';
import type { AppEvent } from '../events.js';

const TaskSchema = Schema.Struct({
  id: Schema.String,
  description: Schema.String,
  status: Schema.Literal('pending', 'in_progress', 'completed')
});

export const TaskGraphProjection = Projection.defineForked<AppEvent>()({
  name: 'TaskGraph',
  state: {
    tasks: Schema.Array(TaskSchema)
  },
  signals: {
    addTask: (task: typeof TaskSchema.Type) => ({ 
      tasks: [task] 
    }),
    updateTaskStatus: (taskId: string, status: string) => ({ 
      tasks: [] // Merged by projection logic
    })
  }
});

Fork Lifecycle Operations

Agents interact with forked projections through three core operations defined in the interface:

Operation Purpose Typical Usage
createFork(parentForkId?) Spawns a new state branch from an optional parent Exploring alternative execution paths
mergeFork(forkId) Integrates a completed fork back into its parent Committing a successful plan variant
abandonFork(forkId) Discards a fork without merging Canceling failed or irrelevant branches

When to use ForkedProjection:

  • Evaluating multiple candidate plans in parallel
  • Implementing backtracking or speculative reasoning
  • Managing hierarchical or recursive task decomposition
  • Running A/B or multi-variant agent behaviors

Projection Type Comparison

Aspect RegularProjection ForkedProjection
State cardinality One state per ID Multiple forks per ID
Branching None Native fork/merge/abandon
Memory footprint Minimal Scales with active forks
API surface define() defineForked()
Source file define.ts defineForked.ts
Best for Linear, temporal state Exploratory, parallel state

Agent Runtime Integration

Both projection types are consumed through Magnitude's surface layer, which provides dependency injection tags for worker initialization. In packages/agent/src/projections/, concrete projections are wired into the agent runtime:

// packages/agent/src/projections/index.ts
import { ChatTitleProjection } from './chatTitle.js';
import { TaskGraphProjection } from './taskGraph.js';
import { GoalProjection } from './goal.js';

export const agentProjections = [
  ChatTitleProjection,
  TaskGraphProjection,
  GoalProjection
] as const;

The runtime automatically manages projection lifecycle, event sourcing, and fork synchronization based on the projection type declared at definition time.

Summary

Frequently Asked Questions

What determines whether to use a regular or forked projection?

Use RegularProjection when your state follows a single timeline without alternatives—conversation history, current goals, or configuration. Use ForkedProjection when agents need to explore multiple possibilities simultaneously, such as evaluating different plans or speculating about outcomes. The overhead of fork management is only justified when branching semantics are required.

How are forked projections garbage collected?

Abandoned forks are dereferenced and eligible for cleanup, though the exact timing depends on the runtime's event sourcing configuration. The abandonFork() operation explicitly signals that a branch will never merge, allowing the projection engine to release associated resources. Merged forks are compacted into their parent state history.

Can a regular projection be converted to forked later?

No—projection type is fixed at definition time through the choice of define() versus defineForked(). However, you can model similar behavior by creating a new forked projection and migrating state through events. The type system enforces this distinction to prevent accidental complexity in simple state cases.

Where are projection implementations tested in the codebase?

Core projection logic is tested in packages/event-core/test/projection/, with separate suites for regular projections (define.test.ts) and forked projections (defineForked.test.ts). These tests verify event sourcing correctness, fork lifecycle operations, and merge semantics under concurrent access patterns.

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 →