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 is Part 2 of the 'AI Agent Engineering' series. It covers state schemas, short-term and long-term memory, and context compression for 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.

The stateless path is Prompt Input → LLM Engine → Token Output. The stateful workflow sends both current state and node output to a reducer.

FromToPayload or operation
State t_0: Initial ContextAgent Node A: LLM QueryInitial state
Agent Node AState Reducer: Annotated ReducerOutput Payload
State t_0The same Annotated ReducerCurrent State
Annotated ReducerState t_1: Updated ContextImmutable Merge
State t_1Agent Node B: Tool CallUpdated state
Agent Node BState Reducer: Add Messages / PatchTool Result
State t_1The same Add Messages / Patch ReducerCurrent State
Add Messages / Patch ReducerState t_2: Execution ReadyValidated State

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 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

Agent memory can be divided by purpose and retention period. This article distinguishes working, episodic, and semantic memory.

User Input enters Short-Term Memory (Working Memory / Message Buffer). Inside the Agent Runtime System, the cycle is Short-Term Memory → Context Window Assembly → LLM Engine → Tool Execution → Short-Term Memory.

FromLong-term store and contentsReturn to Context Window Assembly
Short-Term Memory: Memory Flush / SummarizationEpisodic Memory: Past Execution Traces & Success PatternsVector Similarity Search
Short-Term Memory: Entity ExtractionSemantic Memory: User Profile & Domain Knowledge BaseRAG / Key-Value Lookup

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.

Read internal transitions within their named scope. Each entry point starts only that scope.

ScopeFrom → ToCondition or operation
Whole modelEntry → ActiveSessionUser Input Received
Inside ActiveSessionEntry → ShortTermBufferAppend Message
Inside ActiveSessionShortTermBuffer → ToolExecutionDispatch Tool Call
Inside ActiveSessionToolExecution → ShortTermBufferReturn Tool Result
Inside ActiveSessionShortTermBuffer → TokenBudgetCheckEvaluate Token Count
Outer connectionTokenBudgetCheck → ActiveSessionWithin Budget
Outer connectionTokenBudgetCheck → SummarizationProcessExceeds Threshold (80%)
Inside SummarizationProcessEntry → DynamicSummarizerLLM Compression
Inside SummarizationProcessDynamicSummarizer → FactExtractionExtract Key Facts & Entities
Inside SummarizationProcessDynamicSummarizer → TraceIndexingIndex Success/Failure Trace
Outer connectionFactExtraction → SemanticMemorySave to RDBMS / Document Store
Outer connectionTraceIndexing → EpisodicMemorySave to Vector Store (Experience Replay)
Outer connectionSemanticMemory → ContextAssemblyRAG Retrieval
Outer connectionEpisodicMemory → ContextAssemblyFew-Shot Recall
Outer connectionSummarizationProcess → ContextAssemblyCondensed Summary Buffer
Outer connectionContextAssembly → ActiveSessionRe-assembled Context Window

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.
Context windowOrder from first to last
Full: Problematic StateSystem Prompt → Message (Includes Heavy JSON Tool Result) → Lost in the Middle Area (Attention Decay) → User Query
Compressed: OptimizedSystem Prompt → Condensed Summary / Long-term Memories → Sliding Window (Latest N Messages) → 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.

StepFrom → ToPayload or operation
1User → RuntimeSend User Query / Command
2Runtime → PrunerPass Raw Conversation History
3Pruner → PrunerTruncate Large Tool Outputs (> Threshold)
4Pruner → RuntimeReturn Pruned Message History
5Runtime → EstimatorCalculate Total Context Tokens
6Estimator → RuntimeBudget Exceeded Flag = True
7Runtime → SummarizerSend Old Messages (0..N-SlidingWindow)
8Summarizer → SummarizerGenerate Bulleted Fact Summary
9Summarizer → LTM EngineFlush Extracted Facts & Experience Traces
10Summarizer → RuntimeReturn Condensed Summary Text
11Runtime → RuntimeUpdate Persistent Summary Buffer
12Runtime → EstimatorSlice Recent N Messages (Sliding Window)
13Estimator → RuntimeBudget OK
14Runtime → LLMPass Context Window (System Prompt + Summary + Sliding Window)
15LLM → UserGenerate Final Response / Call Next Tool

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

The AgentMemoryManager example uses Pydantic v2 and Python asyncio to connect token budgets, sliding windows, tool-output truncation, summarization, and memory flushing.

Memory Manager Pipeline Flowchart

Summarize only above the token trigger
Summarize only above the token triggerAgentMemoryManager takes Raw Conversation Input, prunes old Tool Payloads, then estimates System + Summary + History tokens. If Total Tokens > Trigger Limit, it slices Old/Recent history, asynchronously summarizes Old Messages with an LLM, and updates the Summary Buffer with an LTM Flush. Otherwise it skips those three stages. Both paths join at Context Window assembly and return the Final Compressed Context Array: System + Persistent Summary + Recent Window.YesNo1. Raw Conversation Input2. Prune Tool OutputsTruncate Old Tool Payloads3. Token Budget EstimationSystem + Summary + HistoryTokens >Trigger Limit?4A. Slice HistoryOld / Recent4B. Async LLM SummaryOld Messages4C. Update Summary BufferLTM Flush5. Assemble Context WindowFinal Context ArraySystem + Persistent Summary+ Recent Window
Summarize only above the token triggerAgentMemoryManager takes Raw Conversation Input, prunes old Tool Payloads, then estimates System + Summary + History tokens. If Total Tokens > Trigger Limit, it slices Old/Recent history, asynchronously summarizes Old Messages with an LLM, and updates the Summary Buffer with an LTM Flush. Otherwise it skips those three stages. Both paths join at Context Window assembly and return the Final Compressed Context Array: System + Persistent Summary + Recent Window.YesNo1. Raw Conversation Input2. Prune Tool OutputsTruncate Old Tool Payloads3. Token Budget EstimationSystem + Summary + HistoryTokens >Trigger Limit?4A. Slice HistoryOld / Recent4B. Async LLM SummaryOld Messages4C. Update Summary BufferLTM Flush5. Assemble Context WindowFinal Context ArraySystem + Persistent Summary+ Recent Window
Only Yes goes through 4A–4C. No joins the same assembly step.
Read the diagram as text

AgentMemoryManager takes Raw Conversation Input, prunes old Tool Payloads, then estimates System + Summary + History tokens. If Total Tokens > Trigger Limit, it slices Old/Recent history, asynchronously summarizes Old Messages with an LLM, and updates the Summary Buffer with an LTM Flush. Otherwise it skips those three stages. Both paths join at Context Window assembly and return the Final Compressed Context Array: System + Persistent Summary + Recent Window.

Open full size ↗

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 riskSafety guardrail
1. Infinite Loop Token LeakStop Condition & Max Token Limit
2. Context Contamination / HallucinationSanitization & Fact Validation
3. PII Leakage RiskPII Masking before Storage
4. Concurrent State CollisionAtomic State Reducer & Lock

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

State schemas define the data passed between nodes. Memory layers divide what to retain, and context compression controls how much is sent in the current call.

  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