エージェントが時間をかけて役立つには、メモリが必要です。メモリがなければ、会話は毎回ゼロから始まります。エージェントはユーザーが誰か、何を学んだか、何をしていたかを忘れます。メモリがあることで、状態を持たない LLM 呼び出しが、持続的でコンテキストを把握したエージェントになります。
Session API は、Cloudflare Agents SDK 上のエージェント向けメモリ層です。メモリは 2 種類あります。会話履歴(セッションを構成するメッセージとツール呼び出し)と コンテキストメモリ(システムプロンプトに注入され、エージェントが読み取り、書き込み、検索、読み込みできる永続ブロック)です。
単純な同期状態や平坦なチャット履歴以上が必要なときに、このページを使います。小さな UI 状態には 状態の保存と同期 を使います。基本的なチャット永続化なら、AIChatAgent がメッセージを保存します。方針付きの長期メモリなら、Think が Session とコンテキストブロックの上に構築されています。
最も基本的なメモリは会話そのものです。ユーザーとエージェントのメッセージ、エージェントが行ったツール呼び出し、受け取った結果です。Session はこれらを、Session Provider(デフォルトは SQLite)に裏打ちされた木構造のメッセージ履歴に保存します。
import { Session } from "agents/experimental/memory/session";
// Append messages as the conversation progresses
await session.appendMessage({
id: `user-${crypto.randomUUID()}`,
role: "user",
parts: [{ type: "text", text: "What's the status of the deployment?" }],
});
// Read the full conversation history
const history = await session.getHistory();import { Session } from "agents/experimental/memory/session";
// Append messages as the conversation progresses
await session.appendMessage({
id: `user-${crypto.randomUUID()}`,
role: "user",
parts: [{ type: "text", text: "What's the status of the deployment?" }],
});
// Read the full conversation history
const history = await session.getHistory();会話履歴は Durable Object のハイバネーションと退避をまたいで残ります。エージェントが起きると、完全な履歴が SQLite にあります。再生や再構築は不要です。
メッセージは parent_id による木構造で保存され、分岐会話ができます。すでに子がある parentId で appendMessage すると枝ができます。応答の再生成などに使えます。getHistory(leafId) は、木の任意の経路を辿ります。
Session は会話履歴の全文検索も提供します。
const results = await session.search("deployment Friday", { limit: 10 });const results = await session.search("deployment Friday", { limit: 10 });会話が長くなると、コンパクション が古いメッセージを要約し、元データを失わずにコンテキストウィンドウを管理します。
コンテキストメモリは、会話履歴とは別に、システムプロンプトへ注入される永続情報です。ターンをまたいで、アイデンティティ、指示、学習した事実、ナレッジベース、参照資料にアクセスできます。
Session API は 4 種類のコンテキストメモリをサポートし、それぞれ向いている情報が違います。種類はコンテキストブロックを支える プロバイダー で決まります。Session はプロバイダーの能力を自動検出します。
従来のシステムプロンプトです。エージェントのアイデンティティ、性格、指示です。コードベースに直接書く、R2 の SOUL.md から読み込む、API から取得する、といった方法があります。内容はシステムプロンプトに注入され、エージェントは変更できません。
コーディングアシスタントなら、性格と制約を定義する soul を持てます。
import { Session } from "agents/experimental/memory/session";
const session = Session.create(this).withContext("soul", {
provider: {
get: async () =>
"You are a senior TypeScript engineer. You write concise, " +
"well-tested code. You prefer composition over inheritance. " +
"When you are unsure, you say so rather than guessing.",
},
});import { Session } from "agents/experimental/memory/session";
const session = Session.create(this).withContext("soul", {
provider: {
get: async () =>
"You are a senior TypeScript engineer. You write concise, " +
"well-tested code. You prefer composition over inheritance. " +
"When you are unsure, you say so rather than guessing.",
},
});再デプロイせずに性格を更新できるよう、R2 から読み込むこともできます。
const session = Session.create(this).withContext("soul", {
provider: {
get: async () => {
const obj = await env.CONFIG_BUCKET.get("soul.md");
return obj ? obj.text() : "You are a helpful assistant.";
},
},
});const session = Session.create(this).withContext("soul", {
provider: {
get: async () => {
const obj = await env.CONFIG_BUCKET.get("soul.md");
return obj ? obj.text() : "You are a helpful assistant.";
},
},
});読み取り専用ブロックは、get() メソッドだけを持つオブジェクトを渡して定義します。ツールは生成されません。内容はシステムプロンプトに現れ、エージェントは変更できません。
エージェントが自分用に維持するスクラッチパッドです。覚えておく必要があることを書き留める場所です。Claude Code が作業用の todo リストを持つように、カスタマーサポートエージェントが会話中に学んだユーザー情報を追跡するように使います。
const session = Session.create(this)
.withContext("memory", {
description: "Important facts learned during conversation",
maxTokens: 1100,
})
.withContext("todos", {
description: "Task list, track what needs to be done and what is complete",
maxTokens: 2000,
});const session = Session.create(this)
.withContext("memory", {
description: "Important facts learned during conversation",
maxTokens: 1100,
})
.withContext("todos", {
description: "Task list, track what needs to be done and what is complete",
maxTokens: 2000,
});ビルダーで provider を省略すると、Session は SQLite バックエンドの書き込み可能プロバイダーに自動接続します。エージェントは set_context ツールを受け取り、これらのブロックを置換または追記できます。トークン上限は強制されるので、エージェントは maxTokens 予算を超えて書けません。
システムプロンプトは書き込み可能ブロックにトークン使用量を表示し、残り容量をエージェントが把握できるようにします。
══════════════════════════════════════════════
MEMORY (Important facts learned during conversation) [45% — 495/1100 tokens] [writable]
══════════════════════════════════════════════
User prefers dark mode.
User's project uses React and TypeScript.
Deployment target is Cloudflare Workers.
══════════════════════════════════════════════
TODOS (Task list) [12% — 240/2000 tokens] [writable]
══════════════════════════════════════════════
- [x] Set up project scaffolding
- [ ] Add authentication middleware
- [ ] Write integration tests内容はメッセージをまたいで残り、ハイバネーション後も残ります。常にシステムプロンプトに見えるので、エージェントは取得なしで毎ターン確認できます。
大きな情報体(ナレッジベース、ドキュメント、ログ、蓄積したメモ)をシステムプロンプトに全部入れるのは避けたいです。検索可能なコンテキストは、システムプロンプトに要約(例: 「42 entries indexed」)を残し、必要なときに特定のエントリを取得させます。
search() メソッドを持つプロバイダーを渡します。検索の実装は自由です。全文検索、Vectorize によるベクトル検索、外部 API 呼び出しなどです。Session は実装を気にせず、プロバイダーに search() があることだけを見ます。
組み込みの AgentSearchProvider は、デフォルトで Durable Object SQLite と FTS5 を使います。
import { AgentSearchProvider } from "agents/experimental/memory/session";
const session = Session.create(this).withContext("knowledge", {
description:
"Searchable knowledge base, search for relevant information before answering",
provider: new AgentSearchProvider(this),
});import { AgentSearchProvider } from "agents/experimental/memory/session";
const session = Session.create(this).withContext("knowledge", {
description:
"Searchable knowledge base, search for relevant information before answering",
provider: new AgentSearchProvider(this),
});任意の検索機構を裏にした独自プロバイダーも実装できます。
const session = Session.create(this).withContext("knowledge", {
description: "Searchable knowledge base",
provider: {
get: async () => "Product documentation and FAQs",
search: async (query) => {
// Use Vectorize, an external API, whatever you need
const results = await env.VECTORIZE_INDEX.query(
await generateEmbedding(query),
{ topK: 5 },
);
return results.matches.map((m) => m.metadata.text).join("\n\n");
},
set: async (key, content) => {
// Index new content
},
},
});const session = Session.create(this).withContext("knowledge", {
description: "Searchable knowledge base",
provider: {
get: async () => "Product documentation and FAQs",
search: async (query) => {
// Use Vectorize, an external API, whatever you need
const results = await env.VECTORIZE_INDEX.query(
await generateEmbedding(query),
{ topK: 5 },
);
return results.matches.map((m) => m.metadata.text).join("\n\n");
},
set: async (key, content) => {
// Index new content
},
},
});エージェントは照会用の search_context と、新規エントリの索引付け用の set_context を受け取ります。何を検索するかはエージェントが決め、検索の実装はあなたが決めます。
大きなコレクションから特定の情報を探す必要があり、文書全体を読み込まない場合に向いています。
Skills は、エージェントが発見してオンデマンドで読み込める大きなコンテキスト(完全な文書、リファレンスガイド、ランブック、テンプレート)です。棚の参考資料と考えてください。エージェントはタイトルと説明の一覧を見て、現在のタスクに関連するものを選び、読み込み、使い、終わったらアンロードします。
検索可能なコンテキストが大きなコレクションから小さな断片を取るのに対し、Skills は丸ごと読み込む設計です。Skill を読み込むと、文書全体がコンテキストウィンドウに入ります。
Skills は SkillProvider インターフェースに裏打ちされます。Skill プロバイダーには 3 つのメソッドがあります。
get()は、システムプロンプトに出るメタデータ一覧(タイトルと説明)を返しますload(key)は、特定の Skill の全文を取得しますset(key, content, description?)は、Skill エントリを書き込みまたは更新します(任意)
システムプロンプトは利用可能な Skills を一覧で示します。[loadable] タグは、これらのエントリがインラインではないことを LLM に伝えます。全文へはツールが必要です。
══════════════════════════════════════════════
SKILLS [loadable]
══════════════════════════════════════════════
- api-ref: API Reference documentation
- style-guide: Company style guide
- deploy-checklist: Production deployment checklistエージェントはタイトルを見て、現在のタスクに関連する Skill を決め、load_context で全文を作業コンテキストに取り込みます。終わったら unload_context で領域を解放します。Skill プロバイダーが set() を実装していれば、既存 Skill の更新や新規作成もできます。
Agent sees: "- deploy-checklist: Production deployment checklist"
User asks: "Walk me through a production deployment"
Agent calls: load_context({ block: "skills", key: "deploy-checklist" })
→ Full checklist content is loaded into the agent's working context組み込みの R2SkillProvider は、Skills を Cloudflare R2 バケットに保存します。各 Skill は R2 オブジェクトで、説明用のカスタムメタデータを付けられます。
import { Session, R2SkillProvider } from "agents/experimental/memory/session";
const session = Session.create(this)
.withContext("soul", {
provider: {
get: async () =>
[
"You are a helpful assistant with access to skills.",
"When a user asks you to do something, check the SKILLS section",
"for a relevant skill and use load_context to load it.",
].join("\n"),
},
})
.withContext("memory", {
description: "Learned facts",
maxTokens: 1100,
})
.withContext("skills", {
provider: new R2SkillProvider(env.SKILLS_BUCKET, { prefix: "skills/" }),
})
.withCachedPrompt();import { Session, R2SkillProvider } from "agents/experimental/memory/session";
const session = Session.create(this)
.withContext("soul", {
provider: {
get: async () =>
[
"You are a helpful assistant with access to skills.",
"When a user asks you to do something, check the SKILLS section",
"for a relevant skill and use load_context to load it.",
].join("\n"),
},
})
.withContext("memory", {
description: "Learned facts",
maxTokens: 1100,
})
.withContext("skills", {
provider: new R2SkillProvider(env.SKILLS_BUCKET, { prefix: "skills/" }),
})
.withCachedPrompt();prefix オプションは、プロバイダーをバケット内のサブディレクトリに限定します。メタデータ一覧の Skill キーはプレフィックスなしで表示されるので、skills/api-ref はシステムプロンプトでは api-ref になります。
keys で、get() と load() に許可するプレフィックス相対の Skills を制限できます。
new R2SkillProvider(env.SKILLS_BUCKET, {
prefix: "skills/",
keys: ["deploy-checklist", "api-ref"],
});new R2SkillProvider(env.SKILLS_BUCKET, {
prefix: "skills/",
keys: ["deploy-checklist", "api-ref"],
});Wrangler 設定に R2 バケットバインディングを追加します。
{
"r2_buckets": [
{
"binding": "SKILLS_BUCKET",
"bucket_name": "my-agent-skills"
}
]
}[[r2_buckets]]
binding = "SKILLS_BUCKET"
bucket_name = "my-agent-skills"Skills は通常の R2 オブジェクトです。任意の R2 インターフェース(Wrangler CLI、ダッシュボード、Workers API)からアップロードします。
# Upload a skill from a file
wrangler r2 object put my-agent-skills/skills/style-guide --file ./docs/style-guide.md --content-type text/markdown説明(メタデータ一覧に表示)を付けるには、R2 オブジェクトにカスタムメタデータを設定します。
await env.SKILLS_BUCKET.put("skills/api-ref", content, {
customMetadata: { description: "API Reference documentation" },
});await env.SKILLS_BUCKET.put("skills/api-ref", content, {
customMetadata: { description: "API Reference documentation" },
});SkillProvider インターフェースを実装すれば、任意のストレージで Skills を支えられます。
class DatabaseSkillProvider {
db;
constructor(db) {
this.db = db;
}
async get() {
const rows = await this.db
.prepare("SELECT key, description FROM skills ORDER BY key")
.all();
if (rows.results.length === 0) return null;
return rows.results
.map((r) => `- ${r.key}${r.description ? `: ${r.description}` : ""}`)
.join("\n");
}
async load(key) {
const row = await this.db
.prepare("SELECT content FROM skills WHERE key = ?")
.bind(key)
.first();
return row ? row.content : null;
}
async set(key, content, description) {
await this.db
.prepare(
"INSERT INTO skills (key, content, description) VALUES (?, ?, ?) " +
"ON CONFLICT(key) DO UPDATE SET content = ?, description = ?",
)
.bind(key, content, description ?? null, content, description ?? null)
.run();
}
}import type { SkillProvider } from "agents/experimental/memory/session";
class DatabaseSkillProvider implements SkillProvider {
private db: D1Database;
constructor(db: D1Database) {
this.db = db;
}
async get(): Promise<string | null> {
const rows = await this.db
.prepare("SELECT key, description FROM skills ORDER BY key")
.all();
if (rows.results.length === 0) return null;
return rows.results
.map((r) => `- ${r.key}${r.description ? `: ${r.description}` : ""}`)
.join("\n");
}
async load(key: string): Promise<string | null> {
const row = await this.db
.prepare("SELECT content FROM skills WHERE key = ?")
.bind(key)
.first();
return row ? (row.content as string) : null;
}
async set(key: string, content: string, description?: string): Promise<void> {
await this.db
.prepare(
"INSERT INTO skills (key, content, description) VALUES (?, ?, ?) " +
"ON CONFLICT(key) DO UPDATE SET content = ?, description = ?",
)
.bind(key, content, description ?? null, content, description ?? null)
.run();
}
}Session はダックタイピングで load() メソッドを検出し、適切なツールを自動生成します。
| 観点 | Skills | 書き込み可能コンテキスト | 検索可能なコンテキスト |
|---|---|---|---|
| システムプロンプト内 | メタデータ一覧のみ | 全文 | 件数の要約 |
| アクセスパターン | キーで文書全体を読み込む | 常に表示 | クエリで検索 |
| 向いている用途 | 大きな文書、参照資料 | 短いメモ、好み | 小さなエントリの大きなコレクション |
| コンテキストコスト | 低い(読み込むまで) | 内容に比例 | 低い(検索するまで) |
| エージェントが書く? | 任意(set 実装時) |
はい(set_context) |
はい(set_context) |
重要な違いは、Skills が 遅延 であることです。エージェントが必要と判断するまで、システムプロンプト上のコストはほぼゼロです。会話ごとに一部だけが関連する大きな参照資料に向いています。
Session は、コンテキストブロックのプロバイダー種別に応じてツールを自動生成します。これらのツールを、アプリ固有のツールと一緒に LLM へ渡します。
const sessionTools = await session.tools();
const allTools = { ...sessionTools, ...myApplicationTools };
const result = streamText({
model: myModel,
system: await session.freezeSystemPrompt(),
messages: await convertToModelMessages(await session.getHistory()),
tools: allTools,
});const sessionTools = await session.tools();
const allTools = { ...sessionTools, ...myApplicationTools };
const result = streamText({
model: myModel,
system: await session.freezeSystemPrompt(),
messages: await convertToModelMessages(await session.getHistory()),
tools: allTools,
});Session は、存在するプロバイダー種別に応じてツールを動的に生成します。
| ツール | 生成される条件 | 動作 |
|---|---|---|
set_context |
書き込み可能、Skill、または検索ブロックがある | 名前付きブロックへ内容を書き込みます。書き込み可能ブロックでは置換または追記です。Skill / 検索ブロックではキー付きエントリを書き込みます。 |
load_context |
Skill ブロックがある | キーで文書の全文をエージェントのコンテキストへ読み込みます。 |
unload_context |
Skill ブロックがある | 以前読み込んだ文書を外してコンテキスト領域を解放します。文書は再読み込みできます。 |
search_context |
検索ブロックがある | 検索可能ブロック内を全文検索し、関連度順の上位結果を返します。 |
session_search |
SessionManager を使用 |
全セッション横断で検索します(会話横断検索)。 |
ツールには、利用可能なブロックと用途を LLM に伝える説明とパラメータスキーマが付きます。いつどう使うかは、会話に応じてエージェントが決めます。
ツールシグネチャと Session メソッドの全体は Session API リファレンス を参照してください。
コンテキストブロックは、明確なヘッダーとメタデータを持つ構造化システムプロンプトに組み立てられます。各ブロックは、種別と容量を示すタグ付きのラベル付きセクションになります。
══════════════════════════════════════════════
SOUL (Identity) [readonly]
══════════════════════════════════════════════
You are a helpful coding assistant who speaks concisely.
══════════════════════════════════════════════
MEMORY (Important facts) [45% — 495/1100 tokens] [writable]
══════════════════════════════════════════════
User prefers dark mode.
User's project uses React and TypeScript.
══════════════════════════════════════════════
KNOWLEDGE (Searchable knowledge base) [searchable]
══════════════════════════════════════════════
12 entries indexed.
══════════════════════════════════════════════
SKILLS [loadable]
══════════════════════════════════════════════
- api-ref: API Reference documentation
- style-guide: Company style guideタグ([readonly]、[writable]、[searchable]、[loadable])は、各ブロックで可能な操作を LLM に伝えます。トークン予算は書き込み可能ブロックの残り容量を示し、エージェントが自分のメモリを管理しやすくします。
LLM プロバイダー(Anthropic、OpenAI など)はシステムプロンプトのプレフィックスをキャッシュします。連続リクエストで同じシステムプロンプトを共有すると、プロバイダーはそのプレフィックスの再処理を省略でき、レイテンシとコストが下がります。キャッシュを壊す(システムプロンプトを変える)と、この利点は失われます。
Session API はプロンプトキャッシュと連携する設計です。
freezeSystemPrompt()は、初回呼び出しで全コンテキストブロックからシステムプロンプトを描画し、以降はキャッシュ値を返します。エージェントがset_contextでメモリに書いても、ターン間でプロンプトは変わりません。withCachedPrompt()は凍結したプロンプトをストレージに残すので、Durable Object のハイバネーションと退避をまたぎます。エージェントが起きると、全プロバイダーから再取得せず同じプロンプトを読み込みます。
エージェントが set_context で書き込み可能ブロックを更新すると、裏のプロバイダーはすぐ更新されます(データは保存されます)が、凍結済みシステムプロンプトは再描画されません。LLM が更新を見るのは、明示的に refreshSystemPrompt() を呼んだ次のターンです。通常はターンの途中ではなく、会話ターンの間に呼びます。
そのため、複数ステップのツール使用ターン全体でシステムプロンプトは安定し、各ステップでプロバイダーのプレフィックスキャッシュが保たれます。
const session = Session.create(this)
.withContext("soul", {
provider: { get: async () => "You are a helpful assistant." },
})
.withContext("memory", { description: "Learned facts", maxTokens: 1100 })
.withCachedPrompt(); // Persist the frozen prompt across hibernation
// During a conversation turn:
const system = await session.freezeSystemPrompt(); // Same value every call
const tools = await session.tools();
// ... agent calls set_context to update memory ...
// The frozen prompt is NOT changed, prefix cache stays warm
// Between turns (optional, if you want the agent to see its own updates):
await session.refreshSystemPrompt();const session = Session.create(this)
.withContext("soul", {
provider: { get: async () => "You are a helpful assistant." },
})
.withContext("memory", { description: "Learned facts", maxTokens: 1100 })
.withCachedPrompt(); // Persist the frozen prompt across hibernation
// During a conversation turn:
const system = await session.freezeSystemPrompt(); // Same value every call
const tools = await session.tools();
// ... agent calls set_context to update memory ...
// The frozen prompt is NOT changed, prefix cache stays warm
// Between turns (optional, if you want the agent to see its own updates):
await session.refreshSystemPrompt();長い会話は、やがて LLM のコンテキストウィンドウを超えます。コンパクションは 2 層で対処します。マクロコンパクション は古いメッセージ範囲を要約し、マイクロコンパクション は大きすぎる個別メッセージを切り詰めます。
マクロコンパクションは古いメッセージを要約しますが、原本は削除しません。
オーバーレイ を使います。要約は別テーブルに、対象メッセージ範囲をキーにして保存されます。getHistory() を呼ぶと、読み取り時にオーバーレイが透過的に適用されます。コンパクト化した範囲は合成要約メッセージに置き換わります。元メッセージは SQLite に残り、監査、検索、分岐用の完全な会話が保たれます。
Messages: [1] [2] [3] [4] [5] [6] [7] [8] [9] [10]
↓ compaction ↓
Overlay: [1] [2] [SUMMARY of 3-7] [8] [9] [10]
↑ tail protected要点は次のとおりです。
- 非破壊。元メッセージは削除されません。完全な会話は常にデータベースにあります。
- 反復的。会話が再び伸びて次のコンパクションが走ると、既存の要約を LLM に渡して更新します。ゼロから作り直しません。
- 境界を尊重。コンパクション境界は、ツール呼び出しとツール結果のペアを分割しないようずらします。
- 設定可能。
protectHeadは先頭 N 件(通常はシステムコンテキスト)を残し、tailTokenBudgetは直近メッセージをそのまま残します。
import { createCompactFunction } from "agents/experimental/memory/utils/compaction-helpers";
const session = Session.create(this)
.withContext("memory", { maxTokens: 1100 })
.onCompaction(
createCompactFunction({
summarize: (prompt) =>
generateText({ model: myModel, prompt }).then((r) => r.text),
protectHead: 3,
tailTokenBudget: 20000,
minTailMessages: 2,
}),
)
.compactAfter(100_000); // Auto-compact when token estimate exceeds thresholdimport { createCompactFunction } from "agents/experimental/memory/utils/compaction-helpers";
const session = Session.create(this)
.withContext("memory", { maxTokens: 1100 })
.onCompaction(
createCompactFunction({
summarize: (prompt) =>
generateText({ model: myModel, prompt }).then((r) => r.text),
protectHead: 3,
tailTokenBudget: 20000,
minTailMessages: 2,
}),
)
.compactAfter(100_000); // Auto-compact when token estimate exceeds threshold自動コンパクションは、推定トークン数がしきい値を超えたあと、appendMessage() の後に発火します。コンパクション失敗は致命的ではありません。メッセージはすでに保存されています。
マイクロコンパクションは範囲ではなく、個別メッセージ単位です。次の 2 つの問題を扱います。
読み取り時の切り詰め: truncateOlderMessages() は、LLM へ送る前に、古いメッセージのツール出力と長いテキストを短くします。直近のメッセージ(デフォルトは最後の 4 件)はそのままです。コピーに対して動作し、保存済みメッセージは変更しません。
import { truncateOlderMessages } from "agents/experimental/memory/utils";
const history = await session.getHistory();
const truncated = truncateOlderMessages(history);
// Pass truncated history to the LLMimport { truncateOlderMessages } from "agents/experimental/memory/utils";
const history = await session.getHistory();
const truncated = truncateOlderMessages(history);
// Pass truncated history to the LLM行サイズの強制: メッセージを永続化するとき(通常は大きなツール出力を持つアシスタントメッセージ)、SQLite の行サイズ上限と照合します。大きすぎるツール出力はプレビューと、ツール再実行を勧める注記に置き換えます。会話の流れを保ちつつ、個別メッセージがストレージ上限を超えないようにします。