Skip to content

非公式本サイトは非公式の日本語ドキュメントであり、Cloudflare 公式サイトではありません。最新情報はdevelopers.cloudflare.comをご確認ください。

セッション

最終更新 Markdown で表示Agent セットアップ

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
	}
}

セッションの作成

ビルダー API(推奨)

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) 静的ファクトリ。agentsql タグ付きテンプレートメソッドを持つ任意のオブジェクトです(Agent または Durable Object)。
.forSession(sessionId) このセッションを ID で名前空間化します。SessionManager を使わない複数セッションの分離では必須です。
.withContext(label, options?) コンテキストブロックを追加します。コンテキストブロック を参照してください。
.withCachedPrompt(provider?) システムプロンプトの永続化を有効にします。プロンプトは初回利用時に固定され、ハイバネーションと退避を越えて残ります。
.onCompaction(fn) コンパクション関数を登録します。コンパクション を参照してください。
.compactAfter(tokenThreshold, options?) 推定トークン数がしきい値を超えたら自動コンパクションします。.onCompaction() が必要です。しきい値の測り方を制御するには { tokenCounter } を渡します。
.onCompactionError(handler) 自動コンパクションのエラーを扱います。ハンドラーの失敗は飲み込まれ、メッセージ書き込みは致命的になりません。

メッセージ

メッセージは SessionMessage 型です。idroleparts、任意の 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();

分岐

メッセージはツリーを作ります。すでに子がある parentIdappendMessage すると、分岐ができます。ある地点から分岐するすべての子メッセージを得るには 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 のハイバネーションと退避を越えて残ります。

AI ツール

Session は、コンテキストブロックのプロバイダー種別に応じてツールを自動生成します。独自ツールと合わせて LLM に渡します。

const tools = await session.tools();
const allTools = { ...tools, ...myTools };
const tools = await session.tools();
const allTools = { ...tools, ...myTools };

set_context

書き込み可能なブロックがあるときに生成されます。通常ブロック、スキルブロック(キー付き)、検索ブロック(キー付き)へ書き込みます。maxTokens 制限を適用します。

load_context

スキルブロックがあるときに生成されます。SkillProvider からキーで全文を読み込みます。

unload_context

load_context と一緒に生成されます。読み込み済みスキルをアンロードしてコンテキスト領域を空けます。スキルは再読み込みできます。

search_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);

コンパクションの仕組み

  1. 先頭を保護する — 最初の N 件のメッセージはコンパクションしません(デフォルト 3)
  2. 末尾を保護する — 末尾から逆方向に歩き、予算までトークンを積み上げます(デフォルト 20K トークン)
  3. 境界を揃える — ツール呼び出しと結果のペアを分割しないよう境界をずらします
  4. 中間を要約する — 中間部分を構造化フォーマット(Topic、Key Points、Current State、Open Items)で LLM に送ります
  5. オーバーレイを保存するassistant_compactions テーブルに保存し、fromMessageIdtoMessageId でキー付けします
  6. 反復する — 以降のコンパクションでは、既存の要約を置き換えるのではなく更新するよう 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

SessionManager は、1 つの Durable Object 内で複数の名前付きセッションを管理するレジストリです。ライフサイクル管理、便利メソッド、セッション横断検索を提供します。

SessionManager の作成

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 === sessionId
const 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) {
		/* ... */
	},
};

Postgres プロバイダー

デフォルトでは、Session ストレージは Durable Object SQLite を使い、テーブルは遅延作成されます。エージェント横断クエリ、分析、共有ストレージのためにセッションデータを外部 Postgres データベースへ置きたい場合は、PostgresSessionProviderPostgresContextProviderPostgresSearchProvider を使います。

これらのプロバイダーは、コネクションプーリングのために Hyperdrive 経由で Postgres 互換データベースと連携します。

1. Hyperdrive 設定を作る

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 を設定します。

2. テーブルを作る

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);

3. つなぎ込む

pg をインストールし、Hyperdrive の接続文字列からクライアントを作り、Postgres プロバイダーへ渡します。

npm i pg
import { 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 idsession_idparent_idrolecontent(JSON)、created_at を持つツリー構造のメッセージ
assistant_compactions summaryfrom_message_idto_message_id を持つコンパクションオーバーレイ
assistant_fts メッセージ検索用の FTS5 仮想テーブル(porter stemming、unicode トークン化)
assistant_sessions セッションレジストリ(SessionManager のみ)。nameparent_session_idmodelsource、トークン / コストカウンター
cf_agents_context_blocks 永続的なコンテキストブロックストレージ(AgentContextProvider
cf_agents_search_entries / cf_agents_search_fts 検索可能なコンテキストエントリと FTS5 インデックス(AgentSearchProvider

謝辞

  • Session のツリー構造メッセージは Pi に着想を得ています。
  • コンテキストブロックは Letta AI memory blocks に着想を得ています。
  • ブロックの整形は Hermes Agent に着想を得ています。

関連

  • ThinkconfigureSession() 経由で Session を会話ストレージに使う、方針が決まったチャットエージェント
  • チャットエージェント — 独自のメッセージ永続化層を持つ AIChatAgent
  • 状態の保存と同期 — より単純なキーバリュー永続化向けの setState()

役に立ちましたか?