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
- RegularProjection provides single-state, linear state tracking via
Projection.define()inpackages/event-core/src/projection/define.ts - ForkedProjection enables parallel state branches via
Projection.defineForked()inpackages/event-core/src/projection/defineForked.ts - Both types share a common definition schema with signals, but differ in their runtime semantics and fork management capabilities
- The surface layer in
packages/event-core/src/surface/index.tsexposes typed interfaces for both projection families - Agent developers choose between these types based on whether their use case requires linear state evolution or exploratory branching
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →