How to Build Stateless Reducer Agents Using the Redux Pattern

Stateless reducer agents treat the agent loop as a pure function that folds a stream of events into an immutable thread, enabling the process to be restarted anywhere without losing state or context.

The humanlayer/12-factor-agents repository formalizes this architecture in Factor 12 (content/factor-12-stateless-reducer.md). By mirroring the Redux pattern, you can build LLM-driven workflows that are predictable, testable, and resilient to process restarts.

Core Architecture

The stateless reducer pattern relies on three fundamental components that separate computation from side effects.

The Thread as Immutable State

A Thread is implemented as a list of Event objects representing every LLM-generated action and its result. According to the source code in packages/create-12-factor-agent/template/src/agent.ts, this structure holds the immutable event stream that is repeatedly fed to the LLM for each decision cycle.

The Reducer Loop

The agentLoop function (found in packages/create-12-factor-agent/template/src/agent.ts) implements the pure reduce step:

  1. Calls the LLM via b.DetermineNextStep() using the current serialized event list
  2. Appends the tool call as a new event
  3. Executes the tool (if deterministic) and appends the tool response as another event
  4. Re-serializes the entire thread and repeats

Each iteration yields a new, larger event list without mutating prior events.

Externalized Persistence

The FileSystemThreadStore class in packages/create-12-factor-agent/template/src/state.ts persists the thread as JSON and a human-readable text version. This guarantees statelessness—the running process contains no mutable variables, while the durable state lives externally on disk or remote storage.

Why This Pattern Is Stateless

Three constraints ensure the agent remains stateless:

  • No mutable in-process variables—The only evolving data structure is the immutable events array passed through each iteration
  • All side-effects are externalized—Writes to .threads on disk occur outside the reducer logic
  • The reducer is pure—Given the same event list, b.DetermineNextStep returns the same next step (assuming deterministic LLM settings)

This design lets you treat the agent exactly like a Redux reducer: input = previous state + action → output = new state.

Implementing a Stateless Reducer Agent

Follow these steps to implement your own stateless reducer agent using the 12-Factor Agents pattern.

Define Domain Events

Create TypeScript interfaces for each tool call (e.g., AddTool, SubtractTool). The repository includes a calculator example that demonstrates how to type these events.

Build the Reducer Function

Implement handleNextStep as a pure function that receives a tool payload, computes the result, and returns an updated thread with a new tool_response event appended. This function must not modify any global state.

Persist State Externally

Integrate FileSystemThreadStore or implement your own storage layer using Redis or SQL. The store must guarantee that the agent can be resumed anywhere by loading the last persisted thread as the single source of truth.

Code Examples

Minimal Calculator Reducer

This example from packages/create-12-factor-agent/template/src/agent.ts demonstrates the core loop:

import { Thread } from "./agent";
import { b } from "../baml_client"; // LLM scaffolding

async function runThread(initial: Thread) {
  let thread = initial;
  while (true) {
    // 1️⃣ Determine next step from LLM
    const next = await b.DetermineNextStep(thread.serializeForLLM());
    thread.events.push({ type: "tool_call", data: next });

    // 2️⃣ Pure reducer step
    switch (next.intent) {
      case "add":
      case "subtract":
      case "multiply":
      case "divide":
        thread = await handleNextStep(next, thread);
        break;
      case "done_for_now":
      case "request_more_information":
        return thread; // exit point
    }
  }
}

Persisting the Thread

Use FileSystemThreadStore from packages/create-12-factor-agent/template/src/state.ts to maintain statelessness across restarts:

import { FileSystemThreadStore } from "./state";

const store = new FileSystemThreadStore();

async function start() {
  const thread = new Thread([]);
  const id = await store.create(thread);        // persist initial empty thread
  const restored = await store.get(id);         // later, resume from disk
  await runThread(restored!);
}

Using the CLI Generator

The create-12-factor-agent package scaffolds a new project with the reducer pattern pre-configured:

npx create-12-factor-agent my-calculator

Summary

  • Stateless reducer agents treat the agent loop as a pure function that processes immutable event streams
  • The Thread structure in packages/create-12-factor-agent/template/src/agent.ts maintains state as an append-only list of events
  • Externalized persistence via FileSystemThreadStore ensures the process can be restarted anywhere without data loss
  • The pattern matches Redux architecture: previous state + action = new state
  • All side effects (disk writes, API calls) occur outside the pure reducer logic

Frequently Asked Questions

What makes the reducer "pure" in this pattern?

The reducer is pure because b.DetermineNextStep and handleNextStep operate only on their inputs, produce no side effects, and return the same output given the same event history. According to the implementation in packages/create-12-factor-agent/template/src/agent.ts, the function receives the current thread, appends new events immutably, and returns the updated thread without modifying any global variables.

How does serialization work for the LLM context?

The serializeForLLM method converts the event list into a compact, XML-like string representation that fits within the LLM's context window. This serialization is deterministic and idempotent, ensuring that the same thread state always produces the same input for b.DetermineNextStep, as implemented in the template's thread handling logic.

Can I use Redis instead of the filesystem store?

Yes. The FileSystemThreadStore in packages/create-12-factor-agent/template/src/state.ts implements a storage interface that you can replace with Redis, PostgreSQL, or any external database. As long as the store persists the complete events array and can reconstruct the Thread object, the agent remains stateless and resumable from any location.

How do I handle non-deterministic LLM outputs?

While the reducer itself is pure, LLM outputs may vary between calls. To maintain determinism, either configure your LLM provider with temperature=0 and fixed seed values, or treat the LLM response as an external input (similar to user input in Redux). The pattern accounts for this by treating the LLM's decision as an event that gets recorded immutably, preserving the history even if re-running the same prompt might yield different results.

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 →