# Memory and Context Management This guide covers everything you need to know about managing conversation memory and dynamic context in Atomic Agents. Whether you're building a simple chatbot or orchestrating complex multi-agent systems, understanding memory management is essential. ```{contents} :local: :depth: 2 ``` ## Introduction ### What You'll Learn - How conversation history works in Atomic Agents - What "turns" are and how they're tracked - How messages are automatically managed during agent execution - How to persist and restore conversation state - How to use context providers for dynamic information injection - Advanced multi-agent memory patterns ### Prerequisites - Basic familiarity with Atomic Agents ([Quickstart Guide](quickstart.md)) - Understanding of Python classes and async/await ### The Problem This Solves A common question from developers (see [GitHub Issue #58](https://github.com/BrainBlend-AI/atomic-agents/issues/58)): > "In most of the examples only the initial message is added, not any subsequent runs. Is this automatic?" **Yes, it is automatic!** When you call `agent.run(user_input)`, the framework automatically: 1. Adds your input to the conversation history 2. Sends the full history to the LLM 3. Adds the LLM's response to history This guide explains exactly how this works and how to leverage it for complex use cases. --- ## Understanding Memory in Atomic Agents ### The Conversation Model Atomic Agents uses a **turn-based conversation model** where each interaction between user and assistant forms a "turn". The `ChatHistory` class manages this conversation state. ```{mermaid} flowchart LR subgraph Turn1["Turn 1 (turn_id: abc-123)"] U1[User Message] A1[Assistant Response] end subgraph Turn2["Turn 2 (turn_id: def-456)"] U2[User Message] A2[Assistant Response] end subgraph Turn3["Turn 3 (turn_id: ghi-789)"] U3[User Message] A3[Assistant Response] end U1 --> A1 A1 -.-> U2 U2 --> A2 A2 -.-> U3 U3 --> A3 ``` **Key Concepts:** - **Message**: A single piece of content with a role (user, assistant, system) - **Turn**: A logical grouping of related messages (typically user input + assistant response) - **Turn ID**: A UUID that links messages belonging to the same turn - **History**: The complete sequence of messages in a conversation ### Messages and Turns Each message in the history has three components: ```python from atomic_agents.context import Message # Message structure message = Message( role="user", # "user", "assistant", or "system" content=some_schema, # Must be a BaseIOSchema instance turn_id="abc-123" # UUID linking related messages ) ``` **Why Turn IDs Matter:** - Group related messages together - Enable deletion of complete turns (user message + response) - Track conversation flow for debugging - Support conversation branching patterns --- ## ChatHistory Fundamentals ### Creating and Configuring History ```python from atomic_agents.context import ChatHistory # Basic history (unlimited messages) history = ChatHistory() # History with message limit (oldest messages removed when exceeded) history = ChatHistory(max_messages=50) ``` ### Using History with an Agent ```python import instructor import openai from atomic_agents import AtomicAgent, AgentConfig, BaseIOSchema from atomic_agents.context import ChatHistory from pydantic import Field # Define schemas class ChatInput(BaseIOSchema): """User chat message""" message: str = Field(..., description="The user's message") class ChatOutput(BaseIOSchema): """Assistant response""" response: str = Field(..., description="The assistant's response") # Create history and agent history = ChatHistory(max_messages=100) client = instructor.from_openai(openai.OpenAI()) agent = AtomicAgent[ChatInput, ChatOutput]( config=AgentConfig( client=client, model="gpt-5-mini", history=history, ) ) # Each run automatically manages history response1 = agent.run(ChatInput(message="Hello!")) response2 = agent.run(ChatInput(message="What did I just say?")) # The agent remembers the previous message! ``` ### The Turn Lifecycle ```{mermaid} stateDiagram-v2 [*] --> NoTurn: ChatHistory created NoTurn --> ActiveTurn: initialize_turn() called NoTurn --> ActiveTurn: add_message() called ActiveTurn --> ActiveTurn: add_message() same turn ActiveTurn --> NewTurn: initialize_turn() called NewTurn --> ActiveTurn: Generates new UUID ActiveTurn --> NoTurn: All turns deleted note right of ActiveTurn: current_turn_id = UUID note right of NoTurn: current_turn_id = None ``` **Turn Lifecycle Methods:** ```python # Initialize a new turn (generates new UUID) history.initialize_turn() # Get the current turn ID turn_id = history.get_current_turn_id() print(f"Current turn: {turn_id}") # e.g., "abc-123-def-456" # Add a message to the current turn history.add_message("user", ChatInput(message="Hello")) # Messages added without initialize_turn() use the existing turn # or auto-initialize if no turn exists ``` --- ## Automatic Memory Management This section addresses the core question from GitHub Issue #58: **How does automatic message management work?** ### How .run() Manages Memory When you call `agent.run(user_input)`, here's exactly what happens: ```{mermaid} flowchart TD A["agent.run(user_input)"] --> B{user_input
provided?} B -->|Yes| C["history.initialize_turn()
Creates new UUID"] C --> D["history.add_message('user', user_input)
Stores user message"] B -->|No| E["Skip turn initialization
Use existing history"] D --> F["_prepare_messages()
Build message list"] E --> F F --> G["System prompt + history"] G --> H["LLM API call"] H --> I["Receive response"] I --> J["history.add_message('assistant', response)
Stores response"] J --> K["_manage_overflow()
Trim if needed"] K --> L["Return response"] style C fill:#e1f5fe style D fill:#e1f5fe style J fill:#e1f5fe ``` ### Step-by-Step Trace Let's trace through a complete conversation: ```python from atomic_agents import AtomicAgent, AgentConfig, BaseIOSchema from atomic_agents.context import ChatHistory from pydantic import Field class Input(BaseIOSchema): """Input""" text: str = Field(...) class Output(BaseIOSchema): """Output""" reply: str = Field(...) # Create agent with history history = ChatHistory() agent = AtomicAgent[Input, Output](config=AgentConfig( client=client, model="gpt-5-mini", history=history )) # --- TURN 1 --- print(f"Before run: {history.get_message_count()} messages") # 0 messages response1 = agent.run(Input(text="Hi, my name is Alice")) # Internally: # 1. history.initialize_turn() -> turn_id = "abc-123" # 2. history.add_message("user", Input(text="Hi...")) # 3. LLM called with history # 4. history.add_message("assistant", Output(reply="Hello Alice!")) print(f"After run 1: {history.get_message_count()} messages") # 2 messages print(f"Turn ID: {history.get_current_turn_id()}") # "abc-123" # --- TURN 2 --- response2 = agent.run(Input(text="What's my name?")) # Internally: # 1. history.initialize_turn() -> turn_id = "def-456" (NEW turn) # 2. history.add_message("user", Input(text="What's...")) # 3. LLM called with FULL history (all 4 messages) # 4. history.add_message("assistant", Output(reply="Your name is Alice!")) print(f"After run 2: {history.get_message_count()} messages") # 4 messages print(f"Turn ID: {history.get_current_turn_id()}") # "def-456" ``` ### Running Without Input You can call `.run()` without input to continue within the same turn: ```python # First call with input - starts new turn response = agent.run(Input(text="Start a story")) # Subsequent call without input - same turn continues # Useful for: tool follow-ups, multi-step reasoning continuation = agent.run() # No new turn created, uses existing history ``` ### Streaming and Async Behavior All execution methods handle memory the same way: | Method | Memory Behavior | |--------|-----------------| | `agent.run(input)` | Automatic turn init + message add | | `agent.run_stream(input)` | Same as run(), streams response | | `agent.run_async(input)` | Same as run(), async execution | | `agent.run_async_stream(input)` | Same as run(), async + streaming | ```python # Streaming example - memory works identically async for chunk in agent.run_async_stream(Input(text="Hello")): print(chunk.reply, end="", flush=True) # History is updated with complete response after stream finishes ``` --- ## History Persistence and Management ### Serialization: Saving Conversations Save conversation history to disk or database: ```python from atomic_agents.context import ChatHistory # ... after some conversation ... # Serialize to JSON string serialized = history.dump() # Save to file with open("conversation.json", "w") as f: f.write(serialized) # Save to database db.save_conversation(user_id=123, data=serialized) ``` ### Deserialization: Restoring Conversations ```python # Load from file with open("conversation.json", "r") as f: serialized = f.read() # Create new history and load history = ChatHistory() history.load(serialized) # Use with agent agent = AtomicAgent[Input, Output](config=AgentConfig( client=client, model="gpt-5-mini", history=history, # Restored history! )) # Continue the conversation where it left off response = agent.run(Input(text="Where were we?")) ``` ```{warning} Only load serialized data from trusted sources. The `load()` method reconstructs Python classes from the serialized data. ``` ### Overflow Management Control memory usage with `max_messages`: ```python # Keep only last 20 messages history = ChatHistory(max_messages=20) # When 21st message is added, oldest message is removed # This is FIFO (First In, First Out) - oldest messages go first ``` **Strategy for Long Conversations:** ```python # Option 1: Simple limit history = ChatHistory(max_messages=50) # Option 2: Monitor and handle manually if history.get_message_count() > 40: # Maybe summarize old messages before they're lost old_messages = history.get_history()[:10] summary = summarize_messages(old_messages) # Store summary in context provider instead ``` ### History Manipulation **Copying History:** ```python # Create independent copy (deep copy) history_copy = history.copy() # Modifications don't affect original history_copy.add_message("user", Input(text="This only goes in copy")) ``` **Deleting Turns:** ```python # Get the turn ID you want to delete turn_id = history.get_current_turn_id() # Delete all messages with that turn ID history.delete_turn_id(turn_id) # Useful for: removing failed attempts, undo functionality ``` **Resetting History:** ```python # Clear all messages, start fresh agent.reset_history() # or history = ChatHistory() # Create new instance ``` --- ## Multimodal Content in History ChatHistory supports images, PDFs, and audio through Instructor's multimodal types. ### Adding Multimodal Messages ```python from instructor import Image, PDF, Audio from atomic_agents import BaseIOSchema from pydantic import Field from typing import List class ImageAnalysisInput(BaseIOSchema): """Input with images for analysis""" question: str = Field(..., description="Question about the images") images: List[Image] = Field(..., description="Images to analyze") # Create input with images input_with_images = ImageAnalysisInput( question="What's in these images?", images=[ Image.from_path("photo1.jpg"), Image.from_path("photo2.png"), ] ) # Run agent - images are stored in history response = agent.run(input_with_images) ``` ### Multimodal Message Structure When history contains multimodal content, `get_history()` returns a special structure: ```python history_data = history.get_history() for message in history_data: if isinstance(message["content"], list): # Multimodal message json_content = message["content"][0] # Text/JSON data multimodal_objects = message["content"][1:] # Images, PDFs, etc. else: # Text-only message json_content = message["content"] ``` ### Serialization with Multimodal ```{note} Multimodal content with file paths is serialized by path. Ensure files exist at the same paths when loading. ``` ```python # Serialize (file paths are preserved) serialized = history.dump() # When loading, files must be accessible at original paths history.load(serialized) ``` --- ## Dynamic Context with Providers Context providers inject dynamic information into agent system prompts at runtime, complementing the static conversation history. ### Understanding the Difference | Aspect | ChatHistory (Memory) | Context Providers | |--------|---------------------|-------------------| | **Purpose** | Store conversation turns | Inject dynamic context | | **Location** | Message history | System prompt | | **Persistence** | Saved with history | Regenerated each call | | **Use Case** | Conversation continuity | Real-time data (RAG, user info, time) | ```{mermaid} flowchart TB subgraph SystemPrompt["System Prompt (sent to LLM)"] BG[Background Instructions] ST[Steps] subgraph DC["Dynamic Context"] CP1[Context Provider 1] CP2[Context Provider 2] CP3[Context Provider 3] end OI[Output Instructions] end subgraph Messages["Conversation Messages"] H[ChatHistory Messages] end SystemPrompt --> LLM Messages --> LLM LLM --> Response ``` ### Creating Custom Context Providers ```python from atomic_agents.context import BaseDynamicContextProvider class UserContextProvider(BaseDynamicContextProvider): """Provides current user information to the agent.""" def __init__(self): super().__init__(title="Current User") self.user_name: str = "" self.user_role: str = "" self.preferences: dict = {} def get_info(self) -> str: """Called every time the agent runs.""" if not self.user_name: return "No user logged in." info = f"User: {self.user_name} (Role: {self.user_role})" if self.preferences: prefs = ", ".join(f"{k}: {v}" for k, v in self.preferences.items()) info += f"\nPreferences: {prefs}" return info ``` ### Registering Context Providers ```python from atomic_agents import AtomicAgent, AgentConfig from atomic_agents.context import SystemPromptGenerator # Create provider user_provider = UserContextProvider() # Option 1: Register with SystemPromptGenerator system_prompt = SystemPromptGenerator( background=["You are a helpful assistant."], context_providers={"user": user_provider} ) agent = AtomicAgent[Input, Output](config=AgentConfig( client=client, model="gpt-5-mini", system_prompt_generator=system_prompt, )) # Option 2: Register after agent creation agent.register_context_provider("user", user_provider) # Update provider state before running user_provider.user_name = "Alice" user_provider.user_role = "Admin" # Now the agent knows about Alice! response = agent.run(Input(text="What can I do?")) ``` ### Common Context Provider Patterns **RAG (Retrieval-Augmented Generation):** ```python class RAGContextProvider(BaseDynamicContextProvider): """Injects retrieved documents into the prompt.""" def __init__(self, vector_db): super().__init__(title="Relevant Documents") self.vector_db = vector_db self.current_query: str = "" self._cached_results: list = [] def search(self, query: str, top_k: int = 3): """Call before agent.run() to update context.""" self.current_query = query self._cached_results = self.vector_db.search(query, top_k=top_k) def get_info(self) -> str: if not self._cached_results: return "No relevant documents found." docs = [] for i, doc in enumerate(self._cached_results, 1): docs.append(f"Document {i}:\n{doc['content']}\nSource: {doc['source']}") return "\n\n".join(docs) # Usage rag_provider = RAGContextProvider(vector_db) agent.register_context_provider("documents", rag_provider) # Before each query user_query = "How do I reset my password?" rag_provider.search(user_query) # Update context response = agent.run(Input(text=user_query)) ``` **Time-Aware Context:** ```python from datetime import datetime class TimeContextProvider(BaseDynamicContextProvider): """Provides current time information.""" def __init__(self): super().__init__(title="Current Time") def get_info(self) -> str: now = datetime.now() return f"Current date/time: {now.strftime('%Y-%m-%d %H:%M:%S %Z')}" ``` **Session Context:** ```python class SessionContextProvider(BaseDynamicContextProvider): """Tracks session-specific state.""" def __init__(self): super().__init__(title="Session State") self.data: dict = {} def set(self, key: str, value: str): self.data[key] = value def get_info(self) -> str: if not self.data: return "No session data." return "\n".join(f"- {k}: {v}" for k, v in self.data.items()) ``` --- ## Multi-Agent Memory Patterns This section addresses the question from GitHub Issue #58: > "How do I handle a scenario where one agent performs an action, a second agent evaluates it, and then passes results back to the first agent's memory?" Here are five patterns for managing memory across multiple agents. ### Pattern 1: Shared History Multiple agents share the same `ChatHistory` instance, seeing each other's messages. ```{mermaid} flowchart LR subgraph SharedHistory["Shared ChatHistory"] M1[Message 1] M2[Message 2] M3[Message 3] M4[Message 4] end A1[Agent A] --> SharedHistory A2[Agent B] --> SharedHistory A3[Agent C] --> SharedHistory ``` **Use Case:** Agents that need full conversation context (e.g., specialist + generalist). ```python from atomic_agents import AtomicAgent, AgentConfig from atomic_agents.context import ChatHistory # One history shared by all shared_history = ChatHistory() # Agent A - Technical Expert technical_agent = AtomicAgent[Input, Output](config=AgentConfig( client=client, model="gpt-5-mini", history=shared_history, # Same history system_prompt_generator=SystemPromptGenerator( background=["You are a technical expert."] ), )) # Agent B - Communication Expert communication_agent = AtomicAgent[Input, Output](config=AgentConfig( client=client, model="gpt-5-mini", history=shared_history, # Same history! system_prompt_generator=SystemPromptGenerator( background=["You simplify technical explanations."] ), )) # Conversation flow user_input = Input(text="Explain quantum computing") # Technical agent adds to shared history technical_response = technical_agent.run(user_input) # Communication agent sees technical response in history simple_response = communication_agent.run( Input(text="Simplify the above explanation for a child") ) ``` ### Pattern 2: Independent Histories Each agent maintains its own isolated history. ```{mermaid} flowchart TB subgraph Agent_A["Agent A"] HA[History A] end subgraph Agent_B["Agent B"] HB[History B] end subgraph Agent_C["Agent C"] HC[History C] end User --> Agent_A User --> Agent_B User --> Agent_C ``` **Use Case:** Parallel processing, independent tasks, privacy isolation. ```python # Each agent has its own history agent_a = AtomicAgent[Input, Output](config=AgentConfig( client=client, model="gpt-5-mini", history=ChatHistory(), # Independent )) agent_b = AtomicAgent[Input, Output](config=AgentConfig( client=client, model="gpt-5-mini", history=ChatHistory(), # Independent )) # They don't see each other's conversations response_a = agent_a.run(Input(text="Research topic A")) response_b = agent_b.run(Input(text="Research topic B")) ``` (pattern-3-agent-to-agent-messaging)= ### Pattern 3: Agent-to-Agent Messaging Manually transfer outputs between agent memories. **This directly addresses Issue #58.** ```{mermaid} sequenceDiagram participant U as User participant O as Orchestrator participant A as Agent A participant B as Agent B U->>O: Initial request O->>A: run(user_input) Note over A: Turn 1: User + Response
added to A.history A-->>O: Result A O->>O: Manual transfer Note over O: B.history.add_message(
"user", Result A) O->>B: run(None) Note over B: Uses existing history
Turn 2: Response added B-->>O: Result B O->>O: Manual transfer Note over O: A.history.add_message(
"user", Result B) O->>A: run(None) Note over A: Continues with
B's feedback in context A-->>O: Final Result ``` **Use Case:** Agent loops, evaluation cycles, iterative refinement. ```python from atomic_agents import AtomicAgent, AgentConfig, BaseIOSchema from atomic_agents.context import ChatHistory from pydantic import Field class WriterInput(BaseIOSchema): """Writer input""" task: str = Field(...) class WriterOutput(BaseIOSchema): """Writer output""" content: str = Field(...) class ReviewerInput(BaseIOSchema): """Reviewer input""" content_to_review: str = Field(...) class ReviewerOutput(BaseIOSchema): """Reviewer output""" feedback: str = Field(...) approved: bool = Field(...) # Create agents with independent histories writer = AtomicAgent[WriterInput, WriterOutput](config=AgentConfig( client=client, model="gpt-5-mini", history=ChatHistory(), )) reviewer = AtomicAgent[ReviewerInput, ReviewerOutput](config=AgentConfig( client=client, model="gpt-5-mini", history=ChatHistory(), )) def iterative_writing(task: str, max_iterations: int = 3) -> str: """Writer-Reviewer loop with memory transfer.""" # Initial writing writer_response = writer.run(WriterInput(task=task)) for i in range(max_iterations): # Review the content review = reviewer.run(ReviewerInput( content_to_review=writer_response.content )) if review.approved: return writer_response.content # Transfer feedback to writer's memory # This is the key pattern from Issue #58! writer.history.add_message( "user", WriterInput(task=f"Revise based on feedback: {review.feedback}") ) # Writer continues with feedback in context writer_response = writer.run() # No input = use existing history return writer_response.content # Usage final_content = iterative_writing("Write a product description for headphones") ``` ### Pattern 4: Supervisor-Worker with Context Providers Use context providers to share state between supervisor and worker agents. ```{mermaid} flowchart TB subgraph SharedContext["Shared Context Provider"] SC[Task State & Results] end SUP[Supervisor Agent] --> SharedContext W1[Worker 1] --> SharedContext W2[Worker 2] --> SharedContext W3[Worker 3] --> SharedContext SUP -->|Delegates| W1 SUP -->|Delegates| W2 SUP -->|Delegates| W3 W1 -->|Updates| SharedContext W2 -->|Updates| SharedContext W3 -->|Updates| SharedContext ``` ```python class TaskContextProvider(BaseDynamicContextProvider): """Shared context for supervisor-worker pattern.""" def __init__(self): super().__init__(title="Task Progress") self.current_task: str = "" self.subtask_results: dict = {} self.overall_status: str = "pending" def set_task(self, task: str): self.current_task = task self.subtask_results = {} self.overall_status = "in_progress" def add_result(self, subtask: str, result: str): self.subtask_results[subtask] = result def get_info(self) -> str: info = [f"Current Task: {self.current_task}"] info.append(f"Status: {self.overall_status}") if self.subtask_results: info.append("\nCompleted Subtasks:") for task, result in self.subtask_results.items(): info.append(f" - {task}: {result[:100]}...") return "\n".join(info) # Shared context task_context = TaskContextProvider() # All agents see the same context supervisor = AtomicAgent[Input, Output](config=AgentConfig( client=client, model="gpt-5-mini", history=ChatHistory(), )) supervisor.register_context_provider("task", task_context) worker1 = AtomicAgent[Input, Output](config=AgentConfig( client=client, model="gpt-5-mini", history=ChatHistory(), )) worker1.register_context_provider("task", task_context) # Orchestration task_context.set_task("Research and summarize AI trends") # Worker does subtask result1 = worker1.run(Input(text="Research NLP trends")) task_context.add_result("NLP Research", result1.response) # Supervisor sees worker's result via context provider summary = supervisor.run(Input(text="Synthesize the research findings")) ``` ### Pattern 5: Memory-Augmented Loops Combine conversation history with external memory for long-running processes. ```python class LongTermMemory: """External memory store for facts and decisions.""" def __init__(self): self.facts: list = [] self.decisions: list = [] def add_fact(self, fact: str): self.facts.append(fact) def add_decision(self, decision: str): self.decisions.append(decision) def get_summary(self) -> str: summary = [] if self.facts: summary.append("Known Facts:\n" + "\n".join(f"- {f}" for f in self.facts)) if self.decisions: summary.append("Decisions Made:\n" + "\n".join(f"- {d}" for d in self.decisions)) return "\n\n".join(summary) if summary else "No long-term memory yet." class MemoryContextProvider(BaseDynamicContextProvider): def __init__(self, memory: LongTermMemory): super().__init__(title="Long-Term Memory") self.memory = memory def get_info(self) -> str: return self.memory.get_summary() # Setup long_term = LongTermMemory() memory_provider = MemoryContextProvider(long_term) agent = AtomicAgent[Input, Output](config=AgentConfig( client=client, model="gpt-5-mini", history=ChatHistory(max_messages=20), # Short-term limited )) agent.register_context_provider("memory", memory_provider) # Research loop with memory accumulation topics = ["AI Safety", "Quantum Computing", "Climate Tech"] for topic in topics: response = agent.run(Input(text=f"Research {topic} and identify key facts")) # Extract and store important facts in long-term memory long_term.add_fact(f"{topic}: {response.response[:200]}") # ChatHistory may overflow, but long-term memory persists # Agent always has access via context provider # Final synthesis - agent sees all facts via context provider final = agent.run(Input(text="Synthesize all research into recommendations")) ``` --- ## Best Practices ### When to Use Each Pattern | Scenario | Recommended Pattern | |----------|-------------------| | Single agent chatbot | Basic ChatHistory | | Multi-turn with context | ChatHistory + Context Providers | | Parallel independent tasks | Independent Histories | | Sequential pipeline | Agent-to-Agent Messaging | | Iterative refinement loops | Agent-to-Agent Messaging | | Supervisor-worker | Shared Context Providers | | Long-running processes | Memory-Augmented Loops | ### Managing Context Window Limits ```python from atomic_agents.utils import get_context_token_count # Monitor token usage token_info = agent.get_context_token_count() print(f"Total tokens: {token_info.total}") print(f"System prompt: {token_info.system_prompt}") print(f"History: {token_info.history}") print(f"Utilization: {token_info.utilization:.1%}") # Set appropriate limits if token_info.utilization > 0.8: # Consider trimming history or summarizing pass ``` ### Testing Agents with Memory ```python import pytest from atomic_agents.context import ChatHistory @pytest.fixture def fresh_history(): """Provide clean history for each test.""" return ChatHistory() @pytest.fixture def agent_with_history(fresh_history): """Agent with clean history.""" return AtomicAgent[Input, Output](config=AgentConfig( client=mock_client, model="gpt-5-mini", history=fresh_history, )) def test_conversation_continuity(agent_with_history): """Test that agent remembers previous messages.""" agent_with_history.run(Input(text="My name is Bob")) response = agent_with_history.run(Input(text="What's my name?")) assert "Bob" in response.response def test_history_persistence(agent_with_history): """Test serialization/deserialization.""" agent_with_history.run(Input(text="Remember: secret=42")) # Serialize serialized = agent_with_history.history.dump() # Create new history and load new_history = ChatHistory() new_history.load(serialized) assert new_history.get_message_count() == 2 ``` ### Debugging Memory Issues ```python # Inspect current history for msg in history.history: print(f"[{msg.role}] Turn: {msg.turn_id}") print(f" Content: {msg.content.model_dump_json()[:100]}...") print() # Check turn state print(f"Current turn ID: {history.get_current_turn_id()}") print(f"Message count: {history.get_message_count()}") print(f"Max messages: {history.max_messages}") ``` --- ## Troubleshooting ### "Messages aren't being added to history" **Cause:** Calling `run()` without input after resetting history. ```python # Wrong - no messages to work with agent.reset_history() agent.run() # Nothing in history! # Correct agent.reset_history() agent.run(Input(text="Start fresh")) # Provides input ``` ### "Agent doesn't remember previous conversation" **Cause:** Creating new agent instances instead of reusing. ```python # Wrong - new agent = new history each time def handle_message(text): agent = AtomicAgent[Input, Output](config=config) # New instance! return agent.run(Input(text=text)) # Correct - reuse agent instance agent = AtomicAgent[Input, Output](config=config) # Create once def handle_message(text): return agent.run(Input(text=text)) # Reuse ``` ### "How do I pass memory between agents?" See [Pattern 3: Agent-to-Agent Messaging](#pattern-3-agent-to-agent-messaging). ```python # Transfer output to another agent's memory agent_b.history.add_message("user", agent_a_output) agent_b.run() # Now has context from agent A ``` ### "What exactly is a 'turn'?" A **turn** is a logical unit of conversation, typically containing: - One user message - One assistant response - Both sharing the same `turn_id` (UUID) ```python # This is ONE turn: response = agent.run(Input(text="Hello")) # turn_id "abc-123" assigned to both user message and response # This starts a NEW turn: response2 = agent.run(Input(text="Next question")) # turn_id "def-456" assigned to new pair ``` ### "History is too large / context overflow" ```python # Option 1: Limit history size history = ChatHistory(max_messages=30) # Option 2: Monitor and handle if history.get_message_count() > 40: # Summarize or archive old messages pass # Option 3: Use context providers for persistent data # instead of relying on conversation history ``` --- ## API Quick Reference ### ChatHistory | Method | Description | |--------|-------------| | `ChatHistory(max_messages=None)` | Create history with optional limit | | `add_message(role, content)` | Add message to current turn | | `initialize_turn()` | Start new turn with new UUID | | `get_current_turn_id()` | Get current turn's UUID | | `get_history()` | Get all messages as list of dicts | | `get_message_count()` | Get number of messages | | `delete_turn_id(turn_id)` | Delete all messages in a turn | | `dump()` | Serialize to JSON string | | `load(data)` | Deserialize from JSON string | | `copy()` | Create deep copy | ### Message | Field | Type | Description | |-------|------|-------------| | `role` | str | "user", "assistant", or "system" | | `content` | BaseIOSchema | Message content | | `turn_id` | Optional[str] | UUID linking related messages | ### BaseDynamicContextProvider | Method | Description | |--------|-------------| | `__init__(title)` | Create with display title | | `get_info() -> str` | Return context string (override this) | --- ## Next Steps - [Quickstart Guide](quickstart.md) - Get started with Atomic Agents - [Tools Guide](tools.md) - Add capabilities to your agents - [Orchestration Guide](orchestration.md) - Coordinate multiple agents - [Hooks Guide](hooks.md) - Monitor and customize agent behavior - [API Reference](/api/context) - Full API documentation --- ## Summary Key takeaways: 1. **Automatic Memory**: `agent.run(input)` automatically manages history - you don't need to manually add messages 2. **Turns**: A turn groups user input + assistant response with a shared UUID 3. **Persistence**: Use `dump()`/`load()` to save and restore conversations 4. **Context Providers**: Inject dynamic information (RAG, user data, time) into system prompts 5. **Multi-Agent**: Use shared history, agent-to-agent messaging, or context providers depending on your needs For questions or issues, visit our [GitHub repository](https://github.com/BrainBlend-AI/atomic-agents) or [Reddit community](https://www.reddit.com/r/AtomicAgents/).