Session API は、エージェント向けの永続的な会話ストレージです。ツリー構造のメッセージ(Pi ↗ に着想)、コンテキストブロック、コンパクション、全文検索、AI が操作できるツールを備えています。デフォルトでは Durable Object SQLite を使います。共有データベースアクセス、分析、Durable Object 横断のクエリが必要なアプリ向けに、外部 Postgres ストレージも使えます。
import { Agent } from "agents";
import { Session } from "agents/experimental/memory/session";
class MyAgent extends Agent {
session = Session.create(this)
.withContext("soul", {
provider: { get: async () => "You are a helpful assistant." },
})
.withContext("memory", {
description: "Learned facts about the user",
maxTokens: 1100,
})
.withCachedPrompt();
async onMessage(message) {
await this.session.appendMessage(message);
const history = await this.session.getHistory();
const system = await this.session.freezeSystemPrompt();
const tools = await this.session.tools();
// Pass history, system prompt, and tools to your LLM
}
}import { Agent } from "agents";
import { Session } from "agents/experimental/memory/session";
class MyAgent extends Agent {
session = Session.create(this)
.withContext("soul", {
provider: { get: async () => "You are a helpful assistant." },
})
.withContext("memory", {
description: "Learned facts about the user",
maxTokens: 1100,
})
.withCachedPrompt();
async onMessage(message: unknown) {
await this.session.appendMessage(message);
const history = await this.session.getHistory();
const system = await this.session.freezeSystemPrompt();
const tools = await this.session.tools();
// Pass history, system prompt, and tools to your LLM
}
}Session.create(agent) とチェイン可能なビルダーを使います。明示的な provider オプションがないコンテキストプロバイダーは、SQLite に自動接続されます。
const session = Session.create(this)
.withContext("soul", { provider: { get: async () => "You are helpful." } })
.withContext("memory", { description: "Learned facts", maxTokens: 1100 })
.withCachedPrompt()
.onCompaction(myCompactFn)
.compactAfter(100_000);const session = Session.create(this)
.withContext("soul", { provider: { get: async () => "You are helpful." } })
.withContext("memory", { description: "Learned facts", maxTokens: 1100 })
.withCachedPrompt()
.onCompaction(myCompactFn)
.compactAfter(100_000);プロバイダーを細かく制御したい場合に使います。
import {
Session,
AgentSessionProvider,
AgentContextProvider,
} from "agents/experimental/memory/session";
const session = new Session(new AgentSessionProvider(this), {
context: [
{
label: "memory",
description: "Notes",
maxTokens: 500,
provider: new AgentContextProvider(this, "memory"),
},
{ label: "soul", provider: { get: async () => "You are helpful." } },
],
});import {
Session,
AgentSessionProvider,
AgentContextProvider,
} from "agents/experimental/memory/session";
const session = new Session(new AgentSessionProvider(this), {
context: [
{
label: "memory",
description: "Notes",
maxTokens: 500,
provider: new AgentContextProvider(this, "memory"),
},
{ label: "soul", provider: { get: async () => "You are helpful." } },
],
});ビルダーメソッドはすべて、チェインのために this を返します。順序は問いません。プロバイダーは初回利用時に遅延解決されます。
| メソッド | 説明 |
|---|---|
Session.create(agent) |
静的ファクトリ。agent は sql タグ付きテンプレートメソッドを持つ任意のオブジェクトです(Agent または Durable Object)。 |
.forSession(sessionId) |
このセッションを ID で名前空間化します。SessionManager を使わない複数セッションの分離では必須です。 |
.withContext(label, options?) |
コンテキストブロックを追加します。コンテキストブロック を参照してください。 |
.withCachedPrompt(provider?) |
システムプロンプトの永続化を有効にします。プロンプトは初回利用時に固定され、ハイバネーションと退避を越えて残ります。 |
.onCompaction(fn) |
コンパクション関数を登録します。コンパクション を参照してください。 |
.compactAfter(tokenThreshold, options?) |
推定トークン数がしきい値を超えたら自動コンパクションします。.onCompaction() が必要です。しきい値の測り方を制御するには { tokenCounter } を渡します。 |
.onCompactionError(handler) |
自動コンパクションのエラーを扱います。ハンドラーの失敗は飲み込まれ、メッセージ書き込みは致命的になりません。 |
メッセージは SessionMessage 型です。id、role、parts、任意の createdAt を持つ最小の形です。AI SDK の UIMessage は構造的に互換なので、そのまま渡せます。セッションは parent_id 経由でメッセージをツリー構造に保存し、分岐する会話を実現します。
// Append — auto-parents to the latest leaf unless parentId is specified
await session.appendMessage(message);
await session.appendMessage(message, parentId);
// Update an existing message (matched by message.id)
await session.updateMessage(message);
// Delete specific messages
await session.deleteMessages(["msg-1", "msg-2"]);
// Clear all messages and skill state
await session.clearMessages();// Append — auto-parents to the latest leaf unless parentId is specified
await session.appendMessage(message);
await session.appendMessage(message, parentId);
// Update an existing message (matched by message.id)
await session.updateMessage(message);
// Delete specific messages
await session.deleteMessages(["msg-1", "msg-2"]);
// Clear all messages and skill state
await session.clearMessages();// Linear history from root to the latest leaf
const messages = await session.getHistory();
// History to a specific leaf (for branching)
const branch = await session.getHistory(leafId);
// Get a single message
const msg = await session.getMessage("msg-1");
// Get the newest message
const latest = await session.getLatestLeaf();
// Count messages in path
const count = await session.getPathLength();// Linear history from root to the latest leaf
const messages = await session.getHistory();
// History to a specific leaf (for branching)
const branch = await session.getHistory(leafId);
// Get a single message
const msg = await session.getMessage("msg-1");
// Get the newest message
const latest = await session.getLatestLeaf();
// Count messages in path
const count = await session.getPathLength();メッセージはツリーを作ります。すでに子がある parentId で appendMessage すると、分岐ができます。ある地点から分岐するすべての子メッセージを得るには getBranches() を使います。
// Get all child messages that branch from messageId
const branches = await session.getBranches(messageId);// Get all child messages that branch from messageId
const branches = await session.getBranches(messageId);これで応答の再生成のような機能が作れます。ユーザーメッセージ ID を渡すと、元の応答と再生成した応答の両方を得られます。getHistory(leafId) は選んだパスを辿ります。
SQLite FTS5 を使い、会話履歴を全文検索します。
const results = await session.search("deployment Friday", { limit: 10 });
// Returns: Array<{ id, role, content, createdAt? }>const results = await session.search("deployment Friday", { limit: 10 });
// Returns: Array<{ id, role, content, createdAt? }>SQLite バックエンドのセッションは、porter stemming と unicode トークン化の FTS5 を使います。Postgres バックエンドのセッションは、プロバイダーの Postgres 全文検索インデックスを使います。セッションプロバイダーが検索に対応していない場合、search() は例外を投げます。
コンテキストブロックは、システムプロンプトへ注入される永続的なキーバリューの区画です。各ブロックには label、任意の description、動作を決める provider があります。
プロバイダーは 4 種類あり、ダックタイピングで判別されます。
| プロバイダー | インターフェース | 動作 | AI ツール |
|---|---|---|---|
| ContextProvider | get() |
システムプロンプト内の読み取り専用ブロック | — |
| WritableContextProvider | get() + set() |
AI から書き込み可能 | set_context |
| SkillProvider | get() + load() + set?() |
オンデマンドのキー付きドキュメント。get() はメタデータの一覧を返し、load(key) が全文を取得します。 |
load_context, unload_context, set_context |
| SearchProvider | get() + search() + set?() |
全文検索可能なエントリ。get() は要約を返し、search(query) が FTS5 を実行します。 |
search_context, set_context |
AgentContextProvider — SQLite バックエンドの書き込み可能なコンテキスト。ビルダーで明示的なプロバイダーを指定しない場合のデフォルトです。
import { AgentContextProvider } from "agents/experimental/memory/session";
new AgentContextProvider(this, "memory");import { AgentContextProvider } from "agents/experimental/memory/session";
new AgentContextProvider(this, "memory");R2SkillProvider — オンデマンドのドキュメント読み込み向け Cloudflare R2 バケットです。スキルはシステムプロンプトにメタデータとして一覧され、モデルは load_context で必要に応じて全文を読み込みます。
import { R2SkillProvider } from "agents/experimental/memory/session";
Session.create(this).withContext("skills", {
provider: new R2SkillProvider(env.SKILLS_BUCKET, { prefix: "skills/" }),
});import { R2SkillProvider } from "agents/experimental/memory/session";
Session.create(this).withContext("skills", {
provider: new R2SkillProvider(env.SKILLS_BUCKET, { prefix: "skills/" }),
});AgentSearchProvider — SQLite FTS5 で検索できるコンテキストです。エントリはインデックスされ、モデルは search_context で検索できます。
import { AgentSearchProvider } from "agents/experimental/memory/session";
Session.create(this).withContext("knowledge", {
description: "Searchable knowledge base",
provider: new AgentSearchProvider(this),
});import { AgentSearchProvider } from "agents/experimental/memory/session";
Session.create(this).withContext("knowledge", {
description: "Searchable knowledge base",
provider: new AgentSearchProvider(this),
});初期化後にブロックを動的に追加・削除できます。
// Add a new block (auto-wires to SQLite if no provider given)
await session.addContext("extension-notes", {
description: "From extension X",
maxTokens: 500,
});
// Remove it
session.removeContext("extension-notes");
// Rebuild the system prompt to reflect changes
await session.refreshSystemPrompt();// Add a new block (auto-wires to SQLite if no provider given)
await session.addContext("extension-notes", {
description: "From extension X",
maxTokens: 500,
});
// Remove it
session.removeContext("extension-notes");
// Rebuild the system prompt to reflect changes
await session.refreshSystemPrompt();// Read a single block
const block = session.getContextBlock("memory");
// { label, description?, content, tokens, maxTokens?, writable, isSkill, isSearchable }
// Read all blocks
const blocks = session.getContextBlocks();
// Replace content entirely
await session.replaceContextBlock("memory", "User likes coffee.");
// Append content
await session.appendContextBlock("memory", "\nUser prefers dark roast.");// Read a single block
const block = session.getContextBlock("memory");
// { label, description?, content, tokens, maxTokens?, writable, isSkill, isSearchable }
// Read all blocks
const blocks = session.getContextBlocks();
// Replace content entirely
await session.replaceContextBlock("memory", "User likes coffee.");
// Append content
await session.appendContextBlock("memory", "\nUser prefers dark roast.");システムプロンプトは、ヘッダーとメタデータ付きですべてのコンテキストブロックから組み立てられます。
══════════════════════════════════════════════
SOUL (Identity) [readonly]
══════════════════════════════════════════════
You are a helpful assistant.
══════════════════════════════════════════════
MEMORY (Learned facts) [45% — 495/1100 tokens]
══════════════════════════════════════════════
User likes coffee.
User prefers dark roast.// Freeze — first call renders and persists; subsequent calls return cached value
const prompt = await session.freezeSystemPrompt();
// Refresh — re-render from current block state and persist
const updated = await session.refreshSystemPrompt();// Freeze — first call renders and persists; subsequent calls return cached value
const prompt = await session.freezeSystemPrompt();
// Refresh — re-render from current block state and persist
const updated = await session.refreshSystemPrompt();withCachedPrompt() が有効なとき、固定済みプロンプトは Durable Object のハイバネーションと退避を越えて残ります。
Session は、コンテキストブロックのプロバイダー種別に応じてツールを自動生成します。独自ツールと合わせて LLM に渡します。
const tools = await session.tools();
const allTools = { ...tools, ...myTools };const tools = await session.tools();
const allTools = { ...tools, ...myTools };書き込み可能なブロックがあるときに生成されます。通常ブロック、スキルブロック(キー付き)、検索ブロック(キー付き)へ書き込みます。maxTokens 制限を適用します。
スキルブロックがあるときに生成されます。SkillProvider からキーで全文を読み込みます。
load_context と一緒に生成されます。読み込み済みスキルをアンロードしてコンテキスト領域を空けます。スキルは再読み込みできます。
検索ブロックがあるときに生成されます。検索可能なコンテキストブロック内を全文検索します。FTS5 の順位で上位 10 件を返します。
SessionManager でのみ利用できます。すべてのセッションを横断して検索します。
コンパクションは古いメッセージを要約し、会話をトークン上限内に保ちます。元のメッセージは SQLite に残ります。要約は読み取り時に適用される、破壊的でないオーバーレイです。
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,
tokenCounter: async (messages) => estimateWithYourTokenizer({ messages }),
}),
)
.compactAfter(100_000);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,
tokenCounter: async (messages) => estimateWithYourTokenizer({ messages }),
}),
)
.compactAfter(100_000);- 先頭を保護する — 最初の N 件のメッセージはコンパクションしません(デフォルト 3)
- 末尾を保護する — 末尾から逆方向に歩き、予算までトークンを積み上げます(デフォルト 20K トークン)
- 境界を揃える — ツール呼び出しと結果のペアを分割しないよう境界をずらします
- 中間を要約する — 中間部分を構造化フォーマット(Topic、Key Points、Current State、Open Items)で LLM に送ります
- オーバーレイを保存する —
assistant_compactionsテーブルに保存し、fromMessageIdとtoMessageIdでキー付けします - 反復する — 以降のコンパクションでは、既存の要約を置き換えるのではなく更新するよう LLM に渡します
getHistory() を呼ぶと、コンパクションオーバーレイは透過的に適用されます。コンパクションされた範囲は、合成した要約メッセージに置き換わります。
const result = await session.compact();
// Or manage overlays directly
await session.addCompaction("Summary of messages 1-50", "msg-1", "msg-50");
const overlays = await session.getCompactions();const result = await session.compact();
// Or manage overlays directly
await session.addCompaction("Summary of messages 1-50", "msg-1", "msg-50");
const overlays = await session.getCompactions();.compactAfter(threshold) を設定すると、appendMessage() は各書き込みのあとで推定トークン数を確認します。しきい値を超えると compact() が自動で呼ばれます。自動コンパクションの失敗は致命的ではありません。メッセージはすでに保存されています。
デフォルトでは、推定には保存済みメッセージパーツと、Session が管理する固定済みシステムプロンプトが含まれます。Session が管理するコンテキストブロックとキャッシュ済みプロンプトもしきい値に寄与します。Session の外で起きるフレームワーク固有のプロンプト追加やツールスキーマの直列化は含みません。
トークンカウントの判断は 2 つあります。
.compactAfter(threshold, { tokenCounter })は、書き込み後に自動コンパクションを いつ 起こすかを制御します。createCompactFunction({ tokenCounter })は、要約から どの 末尾メッセージを保護するかを制御します。ツールが多い履歴が、Workers 向けヒューリスティックの推定よりはるかに大きいときに使います。
通常、カウンターは 1 つ設定すれば十分です。明示的な createCompactFunction({ tokenCounter }) がなければ、.compactAfter() のカウンターは CompactContext 経由で createCompactFunction の境界歩きにも流れます。1 つのカウンターが「コンパクションすべきか」と「何をコンパクションするか」の両方を駆動します。
モデル報告の usage や独自トークナイザーがあるときは、カスタムカウンターを使います。
const session = Session.create(this)
.onCompaction(myCompactFn)
.compactAfter(100_000, {
tokenCounter: async ({ messages, systemPrompt, contextBlocks }) => {
return estimateWithYourTokenizer({
messages,
systemPrompt,
contextBlocks,
});
},
})
.onCompactionError((err) => {
console.warn("Auto-compaction failed", err);
});const session = Session.create(this)
.onCompaction(myCompactFn)
.compactAfter(100_000, {
tokenCounter: async ({ messages, systemPrompt, contextBlocks }) => {
return estimateWithYourTokenizer({
messages,
systemPrompt,
contextBlocks,
});
},
})
.onCompactionError((err) => {
console.warn("Auto-compaction failed", err);
});SessionManager は、1 つの Durable Object 内で複数の名前付きセッションを管理するレジストリです。ライフサイクル管理、便利メソッド、セッション横断検索を提供します。
import { SessionManager } from "agents/experimental/memory/session";
const manager = SessionManager.create(this)
.withContext("soul", { provider: { get: async () => "You are helpful." } })
.withContext("memory", { description: "Learned facts", maxTokens: 1100 })
.withCachedPrompt()
.onCompaction(myCompactFn)
.compactAfter(100_000)
.withSearchableHistory("history");import { SessionManager } from "agents/experimental/memory/session";
const manager = SessionManager.create(this)
.withContext("soul", { provider: { get: async () => "You are helpful." } })
.withContext("memory", { description: "Learned facts", maxTokens: 1100 })
.withCachedPrompt()
.onCompaction(myCompactFn)
.compactAfter(100_000)
.withSearchableHistory("history");コンテキストブロック、プロンプトキャッシュ、コンパクション設定は、マネージャー経由で作ったすべてのセッションに伝播します。プロバイダーキーはセッション ID で自動的に名前空間化されます。
| メソッド | 説明 |
|---|---|
SessionManager.create(agent) |
静的ファクトリ。 |
.withContext(label, options?) |
すべてのセッション向けのコンテキストブロックテンプレートを追加します。 |
.withCachedPrompt(provider?) |
すべてのセッションでプロンプト永続化を有効にします。 |
.onCompaction(fn) |
すべてのセッション向けにコンパクション関数を登録します。 |
.compactAfter(tokenThreshold, options?) |
すべてのセッションの自動コンパクションしきい値です。Session と同じ tokenCounter オプションに対応します。 |
.onCompactionError(handler) |
管理下セッションの自動コンパクションエラーを扱います。 |
.withSearchableHistory(label) |
セッション横断で検索できる履歴ブロックを追加します。モデルは任意のセッションから過去の会話を検索できます。 |
// Create a new session
const info = await manager.create("My Chat");
// Create with metadata
const info2 = await manager.create("My Chat", {
parentSessionId: "parent-id",
model: "claude-sonnet-4-20250514",
source: "web",
});
// Get session metadata (null if not found)
const session = await manager.get(sessionId);
// List all sessions (ordered by updated_at DESC)
const sessions = await manager.list();
// Rename
await manager.rename(sessionId, "New Name");
// Delete (clears messages too)
await manager.delete(sessionId);// Create a new session
const info = await manager.create("My Chat");
// Create with metadata
const info2 = await manager.create("My Chat", {
parentSessionId: "parent-id",
model: "claude-sonnet-4-20250514",
source: "web",
});
// Get session metadata (null if not found)
const session = await manager.get(sessionId);
// List all sessions (ordered by updated_at DESC)
const sessions = await manager.list();
// Rename
await manager.rename(sessionId, "New Name");
// Delete (clears messages too)
await manager.delete(sessionId);// Get or create the Session instance for an ID
// Lazy — creates on first access, caches for subsequent calls
const session = manager.getSession(sessionId);// Get or create the Session instance for an ID
// Lazy — creates on first access, caches for subsequent calls
const session = manager.getSession(sessionId);これらは下層の Session に委譲し、セッションの updated_at タイムスタンプを更新します。
// Append a single message
await manager.append(sessionId, message, parentId);
// Add or update (upsert)
await manager.upsert(sessionId, message, parentId);
// Batch append (auto-chains parent IDs)
await manager.appendAll(sessionId, messages, parentId);
// Read history
const history = await manager.getHistory(sessionId, leafId);
// Message count
const count = await manager.getMessageCount(sessionId);
// Clear messages
await manager.clearMessages(sessionId);
// Delete specific messages
await manager.deleteMessages(sessionId, ["msg-1"]);// Append a single message
await manager.append(sessionId, message, parentId);
// Add or update (upsert)
await manager.upsert(sessionId, message, parentId);
// Batch append (auto-chains parent IDs)
await manager.appendAll(sessionId, messages, parentId);
// Read history
const history = await manager.getHistory(sessionId, leafId);
// Message count
const count = await manager.getMessageCount(sessionId);
// Clear messages
await manager.clearMessages(sessionId);
// Delete specific messages
await manager.deleteMessages(sessionId, ["msg-1"]);特定メッセージでセッションをフォークします。その地点までの履歴を新しいセッションへコピーします。
const forked = await manager.fork(sessionId, atMessageId, "Forked Chat");
// forked.parent_session_id === sessionIdconst forked = await manager.fork(sessionId, atMessageId, "Forked Chat");
// forked.parent_session_id === sessionId// Add a compaction overlay
await manager.addCompaction(sessionId, summary, fromId, toId);
// Get overlays
const compactions = await manager.getCompactions(sessionId);
// Compact and split — marks old session as ended, creates a continuation
const continuation = await manager.compactAndSplit(
sessionId,
summary,
"Continued Chat",
);// Add a compaction overlay
await manager.addCompaction(sessionId, summary, fromId, toId);
// Get overlays
const compactions = await manager.getCompactions(sessionId);
// Compact and split — marks old session as ended, creates a continuation
const continuation = await manager.compactAndSplit(
sessionId,
summary,
"Continued Chat",
);compactAndSplit() は、インプレースのオーバーレイではなく、要約メッセージ付きの新しいセッションを作ります。元のセッションには end_reason: "compaction" が付きます。
await manager.addUsage(sessionId, inputTokens, outputTokens, cost);await manager.addUsage(sessionId, inputTokens, outputTokens, cost);// Search across all sessions (FTS5)
const results = await manager.search("deployment Friday", { limit: 20 });
// Get tools for the model (includes session_search)
const tools = await manager.tools();// Search across all sessions (FTS5)
const results = await manager.search("deployment Friday", { limit: 20 });
// Get tools for the model (includes session_search)
const tools = await manager.tools();4 つのプロバイダーインターフェースのいずれかを実装し、独自ストレージを差し込めます。
// Read-only context
const myProvider = {
get: async () => "Static content here",
};
// Writable context (enables set_context tool)
const myWritable = {
get: async () => fetchFromMyDB(),
set: async (content) => saveToMyDB(content),
};
// Skill provider (enables load_context tool)
const mySkills = {
get: async () => "- api-ref: API Reference\n- guide: User Guide",
load: async (key) => fetchDocument(key),
set: async (key, content, description) =>
saveDocument(key, content, description),
};
// Search provider (enables search_context tool)
const mySearch = {
get: async () => "42 entries indexed",
search: async (query) => searchMyIndex(query),
set: async (key, content) => indexContent(key, content),
};// Read-only context
const myProvider: ContextProvider = {
get: async () => "Static content here",
};
// Writable context (enables set_context tool)
const myWritable: WritableContextProvider = {
get: async () => fetchFromMyDB(),
set: async (content) => saveToMyDB(content),
};
// Skill provider (enables load_context tool)
const mySkills: SkillProvider = {
get: async () => "- api-ref: API Reference\n- guide: User Guide",
load: async (key) => fetchDocument(key),
set: async (key, content, description) =>
saveDocument(key, content, description),
};
// Search provider (enables search_context tool)
const mySearch: SearchProvider = {
get: async () => "42 entries indexed",
search: async (query) => searchMyIndex(query),
set: async (key, content) => indexContent(key, content),
};SessionProvider を実装して、SQLite ストレージ全体を置き換えることもできます。
const myStorage = {
async getMessage(id) {
/* ... */
},
async getHistory(leafId) {
/* ... */
},
async getLatestLeaf() {
/* ... */
},
async getBranches(messageId) {
/* ... */
},
async getPathLength(leafId) {
/* ... */
},
async appendMessage(message, parentId) {
/* ... */
},
async updateMessage(message) {
/* ... */
},
async deleteMessages(messageIds) {
/* ... */
},
async clearMessages() {
/* ... */
},
async addCompaction(summary, fromId, toId) {
/* ... */
},
async getCompactions() {
/* ... */
},
async searchMessages(query, limit) {
/* ... */
},
};const myStorage: SessionProvider = {
async getMessage(id) {
/* ... */
},
async getHistory(leafId?) {
/* ... */
},
async getLatestLeaf() {
/* ... */
},
async getBranches(messageId) {
/* ... */
},
async getPathLength(leafId?) {
/* ... */
},
async appendMessage(message, parentId?) {
/* ... */
},
async updateMessage(message) {
/* ... */
},
async deleteMessages(messageIds) {
/* ... */
},
async clearMessages() {
/* ... */
},
async addCompaction(summary, fromId, toId) {
/* ... */
},
async getCompactions() {
/* ... */
},
async searchMessages(query, limit) {
/* ... */
},
};デフォルトでは、Session ストレージは Durable Object SQLite を使い、テーブルは遅延作成されます。エージェント横断クエリ、分析、共有ストレージのためにセッションデータを外部 Postgres データベースへ置きたい場合は、PostgresSessionProvider、PostgresContextProvider、PostgresSearchProvider を使います。
これらのプロバイダーは、コネクションプーリングのために Hyperdrive 経由で Postgres 互換データベースと連携します。
Postgres データベース向けの Hyperdrive 設定を作ります。
npx wrangler hyperdrive create my-session-db \
--connection-string="postgresql://user:password@host:port/dbname"次に wrangler.jsonc へ Hyperdrive バインディングを追加します。
{
"$schema": "./node_modules/wrangler/config-schema.json",
"compatibility_flags": [
"nodejs_compat"
],
"hyperdrive": [
{
"binding": "HYPERDRIVE",
"id": "<your-hyperdrive-id>"
}
],
"placement": {
"mode": "smart"
}
}compatibility_flags = ["nodejs_compat"]
[[hyperdrive]]
binding = "HYPERDRIVE"
id = "<your-hyperdrive-id>"
[placement]
mode = "smart"データベースのリージョンが分かっている場合は、クエリレイテンシを下げるためにデータベースの近くへ placement を設定します。
Postgres ユーザーには、実行時にテーブルを作る権限がないことがあります。データベースコンソールでスキーマを一度実行します。
CREATE TABLE IF NOT EXISTS assistant_messages (
id TEXT NOT NULL,
session_id TEXT NOT NULL DEFAULT '',
parent_id TEXT,
role TEXT NOT NULL,
content TEXT NOT NULL,
text_content TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ DEFAULT NOW(),
content_tsv TSVECTOR GENERATED ALWAYS AS (to_tsvector('english', text_content)) STORED,
PRIMARY KEY (session_id, id)
);
CREATE INDEX IF NOT EXISTS idx_assistant_msg_parent
ON assistant_messages (parent_id);
CREATE INDEX IF NOT EXISTS idx_assistant_msg_session
ON assistant_messages (session_id);
CREATE INDEX IF NOT EXISTS idx_assistant_msg_fts
ON assistant_messages USING GIN (content_tsv);
CREATE TABLE IF NOT EXISTS assistant_compactions (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL DEFAULT '',
summary TEXT NOT NULL,
from_message_id TEXT NOT NULL,
to_message_id TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS cf_agents_context_blocks (
label TEXT PRIMARY KEY,
content TEXT NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS cf_agents_search_entries (
label TEXT NOT NULL,
key TEXT NOT NULL,
content TEXT NOT NULL,
content_tsv TSVECTOR GENERATED ALWAYS AS (to_tsvector('english', content)) STORED,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (label, key)
);
CREATE INDEX IF NOT EXISTS idx_search_entries_fts
ON cf_agents_search_entries USING GIN (content_tsv);pg をインストールし、Hyperdrive の接続文字列からクライアントを作り、Postgres プロバイダーへ渡します。
npm i pgyarn add pgpnpm add pgbun add pgimport { Agent } from "agents";
import {
PostgresContextProvider,
PostgresSearchProvider,
PostgresSessionProvider,
Session,
} from "agents/experimental/memory/session";
import { Client } from "pg";
export class MyAgent extends Agent {
session;
pgClient;
async onStart() {
const client = new Client({
connectionString: this.env.HYPERDRIVE.connectionString,
});
await client.connect();
this.pgClient = client;
const sessionId = this.ctx.id.toString();
this.session = Session.create(
new PostgresSessionProvider(client, sessionId),
)
.withContext("soul", {
provider: {
get: async () => "You are a helpful assistant.",
},
})
.withContext("memory", {
description: "Short facts",
maxTokens: 1100,
provider: new PostgresContextProvider(client, `memory_${sessionId}`),
})
.withContext("knowledge", {
description: "Searchable knowledge base",
provider: new PostgresSearchProvider(client),
})
.withCachedPrompt(
new PostgresContextProvider(client, `_prompt_${sessionId}`),
);
}
}import { Agent } from "agents";
import {
PostgresContextProvider,
PostgresSearchProvider,
PostgresSessionProvider,
Session,
} from "agents/experimental/memory/session";
import { Client } from "pg";
export class MyAgent extends Agent<Env> {
private session?: Session;
private pgClient?: Client;
async onStart(): Promise<void> {
const client = new Client({
connectionString: this.env.HYPERDRIVE.connectionString,
});
await client.connect();
this.pgClient = client;
const sessionId = this.ctx.id.toString();
this.session = Session.create(
new PostgresSessionProvider(client, sessionId),
)
.withContext("soul", {
provider: {
get: async () => "You are a helpful assistant.",
},
})
.withContext("memory", {
description: "Short facts",
maxTokens: 1100,
provider: new PostgresContextProvider(client, `memory_${sessionId}`),
})
.withContext("knowledge", {
description: "Searchable knowledge base",
provider: new PostgresSearchProvider(client),
})
.withCachedPrompt(
new PostgresContextProvider(client, `_prompt_${sessionId}`),
);
}
}Session.create() が SQLite バックエンドではなく SessionProvider を受け取ると、SQLite の自動接続をスキップします。
- コンテキストブロックには明示的なプロバイダーが必要です。 データを永続化すべき各
withContext()呼び出しにはproviderオプションが必要です。 withCachedPrompt()にも明示的なプロバイダーが必要です。 固定済みシステムプロンプトを永続化するにはPostgresContextProviderを渡します。- Session のメソッドは非同期です。 同じコードがローカル SQLite と外部ストレージの両方で動くよう、読み書きは
awaitします。 - Broadcaster 対応はスキップされます。 Session イベントの WebSocket ステータスブロードキャストは、SQLite バックエンドのセッションでのみ動作します。
freezeSystemPrompt() は、ストレージからキャッシュ済みプロンプトを返します。初回呼び出しでは、プロバイダーからコンテキストブロックを読み込み、プロンプトを描画して保存します。以降の呼び出しは再描画せず保存値を返します。
コンテキストブロックを強制再読み込みし、プロンプトを再描画して保存値を更新するには refreshSystemPrompt() を使います。
デフォルトでは、ストレージは Durable Object SQLite にあり、テーブルは初回利用時に遅延作成されます。Postgres バックエンドのセッションは、Postgres プロバイダー節で示した外部テーブルを使います。
| テーブル | 用途 |
|---|---|
assistant_messages |
id、session_id、parent_id、role、content(JSON)、created_at を持つツリー構造のメッセージ |
assistant_compactions |
summary、from_message_id、to_message_id を持つコンパクションオーバーレイ |
assistant_fts |
メッセージ検索用の FTS5 仮想テーブル(porter stemming、unicode トークン化) |
assistant_sessions |
セッションレジストリ(SessionManager のみ)。name、parent_session_id、model、source、トークン / コストカウンター |
cf_agents_context_blocks |
永続的なコンテキストブロックストレージ(AgentContextProvider) |
cf_agents_search_entries / cf_agents_search_fts |
検索可能なコンテキストエントリと FTS5 インデックス(AgentSearchProvider) |
- Session のツリー構造メッセージは Pi ↗ に着想を得ています。
- コンテキストブロックは Letta AI memory blocks ↗ に着想を得ています。
- ブロックの整形は Hermes Agent ↗ に着想を得ています。
- Think —
configureSession()経由で Session を会話ストレージに使う、方針が決まったチャットエージェント - チャットエージェント — 独自のメッセージ永続化層を持つ
AIChatAgent - 状態の保存と同期 — より単純なキーバリュー永続化向けの
setState()