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.
(Annotated Reducer)
(Annotated Reducer)
Three Core Principles of State Schema Design
- Single Source of Truth: The
Stateobject 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). - 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.
- Strict Runtime Type Validation: Type validation based on Pydantic v2 or
TypedDictis 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.
| Feature | TypedDict | Pydantic v2 |
|---|---|---|
| Primary Purpose | Built-in type hinting & lightweight data passing between internal graph nodes | Runtime data validation, data serialization, and schema enforcement |
| Validation Timing | No runtime validation (Static Type Checker only) | Real-time runtime validation (pydantic-core C++ engine) |
| Performance Overhead | Zero Overhead (Pure Python Dict) | Microsecond Level (High-performance Rust/C++) |
| Reducer Compatibility | Annotated standard in graph runtimes like LangGraph | API Gateway I/O and DB Persistence serialization models |
| Recommended Usage | Agent Graph Internal State | External Gateway, API Bounds, Persistence Engine |
| Key Features | Key-Value type declaration, IDE autocomplete support | Automatic 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.
- User Input
- Short-Term Memory
Working Memory / Message Buffer - subgraph
- Context Window Assembly
- LLM Engine
- Tool Execution
- end
- Episodic Memory
Past Execution Traces & Success Patterns - Semantic Memory
User Profile & Domain Knowledge Base
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.
- state
- ShortTermBuffer
- ToolExecution
- TokenBudgetCheck
- ActiveSession
- SummarizationProcess
- DynamicSummarizer
- FactExtraction
- TraceIndexing
- SemanticMemory
- EpisodicMemory
- ContextAssembly
Detailed Memory Hierarchy Comparison Matrix (Working vs Episodic vs Semantic vs Procedural)
| Memory Type | Lifecycle | Storage Medium | Retrieval Mechanism | Primary Contents |
|---|---|---|---|---|
Working/Short-Term Memory(Working Memory) | Session-level(Volatile) | In-Memory (Redis, RAM Buffer) | Direct Array Indexing / Sliding Window | Current 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 Query | User profiles, domain ontologies, high-level factual data |
Procedural Memory(Procedural Memory) | Immutable(Fixed) | System Prompt, Code Engine | Direct Prompt Injection / Schema Binding | Agent 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:
- Similarity Search: Upon receiving user input, the system searches the Vector Store for execution traces of the most similar past tasks.
- 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
- Surging Cost and Latency: API costs and Time-to-First-Token (TTFT) increase linearly or quadratically in proportion to the input token count.
- Lost in the Middle: LLM attention mechanisms frequently miss critical instructions or tool execution results positioned in the middle of long prompts.
- Context Drift: As conversations lengthen, initial system instructions and user goals become buried and distorted under accumulated tool-call payload messages.
- subgraph
- System Prompt
- Message - (Includes Heavy JSON Tool Result)
- Lost in the Middle Area (Attention Decay)
- User Query
- end
- System 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.
- User
- Runtime
- Pruner
- Estimator
- Summarizer
- LTM Engine
- LLM
Comparison of 3 Core Context Compression Techniques
| Technique | Trigger | Core Mechanism | Token Reduction Effect | Key Trade-off / Caution |
|---|---|---|---|---|
| 1. Tool Output Pruning | Immediately upon receiving message | Truncates long JSON/HTML/Log data in older tool outputs, leaving only key headers | Up to 80%–90% reduction | LLM cannot re-reference full historical details of older tool outputs |
| 2. Dynamic Summarization | When token budget exceeds 80% | Uses a lightweight LLM (e.g. GPT-4o-mini) to condense older messages into summaries and transfer facts to LTM | Continuous 50%–70% compression | Subtle context or nuanced details may be lost during summarization |
| 3. Sliding Window | On every loop iteration | Retains top system anchor and latest N messages, discarding/moving intermediate messages | Guaranteed Upper Limit | Context 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
(System + Persistent Summary + Recent Window)
In production environments, when implementing
estimate_tokens, it is recommended to bind thetiktokenlibrary 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 Factor | Root Cause | Defense Guardrail | Severity | Applicable Tools & Technologies |
|---|---|---|---|---|
| Infinite Loop Token Leak | Insufficient node retry conditions, repetitive tool calls | Enforce hard max_steps count, suppress consecutive identical tool calls | CRITICAL | State Counter, Pattern Detection Filter |
| Context Drift & Hallucination | System prompt buried by accumulated conversation history | Lock System Prompt Anchor at index 0, perform fact validation | HIGH | System Anchor, Guardrail Reducer |
| Memory Contamination | Direct storage of unverified user claims into LTM | Run fact extraction & verification pipeline prior to LTM transfer | HIGH | LLM Fact Checker, Verification Gate |
| Concurrent State Collision (Race) | Simultaneous state writes in multi-agent/API environments | Session ID-based atomic locks, optimistic locking | MEDIUM | Redis Lock, DB Transaction |
| PII Leakage | Sensitive data written to Vector DB | Apply regex and PII masking engines before LTM persistence | CRITICAL | Presidio, Masking Sanitizer |
1. Preventing Infinite Loop Token Leaks
- Maximum Step Limit: Explicitly set
max_stepsto 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
systemrole 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:
- State must ensure immutability and runtime stability using Pydantic v2,
TypedDict, and the Reducer pattern. - Short-Term Memory token bloat must be managed through Tool Output Pruning, Sliding Window, and LLM Summarization pipelines.
- 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를 통해 운영됩니다.