Agent Memory and State Management: Short-Term/Long-Term Memory and Context Compression
메뉴

AI Agent Engineering

Agent Memory and State Management: Short-Term/Long-Term Memory and Context Compression

Explore memory architecture and state schema design that determine the persistence and reasoning quality of AI agents, short-term/long-term memory integration, and context window compression techniques.

Agent Memory and State Management: Short-Term/Long-Term Memory and Context Compression hero image

This post is Part 2 of the 'AI Agent Engineering' series. Moving beyond simple conversation history passing, we dive deep into state schema design, short-term/long-term memory architectures, and context window compression techniques required to reliably execute complex multi-step tasks.


1. Agent State Schema Design: Pydantic v2 & TypedDict

Large Language Models (LLMs) themselves are merely stateless functions. Given the same prompt and options, they produce independent token outputs each time and cannot remember previous calls or tool execution results on their own.

Therefore, the core of an AI Agent runtime lies in: "How to consistently define, maintain immutability for, and pass the State inside the agent Graph?"

While the LLM itself is stateless, the agent runtime is a stateful state transition system. At each state transition, it deterministically merges the previous state and node outputs through a Reducer function.

Three Core Principles of State Schema Design

  1. Single Source of Truth: The State object passing through agent nodes must be the sole repository containing all information about the task in progress (conversation history, tool results, sub-task success status, user metadata).
  2. Explicit Transition Rules (Reducer Pattern): When a node updates the state, it must explicitly define whether to overwrite, append to a list, or merge existing values.
  3. Strict Runtime Type Validation: Type validation based on Pydantic v2 or TypedDict is essential to prevent invalid data from being passed between nodes.

TypedDict vs Pydantic v2 Comparison & Real-World Implementation

  • TypedDict: A Python built-in type hinting mechanism with near-zero overhead. Advantageous for high-speed data transfer between nodes and Reducer type annotations within agent graphs (e.g., LangGraph).
  • Pydantic v2: Provides robust runtime data validation powered by C++ engine (pydantic-core), custom validators, and data serialization/deserialization features (JSON Schema, Redis/RDBMS Persistence). Crucial for validating agent state at API boundaries or external gateways.
FeatureTypedDictPydantic v2
Primary PurposeBuilt-in type hinting & lightweight data passing between internal graph nodesRuntime data validation, data serialization, and schema enforcement
Validation TimingNo runtime validation (Static Type Checker only)Real-time runtime validation (pydantic-core C++ engine)
Performance OverheadZero Overhead (Pure Python Dict)Microsecond Level (High-performance Rust/C++)
Reducer CompatibilityAnnotated standard in graph runtimes like LangGraphAPI Gateway I/O and DB Persistence serialization models
Recommended UsageAgent Graph Internal StateExternal Gateway, API Bounds, Persistence Engine
Key FeaturesKey-Value type declaration, IDE autocomplete supportAutomatic JSON Schema generation, Validators, Default Factories

Here is a production-grade agent state schema example combining TypedDict and Pydantic v2:

from typing import Annotated, Any, Dict, List, Literal, Optional, TypedDictfrom pydantic import BaseModel, Field, ConfigDict class ChatMessage(BaseModel):    """Single message schema"""    role: Literal["system", "user", "assistant", "tool"]    content: str    name: Optional[str] = None    tool_call_id: Optional[str] = None    metadata: Dict[str, Any] = Field(default_factory=dict) def add_messages(left: List[ChatMessage], right: List[ChatMessage]) -> List[ChatMessage]:    """Reducer function to safely append message lists"""    return left + right # 1. TypedDict-based Graph State Schema (for graph runtimes such as LangGraph)class GraphAgentState(TypedDict):    session_id: str    user_id: str    messages: Annotated[List[ChatMessage], add_messages]    current_step: int    max_steps: int    scratchpad: Dict[str, Any]    is_completed: bool # 2. Pydantic v2-based Runtime API Boundary Validation Modelclass AgentStateModel(BaseModel):    model_config = ConfigDict(arbitrary_types_allowed=True)     session_id: str = Field(description="Unique session identifier for conversation & execution")    user_id: str = Field(description="User ID")    messages: List[ChatMessage] = Field(        default_factory=list, description="Buffer of conversation history and tool execution messages"    )    current_step: int = Field(default=0, ge=0, description="Current loop step count")    max_steps: int = Field(default=15, gt=0, description="Maximum allowed loop steps")    scratchpad: Dict[str, Any] = Field(        default_factory=dict, description="Scratchpad for temporary data sharing between nodes"    )    is_completed: bool = Field(default=False, description="Whether the task is finished")

2. Short-Term Memory vs Long-Term Memory Architecture

Just as the human brain is divided into working memory, episodic memory, and semantic memory, production AI agents feature a memory hierarchy structured by role and volatility.

Memory Hierarchy State Diagram

Conversation context exceeding thresholds in the short-term memory buffer undergoes dynamic summarization and branches off to episodic memory (Vector DB) and semantic memory (Fact Base / RDBMS) for permanent storage.

Detailed Memory Hierarchy Comparison Matrix (Working vs Episodic vs Semantic vs Procedural)

Memory TypeLifecycleStorage MediumRetrieval MechanismPrimary Contents
Working/Short-Term Memory
(Working Memory)
Session-level
(Volatile)
In-Memory
(Redis, RAM Buffer)
Direct Array Indexing / Sliding WindowCurrent loop conversation history, active Tool Output, Scratchpad
Episodic Memory
(Episodic Memory)
Long-term
(Persistent)
Vector Store
(ChromaDB, Pinecone)
Vector Similarity Search (COS / L2)Past success/failure task execution traces, error recovery experiences
Semantic Memory
(Semantic Memory)
Long-term
(Persistent)
RDBMS, Graph DB,
Document Store
Key-Value / RAG / Knowledge Graph QueryUser profiles, domain ontologies, high-level factual data
Procedural Memory
(Procedural Memory)
Immutable
(Fixed)
System Prompt,
Code Engine
Direct Prompt Injection / Schema BindingAgent Persona, Tool Schema Definitions, SOP rules

Applying Experience Replay (Episodic Memory Recall) allows the agent to search for past successful execution traces that overcame similar errors when executing complex data transformations or API calls, injecting them as few-shot examples. This reduces tool compatibility error rates by more than 40% compared to zero-shot prompting.

Long-Term Memory Integration Mechanisms (RAG & Experience Replay)

Long-term memory goes beyond simple RAG (Retrieval-Augmented Generation) document retrieval. Advanced agent engineering utilizes "episodic memory recall" techniques:

  1. Similarity Search: Upon receiving user input, the system searches the Vector Store for execution traces of the most similar past tasks.
  2. In-Context Few-Shot Injection: Retrieved past success cases are inserted as few-shot examples into the system prompt, significantly reducing hallucinations and boosting tool call accuracy.

3. Context Window Limits & Context Compression Strategies

Although LLM context windows have expanded dramatically to 128k or 1M+ tokens, blindly retaining massive context remains a major bottleneck in production.

Indiscriminately preserving huge context windows increases API costs and causes the "Lost in the Middle" phenomenon—where key information in the middle of the prompt is forgotten—greatly degrading the agent's intent comprehension performance.

Three Major Issues with Retaining Massive Context

  1. Surging Cost and Latency: API costs and Time-to-First-Token (TTFT) increase linearly or quadratically in proportion to the input token count.
  2. Lost in the Middle: LLM attention mechanisms frequently miss critical instructions or tool execution results positioned in the middle of long prompts.
  3. Context Drift: As conversations lengthen, initial system instructions and user goals become buried and distorted under accumulated tool-call payload messages.
  1. subgraph
  2. System Prompt
  3. Message - (Includes Heavy JSON Tool Result)
  4. Lost in the Middle Area (Attention Decay)
  5. User Query
  6. end
  7. System Prompt
  8. Condensed Summary / Long-term Memories
  9. Sliding Window (Latest N Messages)
  10. User Query

Context Compression Pipeline Sequence Flow

This sequence illustrates message pruning, token estimation, dynamic summarization, and long-term memory flushing executed when an agent runtime receives a user query to assemble the final LLM prompt window.

Comparison of 3 Core Context Compression Techniques

TechniqueTriggerCore MechanismToken Reduction EffectKey Trade-off / Caution
1. Tool Output PruningImmediately upon receiving messageTruncates long JSON/HTML/Log data in older tool outputs, leaving only key headersUp to 80%–90% reductionLLM cannot re-reference full historical details of older tool outputs
2. Dynamic SummarizationWhen token budget exceeds 80%Uses a lightweight LLM (e.g. GPT-4o-mini) to condense older messages into summaries and transfer facts to LTMContinuous 50%–70% compressionSubtle context or nuanced details may be lost during summarization
3. Sliding WindowOn every loop iterationRetains top system anchor and latest N messages, discarding/moving intermediate messagesGuaranteed Upper LimitContext prior to latest N messages is completely lost (must combine with summarization)

4. Hands-on Implementation: Python-Based Window Memory & State Compression Pipeline

Below is the architecture and production-grade implementation of AgentMemoryManager built with Pydantic v2 and Python asyncio. It handles token budget-based sliding windows, automatic tool output truncation, LLM-driven summarization, and memory flushing.

Memory Manager Pipeline Flowchart

In production environments, when implementing estimate_tokens, it is recommended to bind the tiktoken library to accurately parse BPE token counts for your specific model rather than using character-count approximations.

import asynciofrom typing import List, Dict, Any, Optional, Literalfrom pydantic import BaseModel, Field class ChatMessage(BaseModel):    """Message data model"""    role: Literal["system", "user", "assistant", "tool"] = Field(..., description="Sender role")    content: str = Field(..., description="Message body")    name: Optional[str] = Field(None, description="Tool name or author")    tool_call_id: Optional[str] = Field(None, description="Tool call identifier") class MemoryConfig(BaseModel):    """Memory manager configuration model"""    max_token_budget: int = Field(default=4000, description="Maximum allowed token budget")    sliding_window_size: int = Field(default=10, description="Number of recent messages to preserve in sliding window")    tool_pruning_threshold: int = Field(default=150, description="Character threshold to truncate older tool outputs")    summary_trigger_ratio: float = Field(default=0.8, description="Token budget ratio to trigger summarization") class AgentMemoryManager:    """    Memory manager responsible for preventing token budget overflow    and managing context compression.    """    def __init__(self, config: MemoryConfig) -> None:        self.config: MemoryConfig = config        self.summary_buffer: str = ""     def estimate_tokens(self, text: str) -> int:        """Estimate token count (In production, tiktoken is recommended; uses character approximation here)"""        if not text:            return 0        return max(1, len(text) // 3)     def prune_tool_outputs(self, messages: List[ChatMessage]) -> List[ChatMessage]:        """Truncate large historical tool execution results to preserve token budget"""        pruned_messages: List[ChatMessage] = []        for i, msg in enumerate(messages):            # Preserve tool outputs in the latest 3 messages; prune older ones            if msg.role == "tool" and len(messages) - i > 3:                if len(msg.content) > self.config.tool_pruning_threshold:                    truncated_content = (                        msg.content[: self.config.tool_pruning_threshold]                        + f"\n... [Tool Output Truncated: Total Length {len(msg.content)} chars]"                    )                    pruned_messages.append(                        ChatMessage(                            role=msg.role,                            content=truncated_content,                            name=msg.name,                            tool_call_id=msg.tool_call_id,                        )                    )                    continue            pruned_messages.append(msg)        return pruned_messages     async def summarize_old_messages(self, messages_to_summarize: List[ChatMessage]) -> str:        """Summarize older conversation history using an LLM (mock async LLM call)"""        print(f"[MemoryManager] Summarizing {len(messages_to_summarize)} old messages...")        await asyncio.sleep(0.05)  # Simulate LLM API latency         extracted_facts = [            f"- {m.role}: {m.content[:50]}..."            for m in messages_to_summarize            if m.role in ["user", "assistant"]        ]        return "Previous Conversation Key Summary:\n" + "\n".join(extracted_facts)     async def prepare_context_window(        self, system_prompt: ChatMessage, conversation_history: List[ChatMessage]    ) -> List[ChatMessage]:        """Pipeline to assemble and compress the final context window sent to the LLM"""        # Step 1: Tool Output Pruning        pruned_history = self.prune_tool_outputs(conversation_history)         # Step 2: Total Token Calculation        total_tokens = self.estimate_tokens(system_prompt.content) + self.estimate_tokens(self.summary_buffer)        for msg in pruned_history:            total_tokens += self.estimate_tokens(msg.content)         trigger_limit = int(self.config.max_token_budget * self.config.summary_trigger_ratio)         # Step 3: Summarization & Windowing if token budget exceeded        if total_tokens > trigger_limit and len(pruned_history) > self.config.sliding_window_size:            cutoff_index = len(pruned_history) - self.config.sliding_window_size            old_messages = pruned_history[:cutoff_index]            recent_messages = pruned_history[cutoff_index:]             # Summarize old messages & update summary buffer            new_summary = await self.summarize_old_messages(old_messages)            self.summary_buffer = f"{self.summary_buffer}\n{new_summary}".strip()             pruned_history = recent_messages         # Step 4: Assemble final message list (System Prompt -> Summary Message -> Recent Window)        final_messages: List[ChatMessage] = [system_prompt]         if self.summary_buffer:            final_messages.append(                ChatMessage(                    role="system",                    content=f"[Persistent Memory Summary]\n{self.summary_buffer}",                )            )         final_messages.extend(pruned_history)        return final_messages # --- Usage Example and Test Run ---async def main() -> None:    # Verify compression behavior with 100 token budget and sliding window size of 3    config = MemoryConfig(max_token_budget=100, sliding_window_size=3, tool_pruning_threshold=150)    memory_mgr = AgentMemoryManager(config)     sys_prompt = ChatMessage(role="system", content="You are a skilled Python agent.")     history = [        ChatMessage(role="user", content="How do I configure database connections?"),        ChatMessage(role="assistant", content="Preparing SQLAlchemy configuration for PostgreSQL connection."),        ChatMessage(role="tool", content="{" * 100 + "DB_CONNECTION_SUCCESS_FULL_LOG" + "}" * 100),        ChatMessage(role="user", content="How many connection pool sizes should I set?"),        ChatMessage(role="assistant", content="We recommend 10 by default."),        ChatMessage(role="user", content="Show me an async asyncpg configuration example as well."),    ]     compressed_context = await memory_mgr.prepare_context_window(sys_prompt, history)     print("\n=== Compressed Context Window Passed to Final LLM ===")    for idx, msg in enumerate(compressed_context):        print(f"[{idx}] {msg.role.upper()}: {msg.content[:80]}...") if __name__ == "__main__":    asyncio.run(main())

5. Defensive Engineering Checklist for Production Agents

To prevent incidents caused by inadequate memory and state management in production, agents must adhere to the following checklist and risk mitigation framework.

In production agents, defensive programming measures against state concurrency, personally identifiable information (PII) leakage, and infinite-loop token consumption are imperative.

Defensive Engineering Risk & Response Matrix

Risk FactorRoot CauseDefense GuardrailSeverityApplicable Tools & Technologies
Infinite Loop Token LeakInsufficient node retry conditions, repetitive tool callsEnforce hard max_steps count, suppress consecutive identical tool callsCRITICALState Counter, Pattern Detection Filter
Context Drift & HallucinationSystem prompt buried by accumulated conversation historyLock System Prompt Anchor at index 0, perform fact validationHIGHSystem Anchor, Guardrail Reducer
Memory ContaminationDirect storage of unverified user claims into LTMRun fact extraction & verification pipeline prior to LTM transferHIGHLLM Fact Checker, Verification Gate
Concurrent State Collision (Race)Simultaneous state writes in multi-agent/API environmentsSession ID-based atomic locks, optimistic lockingMEDIUMRedis Lock, DB Transaction
PII LeakageSensitive data written to Vector DBApply regex and PII masking engines before LTM persistenceCRITICALPresidio, Masking Sanitizer

1. Preventing Infinite Loop Token Leaks

  • Maximum Step Limit: Explicitly set max_steps to block infinite retries in specific nodes.
  • Duplicate Call Detection: If identical tool calls and results repeat consecutively 3 times or more, halt execution and isolate to a higher-level exception handler.

2. Blocking Memory Contamination & Hallucination

  • Separate User Claims from Facts: Never directly persist unverified user claims (e.g., "Our revenue grew 10x") into long-term episodic memory. Only transfer facts validated through system verification processes.
  • Preserve System Prompt Anchor: When applying sliding windows, ensure the system role persona and safety guardrails are always pinned at array index 0.

3. Concurrency & Distributed State Management

  • State Atomicity: In multi-agent systems or distributed environments, use Redis Locks or database transactions to prevent state overwrite collisions (race conditions) on the same session_id.
  • PII Masking: Before persisting conversation history into long-term memory (Vector DB), mask personal identifiers such as social security numbers, API keys, and email addresses using regex or PII detector services.

6. Conclusion

In this post, we explored state schema design, memory hierarchy architectures, and context compression techniques that underpin the persistence and reasoning accuracy of AI agents. To summarize key takeaways:

  1. State must ensure immutability and runtime stability using Pydantic v2, TypedDict, and the Reducer pattern.
  2. Short-Term Memory token bloat must be managed through Tool Output Pruning, Sliding Window, and LLM Summarization pipelines.
  3. Long-Term Memory should extend beyond simple context retrieval to episodic memory that recalls an agent's past successful execution traces.

In the upcoming [AI Agent Engineering Episode 3] post, we will cover "Tool Calling & Safe Execution Environment (Sandbox Security & Failure Recovery)" to inspect how agents can invoke internal and external tools accurately and safely.

댓글

GitHub 계정으로 로그인하면 댓글을 남길 수 있습니다. 댓글은 GitHub Discussions를 통해 운영됩니다.

TOP