Memory
An agent keeps its conversation history in a MemoryStore. The default is InMemoryStore, which is lost when the process exits. Configure a store on the agent class with @MemorySpec, from code with MemoryConfig, or pass your own store.
Configure memory on the agent class
@MemorySpec nests inside @AgentSpec. This agent keeps file-backed history, at most 200 entries, within an 8,000-token budget:
package com.example.tnsai.docs;
import com.tnsai.accountability.AuthorityScope;
import com.tnsai.actions.ActionExecutor;
import com.tnsai.agents.Agent;
import com.tnsai.annotations.AgentSpec;
import com.tnsai.annotations.MemorySpec;
import com.tnsai.identity.AgentPrincipal;
import com.tnsai.llm.LLMClient;
import com.tnsai.llm.providers.AnthropicClient;
import com.tnsai.roles.Role;
import java.util.List;
@AgentSpec(
description = "Support agent with persistent history",
memory = @MemorySpec(
type = MemorySpec.MemoryType.CONVERSATION,
capacity = 200,
pruneStrategy = MemorySpec.PruneStrategy.SLIDING_WINDOW,
persistence = MemorySpec.Persistence.FILE,
maxContextTokens = 8000))
public final class MemoryTopicAnnotationsExample extends Agent {
public MemoryTopicAnnotationsExample(AgentPrincipal principal, AuthorityScope scope) {
super("support-agent", null, null, wired(principal, scope));
}
private static ActionExecutor wired(AgentPrincipal principal, AuthorityScope scope) {
ActionExecutor executor = new ActionExecutor();
executor.setPrincipal(principal);
executor.setDefaultScope(scope);
return executor;
}
@Override
protected LLMClient getLLM() {
return new AnthropicClient("claude-sonnet-4-20250514");
}
@Override
protected List<Role> getRoles() {
return List.of();
}
}@Memory is a separate top-level annotation for shared namespaces, consolidation and persistence. It has its own runtime path, so do not mix its fields with @MemorySpec.
Configure memory from code
MemoryConfig carries the same eight fields as @MemorySpec and goes through the same factory:
package com.example.tnsai.docs;
import com.tnsai.accountability.AuthorityScope;
import com.tnsai.accountability.LiabilitySink;
import com.tnsai.agents.Agent;
import com.tnsai.agents.AgentBuilder;
import com.tnsai.annotations.MemorySpec;
import com.tnsai.identity.AgentPrincipal;
import com.tnsai.llm.LLMClient;
import com.tnsai.memory.MemoryConfig;
import com.tnsai.roles.Role;
public final class MemoryTopicBuilderExample {
private MemoryTopicBuilderExample() {}
public static Agent build(
LLMClient llmClient,
Role supportRole,
AgentPrincipal principal,
LiabilitySink liabilitySink,
AuthorityScope authorityScope
) {
return AgentBuilder.create()
.llm(llmClient)
.role(supportRole)
.memoryConfig(MemoryConfig.builder()
.type(MemorySpec.MemoryType.CONVERSATION)
.capacity(200)
.pruneStrategy(MemorySpec.PruneStrategy.SLIDING_WINDOW)
.persistence(MemorySpec.Persistence.FILE)
.maxContextTokens(8000)
.build())
.principal(principal)
.liabilitySink(liabilitySink)
.authorityScope(authorityScope)
.build();
}
}To use a store you built yourself, pass it with AgentBuilder.memoryStore(MemoryStore).
Which settings win
- A store passed to
memoryStore(...)wins over annotations. - On the annotation path, top-level
@Memoryor@VectorMemorywins over@MemorySpec. - With none of them, the agent uses
InMemoryStore.
@MemorySpec fields
live means the runtime uses the field. partial means only part of it works. not implemented means changing it has no effect.
@MemorySpec / MemoryConfig field | Effect | Status |
|---|---|---|
type | Declares a memory flavor, but does not change store selection. | not implemented |
capacity | Wraps the store with a positive entry limit unless pruning is NONE. 0 is unbounded. | live |
pruneStrategy | Selects sliding-window, relevance, summary, token-limit, or no pruning. | live |
persistence | Selects in-memory/file storage or optional Redis/database discovery. | partial |
maxContextTokens | Adds token-aware pruning and supplies the agent context limit. | live |
includeSystemMessages | Is copied into config but has no runtime consumer. | not implemented |
summarizeOnPrune | Wraps the store with summarization before capacity pruning. | live |
connectionUrl | Is passed into discovery but not delivered to a discovered provider. | not implemented |
For the partial fields of @Memory and @VectorMemory, see the annotation catalog.
When capacity, summarizeOnPrune or maxContextTokens wraps the store, search() and size() still reach the store underneath.
MemoryStore
| Method | What it does |
|---|---|
init(agentId) | Binds the store to an agent. A persistent store loads its history here. |
addMessage(role, content) | Adds a message with a role such as user, assistant or system. |
addMessage(Map) | Adds a structured message, for example one with tool calls. The map is copied. |
getHistory() | The full history, unmodifiable. |
getRecentHistory(limit) | The last limit messages. |
replaceHistory(List) | Replaces the whole history. Stores based on AbstractMemoryStore do it under one lock. |
clear() | Removes every message. |
prune(maxTokens) | Removes the oldest messages until the estimated token count fits. System messages are not kept back. |
search(query, limit) | Messages ranked by relevance. The interface default returns an empty list. AbstractMemoryStore ranks by TF-IDF. |
Built-in stores
| Store | Use it for |
|---|---|
InMemoryStore | Tests and short conversations. The default. |
FileMemoryStore | History that survives a restart. It writes <dir>/<agentId>.json after every change; the default directory is .tnsai/memory. |
SharedMemoryStore | One history for several agents, with an access list. |
To write your own store, extend AbstractMemoryStore. It handles locking, pruning and search. Override loadHistory() to read persisted history at init, and onHistoryChanged() to write after each change.
Share memory between agents
SharedMemoryRegistry holds one SharedMemoryStore per namespace.
| Method | What it does |
|---|---|
getOrCreate(namespace) | The namespace's store, created over an InMemoryStore if missing. |
getOrCreate(namespace, supplier) | The same, with the base store from supplier, for example a FileMemoryStore. |
grantAccess(namespace, agentId) | Lets the agent in and records the grant. |
namespacesGrantedTo(agentId) | The namespaces granted to an agent. |
find(namespace), remove(namespace) | Look up or drop a namespace. |
The access list is checked only on a view from viewFor(agentId). The handle that getOrCreate returns does not check it, so give each agent its own view. A call on a view whose agent has no grant throws IllegalStateException.
package com.example.tnsai.docs;
import com.tnsai.memory.FileMemoryStore;
import com.tnsai.memory.InMemoryStore;
import com.tnsai.memory.MemoryStore;
import com.tnsai.memory.SharedMemoryRegistry;
import com.tnsai.memory.SharedMemoryStore;
import com.tnsai.memory.advanced.BM25Index;
import com.tnsai.memory.advanced.ScoredEntry;
import com.tnsai.memory.strategy.MemoryEntry;
import com.tnsai.memory.strategy.MemoryTier;
import java.util.List;
import java.util.Map;
public final class MemoryStoreExample {
private MemoryStoreExample() {}
// History survives a restart: the store writes ./data/memory/agent-1.json.
static List<Map<String, Object>> fileStore() {
MemoryStore store = new FileMemoryStore("./data/memory");
store.init("agent-1");
store.addMessage("user", "Hello");
MemoryStore restored = new FileMemoryStore("./data/memory");
restored.init("agent-1");
return restored.getHistory();
}
// Search and prune the history.
static List<String> searchAndPrune() {
MemoryStore store = new InMemoryStore();
store.init("search-agent");
store.addMessage("user", "Tell me about Java generics");
store.addMessage("assistant", "Java generics give compile-time type safety.");
store.addMessage("user", "How do I create a REST API?");
List<String> hits = store.search("generics type safety", 2);
store.prune(4096);
return hits;
}
// A shared namespace: each agent works through its own view.
static boolean sharedStore() {
SharedMemoryRegistry registry = SharedMemoryRegistry.getInstance();
SharedMemoryStore shared = registry.getOrCreate(
"project-alpha", () -> new FileMemoryStore("./shared-memory"));
registry.grantAccess("project-alpha", "researcher");
SharedMemoryStore researcher = shared.viewFor("researcher");
researcher.init("researcher");
researcher.addMessage("assistant", "Found three sources.");
// No grant for "writer": every call on this view throws IllegalStateException.
SharedMemoryStore writer = shared.viewFor("writer");
return shared.hasAccess("writer");
}
// Keyword ranking over entries you index yourself.
static List<ScoredEntry> keywordSearch() {
BM25Index index = new BM25Index();
index.add(new MemoryEntry("doc-1", "Kubernetes deployment configuration", MemoryTier.WORKING));
index.add(new MemoryEntry("doc-2", "Database connection pooling settings", MemoryTier.WORKING));
return index.search("Kubernetes", 5);
}
}To give two agents the same history, pass each one its view:
package com.example.tnsai.docs;
import com.tnsai.accountability.AuthorityScope;
import com.tnsai.accountability.LiabilitySink;
import com.tnsai.agents.Agent;
import com.tnsai.agents.AgentBuilder;
import com.tnsai.identity.AgentPrincipal;
import com.tnsai.llm.providers.AnthropicClient;
import com.tnsai.llm.providers.OpenAIClient;
import com.tnsai.memory.FileMemoryStore;
import com.tnsai.memory.SharedMemoryRegistry;
import com.tnsai.memory.SharedMemoryStore;
import com.tnsai.roles.Role;
public final class MemoryBuilderIntegrationExample {
private MemoryBuilderIntegrationExample() {}
public static Agent defaultMemory(
Role role,
AgentPrincipal principal,
LiabilitySink sink,
AuthorityScope scope
) {
return AgentBuilder.create()
.llm(new AnthropicClient("claude-sonnet-4-20250514"))
.role(role)
.principal(principal)
.liabilitySink(sink)
.authorityScope(scope)
.build();
}
public static Agent fileMemory(
Role role,
AgentPrincipal principal,
LiabilitySink sink,
AuthorityScope scope
) {
return AgentBuilder.create()
.llm(new AnthropicClient("claude-sonnet-4-20250514"))
.role(role)
.memoryStore(new FileMemoryStore("./data/memory"))
.principal(principal)
.liabilitySink(sink)
.authorityScope(scope)
.build();
}
public static Agent[] sharedMemory(
Role role1,
Role role2,
AgentPrincipal principal1,
AgentPrincipal principal2,
LiabilitySink sink,
AuthorityScope scope
) {
SharedMemoryRegistry registry = SharedMemoryRegistry.getInstance();
registry.grantAccess("team-namespace", "agent-1");
registry.grantAccess("team-namespace", "agent-2");
SharedMemoryStore shared = registry.getOrCreate("team-namespace");
Agent agent1 = AgentBuilder.create()
.id("agent-1")
.llm(new AnthropicClient("claude-sonnet-4-20250514"))
.role(role1)
.memoryStore(shared.viewFor("agent-1"))
.principal(principal1)
.liabilitySink(sink)
.authorityScope(scope)
.build();
Agent agent2 = AgentBuilder.create()
.id("agent-2")
.llm(new OpenAIClient("gpt-4o"))
.role(role2)
.memoryStore(shared.viewFor("agent-2"))
.principal(principal2)
.liabilitySink(sink)
.authorityScope(scope)
.build();
return new Agent[] { agent1, agent2 };
}
}@Memory(shared = true, shareWith = ...) sets this up for you and hands each agent its view.
More retrieval tools
com.tnsai.memory.advanced has building blocks for your own retrieval: BM25Index (keyword ranking, shown above), VectorMemoryStore (embedding search), HybridMemoryRetriever (both, merged by reciprocal rank fusion), SkillMemoryStore, MemoryImportanceScorer, StalenessDetector, and SessionSerializer with SessionRestorer. The keyword, vector and hybrid knowledge strategies are built on the first three. An agent's history does not use them. See the Javadoc for their constructors.
Related
- Create an agent: the builder methods used above.
- Long runs: runs that outlive one request.
- Knowledge: ground answers in your files instead of history.
- Annotation catalog:
@Memoryand@VectorMemory.