Skip to content
tnsaiDocs
GitHub

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 @Memory or @VectorMemory wins 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 fieldEffectStatus
typeDeclares a memory flavor, but does not change store selection.not implemented
capacityWraps the store with a positive entry limit unless pruning is NONE. 0 is unbounded.live
pruneStrategySelects sliding-window, relevance, summary, token-limit, or no pruning.live
persistenceSelects in-memory/file storage or optional Redis/database discovery.partial
maxContextTokensAdds token-aware pruning and supplies the agent context limit.live
includeSystemMessagesIs copied into config but has no runtime consumer.not implemented
summarizeOnPruneWraps the store with summarization before capacity pruning.live
connectionUrlIs 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

MethodWhat 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

StoreUse it for
InMemoryStoreTests and short conversations. The default.
FileMemoryStoreHistory that survives a restart. It writes <dir>/<agentId>.json after every change; the default directory is .tnsai/memory.
SharedMemoryStoreOne 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.

MethodWhat 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.