# 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:

<!-- java-contract: src/main/java/com/example/tnsai/docs/MemoryTopicAnnotationsExample.java -->
```java
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:

<!-- java-contract: src/main/java/com/example/tnsai/docs/MemoryTopicBuilderExample.java -->
```java
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` 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](../annotations/catalog.md#6-memory).

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

<!-- java-contract: src/main/java/com/example/tnsai/docs/MemoryStoreExample.java -->
```java
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:

<!-- java-contract: src/main/java/com/example/tnsai/docs/MemoryBuilderIntegrationExample.java -->
```java
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](../rag/index.md) are built on the first three. An agent's history does not use them. See the Javadoc for their constructors.

## Related

- [Create an agent](create.md): the builder methods used above.
- [Long runs](long-runs.md): runs that outlive one request.
- [Knowledge](../rag/index.md): ground answers in your files instead of history.
- [Annotation catalog](../annotations/catalog.md#6-memory): `@Memory` and `@VectorMemory`.
