Get started
- Prerequisites
- How agent memory works
- 1. Create a project
- 2. Create a namespace
- 3. Configure bindings
- 4. Add memory recall as a tool
- 5. Extract memories from conversation
- 6. (Optional) Store explicit memories when needed
- 7. Test the app
- Next steps
import { LinkCard, PackageManagers, Render, TypeScriptExample, WranglerConfig, } from "~/components";
Add Agent Memory to an agent so it can recall durable context across conversations.
This guide uses the Agents SDK to expose memory recall as a model-callable tool. The same pattern applies if you use another agent framework: store memories with ingest() or remember(), expose recall() through one of your agent's tools, and use the system prompt to tell the model when to search memory.
Prerequisites
You also need access to Agent Memory.
How agent memory works
Use recall() when the model needs relevant memory to answer or act. Use ingest() when you have conversation messages and want Agent Memory to extract durable memories automatically. Use remember() when your agent already knows the exact memory to store.
Do not call ingest() after every model turn. Instead, batch ingestion after the user goes idle, when a conversation is compacted, or at another natural checkpoint.
1. Create a project
Create a Worker project:
<Render file="c3-post-run-steps-hello-world" product="workers" params={{ type: "Worker only", lang: "TypeScript", }} />
Move into the project directory:
cd memory-agent
Install the dependencies used by this guide:
2. Create a namespace
A namespace scopes the memory profiles for your application. Create one with Wrangler:
You will use the namespace name, my-agent, in your Worker binding.
3. Configure bindings
Add an agent_memory binding to your Wrangler configuration, and register your agent's Durable Object.
name = "memory-agent"
main = "src/server.ts"
upload_source_maps = true
compatibility_date = "$today"
compatibility_flags = ["nodejs_compat"]
[ai]
binding = "AI"
[[agent_memory]]
binding = "MEMORY"
namespace = "my-agent"
[[durable_objects.bindings]]
name = "ChatAgent"
class_name = "ChatAgent"
[[migrations]]
tag = "v1"
new_sqlite_classes = ["ChatAgent"]
Generate local TypeScript types for your bindings:
4. Add memory recall as a tool
The model cannot use memory just because your application has a memory binding. You need to expose recall through a tool and instruct the model when to call it.
This guide builds the agent on a Durable Object with the Agents SDK Lifecycle. The Sessions capability stores the conversation. agents/sessions is experimental, so its API may change between releases.
Create src/server.ts. It defines the system prompt and a search_memory tool that calls recall():
import { DurableObject } from "cloudflare:workers";
import { tool } from "ai";
import { z } from "zod";
import { Lifecycle } from "agents/lifecycle";
import { routeAgentRequest } from "agents/routing";
import { Sessions } from "agents/sessions";
const SYSTEM_PROMPT = `You are a helpful assistant.
Long-term memory is available through the search_memory tool.
MEMORY POLICY
- Search memory with search_memory when the user asks what you know or remember about them.
- Search memory when the request depends on prior sessions, preferences, project state, conventions, decisions, or long-running tasks.
- Phrase memory searches as concise topics, not questions.
- Do not search memory to repeat something the user just said in the current conversation.
- When search_memory returns results, always incorporate them into your response. The results are real memories from previous conversations.
- Treat recalled memories as helpful context, not guaranteed truth. If a memory is important for an irreversible action, confirm with the user.`;
const MEMORY_PROFILE_NAME = "demo-user";
export class ChatAgent extends DurableObject<Env> {
readonly sessions = new Sessions();
readonly lifecycle = Lifecycle.install(this).use(this.sessions);
readonly session = this.sessions.session();
memoryTools() {
return {
search_memory: tool({
description:
"Search durable memory: facts, events, instructions, and tasks from prior conversations.",
inputSchema: z.object({
query: z.string().describe("A concise topic to search for"),
}),
execute: async ({ query }) => {
const profile = await this.env.MEMORY.getProfile(MEMORY_PROFILE_NAME);
const { answer } = await profile.recall(query, {
responseLength: "short",
});
return answer || "No relevant memories found.";
},
}),
};
}
}
export default {
async fetch(request: Request, env: Env) {
return (
(await routeAgentRequest(request, env)) ??
new Response("Not found", { status: 404 })
);
},
} satisfies ExportedHandler<Env>;
The system prompt is as important as the tool. It tells the model when to call search_memory, when not to call it, and how to treat recalled memory. The prompt is the same on every turn, so the model provider can keep it in its prompt cache.
5. Extract memories from conversation
Next, give your agent a way to add durable memories. In a chat agent, the usual path is to store the conversation in a session, then call ingest() after the user goes idle. The Scheduler capability runs the ingest later, even if the Durable Object is evicted in the meantime.
Replace the imports at the top of src/server.ts:
import { DurableObject } from "cloudflare:workers";
import {
convertToModelMessages,
generateText,
stepCountIs,
tool,
type UIMessage,
} from "ai";
import { z } from "zod";
import { Lifecycle } from "agents/lifecycle";
import { createAI } from "agents/models/ai-sdk";
import { routeAgentRequest } from "agents/routing";
import { Scheduler } from "agents/schedules";
import { Sessions } from "agents/sessions";
Add the ingestion delay near the top of the file, after the imports:
const MEMORY_INGEST_DELAY_SECONDS = 10;
Then update ChatAgent with the following shape. The comment marks where to keep memoryTools() from step 4.
export class ChatAgent extends DurableObject<Env> {
readonly sessions = new Sessions();
readonly scheduler = new Scheduler({
callbacks: {
ingestMemory: () => this.runIngest(),
},
});
readonly lifecycle = Lifecycle.install(this)
.use(this.sessions)
.use(this.scheduler);
readonly session = this.sessions.session();
// Keep `memoryTools()` from step 4 here.
async chat(message: string): Promise<string> {
// RPC calls do not start the Lifecycle on their own.
await this.lifecycle.start();
await this.session.appendMessage({
id: `user-${crypto.randomUUID()}`,
role: "user",
parts: [{ type: "text", text: message }],
});
await this.scheduleIngest();
const ai = createAI({ binding: this.env.AI });
const result = await generateText({
model: ai("@cf/zai-org/glm-5.3-flash"),
system: SYSTEM_PROMPT,
messages: await convertToModelMessages(
(await this.session.getHistory()) as UIMessage[],
),
tools: this.memoryTools(),
stopWhen: stepCountIs(5),
});
await this.session.appendMessage({
id: `assistant-${crypto.randomUUID()}`,
role: "assistant",
parts: [{ type: "text", text: result.text }],
});
return result.text;
}
private async scheduleIngest() {
// Replace any pending ingest, so it runs once the user goes idle.
for (const schedule of await this.scheduler.list()) {
if (schedule.callback === "ingestMemory") {
await this.scheduler.cancel(schedule.id);
}
}
await this.scheduler.set(MEMORY_INGEST_DELAY_SECONDS, "ingestMemory");
}
private async runIngest(): Promise<{ ingested: number }> {
const cursor = (await this.ctx.storage.get<number>("ingestCursor")) ?? 0;
const history = await this.session.getHistory();
const messages = history
.slice(cursor)
.filter(
(message) => message.role === "user" || message.role === "assistant",
)
.map((message) => ({
role: message.role as "user" | "assistant",
content: message.parts
.map((part) =>
part.type === "text" && typeof part.text === "string"
? part.text
: "",
)
.filter(Boolean)
.join("\n\n"),
}))
.filter((message) => message.content);
if (messages.length > 0) {
const profile = await this.env.MEMORY.getProfile(MEMORY_PROFILE_NAME);
await profile.ingest(messages, { sessionId: this.lifecycle.name });
}
await this.ctx.storage.put("ingestCursor", history.length);
return { ingested: messages.length };
}
}
Replace the default export with a small test endpoint. Each conversationId maps to a separate Durable Object with its own session history. The Worker calls chat() over RPC:
export default {
async fetch(request: Request, env: Env) {
const url = new URL(request.url);
if (request.method === "POST" && url.pathname === "/chat") {
const { message, conversationId = "default" } =
(await request.json()) as {
message?: string;
conversationId?: string;
};
if (!message) {
return Response.json({ error: "Missing message" }, { status: 400 });
}
const agent = env.ChatAgent.getByName(conversationId);
const response = await agent.chat(message);
return Response.json({ response });
}
return (
(await routeAgentRequest(request, env)) ??
new Response("Not found", { status: 404 })
);
},
} satisfies ExportedHandler<Env>;
The ingestion path is scheduleIngest() and runIngest(). Each user message cancels the previous pending ingest and schedules a new one, so Agent Memory processes the conversation after the user goes idle instead of after every agent turn.
runIngest() keeps a cursor in the Durable Object's storage, so each batch only includes messages that have not already been ingested. The sessionId groups memories by conversation inside the shared memory profile.
This demo uses one Agent Memory profile, demo-user, across multiple conversations. In production, choose profile names that match your application scope, such as users, teams, tenants, or organizations.
You can also run the ingestion logic when a session is compacted. The important constraint is to ingest in batches at natural checkpoints, not after every agent turn. In production, choose an ingest delay that matches your application's user experience.
6. (Optional) Store explicit memories when needed
Automatic ingestion is enough for most apps. If you want the model to store a specific memory immediately, add a server-side tool whose execute function calls remember().
Use this when the agent already knows the exact memory to store. For example, the model might call a rememberMemory tool after the user says: "Remember that I prefer concise answers."
If the model can call a memory-write tool, add system prompt instructions that define what is worth remembering and when to ask for confirmation. For many agents, automatic conversation ingestion is simpler and safer than giving the model a direct memory-write tool.
7. Test the app
Start local development:
Ask the first conversation to remember a durable preference:
curl -X POST "http://localhost:8787/chat" \
-H "Content-Type: application/json" \
-d '{"conversationId":"first-chat","message":"I prefer TypeScript examples and concise answers."}'
Wait at least 30 seconds before sending the next request. The code schedules ingestion to run 10 seconds after the user goes idle, and Agent Memory then needs additional time to extract, classify, and index the memories before they are available for recall.
Ask a different conversation a question that depends on durable memory. This request uses a different session history but the same Agent Memory profile.
curl -X POST "http://localhost:8787/chat" \
-H "Content-Type: application/json" \
-d '{"conversationId":"second-chat","message":"What do you know or remember about me and my preferences?"}'
The model should call search_memory, receive recalled memory from Agent Memory, and use that context in its response. The second conversation has no shared session history with the first, so any knowledge of user preferences comes from Agent Memory.