INTEGRITY Документация

Состояние и память разговора

Agents нужна память, чтобы приносить пользу в долгосрочной перспективе. Без неё каждый разговор начинается с нуля. Агент забывает, кто такой пользователь, чему он научился и что делал. Именно память превращает вызов LLM без сохранения состояния в устойчивого агента, осознающего контекст.

Session API предоставляет слой памяти для агентов, построенных на Cloudflare Agents SDK. Он управляет двумя видами памяти: история разговора (сообщения и вызовы инструментов, составляющие сессию) и память контекста (постоянные блоки, встраиваемые в системный промпт, которые агент может читать, записывать, искать и загружать).

Используйте эту страницу, если вам нужно больше, чем простое синхронизированное состояние или плоская история чата. Для небольшого состояния UI используйте Хранение и синхронизация состояния. Для базового сохранения чата AIChatAgent хранит сообщения за вас. Для продуманной долгосрочной памяти Think использует Session и контекстные блоки.

История разговора

Самый базовый тип памяти: сам разговор, то есть сообщения между пользователем и агентом, вызовы инструментов, которые сделал агент, и полученные им результаты. Session хранит всё это в виде древовидной истории сообщений поверх Session Provider, по умолчанию использующего SQLite.

import { Session } from "agents/experimental/memory/session";

// Append messages as the conversation progresses
await session.appendMessage({
	id: `user-${crypto.randomUUID()}`,
	role: "user",
	parts: [{ type: "text", text: "What's the status of the deployment?" }],
});

// Read the full conversation history
const history = await session.getHistory();
import { Session } from "agents/experimental/memory/session";

// Append messages as the conversation progresses
await session.appendMessage({
	id: `user-${crypto.randomUUID()}`,
	role: "user",
	parts: [{ type: "text", text: "What's the status of the deployment?" }],
});

// Read the full conversation history
const history = await session.getHistory();

История разговора сохраняется при гибернации и вытеснении Durable Object. Когда агент снова активируется, полная история доступна в SQLite: воспроизводить или восстанавливать её не нужно.

Сообщения хранятся в древовидной структуре через parent_id, что позволяет ветвить разговоры. Когда вы appendMessage с parentId у которого уже есть дочерние элементы, вы создаёте ветку, что полезно для таких функций, как регенерация ответа. getHistory(leafId) проходит по любому выбранному пути в дереве.

Session также предоставляет полнотекстовый поиск по истории переписки:

const results = await session.search("deployment Friday", { limit: 10 });
const results = await session.search("deployment Friday", { limit: 10 });

По мере того как диалоги становятся длиннее, компактизация суммаризирует более старые сообщения, чтобы контекстное окно оставалось управляемым без потери исходных данных.

Память контекста

Память контекста представляет собой постоянную информацию, встраиваемую в системный промпт отдельно от истории разговора. Она даёт агенту доступ к идентификации, инструкциям, усвоенным фактам, базам знаний и справочным материалам в каждом ходе.

Session API поддерживает четыре типа контекстной памяти, каждый из которых подходит для своего рода информации. Тип определяется провайдер лежащего в основе блока контекста. Session автоматически определяет возможности провайдера.

Контекст только для чтения

Это ваш традиционный системный промпт: идентичность агента, его характер и инструкции. Вы можете написать его прямо в коде или загрузить из SOUL.md файл в R2 или получите его через API. Содержимое встраивается в системный промпт, и агент не может его изменить.

У ассистента для написания кода может быть душа, которая определяет его личность и ограничения:

import { Session } from "agents/experimental/memory/session";

const session = Session.create(this).withContext("soul", {
	provider: {
		get: async () =>
			"You are a senior TypeScript engineer. You write concise, " +
			"well-tested code. You prefer composition over inheritance. " +
			"When you are unsure, you say so rather than guessing.",
	},
});
import { Session } from "agents/experimental/memory/session";

const session = Session.create(this).withContext("soul", {
	provider: {
		get: async () =>
			"You are a senior TypeScript engineer. You write concise, " +
			"well-tested code. You prefer composition over inheritance. " +
			"When you are unsure, you say so rather than guessing.",
	},
});

Или загрузите его из R2, чтобы обновлять личность агента без повторного развёртывания:

const session = Session.create(this).withContext("soul", {
	provider: {
		get: async () => {
			const obj = await env.CONFIG_BUCKET.get("soul.md");
			return obj ? obj.text() : "You are a helpful assistant.";
		},
	},
});
const session = Session.create(this).withContext("soul", {
	provider: {
		get: async () => {
			const obj = await env.CONFIG_BUCKET.get("soul.md");
			return obj ? obj.text() : "You are a helpful assistant.";
		},
	},
});

Блоки только для чтения задаются передачей объекта, содержащего только get() метод. Инструменты не создаются. Содержимое появляется в системном промпте, и агент не может его изменить.

Краткосрочный контекст, доступный для записи

Представьте это как блокнот, который агент ведёт для себя самого: место, куда он записывает то, что нужно запомнить. Похоже на то, как Claude Code ведёт список задач для выполнения, или как агент службы поддержки может фиксировать то, что узнал о пользователе в ходе разговора.

const session = Session.create(this)
	.withContext("memory", {
		description: "Important facts learned during conversation",
		maxTokens: 1100,
	})
	.withContext("todos", {
		description: "Task list, track what needs to be done and what is complete",
		maxTokens: 2000,
	});
const session = Session.create(this)
	.withContext("memory", {
		description: "Important facts learned during conversation",
		maxTokens: 1100,
	})
	.withContext("todos", {
		description: "Task list, track what needs to be done and what is complete",
		maxTokens: 2000,
	});

Если не указать provider опцию в builder, Session автоматически подключается к записываемому провайдеру на основе SQLite. Agent получает set_context инструмент, который позволяет заменять содержимое этих блоков или добавлять его в конец. Лимиты токенов соблюдаются принудительно, поэтому агент не может записать больше, чем maxTokens бюджет позволяет.

Системный промпт отображает записываемые блоки с индикатором использования токенов, чтобы агент знал, сколько места у него осталось:

══════════════════════════════════════════════
MEMORY (Important facts learned during conversation) [45% — 495/1100 tokens] [writable]
══════════════════════════════════════════════
User prefers dark mode.
User's project uses React and TypeScript.
Deployment target is Cloudflare Workers.

══════════════════════════════════════════════
TODOS (Task list) [12% — 240/2000 tokens] [writable]
══════════════════════════════════════════════
- [x] Set up project scaffolding
- [ ] Add authentication middleware
- [ ] Write integration tests

Содержимое сохраняется между сообщениями и переживает гибернацию. Оно всегда видно в системном промпте, поэтому агент видит его на каждом ходу, ничего дополнительно не запрашивая.

Контекст с возможностью поиска

Когда у вас большой объём информации (база знаний, документация, логи, накопленные заметки), не стоит помещать всё это в системный промпт целиком. Контекст с возможностью поиска хранит в системном промпте только краткую сводку (например, «проиндексировано 42 записи») и позволяет агенту получать нужные записи по мере необходимости.

Провайдеру вы передаёте search() метод. Как именно устроен поиск, решаете вы сами: полнотекстовый поиск, векторный поиск через Vectorize, вызов внешнего API или что-то ещё. Session не заботится о реализации, важно лишь то, что у поставщика есть search() метод.

Встроенный AgentSearchProvider использует Durable Object SQLite с FTS5 по умолчанию:

import { AgentSearchProvider } from "agents/experimental/memory/session";

const session = Session.create(this).withContext("knowledge", {
	description:
		"Searchable knowledge base, search for relevant information before answering",
	provider: new AgentSearchProvider(this),
});
import { AgentSearchProvider } from "agents/experimental/memory/session";

const session = Session.create(this).withContext("knowledge", {
	description:
		"Searchable knowledge base, search for relevant information before answering",
	provider: new AgentSearchProvider(this),
});

Но вы можете реализовать собственный провайдер, использующий любой механизм поиска:

const session = Session.create(this).withContext("knowledge", {
	description: "Searchable knowledge base",
	provider: {
		get: async () => "Product documentation and FAQs",
		search: async (query) => {
			// Use Vectorize, an external API, whatever you need
			const results = await env.VECTORIZE_INDEX.query(
				await generateEmbedding(query),
				{ topK: 5 },
			);
			return results.matches.map((m) => m.metadata.text).join("\n\n");
		},
		set: async (key, content) => {
			// Index new content
		},
	},
});
const session = Session.create(this).withContext("knowledge", {
	description: "Searchable knowledge base",
	provider: {
		get: async () => "Product documentation and FAQs",
		search: async (query) => {
			// Use Vectorize, an external API, whatever you need
			const results = await env.VECTORIZE_INDEX.query(
				await generateEmbedding(query),
				{ topK: 5 },
			);
			return results.matches.map((m) => m.metadata.text).join("\n\n");
		},
		set: async (key, content) => {
			// Index new content
		},
	},
});

Агент получает search_context инструмент для запросов и set_context инструмент для индексирования новых записей. Он решает, что искать, а вы решаете, как работает поиск.

Это правильный выбор, когда агенту нужно найти конкретные фрагменты информации в большой коллекции, а не загружать целые документы.

Загружаемый контекст (Skills)

Skills представляют собой крупные фрагменты контекста (полноценные документы, справочные руководства, runbook и шаблоны), которые агент может обнаруживать и загружать по запросу. Представьте их как справочные материалы на полке: агент видит список названий и описаний, выбирает то, что относится к текущей задаче, загружает нужный материал, использует его и выгружает по завершении работы.

В отличие от контекста с поиском, где агент извлекает небольшие фрагменты из более крупной коллекции, skills предназначены для загрузки целиком. Когда агент загружает skill, он получает весь документ в своём контекстном окне.

В основе Skills лежит SkillProvider интерфейс. У провайдера навыков есть три метода:

Системный промпт показывает доступные навыки в виде списка. [loadable] тег сообщает LLM, что эти записи не являются встроенными. Чтобы получить доступ к полному содержимому, ей нужно использовать инструмент:

══════════════════════════════════════════════
SKILLS [loadable]
══════════════════════════════════════════════
- api-ref: API Reference documentation
- style-guide: Company style guide
- deploy-checklist: Production deployment checklist

Агент видит названия, решает, какой навык подходит для текущей задачи, и использует load_context чтобы загрузить весь контент в свой рабочий контекст. По завершении используется unload_context чтобы освободить место. Когда провайдер навыков реализует set(), агент также может записывать данные обратно, обновляя существующие навыки или создавая новые.

Agent sees: "- deploy-checklist: Production deployment checklist"
User asks: "Walk me through a production deployment"
Agent calls: load_context({ block: "skills", key: "deploy-checklist" })
→ Full checklist content is loaded into the agent's working context

Навыки на основе R2

Встроенный R2SkillProvider хранит skills в бакете Cloudflare R2. Каждый skill является объектом R2 с необязательными пользовательскими метаданными для описаний.

import { Session, R2SkillProvider } from "agents/experimental/memory/session";

const session = Session.create(this)
	.withContext("soul", {
		provider: {
			get: async () =>
				[
					"You are a helpful assistant with access to skills.",
					"When a user asks you to do something, check the SKILLS section",
					"for a relevant skill and use load_context to load it.",
				].join("\n"),
		},
	})
	.withContext("memory", {
		description: "Learned facts",
		maxTokens: 1100,
	})
	.withContext("skills", {
		provider: new R2SkillProvider(env.SKILLS_BUCKET, { prefix: "skills/" }),
	})
	.withCachedPrompt();
import { Session, R2SkillProvider } from "agents/experimental/memory/session";

const session = Session.create(this)
	.withContext("soul", {
		provider: {
			get: async () =>
				[
					"You are a helpful assistant with access to skills.",
					"When a user asks you to do something, check the SKILLS section",
					"for a relevant skill and use load_context to load it.",
				].join("\n"),
		},
	})
	.withContext("memory", {
		description: "Learned facts",
		maxTokens: 1100,
	})
	.withContext("skills", {
		provider: new R2SkillProvider(env.SKILLS_BUCKET, { prefix: "skills/" }),
	})
	.withCachedPrompt();

prefix опция ограничивает провайдера подкаталогом в бакете. Ключи навыков в списке метаданных отображаются без префикса, поэтому skills/api-ref становится api-ref в системном промпте.

Используйте keys чтобы разрешить определённые skills с относительным префиксом для get() и load():

new R2SkillProvider(env.SKILLS_BUCKET, {
	prefix: "skills/",
	keys: ["deploy-checklist", "api-ref"],
});
new R2SkillProvider(env.SKILLS_BUCKET, {
	prefix: "skills/",
	keys: ["deploy-checklist", "api-ref"],
});

Добавьте привязку бакета R2 в конфигурацию Wrangler:

{
  "r2_buckets": [
    {
      "binding": "SKILLS_BUCKET",
      "bucket_name": "my-agent-skills"
    }
  ]
}
[[r2_buckets]]
binding = "SKILLS_BUCKET"
bucket_name = "my-agent-skills"

Skills представляют собой обычные объекты R2. Загружайте их через любой интерфейс R2 (Wrangler CLI, дашборд или Workers API):

# Upload a skill from a file
wrangler r2 object put my-agent-skills/skills/style-guide --file ./docs/style-guide.md --content-type text/markdown

Чтобы добавить описания (отображаются в списке метаданных), задайте пользовательские метаданные для объекта R2:

await env.SKILLS_BUCKET.put("skills/api-ref", content, {
	customMetadata: { description: "API Reference documentation" },
});
await env.SKILLS_BUCKET.put("skills/api-ref", content, {
	customMetadata: { description: "API Reference documentation" },
});

Пользовательские провайдеры навыков

Вы можете подключить к skill любое хранилище, реализовав SkillProvider интерфейс:

class DatabaseSkillProvider {
	db;

	constructor(db) {
		this.db = db;
	}

	async get() {
		const rows = await this.db
			.prepare("SELECT key, description FROM skills ORDER BY key")
			.all();
		if (rows.results.length === 0) return null;
		return rows.results
			.map((r) => `- ${r.key}${r.description ? `: ${r.description}` : ""}`)
			.join("\n");
	}

	async load(key) {
		const row = await this.db
			.prepare("SELECT content FROM skills WHERE key = ?")
			.bind(key)
			.first();
		return row ? row.content : null;
	}

	async set(key, content, description) {
		await this.db
			.prepare(
				"INSERT INTO skills (key, content, description) VALUES (?, ?, ?) " +
					"ON CONFLICT(key) DO UPDATE SET content = ?, description = ?",
			)
			.bind(key, content, description ?? null, content, description ?? null)
			.run();
	}
}
import type { SkillProvider } from "agents/experimental/memory/session";

class DatabaseSkillProvider implements SkillProvider {
	private db: D1Database;

	constructor(db: D1Database) {
		this.db = db;
	}

	async get(): Promise<string | null> {
		const rows = await this.db
			.prepare("SELECT key, description FROM skills ORDER BY key")
			.all();
		if (rows.results.length === 0) return null;
		return rows.results
			.map((r) => `- ${r.key}${r.description ? `: ${r.description}` : ""}`)
			.join("\n");
	}

	async load(key: string): Promise<string | null> {
		const row = await this.db
			.prepare("SELECT content FROM skills WHERE key = ?")
			.bind(key)
			.first();
		return row ? (row.content as string) : null;
	}

	async set(key: string, content: string, description?: string): Promise<void> {
		await this.db
			.prepare(
				"INSERT INTO skills (key, content, description) VALUES (?, ?, ?) " +
					"ON CONFLICT(key) DO UPDATE SET content = ?, description = ?",
			)
			.bind(key, content, description ?? null, content, description ?? null)
			.run();
	}
}

Session определяет load() метод через duck-typing и автоматически создаёт соответствующие инструменты.

Skills и другие типы памяти

Аспект Skills Контекст, доступный для записи Контекст с возможностью поиска
В системном промпте Только список метаданных Полное содержимое Количество сводок
Шаблон доступа Загружает весь документ по ключу Всегда виден Поиск по запросу
Подходит для Большие документы, справочные материалы Короткие заметки, предпочтения Большие коллекции небольших записей
Стоимость контекста Низкий (пока не загружено) Пропорционально содержимому Низкий (пока не выполнен поиск)
Agent записывает? Необязательно (если set реализовано) Да (через set_context) Да (через set_context)

Ключевое различие: skill являются ленивый. Они почти ничего не стоят в системном промпте, пока агент не решит, что ему нужен один из них. Это делает их идеальными для больших справочных материалов, где для конкретного диалога релевантна лишь часть.

Как агенты взаимодействуют с памятью

Session автоматически создаёт инструменты на основе типов провайдеров ваших контекстных блоков. Эти инструменты передаются вашей LLM вместе с собственными инструментами приложения:

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

const result = streamText({
	model: myModel,
	system: await session.freezeSystemPrompt(),
	messages: await convertToModelMessages(await session.getHistory()),
	tools: allTools,
});
const sessionTools = await session.tools();
const allTools = { ...sessionTools, ...myApplicationTools };

const result = streamText({
	model: myModel,
	system: await session.freezeSystemPrompt(),
	messages: await convertToModelMessages(await session.getHistory()),
	tools: allTools,
});

Сгенерированные инструменты

Session динамически создаёт инструменты в зависимости от того, какие типы провайдеров присутствуют:

Инструмент Создаётся, когда Что это делает
set_context Существует хотя бы один блок writable, skill или search Записывает содержимое в именованный блок. Для блоков, доступных для записи, заменяет или дополняет данные. Для блоков skill и search записывает запись с ключом.
load_context Существует хотя бы один блок skill Загружает полное содержимое документа по ключу в контекст агента.
unload_context Существует хотя бы один блок skill Освобождает место в контексте, удаляя ранее загруженный документ. Документ остаётся доступным для повторной загрузки.
search_context Существует хотя бы один блок search Полнотекстовый поиск в блоке с возможностью поиска. Возвращает лучшие результаты, отсортированные по релевантности.
session_search Использование SessionManager Выполняет поиск по всем сессиям (поиск между разговорами).

Инструменты включают описания и схемы параметров, которые сообщают LLM, какие блоки доступны и для чего они нужны. Агент решает, когда и как их использовать, исходя из разговора.

Полные сигнатуры инструментов и все методы Session см. в Справочник по API Session.

Системный промпт

Блоки контекста собираются в структурированный системный промпт с чёткими заголовками и метаданными. Каждый блок получает отдельный раздел с тегами, указывающими его тип и вместимость:

══════════════════════════════════════════════
SOUL (Identity) [readonly]
══════════════════════════════════════════════
You are a helpful coding assistant who speaks concisely.

══════════════════════════════════════════════
MEMORY (Important facts) [45% — 495/1100 tokens] [writable]
══════════════════════════════════════════════
User prefers dark mode.
User's project uses React and TypeScript.

══════════════════════════════════════════════
KNOWLEDGE (Searchable knowledge base) [searchable]
══════════════════════════════════════════════
12 entries indexed.

══════════════════════════════════════════════
SKILLS [loadable]
══════════════════════════════════════════════
- api-ref: API Reference documentation
- style-guide: Company style guide

Теги ([readonly], [writable], [searchable], [loadable]) сообщают LLM, какой тип взаимодействия возможен с каждым блоком. Бюджеты токенов показывают агенту, сколько места остаётся в доступных для записи блоках, помогая ему управлять собственной памятью.

Подводные камни

Кэширование промптов

Провайдеры LLM (Anthropic, OpenAI и другие) кешируют префикс системного промпта. Если подряд идущие запросы используют один и тот же системный промпт, провайдер может не обрабатывать этот префикс заново, что снижает задержку и стоимость. При изменении системного промпта кеш сбрасывается, и это преимущество теряется.

Session API спроектирован для работы с кешированием промптов:

Когда агент использует set_context чтобы обновить доступный для записи блок, базовый провайдер обновляется немедленно (данные сохраняются), но зафиксированный системный промпт остаётся не заново отрисован. LLM увидит обновление только на следующем ходу, если вы явно вызовете refreshSystemPrompt(), что обычно делается между ходами разговора, а не в середине хода.

Это означает, что системный промпт остаётся неизменным на протяжении всего многошагового хода с использованием инструментов, сохраняя кеш префикса провайдера на каждом шаге.

const session = Session.create(this)
	.withContext("soul", {
		provider: { get: async () => "You are a helpful assistant." },
	})
	.withContext("memory", { description: "Learned facts", maxTokens: 1100 })
	.withCachedPrompt(); // Persist the frozen prompt across hibernation

// During a conversation turn:
const system = await session.freezeSystemPrompt(); // Same value every call
const tools = await session.tools();

// ... agent calls set_context to update memory ...
// The frozen prompt is NOT changed, prefix cache stays warm

// Between turns (optional, if you want the agent to see its own updates):
await session.refreshSystemPrompt();
const session = Session.create(this)
	.withContext("soul", {
		provider: { get: async () => "You are a helpful assistant." },
	})
	.withContext("memory", { description: "Learned facts", maxTokens: 1100 })
	.withCachedPrompt(); // Persist the frozen prompt across hibernation

// During a conversation turn:
const system = await session.freezeSystemPrompt(); // Same value every call
const tools = await session.tools();

// ... agent calls set_context to update memory ...
// The frozen prompt is NOT changed, prefix cache stays warm

// Between turns (optional, if you want the agent to see its own updates):
await session.refreshSystemPrompt();

Компактизация

Длинные беседы со временем превышают контекстное окно LLM. Компактизация решает эту проблему на двух уровнях: макрокомпактизация суммаризирует диапазоны более старых сообщений, а микрокомпактизация усекает отдельные сообщения, которые слишком велики.

Макрокомпактизация

Макрокомпактизация суммирует более старые сообщения, но никогда не удаляет оригиналы.

Использует оверлеи: сводка сохраняется в отдельной таблице с ключом по диапазону сообщений, который она охватывает. Когда getHistory() вызывается, наложения применяются прозрачно во время чтения. Сжатый диапазон заменяется синтетическим сообщением-сводкой. Исходные сообщения остаются в SQLite, сохраняя полный разговор для аудита, поиска и ветвления.

Messages:  [1] [2] [3] [4] [5] [6] [7] [8] [9] [10]
                    ↓ compaction ↓
Overlay:   [1] [2] [SUMMARY of 3-7]           [8] [9] [10]
                                                ↑ tail protected

Ключевые моменты:

import { createCompactFunction } from "agents/experimental/memory/utils/compaction-helpers";

const session = Session.create(this)
	.withContext("memory", { maxTokens: 1100 })
	.onCompaction(
		createCompactFunction({
			summarize: (prompt) =>
				generateText({ model: myModel, prompt }).then((r) => r.text),
			protectHead: 3,
			tailTokenBudget: 20000,
			minTailMessages: 2,
		}),
	)
	.compactAfter(100_000); // Auto-compact when token estimate exceeds threshold
import { createCompactFunction } from "agents/experimental/memory/utils/compaction-helpers";

const session = Session.create(this)
	.withContext("memory", { maxTokens: 1100 })
	.onCompaction(
		createCompactFunction({
			summarize: (prompt) =>
				generateText({ model: myModel, prompt }).then((r) => r.text),
			protectHead: 3,
			tailTokenBudget: 20000,
			minTailMessages: 2,
		}),
	)
	.compactAfter(100_000); // Auto-compact when token estimate exceeds threshold

Автокомпактизация запускается после appendMessage() когда оценочное количество токенов превышает порог. Сбой компактизации не является критическим: сообщение уже сохранено.

Микрокомпактизация

Микрокомпактизация работает на уровне отдельных сообщений, а не по диапазонам. Она решает две проблемы:

Усечение при чтении: truncateOlderMessages() укорачивает результаты работы инструментов и длинный текст в более старых сообщениях перед отправкой их в LLM. Последние сообщения (по умолчанию 4) остаются без изменений. Это происходит с копией: сохранённые сообщения не изменяются.

import { truncateOlderMessages } from "agents/experimental/memory/utils";

const history = await session.getHistory();
const truncated = truncateOlderMessages(history);
// Pass truncated history to the LLM
import { truncateOlderMessages } from "agents/experimental/memory/utils";

const history = await session.getHistory();
const truncated = truncateOlderMessages(history);
// Pass truncated history to the LLM

Контроль размера строки: когда сообщение сохраняется (как правило, это сообщение ассистента с большим объёмом вывода инструмента), оно проверяется на соответствие ограничению размера строки SQLite. Слишком объёмный вывод инструмента заменяется предпросмотром и примечанием с рекомендацией повторно запустить инструмент. Это не позволяет отдельным сообщениям превышать лимиты хранения, сохраняя при этом связность диалога.

Справочник по API Session

Полное справочное описание API для Session: сообщения, блоки контекста, компактизация, поиск, инструменты и пользовательские провайдеры.

Хранение и синхронизация состояния

setState() для более простого хранения данных по принципу ключ-значение и синхронизации в реальном времени.

Think

Чат-агент с фиксированными архитектурными соглашениями и встроенной интеграцией Session через configureSession().