ReAct and the Birth of Agent Memory: Why Reasoning Is Memory Creation

There are thousands of articles explaining ReAct as “reasoning plus acting.” The model thinks, then acts, then observes, then thinks again. That description is correct but shallow. It misses the deeper architectural contribution — the one that actually explains why ReAct became the foundation for every modern AI agent.

ReAct did not merely combine reasoning with tool use. It introduced explicit working memory into LLM agents. Every thought the model generates updates its internal state. Every observation from the environment writes new information into that state. Every subsequent reasoning step reads from this accumulated memory to make better decisions. The context window becomes a living, evolving memory store that the agent reads from and writes to at every step.

This is the real reason ReAct works. Not because the model can call Wikipedia. Not because it can “think out loud.” But because it maintains a persistent state representation that accumulates knowledge, tracks goals, records failures, and informs future decisions. The tight loop of thought-action-observation is not just a prompting format — it is a memory architecture.

Once I understood ReAct through this lens, the entire lineage of modern agent memory systems — MemGPT, Letta, SimpleMem, Mem0, LangMem — stopped looking like independent innovations and started looking like natural extensions of a single idea: agents need memory, and ReAct was the first paper to give it to them.


The Missing Perspective

Ask any engineer what ReAct introduced, and they will say:

“It combined chain-of-thought reasoning with tool calling in a single loop.”

That answer is technically correct. It is also incomplete in a way that obscures the paper’s most important contribution.

The standard framing:

ReAct = Reasoning + Acting

The more accurate framing:

ReAct = Reasoning + Memory + Acting

Without memory, an agent cannot:

  • Track goals across multiple steps
  • Remember what it already searched for
  • Avoid repeating failed strategies
  • Know what worked in earlier steps
  • Maintain progress toward a complex objective
  • Ground reasoning in previously observed facts
  • Recover from uninformative results

Chain-of-thought alone cannot do any of this. CoT generates a static reasoning trace in one shot — it has no mechanism to update its beliefs based on new information. It reasons from the model’s frozen training-time knowledge, never from runtime observations.

Action-only agents cannot do this either. They call tools and receive observations, but without intermediate reasoning, they have no mechanism to interpret observations, adjust strategy, or maintain goal-directed behavior across many steps.

ReAct’s interleaved format solves both problems simultaneously by creating an evolving memory that accumulates with each step:

graph TD
    subgraph Memory["Agent Working Memory (Context)"]
        direction TB
        M1["Initial state: Question only"]
        M2["+ Thought 1: Goal decomposition"]
        M3["+ Action 1: Search query"]
        M4["+ Observation 1: Retrieved facts"]
        M5["+ Thought 2: Interpret results, update plan"]
        M6["+ Action 2: Follow-up search"]
        M7["+ Observation 2: New facts"]
        M8["+ Thought 3: Synthesize and conclude"]

        M1 --> M2 --> M3 --> M4 --> M5 --> M6 --> M7 --> M8
    end

    M8 --> F[Final Answer]

    style Memory fill:#f5f5f5

At step 1, the agent knows only the question. By step 8, it has accumulated a rich state representation containing decomposed subgoals, search results, interpretations, plan adjustments, and synthesized conclusions. Every token in that context is memory — and the model reads from all of it when generating each new step.


Where Memory Appears Inside ReAct

The paper’s formal notation reveals the memory mechanism explicitly, though it is not framed as such. The key formulation:

The Augmented Action Space

Standard agents operate with action space A (tool calls that change the external environment). ReAct extends this to:

A-hat = A ∪ L

Where L is the space of natural language. A “thought” is an action in L — it does not change the external environment, but it changes the agent’s internal state.

This is the critical distinction. A thought is not passive observation. It is an active state mutation. When the model writes:

“Thought 3: The search for ‘Front Row’ returned a TV show, not the software application. I need to search for ‘Front Row Apple software’ instead.”

This is not decoration. This is the agent:

  1. Reading from memory (the previous observation)
  2. Interpreting what went wrong (search returned wrong entity)
  3. Writing a corrected plan to memory (try a more specific query)
  4. Committing to this interpretation for all future steps

The Context Update Rule

The paper defines the context update as:

c_(t+1) = (c_t, a_t, o_t)

For a thought (where a_t ∈ L), the observation o_t is empty — but the context still grows:

c_(t+1) = (c_t, thought_t)

For an action (where a_t ∈ A), the environment returns an observation:

c_(t+1) = (c_t, action_t, observation_t)

In both cases, the context grows monotonically. Every thought and every observation is permanently appended to the agent’s memory. The policy at time t+1 conditions on the entire accumulated history:

π(a_(t+1) | c_(t+1)) = π(a_(t+1) | o_1, a_1, o_2, a_2, ..., o_t, a_t, o_(t+1))

This is working memory. The model’s behavior at each step is conditioned not just on the question, but on every thought it has had and every observation it has received. The reasoning trace is not cosmetic — it is the state representation that the model uses to make decisions.

A Concrete Example

From the paper’s HotpotQA trajectory:

Question: What is the elevation range for the area that the eastern
sector of the Colorado orogeny extends into?

Thought 1: I need to search Colorado orogeny, find the area that the
eastern sector extends into, then find the elevation range of that area.

Action 1: Search[Colorado orogeny]

Observation 1: The Colorado orogeny was an episode of mountain building...
The eastern sector extends into the High Plains.

Thought 2: The eastern sector extends into the High Plains. I need to
search High Plains and find its elevation range.

Action 2: Search[High Plains]

Observation 2: High Plains refers to one of the two distinct land regions...
The High Plains rise in elevation from around 1,800 to 7,000 ft.

Thought 3: High Plains rise from around 1,800 to 7,000 ft, so the
elevation range is approximately 1,800 to 7,000 ft.

Action 3: Finish[approximately 1,800 to 7,000 ft]

Trace the memory state at each step:

Step Memory Contains New Knowledge Written
After Thought 1 Decomposed subgoals: (1) find area, (2) find elevation Goal structure
After Obs 1 “Eastern sector extends into High Plains” Entity resolution
After Thought 2 Updated plan: search High Plains for elevation Plan refinement
After Obs 2 “1,800 to 7,000 ft” Target fact
After Thought 3 Synthesized answer from accumulated evidence Conclusion

Without Thought 1, the agent has no plan structure to guide retrieval. Without Thought 2, the agent cannot connect “High Plains” (from Observation 1) to the next search query. Without Thought 3, the agent cannot synthesize two observations into a final answer. Each thought reads from accumulated memory and writes a new state that future steps depend on.


Working Memory vs Long-Term Memory

ReAct’s memory has a fundamental limitation: it lives entirely within the context window. When the context fills up, the memory is gone. When a new conversation starts, all accumulated knowledge from the previous interaction is lost.

This is pure working memory — transient, bounded, and volatile. It is the equivalent of holding information “in mind” during a single problem-solving session.

The evolution from ReAct’s working memory to modern agent memory systems follows a clear progression:

graph TD
    subgraph WM["Working Memory (ReAct, 2023)"]
        W1["Context window accumulation"]
        W2["Lost at session end"]
        W3["No persistence"]
        W4["No importance filtering"]
    end

    subgraph STM["Short-Term Memory (MemGPT, 2023)"]
        S1["Main context + overflow buffer"]
        S2["Active memory management"]
        S3["Page in/out from storage"]
        S4["Still session-scoped"]
    end

    subgraph SEM["Semantic Memory (Mem0, 2024)"]
        S5["Extracted facts and preferences"]
        S6["Cross-session persistence"]
        S7["Entity-centric storage"]
        S8["Retrieval by relevance"]
    end

    subgraph LTM["Long-Term Memory (Letta, 2024)"]
        L1["Structured memory blocks"]
        L2["Tool-based memory access"]
        L3["Memory as first-class agent capability"]
        L4["Tiered storage architecture"]
    end

    subgraph PM["Production Memory (SimpleMem, LangMem)"]
        P1["Importance scoring"]
        P2["Compression and summarization"]
        P3["Forgetting and decay"]
        P4["Memory lifecycle management"]
    end

    WM --> STM --> SEM --> LTM --> PM

Comparison of Memory Architectures

System Memory Type Persistence Capacity Filtering Retrieval
ReAct Working memory Session only Context window None (store everything) Sequential scan (attention)
MemGPT Tiered working memory Session + overflow Context + external buffer Page in/out by relevance Explicit memory search tool
Letta Structured memory blocks Persistent across sessions Unlimited (external DB) Block-level management Tool-based retrieval
Mem0 Semantic memory Persistent Vector store Entity extraction Similarity search
LangMem Episodic + semantic Persistent Configurable stores Importance scoring Multi-strategy retrieval
SimpleMem Production memory Persistent + lifecycle Scored and compressed Importance + decay + compression Relevance + recency

The progression is clear: each system builds on ReAct’s insight (agents need memory) while addressing its limitations (bounded capacity, no persistence, no filtering). But the primitive operation — writing state updates during reasoning — remains unchanged from ReAct.


Memory Propagation: How State Evolves During Execution

Understanding exactly how memory propagates through a ReAct trajectory reveals why the pattern is so powerful and why it became the template for all subsequent agent architectures.

The Propagation Pipeline

sequenceDiagram
    participant Q as Question
    participant T as Thought Generator
    participant M as Memory (Context)
    participant A as Action Executor
    participant E as Environment
    participant P as Planner

    Q->>M: Write initial state (question)
    M->>T: Read full context
    T->>M: Write Thought 1 (goal decomposition)
    
    M->>P: Read updated context
    P->>A: Determine action
    A->>E: Execute tool call
    E->>M: Write Observation 1
    
    M->>T: Read full context (question + thought + obs)
    T->>M: Write Thought 2 (interpret + replan)
    
    M->>P: Read updated context
    P->>A: Determine next action
    A->>E: Execute tool call
    E->>M: Write Observation 2
    
    M->>T: Read full context (all accumulated state)
    T->>M: Write Thought 3 (synthesize)
    T->>A: Finish[answer]

Memory Operations

Each step in a ReAct trajectory performs one or more fundamental memory operations:

Memory Accumulation: Every thought and observation appends to the context. The memory grows monotonically — nothing is ever removed in the original formulation.

# Memory accumulation in ReAct
context = [question]

# Step 1: Thought writes to memory
thought_1 = llm.generate(context)  # "I need to search..."
context.append(thought_1)          # Memory grows

# Step 2: Action + observation writes to memory
action_1 = parse_action(thought_1)
observation_1 = environment.execute(action_1)
context.append(f"Action: {action_1}")
context.append(f"Observation: {observation_1}")  # Memory grows again

# Step 3: Next thought reads ALL accumulated memory
thought_2 = llm.generate(context)  # Conditioned on everything above
context.append(thought_2)          # Memory grows further

Memory Retrieval: The transformer’s attention mechanism performs retrieval implicitly. When generating Thought 2, the model attends to all prior tokens — question, Thought 1, Action 1, and Observation 1. This is unstructured retrieval: the model decides which parts of memory are relevant through learned attention patterns.

Memory Mutation (via new thoughts): The model cannot edit previous thoughts, but it can write new thoughts that supersede or refine earlier ones. “Thought 3: My earlier plan was wrong because X. The correct approach is Y.” This is append-only mutation — the old state remains in context, but the new thought signals that the model’s beliefs have changed.

Memory-Informed Planning: Each thought implicitly reads from accumulated memory to determine the next action. The model’s attention over prior observations and thoughts informs what tool to call next and with what parameters.

What Makes This Powerful

The power of ReAct’s memory propagation comes from a property that pure CoT lacks: grounded state updates. In chain-of-thought, every reasoning step is conditioned only on the question and prior reasoning steps — all generated from the model’s frozen training-time knowledge. In ReAct, observations inject new, runtime-specific information into memory. The reasoning at step t+1 is conditioned on facts that did not exist in the model’s weights.

graph LR
    subgraph CoT["Chain-of-Thought Memory"]
        C1[Question] --> C2[Thought 1\nfrom training knowledge]
        C2 --> C3[Thought 2\nfrom training knowledge]
        C3 --> C4[Thought 3\nfrom training knowledge]
        C4 --> CA[Answer]
    end

    subgraph ReAct["ReAct Memory"]
        R1[Question] --> R2[Thought 1\nfrom training knowledge]
        R2 --> R3[Observation 1\nFROM REAL WORLD]
        R3 --> R4[Thought 2\nfrom memory + new facts]
        R4 --> R5[Observation 2\nFROM REAL WORLD]
        R5 --> R6[Thought 3\nfrom full accumulated state]
        R6 --> RA[Answer]
    end

    style CoT fill:#ffebee
    style ReAct fill:#e8f5e9

CoT hallucinates because its memory contains only model-generated content — there is no external grounding. ReAct’s memory contains both model-generated reasoning and externally-sourced observations. The hallucination rate drops from 56% (CoT on HotpotQA) to effectively 0% for ReAct because every factual claim is grounded in a retrieved observation stored in memory.


What Should Actually Be Stored?

ReAct stores everything. Every thought, every action, every observation — all appended to context without filtering. For short tasks (3-7 steps), this works. For longer tasks, it creates problems:

  • Context fills with irrelevant observations from failed searches
  • Early thoughts become stale as the plan evolves
  • Noise accumulates, degrading reasoning quality
  • Token cost grows linearly with trajectory length

The question that modern memory systems answer — and that ReAct does not — is: what deserves to survive?

Signal vs Noise in Agent Memory

Worth Storing (Signal) Not Worth Storing (Noise)
Confirmed facts from observations Failed search queries that returned nothing
Active subgoals and their status Superseded plan fragments
Key entities and their relationships Verbose observation text (summarize instead)
Constraints discovered during execution Intermediate arithmetic steps
Tool outputs that inform future decisions Repeated information across observations
Error patterns (what failed and why) Formatting tokens and boilerplate
User preferences and requirements Standard tool call syntax
Execution history (what was tried) Full raw API responses

The Filtering Problem

In production, an agent executing 50+ tool calls generates thousands of tokens of accumulated context. Most of it is noise by the time the agent reaches step 40. The challenge is identifying what to keep, what to compress, and what to discard — without losing information that later steps might need.

This is the problem that every post-ReAct memory system addresses:

  • MemGPT pages information in and out of the active context based on relevance
  • Letta structures memory into typed blocks with explicit read/write semantics
  • Mem0 extracts semantic facts and stores them in a queryable format
  • SimpleMem scores importance, compresses low-value entries, and implements decay

All of them start from ReAct’s insight (the agent needs memory) and add what ReAct lacks (memory management).


Connection to SimpleMem

ReAct’s memory philosophy is: store everything, let attention sort it out.

SimpleMem’s philosophy is: store deliberately, score importance, compress aggressively, forget intentionally.

The gap between these two positions defines the evolution from research prototype to production memory system.

What SimpleMem Adds Beyond ReAct

flowchart TD
    subgraph ReAct["ReAct Memory Model"]
        R1[Observation arrives] --> R2[Append to context]
        R2 --> R3[Hope attention finds it later]
    end

    subgraph SimpleMem["SimpleMem Memory Model"]
        S1[Observation arrives] --> S2[Score importance]
        S2 --> S3{Score > threshold?}
        S3 -->|Yes| S4[Store in appropriate tier]
        S3 -->|No| S5[Discard or compress]
        S4 --> S6[Assign metadata + TTL]
        S6 --> S7[Available for retrieval]
        S7 --> S8[Periodic consolidation]
        S8 --> S9[Decay low-access entries]
        S9 --> S10[Compress old entries]
        S10 --> S11[Archive or forget]
    end

    style ReAct fill:#fff3e0
    style SimpleMem fill:#e8f5e9

Memory Lifecycle Comparison

Operation ReAct SimpleMem
Write Always (append all) Conditional (importance gate)
Read Implicit (attention over full context) Explicit (retrieval by query)
Update Append-only (write new thought) In-place (modify existing entry)
Delete Never Decay + TTL + explicit forget
Compress Never Summarize old entries
Prioritize Recency bias (attention decay) Importance score + recency + access frequency
Persist Session only (context window) Cross-session (external store)

The Importance Scoring Insight

SimpleMem’s key addition is asking: “Given everything the agent observed and reasoned about, what is actually important enough to remember?”

Importance scoring in practice:

def score_importance(memory_entry: str, context: dict) -> float:
    """
    Score how important a memory entry is for future reasoning.
    
    High importance:
    - Facts that resolve ambiguity
    - Constraints that limit future actions
    - Entity relationships central to the task
    - Error patterns (what failed and why)
    
    Low importance:
    - Intermediate computations already superseded
    - Verbose tool output (summarize instead)
    - Standard observations with no novel information
    - Duplicate information already stored
    """
    factors = {
        "novelty": compute_novelty(memory_entry, context["existing_memories"]),
        "task_relevance": compute_relevance(memory_entry, context["active_goals"]),
        "information_density": compute_density(memory_entry),
        "future_utility": estimate_future_use(memory_entry, context["remaining_plan"]),
    }
    
    return weighted_sum(factors)

This is the bridge from ReAct to production: not just “can the agent remember?” but “should the agent remember this specific thing, and for how long?”


Production Memory Architecture

A production agent memory system builds on ReAct’s working memory with persistence, filtering, tiering, and lifecycle management:

flowchart TD
    subgraph Agent["Agent Execution Loop"]
        LLM[LLM Reasoning Engine]
        PLAN[Planner]
        ACT[Action Executor]
    end

    subgraph MemoryManager["Memory Manager"]
        direction TB
        GATE[Importance Gate]
        SCORE[Importance Scorer]
        COMP[Compressor]
        SUMM[Summarizer]
        GC[Garbage Collector]
        REFL[Reflection Engine]
        CONS[Memory Consolidation]
    end

    subgraph Storage["Memory Storage Tiers"]
        WM["Working Memory\n(active context window)"]
        STM["Short-Term Memory\n(session buffer, 1-hour TTL)"]
        LTM["Long-Term Memory\n(persistent store)"]
        VS["Vector Store\n(semantic retrieval)"]
        KG["Knowledge Graph\n(entity relationships)"]
        META["Structured Metadata\n(scores, timestamps, access counts)"]
    end

    LLM -->|"Thought (state update)"| GATE
    ACT -->|"Observation"| GATE
    GATE --> SCORE
    SCORE -->|"High importance"| WM
    SCORE -->|"Medium importance"| STM
    SCORE -->|"Low importance"| COMP
    COMP --> STM

    WM -->|"Read (every step)"| LLM
    STM -->|"Retrieve (on demand)"| LLM
    LTM -->|"Retrieve (by query)"| LLM

    CONS -->|"Periodic"| STM
    CONS -->|"Consolidate"| LTM
    GC -->|"Decay + prune"| STM
    REFL -->|"Reflect on trajectory"| LTM
    SUMM -->|"Compress old entries"| LTM

    LTM --> VS
    LTM --> KG
    LTM --> META

Component Responsibilities

Component Role Trigger
Importance Gate Decide whether to store, compress, or discard Every new thought/observation
Importance Scorer Assign numeric score based on novelty, relevance, density On write
Compressor Reduce verbose entries to essential information Low-score entries
Summarizer Collapse multiple related entries into one Periodic consolidation
Garbage Collector Remove entries below score threshold or past TTL Scheduled
Reflection Engine Generate meta-observations about patterns End of task / periodic
Memory Consolidation Move short-term entries to long-term storage Session end / threshold

Memory Lifecycle

Every memory entry in a production agent system moves through a lifecycle:

stateDiagram-v2
    [*] --> Observed: New thought/observation
    Observed --> Scored: Importance evaluation
    Scored --> Active: High score
    Scored --> Compressed: Medium score
    Scored --> Discarded: Low score
    
    Active --> Retrieved: Agent queries memory
    Retrieved --> Active: Access count++
    Active --> Stale: No access + time decay
    Stale --> Compressed: Consolidation pass
    Compressed --> Archived: Further decay
    Archived --> Retrieved: Rare but possible
    Archived --> Forgotten: TTL expired
    
    Active --> Updated: New conflicting info
    Updated --> Active: Score refreshed
    
    Discarded --> [*]
    Forgotten --> [*]

Lifecycle Operations in Detail

Observe: A new piece of information enters the system — either from the agent’s own reasoning (thought) or from the external environment (observation).

Score: The importance scorer evaluates the entry. Scoring factors include:

  • Novelty (does this tell us something we do not already know?)
  • Task relevance (does this relate to active goals?)
  • Information density (ratio of useful facts to total tokens)
  • Future utility (will downstream reasoning need this?)
  • Contradiction (does this conflict with existing memory — conflicts are high-value)

Store: High-score entries go to working memory (immediately available). Medium-score entries go to short-term memory (retrievable on demand). Low-score entries are either compressed or discarded.

Compress: Verbose entries are reduced to their essential content. A 200-token observation from Wikipedia might be compressed to a 30-token fact. The original is discarded; the compressed version persists.

Archive: Entries that have not been accessed recently and whose scores have decayed below a threshold move to long-term storage. They remain retrievable but are not automatically included in the agent’s active context.

Retrieve: When the agent needs information, the memory manager searches across tiers. Working memory is always visible. Short-term and long-term memory are searched by relevance to the current reasoning state.

Update: When new information contradicts an existing memory entry, the entry is updated in place (or the old entry is marked stale and a new one created). Contradiction detection is itself a memory operation.

Forget: Entries whose importance has decayed below threshold and whose TTL has expired are permanently removed. Forgetting is not failure — it is essential for maintaining a high signal-to-noise ratio in memory.


Engineering Implementation

A production-grade memory system for an AI agent, building from ReAct’s primitive operations to a full lifecycle-managed architecture:

from dataclasses import dataclass, field
from typing import Optional
from datetime import datetime, timedelta
import hashlib


@dataclass
class MemoryEntry:
    content: str
    entry_type: str  # "thought", "observation", "fact", "plan", "reflection"
    importance: float
    created_at: datetime
    last_accessed: datetime
    access_count: int = 0
    ttl: Optional[timedelta] = None
    source_step: int = 0
    compressed: bool = False
    tags: list[str] = field(default_factory=list)
    
    @property
    def id(self) -> str:
        return hashlib.sha256(
            f"{self.content}:{self.created_at.isoformat()}".encode()
        ).hexdigest()[:16]
    
    @property
    def is_expired(self) -> bool:
        if self.ttl is None:
            return False
        return datetime.now() - self.created_at > self.ttl
    
    @property
    def effective_importance(self) -> float:
        hours_since_access = (
            datetime.now() - self.last_accessed
        ).total_seconds() / 3600
        decay = 0.95 ** hours_since_access
        recency_bonus = min(self.access_count * 0.05, 0.3)
        return self.importance * decay + recency_bonus


class ImportanceScorer:
    def __init__(self, llm_client):
        self.llm = llm_client
    
    def score(
        self,
        entry: str,
        active_goals: list[str],
        existing_memories: list[MemoryEntry],
    ) -> float:
        novelty = self._compute_novelty(entry, existing_memories)
        relevance = self._compute_relevance(entry, active_goals)
        density = self._compute_density(entry)
        
        return (0.4 * relevance) + (0.35 * novelty) + (0.25 * density)
    
    def _compute_novelty(
        self, entry: str, existing: list[MemoryEntry]
    ) -> float:
        if not existing:
            return 1.0
        
        existing_content = " ".join(m.content for m in existing[-20:])
        overlap = len(
            set(entry.lower().split()) & set(existing_content.lower().split())
        )
        total = len(set(entry.lower().split()))
        
        if total == 0:
            return 0.0
        return 1.0 - (overlap / total)
    
    def _compute_relevance(
        self, entry: str, goals: list[str]
    ) -> float:
        if not goals:
            return 0.5
        
        goal_terms = set()
        for goal in goals:
            goal_terms.update(goal.lower().split())
        
        entry_terms = set(entry.lower().split())
        overlap = len(entry_terms & goal_terms)
        
        return min(overlap / max(len(goal_terms), 1), 1.0)
    
    def _compute_density(self, entry: str) -> float:
        words = entry.split()
        if len(words) == 0:
            return 0.0
        
        stopwords = {"the", "a", "an", "is", "are", "was", "were", "in", "on", "at"}
        content_words = [w for w in words if w.lower() not in stopwords]
        
        return len(content_words) / len(words)


class MemoryCompressor:
    def __init__(self, llm_client):
        self.llm = llm_client
    
    def compress(self, entry: MemoryEntry) -> MemoryEntry:
        if len(entry.content.split()) <= 30:
            return entry
        
        compressed_content = self._summarize(entry.content)
        
        return MemoryEntry(
            content=compressed_content,
            entry_type=entry.entry_type,
            importance=entry.importance * 0.9,
            created_at=entry.created_at,
            last_accessed=entry.last_accessed,
            access_count=entry.access_count,
            ttl=entry.ttl,
            source_step=entry.source_step,
            compressed=True,
            tags=entry.tags,
        )
    
    def _summarize(self, content: str) -> str:
        response = self.llm.generate(
            f"Compress to essential facts only (max 30 words): {content}"
        )
        return response.strip()


class WorkingMemory:
    """Immediate context — always visible to the agent."""
    
    def __init__(self, max_entries: int = 20):
        self.entries: list[MemoryEntry] = []
        self.max_entries = max_entries
    
    def add(self, entry: MemoryEntry):
        self.entries.append(entry)
        if len(self.entries) > self.max_entries:
            self._evict_lowest()
    
    def read_all(self) -> list[MemoryEntry]:
        for entry in self.entries:
            entry.last_accessed = datetime.now()
            entry.access_count += 1
        return self.entries
    
    def _evict_lowest(self):
        self.entries.sort(key=lambda e: e.effective_importance, reverse=True)
        evicted = self.entries[self.max_entries:]
        self.entries = self.entries[:self.max_entries]
        return evicted


class LongTermMemory:
    """Persistent storage — retrieved on demand."""
    
    def __init__(self):
        self.entries: list[MemoryEntry] = []
    
    def store(self, entry: MemoryEntry):
        self.entries.append(entry)
    
    def retrieve(self, query: str, top_k: int = 5) -> list[MemoryEntry]:
        scored = []
        query_terms = set(query.lower().split())
        
        for entry in self.entries:
            if entry.is_expired:
                continue
            entry_terms = set(entry.content.lower().split())
            relevance = len(query_terms & entry_terms) / max(len(query_terms), 1)
            scored.append((relevance * entry.effective_importance, entry))
        
        scored.sort(key=lambda x: x[0], reverse=True)
        
        results = [entry for _, entry in scored[:top_k]]
        for entry in results:
            entry.last_accessed = datetime.now()
            entry.access_count += 1
        
        return results
    
    def garbage_collect(self, min_importance: float = 0.1):
        self.entries = [
            e for e in self.entries
            if not e.is_expired and e.effective_importance >= min_importance
        ]


class MemoryManager:
    """
    Production memory manager implementing the full lifecycle.
    Extends ReAct's append-only context with importance scoring,
    tiered storage, compression, and decay.
    """
    
    def __init__(self, llm_client, config: dict = None):
        config = config or {}
        self.scorer = ImportanceScorer(llm_client)
        self.compressor = MemoryCompressor(llm_client)
        self.working = WorkingMemory(
            max_entries=config.get("working_memory_size", 20)
        )
        self.long_term = LongTermMemory()
        self.active_goals: list[str] = []
        self.importance_threshold = config.get("importance_threshold", 0.3)
        self.compression_threshold = config.get("compression_threshold", 0.5)
        self.step_counter = 0
    
    def observe(self, content: str, entry_type: str = "observation") -> Optional[MemoryEntry]:
        """Process a new thought or observation through the importance gate."""
        self.step_counter += 1
        
        score = self.scorer.score(
            content, self.active_goals, self.working.entries
        )
        
        if score < self.importance_threshold:
            return None
        
        entry = MemoryEntry(
            content=content,
            entry_type=entry_type,
            importance=score,
            created_at=datetime.now(),
            last_accessed=datetime.now(),
            source_step=self.step_counter,
            ttl=self._assign_ttl(score),
            tags=self._extract_tags(content),
        )
        
        if score >= self.compression_threshold:
            self.working.add(entry)
        else:
            compressed = self.compressor.compress(entry)
            self.working.add(compressed)
        
        return entry
    
    def retrieve_context(self, query: str = "") -> str:
        """Build context string from working + relevant long-term memory."""
        parts = []
        
        working_entries = self.working.read_all()
        if working_entries:
            parts.append("=== Active Memory ===")
            for entry in working_entries:
                parts.append(f"[{entry.entry_type}] {entry.content}")
        
        if query:
            long_term_entries = self.long_term.retrieve(query, top_k=5)
            if long_term_entries:
                parts.append("\n=== Retrieved Memory ===")
                for entry in long_term_entries:
                    parts.append(f"[{entry.entry_type}] {entry.content}")
        
        return "\n".join(parts)
    
    def consolidate(self):
        """Move stale working memory to long-term storage."""
        evicted = []
        remaining = []
        
        for entry in self.working.entries:
            if entry.effective_importance < self.compression_threshold:
                compressed = self.compressor.compress(entry)
                self.long_term.store(compressed)
                evicted.append(entry)
            else:
                remaining.append(entry)
        
        self.working.entries = remaining
        self.long_term.garbage_collect()
        
        return len(evicted)
    
    def set_goals(self, goals: list[str]):
        """Update active goals — affects importance scoring."""
        self.active_goals = goals
    
    def reflect(self, trajectory_summary: str) -> MemoryEntry:
        """Generate a reflection entry from the current trajectory."""
        entry = MemoryEntry(
            content=f"Reflection: {trajectory_summary}",
            entry_type="reflection",
            importance=0.8,
            created_at=datetime.now(),
            last_accessed=datetime.now(),
            source_step=self.step_counter,
            tags=["reflection", "meta"],
        )
        self.long_term.store(entry)
        return entry
    
    def _assign_ttl(self, importance: float) -> Optional[timedelta]:
        if importance >= 0.8:
            return None  # No expiry for high-importance entries
        elif importance >= 0.5:
            return timedelta(hours=24)
        else:
            return timedelta(hours=1)
    
    def _extract_tags(self, content: str) -> list[str]:
        tags = []
        if any(w in content.lower() for w in ["error", "failed", "wrong"]):
            tags.append("error")
        if any(w in content.lower() for w in ["plan", "goal", "next"]):
            tags.append("planning")
        if any(w in content.lower() for w in ["found", "result", "answer"]):
            tags.append("finding")
        return tags

Using the Memory Manager in a ReAct Loop

async def react_with_memory(
    question: str,
    tools: dict,
    llm_client,
    max_steps: int = 15,
) -> str:
    memory = MemoryManager(llm_client)
    memory.set_goals([question])
    
    memory.observe(f"Task: {question}", entry_type="thought")
    
    for step in range(max_steps):
        context = memory.retrieve_context(query=question)
        
        response = await llm_client.generate(
            system="You are a reasoning agent. Generate either a Thought or an Action.",
            user=f"Context:\n{context}\n\nStep {step + 1}:",
        )
        
        if response.startswith("Thought:"):
            thought_content = response[len("Thought:"):].strip()
            memory.observe(thought_content, entry_type="thought")
        
        elif response.startswith("Action:"):
            action_content = response[len("Action:"):].strip()
            tool_name, args = parse_action(action_content)
            
            if tool_name == "Finish":
                return args
            
            observation = await tools[tool_name].execute(args)
            memory.observe(
                f"Called {tool_name}({args}) -> {observation}",
                entry_type="observation",
            )
        
        if step % 5 == 4:
            memory.consolidate()
    
    memory.reflect(f"Failed to answer '{question}' in {max_steps} steps")
    return "Unable to determine answer within step limit."

How Modern Agent Frameworks Handle Memory

Every major agent framework has adopted and extended ReAct’s memory primitive. The implementations vary, but the core insight remains: agents need persistent, managed state that evolves during execution.

Framework Comparison

Framework Memory Model Persistence Management Strategy
LangGraph State dict passed between nodes Graph execution lifetime Explicit state schema; checkpointing
CrewAI Shared memory between agents Task lifetime Short-term (context) + long-term (external) + entity memory
OpenAI Agents Conversation context + tool results Conversation lifetime Token-limited context; no explicit management
Claude Extended thinking + conversation Conversation lifetime Internal reasoning (not visible to user) + visible context
Google ADK Session state + memory services Configurable Tool-based memory access; explicit store/retrieve
Microsoft AutoGen Message history between agents Conversation lifetime Teachability plugin for cross-session
Letta Core memory blocks + archival Persistent (database) Agent manages own memory via tools
MemGPT Main context + recall storage Persistent Virtual memory with page-in/page-out

LangGraph: Explicit State as Memory

LangGraph makes the memory graph-shaped: state is a typed dictionary that flows between processing nodes. Each node can read and write specific state fields.

from langgraph.graph import StateGraph
from typing import TypedDict, Annotated

class AgentState(TypedDict):
    messages: list         # Working memory (conversation)
    findings: list         # Accumulated facts
    current_plan: str      # Active plan
    search_history: list   # What was already tried
    step_count: int        # Execution tracking

def reasoning_node(state: AgentState) -> AgentState:
    # Read from memory (all state fields visible)
    # Write to memory (return updated state)
    return {"current_plan": updated_plan, "step_count": state["step_count"] + 1}

Letta: Memory as Agent Capability

Letta’s key innovation is making memory management a tool that the agent itself calls — the agent decides what to remember:

# The agent can call these memory tools during execution:
# core_memory_append(key, content)   - write to persistent memory
# core_memory_replace(key, old, new) - update existing memory
# archival_memory_insert(content)    - store in long-term archive
# archival_memory_search(query)      - retrieve from archive
# conversation_search(query)         - search past conversations

This is the logical endpoint of ReAct’s insight: if thoughts are memory writes, then memory management should itself be an action the agent can take deliberately.

The Common Thread

Despite implementation differences, every framework shares the same primitive that ReAct introduced:

  1. State accumulates during execution (thoughts, observations, results)
  2. Future reasoning conditions on accumulated state
  3. The agent’s behavior changes based on what it has seen and thought

The evolution is in how that state is managed: from ReAct’s unmanaged append-only context, to MemGPT’s virtual memory, to Letta’s self-managed memory, to SimpleMem’s lifecycle-managed production memory.


My Engineering Perspective

One of the biggest insights I took away from ReAct was not that the model could call external tools. Tool calling is straightforward engineering — parse an action, call an API, return the result. What changed my thinking was the realization that every reasoning step modifies the agent’s future behavior.

When the model writes “Thought 2: The search for Front Row returned a TV show, not the software application,” it is not just generating text. It is updating its internal state in a way that causally determines what happens next. Without that thought in memory, the next action would be random or repetitive. With that thought, the next action is informed and corrective.

This means reasoning is not just inference. It is memory creation.

Every thought token generated by a ReAct agent is simultaneously:

  • An inference step (computing what to think)
  • A memory write (storing that thought for future use)
  • A state transition (changing the distribution over future actions)

Once I viewed ReAct through that lens, the entire subsequent literature on agent memory became legible as a coherent research program rather than disconnected innovations:

  • MemGPT asks: “What if the memory could overflow beyond the context window?”
  • Letta asks: “What if the agent could deliberately manage its own memory?”
  • Mem0 asks: “What if we extracted structured facts from the memory stream?”
  • SimpleMem asks: “What if we scored importance and let irrelevant memories decay?”
  • LangMem asks: “What if we could consolidate episodic memory into semantic knowledge?”

These are all natural questions once you accept the premise that ReAct established: agents are memory-driven systems. The reasoning loop is not a chain of thoughts — it is a memory construction and retrieval pipeline that happens to use natural language as its encoding format.

Modern AI agents are no longer just tool callers. They are memory-driven systems whose behavior is determined not by a static prompt, but by an evolving internal state that accumulates through experience. ReAct was the first paper to make this architecture explicit. Everything built since is elaboration on that foundation.


Key Takeaways

Chain-of-thought taught LLMs to reason. It demonstrated that intermediate reasoning steps before the answer dramatically improve accuracy on multi-step tasks. But CoT reasons in isolation — it never touches the external world, never updates its beliefs based on new information, and operates from frozen training-time knowledge.

ReAct taught LLMs to remember while reasoning. By interleaving thoughts with grounded observations, it created an evolving memory that accumulates runtime knowledge. Each step reads from and writes to this memory, producing goal-directed behavior that adapts to what the agent discovers.

SimpleMem teaches agents what is worth remembering. It introduces importance scoring, decay, compression, and lifecycle management — transforming ReAct’s unbounded, unfiltered memory into a production-grade system that maintains high signal-to-noise over arbitrarily long task horizons.

That progression — reason → remember → curate — represents the evolution of modern AI agents. Each step builds on the previous one. Each addresses a limitation of its predecessor. And all three start from the same primitive: an LLM generating tokens that serve simultaneously as reasoning and as memory.

The next time you implement an agent system, ask yourself not just “what tools can it call?” but “what does it remember, how does it remember it, and how does that memory shape its future behavior?” That question — more than tool selection, more than prompt engineering, more than model choice — is what determines whether your agent can handle complex, multi-step tasks reliably.

ReAct answered it first. The rest of the field has been elaborating on that answer ever since.