# 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/).