Memory
TnsAI.Core provides a pluggable memory system for agent conversation history. The `MemoryStore` interface defines storage, retrieval, pruning, and search operations. Four implementations cover different persistence and sharing requirements. The `AgentBuilder.memoryStore()` method wires a store into an agent.
MemoryStore Interface
MemoryStore is the contract that all memory implementations must follow. It defines how an agent saves, retrieves, prunes, and searches its conversation history. You pick an implementation based on your persistence needs (in-memory for development, file-based for production, shared for multi-agent setups).
public interface MemoryStore {
void init(String agentId);
void addMessage(String role, String content);
void addMessage(Map<String, Object> message);
List<Map<String, Object>> getHistory();
List<Map<String, Object>> getRecentHistory(int limit);
void clear();
void prune(int maxTokens);
default List<String> search(String query, int limit);
}| Method | Description |
|---|---|
init(String agentId) | Initializes the store for a specific agent. Triggers history loading for persistent stores. |
addMessage(String role, String content) | Adds a simple message with role (user, assistant, system) and content. |
addMessage(Map<String, Object> message) | Adds a structured message (e.g., with tool calls, metadata). The map is defensively copied. |
getHistory() | Returns the full conversation history as an unmodifiable list. |
getRecentHistory(int limit) | Returns the last N messages. |
clear() | Removes all messages. |
prune(int maxTokens) | Removes oldest messages until the estimated token count is within the limit. System prompts are preserved when possible. |
search(String query, int limit) | Semantic search over history. Default returns empty list. AbstractMemoryStore provides a TF-IDF implementation. |
AbstractMemoryStore
If you want to create your own memory store, extend AbstractMemoryStore instead of implementing MemoryStore from scratch. It handles thread safety, pruning, and search out of the box -- you only need to define how history is loaded and what happens after it changes.
All built-in implementations extend AbstractMemoryStore, which provides:
- Thread safety via
ReentrantLock(Virtual Thread friendly, avoids carrier thread pinning) - LinkedList backing for O(1) removal during pruning
- TF-IDF search using log-normalized term frequency and smooth inverse document frequency
- Extension hooks:
onHistoryChanged()andloadHistory()
Subclasses only need to override two methods:
// Called after any modification (addMessage, clear, prune)
protected void onHistoryChanged() { }
// Called during init() to load persisted history
protected void loadHistory() { }Additional utility method:
// Returns current history size
public int size();InMemoryStore
The simplest memory store -- it keeps conversation history in a plain list in memory. This is the default when you do not configure any memory store on your agent. It is fast and requires zero setup, but all data is lost when the process exits.
public class InMemoryStore extends AbstractMemoryStore {
public InMemoryStore();
}All functionality is inherited from AbstractMemoryStore with no persistence hooks. Suitable for:
- Development and testing
- Short-lived conversations
- Scenarios where persistence is not required
This is the default store used by AgentBuilder when no store is explicitly configured.
FileMemoryStore
When you need conversations to survive restarts, use FileMemoryStore. It writes each agent's history to a JSON file on disk after every change and reloads it automatically when the agent initializes.
public class FileMemoryStore extends AbstractMemoryStore {
public FileMemoryStore(); // uses default dir: .tnsai/memory/
public FileMemoryStore(String storageDirPath);
public Path getAgentFile(); // path to agent's JSON file
}Each agent's history is stored in a separate file: {storageDirPath}/{agentId}.json.
Suitable for:
- Production environments requiring conversation persistence
- Long-running agents that need to resume conversations
- Multi-session scenarios
Example:
FileMemoryStore store = new FileMemoryStore("./data/memory");
store.init("agent-1");
store.addMessage("user", "Hello");
// Automatically saved to ./data/memory/agent-1.json
// On restart, history is loaded from file
FileMemoryStore restored = new FileMemoryStore("./data/memory");
restored.init("agent-1");
List<Map<String, Object>> history = restored.getHistory(); // Contains previous messagesSharedMemoryStore
In multi-agent systems, you often want several agents to read and write the same conversation history. SharedMemoryStore wraps any other MemoryStore and adds an access-control layer so only authorized agents can interact with the shared memory.
public class SharedMemoryStore implements MemoryStore {
public SharedMemoryStore(MemoryStore delegate, String namespace);
public void grantAccess(String agentId);
public void revokeAccess(String agentId);
public boolean hasAccess(String agentId);
public String getNamespace();
}Thread safety uses ReentrantReadWriteLock for high-throughput concurrent reads with exclusive writes. Only the first authorized agent to call init() initializes the underlying store.
Calling init() with an unauthorized agent ID throws IllegalStateException.
Example:
MemoryStore base = new InMemoryStore();
SharedMemoryStore shared = new SharedMemoryStore(base, "team-chat");
shared.grantAccess("agent-a");
shared.grantAccess("agent-b");
// First agent initializes the shared memory
shared.init("agent-a");
// Both agents can read and write
shared.addMessage("assistant", "Hello from agent-a");
List<Map<String, Object>> history = shared.getHistory(); // visible to all authorized agentsSharedMemoryRegistry
When you have many shared memory namespaces across your application, SharedMemoryRegistry acts as a central lookup. It ensures each namespace has exactly one SharedMemoryStore instance, creating one on demand if it does not exist yet. The framework uses this internally when processing @Memory(shared = true, shareWith = {...}) annotations.
public final class SharedMemoryRegistry {
public static SharedMemoryRegistry getInstance();
public SharedMemoryStore getOrCreate(String namespace);
public SharedMemoryStore getOrCreate(String namespace, MemoryStore baseStore);
public Optional<SharedMemoryStore> find(String namespace);
public boolean remove(String namespace);
public void clear();
public int size();
}getOrCreate(String namespace) creates a SharedMemoryStore wrapping an InMemoryStore if the namespace does not exist. Use the two-argument overload to provide a custom base store (e.g., FileMemoryStore for persistence).
Example:
SharedMemoryRegistry registry = SharedMemoryRegistry.getInstance();
// Get or create a shared namespace
SharedMemoryStore shared = registry.getOrCreate("project-alpha");
shared.grantAccess("researcher");
shared.grantAccess("writer");
// With custom persistence
SharedMemoryStore persistent = registry.getOrCreate(
"persistent-namespace",
new FileMemoryStore("./shared-memory")
);
// Look up existing namespace
Optional<SharedMemoryStore> found = registry.find("project-alpha");
// Clean up
registry.remove("project-alpha");MemoryStoreFactory
If you use the @MemorySpec annotation to configure memory declaratively, MemoryStoreFactory is what turns that annotation into an actual MemoryStore instance at runtime. It reads the annotation's fields and automatically wraps the base store with decorators for capacity limits, token budgets, or summarization as needed.
public final class MemoryStoreFactory {
public static MemoryStore create(MemorySpec config);
public static MemoryStore createDefault(); // returns new InMemoryStore()
}Supported persistence types: IN_MEMORY, FILE, REDIS (via SPI), DATABASE (via SPI).
The factory automatically wraps the base store with decorators based on configuration:
- CapacityAwareMemoryStore -- enforces message count limits with configurable prune strategy
- SummarizingMemoryStore -- summarizes old messages instead of deleting them when capacity is exceeded
- TokenAwareMemoryStore -- enforces token limits by pruning after each message
Integration with AgentBuilder
The most common way to configure memory is through the AgentBuilder. Call .memoryStore() to plug in any MemoryStore implementation. If you skip this step, the agent defaults to InMemoryStore (volatile, no persistence).
// Default (in-memory, volatile)
Agent agent = AgentBuilder.create()
.model("claude-sonnet-4")
.build();
// Persistent file storage
Agent agent = AgentBuilder.create()
.model("claude-sonnet-4")
.memoryStore(new FileMemoryStore("./data/memory"))
.build();
// Shared memory between agents
SharedMemoryStore shared = SharedMemoryRegistry.getInstance()
.getOrCreate("team-namespace");
shared.grantAccess("agent-1");
shared.grantAccess("agent-2");
Agent agent1 = AgentBuilder.create()
.model("claude-sonnet-4")
.memoryStore(shared)
.build();
Agent agent2 = AgentBuilder.create()
.model("gpt-4o")
.memoryStore(shared)
.build();Code Examples
These examples demonstrate typical memory usage patterns, from basic conversation persistence to semantic search and token-aware pruning.
Conversation with History Management
This example creates an agent with file-based memory so conversations survive restarts. You can also access the memory store directly to inspect recent history.
Agent agent = AgentBuilder.create()
.model("claude-sonnet-4")
.memoryStore(new FileMemoryStore("./memory"))
.build();
// Conversation persists across restarts
agent.chat("What is the capital of France?");
agent.chat("What about Germany?");
// Access history directly
MemoryStore memory = agent.getMemoryStore();
List<Map<String, Object>> recent = memory.getRecentHistory(5);Semantic Search over History
The built-in TF-IDF search lets you find past messages by meaning rather than scrolling through the full history. This is useful for agents that need to recall earlier context from a long conversation.
MemoryStore store = new InMemoryStore();
store.init("search-agent");
store.addMessage("user", "Tell me about Java generics");
store.addMessage("assistant", "Java generics provide compile-time type safety...");
store.addMessage("user", "How do I create a REST API?");
store.addMessage("assistant", "You can use Spring Boot or Javalin...");
// TF-IDF search finds relevant messages
List<String> results = store.search("generics type safety", 2);
// Returns messages about Java generics ranked by relevanceToken-Aware Pruning
LLMs have a maximum context window size. When your conversation history grows too large, call prune() to remove the oldest messages until the estimated token count fits within the model's limit. System prompts are preserved when possible.
MemoryStore store = new InMemoryStore();
store.init("pruning-agent");
// Add many messages
for (int i = 0; i < 1000; i++) {
store.addMessage("user", "Message " + i + " with some content");
store.addMessage("assistant", "Response to message " + i);
}
// Prune to fit context window
store.prune(4096);
// Oldest messages are removed, keeping history within token limitAdvanced Memory
The com.tnsai.memory.advanced package provides production-grade memory capabilities for agents that need more than simple history management. These include vector-based semantic search, keyword-based BM25 indexing, hybrid retrieval that combines both, skill persistence, importance-based pruning, staleness detection, and full session serialization.
VectorMemoryStore
When you need to find past messages based on meaning (not just keywords), VectorMemoryStore converts each message into an embedding vector and uses cosine similarity to find the closest matches. This powers true semantic retrieval over conversation history.
VectorMemoryStore vectorStore = VectorMemoryStore.builder()
.embeddingClient(embeddingClient)
.dimensions(1536)
.build();
vectorStore.init("agent-1");
vectorStore.addMessage("user", "The deployment uses Kubernetes with 3 replicas");
// Semantic search -- finds relevant messages even with different wording
List<String> results = vectorStore.search("container orchestration setup", 5);BM25Index
While vector search excels at finding semantically similar content, sometimes you need exact keyword matching. BM25Index uses the BM25 scoring algorithm (the same approach behind traditional search engines) to rank documents by how well they match specific terms.
BM25Index index = new BM25Index();
index.addDocument("doc-1", "Kubernetes deployment configuration");
index.addDocument("doc-2", "Database connection pooling settings");
List<BM25Index.ScoredDocument> results = index.search("Kubernetes", 5);HybridMemoryRetriever
For the best retrieval quality, combine both approaches. HybridMemoryRetriever runs a vector search and a BM25 keyword search in parallel, then merges the results using Reciprocal Rank Fusion (RRF). This captures both semantic meaning and exact keyword matches in a single query.
HybridMemoryRetriever retriever = HybridMemoryRetriever.builder()
.vectorStore(vectorStore)
.bm25Index(bm25Index)
.vectorWeight(0.6)
.keywordWeight(0.4)
.fusionK(60)
.build();
List<String> results = retriever.search("deployment configuration", 10);RRF fusion merges the ranked lists from both retrieval methods, giving high-quality results that capture both semantic meaning and exact keyword matches.
SkillMemoryStore
Agents can learn reusable procedures and remember them across sessions. SkillMemoryStore saves these skills as Markdown files on disk, making them human-readable, easy to version-control with Git, and editable by hand if needed.
SkillMemoryStore skillStore = new SkillMemoryStore(".tnsai/skills/");
skillStore.saveSkill("deploy-k8s", SkillEntry.builder()
.name("deploy-k8s")
.description("Deploy application to Kubernetes cluster")
.steps(List.of("Build Docker image", "Push to registry", "Apply manifests"))
.tags(Set.of("devops", "kubernetes"))
.build());
Optional<SkillEntry> skill = skillStore.loadSkill("deploy-k8s");
List<SkillEntry> devopsSkills = skillStore.findByTag("devops");MemoryImportanceScorer
When memory grows too large and needs pruning, not all entries are equally valuable. MemoryImportanceScorer assigns an importance score to each entry based on recency, access frequency, semantic distinctiveness, and user-marked importance, so the pruning process can keep the most valuable memories.
MemoryImportanceScorer scorer = new MemoryImportanceScorer();
double importance = scorer.score(memoryEntry);
// Factors: recency, access frequency, semantic distinctiveness, user-marked importanceStalenessDetector
Over time, memory accumulates near-duplicate or outdated entries that waste context window space. StalenessDetector uses Jaccard similarity to find entries that are too similar to each other, and age thresholds to flag entries that are too old, so you can clean them up.
StalenessDetector detector = StalenessDetector.builder()
.similarityThreshold(0.85) // entries above 85% similarity are stale
.maxAge(Duration.ofDays(30)) // entries older than 30 days are candidates
.build();
List<String> staleIds = detector.detectStale(memoryEntries);SessionSerializer / SessionRestorer
If you need to save and restore an agent's complete session state (not just conversation history, but all runtime state), these utilities serialize the session to bytes and restore it later. This is useful for long-running agents that need to survive process restarts without losing their place.
// Save session state
SessionSerializer serializer = new SessionSerializer();
byte[] data = serializer.serialize(agent.getSession());
Files.write(Path.of("session-backup.bin"), data);
// Restore session state
SessionRestorer restorer = new SessionRestorer();
AgentSession restored = restorer.restore(Files.readAllBytes(Path.of("session-backup.bin")));
agent.restoreSession(restored);Knowledge Base & RAG
TnsAI provides a built-in Retrieval-Augmented Generation (RAG) system through the `KnowledgeBase` interface, `Document` model, and `@KnowledgeSource` annotation. Agents can retrieve relevant context from vector databases, files, URLs, or in-memory stores before making LLM calls.
Output Parsing & Serialization
TnsAI provides type-safe output parsing for converting raw LLM responses into structured Java objects, and a multi-format serialization system for producing structured output.