Memory
AgentEnsemble v2.0.0 introduces task-scoped cross-execution memory, allowing agents to
accumulate and recall information across separate Ensemble.run() invocations. Memory is
organized into named scopes and backed by a pluggable MemoryStore.
Quick Start
Section titled “Quick Start”// Create a store -- use inMemory() for dev/testingMemoryStore store = MemoryStore.inMemory();
// Tasks declare which scopes they read from and write toTask researchTask = Task.builder() .description("Research current AI trends") .expectedOutput("A research report") .agent(researcher) .memory("ai-research") // declares the "ai-research" scope .build();
// Wire the store to the ensembleEnsembleOutput output = Ensemble.builder() .agent(researcher) .task(researchTask) .memoryStore(store) .build() .run();After the run, store.retrieve("ai-research", ...) will return the task’s output.
On a second run with the same store, the agent’s prompt will include entries from the first run.
How It Works
Section titled “How It Works”-
At task startup: The framework retrieves entries from every declared scope and injects them into the agent’s prompt as
## Memory: {scope}sections. -
At task completion: The framework stores the task output into every declared scope.
-
Cross-run persistence: Because entries are stored in the
MemoryStore(not discarded between runs), agents in later runs automatically see outputs from earlier runs.
Declaring Scopes on Tasks
Section titled “Declaring Scopes on Tasks”Three builder overloads are available:
// Single scope by nameTask.builder() .description("Research AI trends") .memory("research") .build()
// Multiple scopes by nameTask.builder() .description("Write summary") .memory("research", "draft-history") .build()
// Fully configured scope with evictionTask.builder() .description("Research AI trends") .memory(MemoryScope.builder() .name("research") .keepLastEntries(10) // retain only the 10 most recent entries .build()) .build()MemoryStore Implementations
Section titled “MemoryStore Implementations”In-Memory (development and testing)
Section titled “In-Memory (development and testing)”MemoryStore store = MemoryStore.inMemory();Entries are accumulated in insertion order per scope. Retrieval returns the most recent
entries (no semantic similarity search). Suitable for development and single-JVM runs.
Entries do not survive JVM restarts; reuse the same instance across multiple
ensemble.run() calls to simulate cross-run persistence in tests.
Embedding-Based (production)
Section titled “Embedding-Based (production)”EmbeddingModel embeddingModel = OpenAiEmbeddingModel.builder() .apiKey(System.getenv("OPENAI_API_KEY")) .modelName("text-embedding-3-small") .build();
// Use any LangChain4j EmbeddingStore for durabilityEmbeddingStore<TextSegment> embeddingStore = ChromaEmbeddingStore.builder() .baseUrl("http://localhost:8000") .collectionName("agentensemble-memory") .build();
MemoryStore store = MemoryStore.embeddings(embeddingModel, embeddingStore);Entries are embedded on storage and retrieved via semantic similarity search. The backing
EmbeddingStore controls durability (in-memory, Chroma, Qdrant, Pinecone, pgvector, etc.).
Scope Isolation
Section titled “Scope Isolation”A task can only read from scopes it explicitly declares. If task A stores output in
"research" and task B declares only "drafts", task B’s prompt will not contain
task A’s output.
// Task A: stores into "research"Task taskA = Task.builder() .description("Research confidential data") .memory("research") .build();
// Task B: only declares "drafts" -- cannot see "research" entriesTask taskB = Task.builder() .description("Write public article") .memory("drafts") .build();Eviction Policies
Section titled “Eviction Policies”MemoryScope supports optional eviction to keep scope sizes bounded:
// Retain only the 5 most recent entriesMemoryScope.builder() .name("research") .keepLastEntries(5) .build()
// Retain only entries stored within the past 7 daysMemoryScope.builder() .name("research") .keepEntriesWithin(Duration.ofDays(7)) .build()Eviction is applied after each task stores its output. For MemoryStore.embeddings(),
eviction is a no-op (embedding stores generally do not support deletion of individual entries).
MemoryTool: Explicit Agent Access
Section titled “MemoryTool: Explicit Agent Access”Agents can also interact with memory directly during their ReAct loop using MemoryTool:
MemoryStore store = MemoryStore.inMemory();
Agent researcher = Agent.builder() .role("Researcher") .goal("Research and remember important facts") .tools(MemoryTool.of("research", store)) .llm(llm) .build();MemoryTool provides two tool methods the LLM can call:
storeMemory(key, value)— store an arbitrary factretrieveMemory(query)— retrieve relevant memories by query
When the same MemoryStore instance is used for both MemoryTool and
Ensemble.builder().memoryStore(...), explicit tool access and automatic scope-based
access share the same backing store.
Cross-Run Persistence Example
Section titled “Cross-Run Persistence Example”MemoryStore store = MemoryStore.inMemory(); // reused across runs
Task researchTask = Task.builder() .description("Research AI trends for week {week}") .expectedOutput("Weekly AI intelligence briefing") .agent(analyst) .memory("weekly-research") .build();
Ensemble ensemble = Ensemble.builder() .agent(analyst) .task(researchTask) .memoryStore(store) .build();
// Week 1 -- no prior entriesensemble.run(Map.of("week", "2026-01-06"));
// Week 2 -- analyst sees Week 1 output in "## Memory: weekly-research" sectionensemble.run(Map.of("week", "2026-01-13"));
// Week 3 -- analyst sees both Week 1 and Week 2 outputsensemble.run(Map.of("week", "2026-01-20"));See the full example.
Multiple Tasks Sharing a Scope
Section titled “Multiple Tasks Sharing a Scope”Multiple tasks can declare the same scope name. Each task writes its output to the scope after it completes, so later tasks (in sequential workflow) see earlier tasks’ outputs.
MemoryStore store = MemoryStore.inMemory();
Task research = Task.builder() .description("Research AI trends") .memory("ai-project") .build();
Task analysis = Task.builder() .description("Analyse the research findings") .memory("ai-project") // also declares "ai-project" -- sees research output .build();
Ensemble.builder() .task(research) .task(analysis) .memoryStore(store) .build() .run();Memory in Prompts
Section titled “Memory in Prompts”When a task has declared scopes and the scope has prior entries, the agent’s user prompt contains a section for each scope:
## Memory: ai-projectThe following information from scope "ai-project" may be relevant:
---Research findings from previous run: AI is accelerating in healthcare and finance...---
## TaskAnalyse the research findings