With the September 2026 release of CAP, CAP now has a first-class plugin for building AI agents directly within CAP applications. You can annotate a CDS service with @AGent, expose it through the A2A protocol, use CAP service capabilities such as actions/functions as tools - all while staying within the CAP application model.
In my last article of building CAP-based Durable Agents, I explained in brief how CAP has most of the foundational capabilities needed for building production-ready agents, such as identity, authorization, multitenancy, persistence, telemetry, audit-logging and lifecycle management. And went on to introduce the @mi8y/cds-langgraph-persistence plugin for durable state management within CAP agents. But it's already obsolete by now 😭, as CAP itself has a built-in Checkpointer mechanism. Nevertheless, the concepts discussed there still apply and provide useful context for understanding the how short-term memory works and how Checkpointer enables it.
But preserving one conversation is different from remembering useful information across multiple conversations.
- (User-specific long-term memory) A Support Agent may need to remember a user's communication preference.
- (Project-specific long-term memory) A Project Assistant may need to recall a decision from an earlier session.
- (Organization-specific long-term memory) A Business Agent may need to retain context learned over several interactions.
In this post, let's narrow the scope and focus on this second persistence layer: long-term memory. We'll see how long-term memory differs from conversation state, how LangGraph's Store model works, and how the new @mi8y/cap-agents-memory add-on brings persistent, cross-session memory to CAP Agents.
CAP Agents are stateful, but what does that mean?
Agents receive messages, invoke tools, accumulate intermediate results, and may pause while waiting for human input. All of this forms the state of a particular agent thread.
The CdsCheckpointer built into CAP Agents persists this state. It allows an agent to continue a conversation, resume an interrupted workflow, and support human-in-the-loop interactions without requiring developers to manually manage the message history.
However, checkpointed state belongs to a particular thread. Consider the following interaction:
Conversation 1
--------------
User: Remember that I'm based out of Walldorf.
Agent: I'll remember that.A few days later, the same user starts a new conversation:
Conversation 2
--------------
User: How long will it take to get to London?
Agent: If you tell me your current location, I can estimate the travel time to London.The new conversation has a different thread and therefore its own checkpoint history. But the user's preferences don't really belong to either conversation. They belong to the user and should remain available across both.
This gives us two complementary kinds of memory:
Checkpointer Long-term Store
| Persists | Agent messages and graph state | Application-defined facts and knowledge |
| Scope | One conversation or workflow thread | Across threads and sessions |
| Typical use | Conversation continuity, resumability, human-in-the-loop | Preferences, profile facts, prior decisions, learned context |
| Access | Automatically through the thread ID | Explicitly from tools, graph nodes, or application code |
A simple way to think about the difference is:
A checkpointer helps an agent continue a conversation. Long-term memory helps it continue a relationship.
What are LangGraph Stores?
@cap-js/agents uses LangChain and LangGraph as part of its agent runtime. LangGraph defines a Store interface specifically for long-term, cross-thread memory.
Each Store item consists of three parts:
- A namespace that groups related memories. (imagine like a directory/folder)
- A key that identifies an item within that namespace. (imagine like a filename within a directory)
- A JSON value containing the information to retain. (imagine like the contents of a file)
For example:
await store.put(["users", "preferences"], "alice", {
preferredTone: "concise",
preferredUnits: "metric",
});
const preferences = await store.get(
["users", "preferences"],
"alice",
);Here, the Store item can be understood as:
Namespace: ["users", "preferences"]
Key: "alice"
Value: {
preferredTone: "concise",
preferredUnits: "metric"
}Unlike checkpoint state, this item isn't tied to a thread_id. Any authorised agent thread that knows the namespace and key can retrieve it.
The Store API also supports deleting items, listing namespaces, batching operations, and searching within a namespace. This makes it suitable for more than simple preferences - you can model memories around users, customers, projects, cases, business objects, or any other domain concept relevant to the agent.
Announcing the cap-agents-memory add-on
To bring LangGraph's long-term Store model into CAP, I'm announcing the NPM package @mi8y/cap-agents-memory.
The package is designed as an add-on for @cap-js/agents and extends it with a persistence layer for application-defined memories that can be shared across agent conversations.
It combines two roles:
- A CDS plugin that provides reusable memory aspects and default persistence entities.
- A LangGraph Store adapter that implements the Store API using CAP CDS queries.
The included CdsMemoryStore supports the standard Store operations:
putandgetmemory items.deleteitems that are no longer needed.searchby namespace, query, and metadata filters.listNamespacesfor discovering memory groups.batchfor executing Store operations together.
It also supports optional LangChain embeddings for vector-assisted retrieval. Without embeddings, query-based search matches stored field values. With embeddings configured, the Store persists vectors alongside the memory fields. The generated CDS vector dimension can be adjusted to match the selected embedding model.
The code is open-source and MIT licensed and available here https://github.com/mi8y/cap-agents-addons/tree/main/packages/cap-agents-memory.
Attaching it to an agent looks like this:
import { CdsMemoryStore } from "@mi8y/cap-agents-memory";
const memory = new CdsMemoryStore({
name: "user_preferences_memory",
});
const agent = createAgent({
model,
tools,
store: memory,
});The name identifies the Store in persistence and allows multiple agents or memory domains to share the same CAP database without colliding.
Getting started with a CAP Agent
Let's add long-term memory to a CAP Agent and let it remember a user's preferences across conversations.
1. Add CAP Agents to your project
If your project doesn't already use CAP Agents, install the plugin:
npm add @cap-js/agentsA CDS service can then be declared as an agent using the @AGent annotation:
@AGent
service AgentService {}See the official CAP-level Agents documentation for the different ways of defining and configuring CAP Agents.
2. Install the memory add-on
npm install @mi8y/cap-agents-memory3. Add the CDS entities
Run the following command in the CAP project:
cds add agent-memoryThis creates db/agent-memory.cds with the default StoreItems and StoreItemFields entities. You can customize the generated model when needed - for eg. to adjust the vector dimension of embedding field to match that of your embedding model.
The entities are then deployed as part of the regular CAP database lifecycle.
4. Add tools for saving and retrieving preferences
The following tools use the authenticated CAP user ID as the Store key and users/preferences as the namespace:
import cds from "@sap/cds";
import { createAgent, tool } from "langchain";
import { CdsMemoryStore } from "@mi8y/cap-agents-memory";
import { z } from "zod";
const saveUserPreferences = tool(
async ({ text }, config) => {
const userId = cds.context.user.id;
await config.store.put(
["users", "preferences"],
userId,
{ pref: text },
);
return `Preferences for user ${userId} saved successfully.`;
},
{
name: "save_user_prefs",
description: "Save user preferences",
schema: z.object({
text: z.string().describe("User preferences to save"),
}),
},
);
const getUserPreferences = tool(
async (_, config) => {
const userId = cds.context.user.id;
const preferences = await config.store.get(
["users", "preferences"],
userId,
);
return preferences
? `Preferences for user ${userId}: ${preferences.value.pref}`
: `No preferences found for user ${userId}.`;
},
{
name: "get_user_prefs",
description: "Get user preferences",
schema: z.object({}),
},
);A few important points:
- The user identity comes from
cds.context.user.id, not from a tool argument selected by the model. - The tools use
config.store, which is the Store attached to the agent graph. - The namespace identifies the type of memory, while the key identifies its owner.
- Writing the same namespace and key again updates that memory item.
5. Attach the Store to the CAP Agent
CAP Agents exposes a buildGraph event that can be used to customise the underlying agent. We can keep the tools, model, system prompt, and middleware supplied by CAP Agents while adding our memory tools and Store:
export class AgentService extends cds.ApplicationService {
init() {
this.on("buildGraph", async () => {
const tools = await this.send("buildTools");
const model = await this.send("buildModel", { tools });
const systemPrompt = await this.send("buildSystemPrompt");
const middleware = await this.send("buildMiddleware", {
tools,
model,
});
const memory = new CdsMemoryStore({
name: "user_preferences_memory",
});
const agent = createAgent({
model,
tools: [
...tools,
saveUserPreferences,
getUserPreferences,
],
systemPrompt,
middleware,
store: memory,
});
return agent.graph;
});
return super.init();
}
}The CAP Agent runtime still provides the model, generated tools, system prompt, middleware, A2A endpoint, and checkpointing. The memory add-on contributes only the cross-session Store and the tools that use it.
6. See cross-session memory in action
Start the CAP application and open the agent preview:
cds watchIn the first conversation, ask the agent to remember a preference:
User: Remember that I prefer pizzas.
Agent: I'll remember that you prefer pizzas.Now start a completely new conversation and ask:
User: What food do I prefer?
Agent: You prefer pizzas.The second conversation has a different agent thread, so the answer doesn't come from the first conversation's checkpoint. The preference is retrieved from the long-term Store using the authenticated user's ID.
If another user asks the same question, that user gets a separate memory item because the Store key is different. If the application is multitenant, CAP additionally routes each tenant to its own persistence context.
You can find the complete runnable example in examples/cap-agents-memory.
Advanced: Customising the memory model
The entities generated by cds add agent-memory are useful defaults, but the memory model can be adapted to the application.
The package exports the StoreItem and StoreItemField CDS aspects. You can implement these aspects with custom entities and add application-specific fields, annotations, or vector dimensions:
using { StoreItem, StoreItemField }
from '@mi8y/cap-agents-memory';
namespace my.app.memory;
entity Items : StoreItem {
fields : Composition of many Fields
on fields.item = $self;
}
entity Fields : StoreItemField {
item : Association to Items
on item.graphName = $self.graphName
and item.namespace = $self.namespace
and item.id = $self.id;
embedding : Vector(3072); // <--- you can customize this as per your embedding model
}Configure the Store with the fully qualified entity names:
const memory = new CdsMemoryStore({
name: "support-memory",
fqnStoreItemsEntity: "my.app.memory.Items",
fqnStoreItemFieldsEntity: "my.app.memory.Fields",
});This keeps the Store API independent of the physical memory model while still allowing the CDS schema to follow application requirements.
Practical best practices for long-term memory
Once an agent can remember across sessions, a few patterns help keep that memory useful and safe.
Be deliberate about what becomes memory
A Store persists what the application writes; it doesn't decide whether a fact is useful or correct. Define clear rules for which information should be retained and which should remain inside the current conversation.Use a consistent namespace strategy
Namespaces become part of the application's memory architecture. Document a convention before multiple tools or agents begin writing data. For example:["users", userId, "preferences"] ["customers", customerId, "context"] ["projects", projectId, "decisions"]Use deterministic keys for changing facts
If a value changes over time, write it using a stable key so a newer value replaces the older one:
await store.put(
["users", userId, "preferences"],
"response-style",
{ value: "concise" },
);This avoids accumulating contradictory memories such as both "prefers detailed answers" and "prefers concise answers."
Derive identity from the CAP context
Avoid allowing the model or client to choose which user's memory to access. Use authenticated identity and enforce any additional authorization required by the business domain.Plan memory lifecycle and deletion
Long-term doesn't necessarily mean permanent. Consider expiration, deletion on user request, cleanup for inactive users, and removal of superseded or obsolete memories.Treat memory as potentially sensitive data
Memories may contain preferences, profile information, or business context. Apply data minimisation (through PII removal), purpose limitation, authorization, and the same DPP compliance controls used for other application data.Retrieve only what is relevant
Injecting every stored memory into the model context increases token usage and can reduce answer quality. Use Store API provided ways such as namespaces, metadata filters, limits, and - where appropriate - query-based retrieval to select a small relevant set.Evaluate recall, not just persistence
Test whether the correct memory was stored, whether it can be retrieved for a relevant request, whether unrelated memories are excluded, and whether the agent uses the retrieved information appropriately. Also verify that one user or project cannot access another's memory.
@cap-js/agents provides the agent runtime and the state of the current conversation. This add-on (@mi8y/cap-agents-memory) gives that agent something useful to remember when the next conversation begins.
Disclaimer: This article's narrative was defined and type-checked by AI.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.