Agent Memory をエージェントに追加すると、会話をまたいで永続的なコンテキストを思い出せます。
このガイドでは Agents SDK と Session API を使い、メモリのリコールをモデルから呼び出せるツールとして公開します。別のエージェントフレームワークでも同じパターンです。ingest() または remember() でメモリを保存し、エージェントのツールのひとつから recall() を公開し、いつメモリを検索するかをシステムプロンプトでモデルに伝えます。
- Cloudflare アカウント ↗ に登録します。
Node.js↗ をインストールします。
Node.js のバージョンマネージャー
権限の問題を避け、Node.js のバージョンを切り替えられるよう、Volta ↗ や nvm ↗ などの Node バージョンマネージャーを使います。このガイドの後半で説明する Wrangler には、Node バージョン 16.17.0 以降が必要です。
Agent Memory へのアクセスも必要です。
モデルが回答や操作に関連メモリを必要とするときは recall() を使います。会話メッセージがあり、Agent Memory に永続メモリを自動抽出させたいときは ingest() を使います。保存するメモリの内容がすでに分かっているときは remember() を使います。
モデルのターンごとに ingest() を呼ばないでください。代わりに、ユーザーがアイドルになったあと、会話がコンパクト化されたとき、またはほかの自然なチェックポイントで、取り込みをまとめて実行します。
Worker プロジェクトを作成します。
npm create cloudflare@latest -- memory-agentyarn create cloudflare memory-agentpnpm create cloudflare@latest memory-agentセットアップでは、次のオプションを選びます。
- What would you like to start with? では、
Hello World exampleを選びます。 - Which template would you like to use? では、
Worker onlyを選びます。 - Which language do you want to use? では、
TypeScriptを選びます。 - Do you want to use git for version control? では、
Yesを選びます。 - Do you want to deploy your application? では、
Noを選びます(デプロイ前にいくつか変更します)。
プロジェクトディレクトリに移動します。
cd memory-agentこのガイドで使う依存関係をインストールします。
npm i agents ai workers-ai-provideryarn add agents ai workers-ai-providerpnpm add agents ai workers-ai-providerbun add agents ai workers-ai-provider名前空間 は、アプリケーションのメモリプロファイルをスコープします。Wrangler で作成します。
npx wrangler agent-memory namespace create my-agentyarn wrangler agent-memory namespace create my-agentpnpm wrangler agent-memory namespace create my-agentWorker のバインディングでは、この名前空間名 my-agent を使います。
Wrangler 設定に agent_memory バインディングを追加します。Agents SDK を使う場合は、エージェントの Durable Object も登録します。
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "memory-agent",
"main": "src/server.ts",
// Set this to today's date
"compatibility_date": "2026-09-20",
"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"
]
}
]
}name = "memory-agent"
main = "src/server.ts"
# Set this to today's date
compatibility_date = "2026-09-20"
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"]バインディング用のローカル TypeScript 型を生成します。
npx wrangler typesyarn wrangler typespnpm wrangler typesアプリケーションにメモリバインディングがあるだけでは、モデルはメモリを使えません。リコールをツールとして公開し、いつ呼び出すかをモデルに指示する必要があります。
Agents SDK の Session API では、検索可能なコンテキストプロバイダーを追加します。Session は、プロバイダーの search() メソッドをモデル向けの search_context ツールにします。
src/server.ts を作成し、リコールの設定を追加します。
import { Agent, routeAgentRequest } from "agents";
import { Session } from "agents/experimental/memory/session";
const INSTRUCTIONS = "You are a helpful assistant.";
const MEMORY_CONTEXT = `Long-term memory is available through the search_context tool.
MEMORY POLICY
- Search memory with search_context 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_context 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 Agent {
initialState = { cursor: 0, nextIngestAt: null };
session = Session.create(this)
.withContext("instructions", {
provider: { get: async () => INSTRUCTIONS },
})
.withContext("memory", {
description:
"Searchable durable memory: facts, events, instructions, and tasks from prior conversations.",
provider: {
get: async () => MEMORY_CONTEXT,
search: 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.";
},
},
})
.withCachedPrompt();
}
export default {
async fetch(request, env) {
return (
(await routeAgentRequest(request, env)) ??
new Response("Not found", { status: 404 })
);
},
};import { Agent, routeAgentRequest } from "agents";
import { Session } from "agents/experimental/memory/session";
const INSTRUCTIONS = "You are a helpful assistant.";
const MEMORY_CONTEXT = `Long-term memory is available through the search_context tool.
MEMORY POLICY
- Search memory with search_context 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_context 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";
type ChatAgentState = {
cursor: number;
nextIngestAt: number | null;
};
export class ChatAgent extends Agent<Env, ChatAgentState> {
initialState: ChatAgentState = { cursor: 0, nextIngestAt: null };
session = Session.create(this)
.withContext("instructions", {
provider: { get: async () => INSTRUCTIONS },
})
.withContext("memory", {
description:
"Searchable durable memory: facts, events, instructions, and tasks from prior conversations.",
provider: {
get: async () => MEMORY_CONTEXT,
search: async (query: string) => {
const profile = await this.env.MEMORY.getProfile(MEMORY_PROFILE_NAME);
const { answer } = await profile.recall(query, {
responseLength: "short",
});
return answer || "No relevant memories found.";
},
},
})
.withCachedPrompt();
}
export default {
async fetch(request: Request, env: Env) {
return (
(await routeAgentRequest(request, env)) ??
new Response("Not found", { status: 404 })
);
},
} satisfies ExportedHandler<Env>;システムプロンプトはツールと同じくらい重要です。search_context をいつ呼び出すか、いつ呼ばないか、リコールしたメモリをどう扱うかをモデルに伝えます。
次に、永続メモリを追加する手段をエージェントに与えます。チャットエージェントでは、会話を Session に保存し、ユーザーがアイドルになったあとで ingest() を呼ぶのが一般的です。
agents の import を変更し、AI SDK の import を追加します。ステップ 4 の Session import はそのまま残します。
import { Agent, getAgentByName, routeAgentRequest } from "agents";
import { convertToModelMessages, generateText, stepCountIs } from "ai";
import { createWorkersAI } from "workers-ai-provider";import { Agent, getAgentByName, routeAgentRequest } from "agents";
import { convertToModelMessages, generateText, stepCountIs } from "ai";
import type { UIMessage } from "ai";
import { createWorkersAI } from "workers-ai-provider";ファイル先頭の import の下に、取り込みの遅延を追加します。
const MEMORY_INGEST_DELAY_SECONDS = 10;const MEMORY_INGEST_DELAY_SECONDS = 10;次に、ChatAgent を次の形に更新します。コメントは、ステップ 4 の Session 設定を残す位置を示します。
export class ChatAgent extends Agent {
initialState = { cursor: 0, nextIngestAt: null };
// Keep the `session = Session.create(this)` setup from step 4 here.
async chat(message) {
const userMessage = {
id: `user-${crypto.randomUUID()}`,
role: "user",
parts: [{ type: "text", text: message }],
};
await this.session.appendMessage(userMessage);
await this.scheduleIngest();
const workersai = createWorkersAI({ binding: this.env.AI });
const result = await generateText({
model: workersai("@cf/zai-org/glm-4.7-flash"),
system: await this.session.freezeSystemPrompt(),
messages: await convertToModelMessages(await this.session.getHistory()),
tools: await this.session.tools(),
stopWhen: stepCountIs(5),
});
const assistantMessage = {
id: `assistant-${crypto.randomUUID()}`,
role: "assistant",
parts: [{ type: "text", text: result.text }],
};
await this.session.appendMessage(assistantMessage);
return result.text;
}
async ingestScheduledMemory() {
await this.runIngest();
}
async scheduleIngest() {
await this.cancelPendingIngest();
await this.schedule(
MEMORY_INGEST_DELAY_SECONDS,
"ingestScheduledMemory",
{},
);
this.setState({
...this.state,
nextIngestAt: Date.now() + MEMORY_INGEST_DELAY_SECONDS * 1000,
});
}
async cancelPendingIngest() {
const pending = await this.listSchedules();
for (const schedule of pending) {
if (schedule.callback === "ingestScheduledMemory") {
await this.cancelSchedule(schedule.id);
}
}
}
async runIngest() {
const history = await this.session.getHistory();
const messages = history
.slice(this.state.cursor)
.filter(
(message) => message.role === "user" || message.role === "assistant",
)
.map((message) => ({
role: message.role,
content: message.parts
.map((part) => (part.type === "text" ? part.text : ""))
.filter(Boolean)
.join("\n\n"),
}))
.filter((message) => message.content);
if (messages.length === 0) {
this.setState({ ...this.state, nextIngestAt: null });
return { ingested: 0 };
}
const profile = await this.env.MEMORY.getProfile(MEMORY_PROFILE_NAME);
await profile.ingest(messages, { sessionId: this.name });
this.setState({
...this.state,
cursor: history.length,
nextIngestAt: null,
});
return { ingested: messages.length };
}
}export class ChatAgent extends Agent<Env, ChatAgentState> {
initialState: ChatAgentState = { cursor: 0, nextIngestAt: null };
// Keep the `session = Session.create(this)` setup from step 4 here.
async chat(message: string): Promise<string> {
const userMessage: UIMessage = {
id: `user-${crypto.randomUUID()}`,
role: "user",
parts: [{ type: "text", text: message }],
};
await this.session.appendMessage(userMessage);
await this.scheduleIngest();
const workersai = createWorkersAI({ binding: this.env.AI });
const result = await generateText({
model: workersai("@cf/zai-org/glm-4.7-flash"),
system: await this.session.freezeSystemPrompt(),
messages: await convertToModelMessages(
(await this.session.getHistory()) as UIMessage[],
),
tools: await this.session.tools(),
stopWhen: stepCountIs(5),
});
const assistantMessage: UIMessage = {
id: `assistant-${crypto.randomUUID()}`,
role: "assistant",
parts: [{ type: "text", text: result.text }],
};
await this.session.appendMessage(assistantMessage);
return result.text;
}
async ingestScheduledMemory() {
await this.runIngest();
}
private async scheduleIngest() {
await this.cancelPendingIngest();
await this.schedule(
MEMORY_INGEST_DELAY_SECONDS,
"ingestScheduledMemory",
{},
);
this.setState({
...this.state,
nextIngestAt: Date.now() + MEMORY_INGEST_DELAY_SECONDS * 1000,
});
}
private async cancelPendingIngest() {
const pending = await this.listSchedules();
for (const schedule of pending) {
if (schedule.callback === "ingestScheduledMemory") {
await this.cancelSchedule(schedule.id);
}
}
}
private async runIngest(): Promise<{ ingested: number }> {
const history = (await this.session.getHistory()) as UIMessage[];
const messages = history
.slice(this.state.cursor)
.filter(
(message) => message.role === "user" || message.role === "assistant",
)
.map((message) => ({
role: message.role,
content: message.parts
.map((part) => (part.type === "text" ? part.text : ""))
.filter(Boolean)
.join("\n\n"),
}))
.filter((message) => message.content);
if (messages.length === 0) {
this.setState({ ...this.state, nextIngestAt: null });
return { ingested: 0 };
}
const profile = await this.env.MEMORY.getProfile(MEMORY_PROFILE_NAME);
await profile.ingest(messages, { sessionId: this.name });
this.setState({
...this.state,
cursor: history.length,
nextIngestAt: null,
});
return { ingested: messages.length };
}
}default export を、小さなテスト用エンドポイントに置き換えます。各 conversationId は、独自の Session 履歴を持つ別の Agent インスタンスに対応します。
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (request.method === "POST" && url.pathname === "/chat") {
const { message, conversationId = "default" } = await request.json();
if (!message) {
return Response.json({ error: "Missing message" }, { status: 400 });
}
const agent = await getAgentByName(env.ChatAgent, conversationId);
const response = await agent.chat(message);
return Response.json({ response });
}
return (
(await routeAgentRequest(request, env)) ??
new Response("Not found", { status: 404 })
);
},
};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 = await getAgentByName(env.ChatAgent, 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>;取り込みの経路は scheduleIngest() と runIngest() です。ユーザーメッセージごとに、保留中の取り込みをキャンセルして新しい取り込みをスケジュールします。そのため Agent Memory は、エージェントのターンごとではなく、ユーザーがアイドルになったあとに会話を処理します。
runIngest() はカーソルを使い、まだ取り込んでいないメッセージだけを各バッチに含めます。sessionId は、共有メモリプロファイル内で会話ごとにメモリをグループ化します。
このデモでは、複数の会話で 1 つの Agent Memory プロファイル demo-user を使います。本番では、ユーザー、チーム、テナント、組織など、アプリケーションのスコープに合うプロファイル名を選びます。
Session のコンパクションフックから取り込みロジックを呼ぶこともできます。重要な制約は、エージェントのターンごとではなく、自然なチェックポイントでまとめて取り込むことです。本番では、アプリケーションのユーザー体験に合った取り込み遅延を選びます。
多くのアプリでは、自動取り込みで十分です。モデルに特定のメモリをすぐ保存させたい場合は、execute 関数が remember() を呼ぶサーバーサイドツールを追加します。
保存するメモリの内容がすでに分かっているときに使います。たとえば、ユーザーが「簡潔な回答を好むことを覚えておいて」と言ったあと、モデルが rememberMemory ツールを呼ぶことがあります。
モデルがメモリ書き込みツールを呼べる場合は、何を覚える価値があるか、いつ確認を取るかを定義するシステムプロンプトの指示を追加します。多くのエージェントでは、モデルに直接のメモリ書き込みツールを渡すより、会話の自動取り込みのほうが簡単で安全です。
ローカル開発を開始します。
npx wrangler devyarn wrangler devpnpm wrangler dev最初の会話で、永続的な好みを覚えてもらうよう依頼します。
curl -X POST "http://localhost:8787/chat" \
-H "Content-Type: application/json" \
-d '{"conversationId":"first-chat","message":"I prefer TypeScript examples and concise answers."}'次のリクエストを送る前に、少なくとも 30 秒待ちます。コードは、ユーザーがアイドルになってから 10 秒後に取り込みを実行するようスケジュールします。そのあと Agent Memory は、抽出、分類、インデックス作成に追加の時間を必要とし、完了してからリコールできるようになります。
別の会話で、永続メモリに依存する質問をします。このリクエストは Session 履歴は異なりますが、同じ Agent Memory プロファイルを使います。
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?"}'モデルは search_context を呼び、Agent Memory からリコールしたメモリを受け取り、そのコンテキストを応答に使います。2 つ目の会話は 1 つ目と Session 履歴を共有しないため、ユーザーの好みに関する知識は Agent Memory から来ます。