← Создание агентов на Cloudflare / agents / runtime / lifecycle
Сеансы
Session API предоставляет агентам постоянное хранилище переписки с сообщениями, организованными в древовидную структуру (по аналогии с Pi ↗), контекстные блоки, компактизацию, полнотекстовый поиск и инструменты, управляемые ИИ. По умолчанию используется 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
}
}Создание сеанса
Builder 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." } },
],
});Методы Builder
Все методы builder возвращают this для цепочки вызовов. Порядок не важен: провайдеры разрешаются лениво при первом использовании.
| Метод | Описание |
|---|---|
Session.create(agent) |
Статическая фабрика. agent это любой объект с sql метод с tagged template (ваш 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();Ветвление
Сообщения образуют дерево. Когда вы appendMessage с parentId у которого уже есть дочерние элементы, вы создаёте ветку. Используйте 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 используют FTS5 со стеммингом Porter и юникод-токенизацией. Сессии на основе Postgres используют полнотекстовый индекс Postgres, предоставляемый провайдером. search() выбрасывает исключение, если провайдер сессий не поддерживает search.
Блоки контекста
Блоки контекста представляют собой постоянные секции в формате ключ-значение, встраиваемые в системный промпт. У каждого блока есть метка, необязательный описание, а также провайдер который определяет его поведение.
Типы провайдеров
Существует четыре типа провайдеров, которые определяются через duck-typing:
| Провайдер | Интерфейс | Поведение | ИИ-инструмент |
|---|---|---|---|
| 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. Используется по умолчанию, если builder применяется без явно указанного provider.
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 для загрузки документов по требованию. Skills перечислены в системном промпте как метаданные; модель загружает полное содержимое по требованию через 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();Замороженный промпт сохраняется при гибернации и вытеснении Durable Object, когда withCachedPrompt() включён.
ИИ-инструменты
Session автоматически генерирует инструменты на основе типов провайдеров ваших контекстных блоков. Передавайте их в LLM вместе с собственными инструментами.
const tools = await session.tools();
const allTools = { ...tools, ...myTools };const tools = await session.tools();
const allTools = { ...tools, ...myTools };set_context
Создаётся, когда существует хотя бы один доступный для записи блок. Выполняет запись в обычные блоки, блоки skill (с ключом) или блоки search (с ключом). Обеспечивает соблюдение maxTokens лимиты.
load_context
Создаётся, когда существует хотя бы один блок skill. Загружает полное содержимое по ключу из SkillProvider.
unload_context
Создаётся вместе с load_context. Освобождает место в контексте, выгружая ранее загруженный навык. Навык остаётся доступным для повторной загрузки.
search_context
Создаётся, когда существует хотя бы один блок search. Полнотекстовый поиск в контекстном блоке с возможностью поиска. Возвращает 10 лучших результатов по рейтингу FTS5.
session_search
Доступно на 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)
- Защита конца : проходит назад от конца, накапливая токены до заданного бюджета (по умолчанию 20 тысяч токенов)
- Выравнивание границ : сдвигает границы, чтобы не разбивать пары вызов и результат инструмента
- Суммаризация середины : отправляет среднюю часть в 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.
Подсчёт токенов включает два решения:
.compactAfter(threshold, { tokenCounter })управляет когда автоматическая компактизация запускается после записей.createCompactFunction({ tokenCounter })управляет который последних сообщений защищены от суммаризации. Используйте это, когда истории с большим количеством вызовов инструментов значительно превышают то, что может оценить эвристика, безопасная для Workers.
Обычно достаточно настроить один счётчик. .compactAfter() счётчик также передаётся в createCompactFunction: обход границ (через CompactContext) если явно не задан createCompactFunction({ tokenCounter }) задан, поэтому один и тот же счётчик определяет и «нужно ли компактизировать?», и «что компактизировать?».
Используйте собственный счётчик, если у вас есть данные об использовании от модели или собственный токенизатор:
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 это реестр для нескольких именованных сессий в пределах одного 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");Настройки блоков контекста, кеширования промптов и компактизации распространяются на все сессии, созданные через менеджер. Ключи провайдера автоматически получают отдельное пространство имён по идентификатору сессии.
Методы Builder
| Метод | Описание |
|---|---|
SessionManager.create(agent) |
Статическая фабрика. |
.withContext(label, options?) |
Добавьте шаблон блока контекста для всех сессий. |
.withCachedPrompt(provider?) |
Включите сохранение промпта для всех сессий. |
.onCompaction(fn) |
Зарегистрируйте функцию компактизации для всех сессий. |
.compactAfter(tokenThreshold, options?) |
Порог автокомпактизации для всех сессий. Поддерживает те же tokenCounter опцию как Session. |
.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();Пользовательские провайдеры
Реализуйте любой из четырёх интерфейсов провайдера, чтобы подключить собственное хранилище:
// 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 для межагентных запросов, аналитики или общего хранилища, используйте PostgresSessionProvider, PostgresContextProvider, а также PostgresSearchProvider.
Эти провайдеры работают с Postgres-совместимыми базами данных через Hyperdrive для пула соединений.
1. Создайте конфигурацию Hyperdrive
Создайте конфигурацию Hyperdrive для своей базы данных Postgres:
npx wrangler hyperdrive create my-session-db \
--connection-string="postgresql://user:password@host:port/dbname"Затем добавьте биндинг Hyperdrive в wrangler.jsonc:
{
"$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"Если вам известен регион вашей базы данных, настройте размещение ближе к ней, чтобы снизить задержку запросов.
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 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() получает SessionProvider вместо провайдера на основе SQLite, автоматическое подключение SQLite пропускается:
- Блокам контекста нужны явные провайдеры. Каждый
withContext()вызову, который должен сохранять данные, нужнаproviderопцию. withCachedPrompt()требуется явно указанный провайдер. ПередайтеPostgresContextProviderчтобы сохранить зафиксированный системный промпт.- Методы Session асинхронны. Используйте
awaitдля чтения и записи, чтобы один и тот же код работал как с локальным SQLite, так и с внешним хранилищем. - Поддержка Broadcaster пропускается. Трансляция статуса WebSocket для событий Session работает только с сессиями на основе SQLite.
Жизненный цикл системного промпта
freezeSystemPrompt() возвращает закешированный prompt из хранилища. При первом вызове он загружает context blocks от providers, формирует prompt и сохраняет его. Последующие вызовы возвращают сохранённое значение без повторного формирования.
Используйте 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, юникод-токенизация) |
assistant_sessions |
Реестр Session (только для 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 ↗.
- Форматирование блоков вдохновлено Hermes Agent ↗.
См. также
- Think : чат-агент с продуманными настройками по умолчанию, который использует Session для хранения переписки через
configureSession() - Чат-агенты,
AIChatAgentс собственным слоем сохранения сообщений - Хранение и синхронизация состояния,
setState()для более простого хранения ключ-значение