# How to Build Stateless Reducer Agents Using the Redux Pattern

> Build stateless reducer agents using the Redux pattern. Treat agent loops as pure functions for easy restarting without losing state or context. Learn how for your projects.

- Repository: [HumanLayer/12-factor-agents](https://github.com/humanlayer/12-factor-agents)
- Tags: best-practices
- Published: 2026-05-19

---

**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](https://github.com/humanlayer/12-factor-agents/blob/main/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`](https://github.com/humanlayer/12-factor-agents/blob/main/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`](https://github.com/humanlayer/12-factor-agents/blob/main/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`](https://github.com/humanlayer/12-factor-agents/blob/main/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`](https://github.com/humanlayer/12-factor-agents/blob/main/packages/create-12-factor-agent/template/src/agent.ts) demonstrates the core loop:

```typescript
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`](https://github.com/humanlayer/12-factor-agents/blob/main/packages/create-12-factor-agent/template/src/state.ts) to maintain statelessness across restarts:

```typescript
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:

```bash
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`](https://github.com/humanlayer/12-factor-agents/blob/main/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`](https://github.com/humanlayer/12-factor-agents/blob/main/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`](https://github.com/humanlayer/12-factor-agents/blob/main/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.