# Роли агента (Agent Roles) — проектный план > Статус: проработанная фича, **не реализована**. Контекст: gitmost — форк Docmost. > Идея: дать возможность создавать переиспользуемые **роли агента** (например > «Корректор» или «Факт-чекер, который ходит в веб и проверяет факты») и > заводить чат, привязанный к выбранной роли. Роль задаёт поведение агента > (системный промпт) и, опционально, модель. > > Зафиксированные решения по объёму (см. раздел «Развилки»): > - **Владение** — только **админские, общие на воркспейс** роли (как провайдер и > внешние MCP-серверы сегодня). Личных ролей в v1 нет. > - **Гейтинг инструментов** — **нет**. Роль меняет только инструкции и (опц.) модель; > набор инструментов всегда полный (тот же, что у обычного чата). Ограничение > возможностей по ролям отложено (см. «Возможные расширения»). > - **Артефакт этого шага** — только дизайн-документ; код не пишется. ## Зачем это (и почему ложится в текущую архитектуру) Сегодня у встроенного AI-агента нет понятия персоны/роли на уровне чата: вся настройка поведения — один системный промпт **на весь воркспейс**. Пользователь хочет заводить разные чаты под разные задачи (вычитка орфографии, проверка фактов по вебу и т. д.), каждый — со своей инструкцией и, возможно, своей моделью. Три факта из текущего кода определяют дизайн (всё сверено по исходникам): 1. **Системный промпт — только на уровне воркспейса.** Собирается в [ai-chat.prompt.ts](../apps/server/src/core/ai-chat/ai-chat.prompt.ts), функция `buildSystemPrompt()`, по слоям: *базовая персона* (`workspace.settings.ai.provider.systemPrompt` либо `DEFAULT_PROMPT`) → *контекст* (имя воркспейса, открытая страница) → несъёмный `SAFETY_FRAMEWORK`. Персоны на чат сейчас нет — её надо добавить как ещё один слой. 2. **Инструменты — всегда все включены.** В [ai-chat.service.ts](../apps/server/src/core/ai-chat/ai-chat.service.ts): `const tools = { ...external.tools, ...docmostTools }`. ~40 Docmost-инструментов строит `AiChatToolsService.forUser()` ([tools/ai-chat-tools.service.ts](../apps/server/src/core/ai-chat/tools/ai-chat-tools.service.ts)), внешние MCP-инструменты подмешивает `mcpClients.toolsFor(workspaceId)` ([external-mcp/mcp-clients.service.ts](../apps/server/src/core/ai-chat/external-mcp/mcp-clients.service.ts)). Механизма включать подмножество инструментов нет — есть только CASL-проверка в момент вызова (через персональный loopback-токен). **По зафиксированному решению этот механизм мы и не вводим** — роль не трогает набор инструментов. 3. **Веб-доступ уже решён внешними MCP.** Внешние MCP-серверы (`ai_mcp_servers`, напр. Tavily) с SSRF-защитой ([external-mcp/ssrf-guard.ts](../apps/server/src/core/ai-chat/external-mcp/ssrf-guard.ts)) и шифрованием заголовков — это и есть «факт-чекер ходит в гугл». Поскольку гейтинга нет, веб-инструменты **уже доступны каждому чату**, если админ подключил соответствующий MCP-сервер. Роль «Факт-чекер» работает чисто за счёт инструкции «проверяй факты по веб-источникам и цитируй ссылки» — она направляет модель пользоваться уже доступными инструментами, а не добавляет их. 4. **Чат создаётся неявно** при первом сообщении: клиент ([chat-thread.tsx](../apps/client/src/features/ai-chat/components/chat-thread.tsx)) шлёт POST `/api/ai-chat/stream` с `chatId: null`, сервер ([ai-chat.controller.ts](../apps/server/src/core/ai-chat/ai-chat.controller.ts)) создаёт строку `ai_chats`. Привязать чат к роли можно одним новым полем `role_id`, которое клиент передаёт один раз при первом сообщении. **Вывод:** роль — это тонкий слой поверх существующего пайплайна. Нужны: новая таблица ролей + админский CRUD, поле `ai_chats.role_id`, новый слой в `buildSystemPrompt()`, опциональный override модели в `getChatModel()`, пикер роли и управление ролями в UI. Граница безопасности (CASL через loopback-токен) **не меняется** — роль её не ослабляет и не усиливает (см. «Безопасность»). ## Модель **Роль (Agent Role)** — именованный, общий на воркспейс пресет, который связывает: | Часть | Что задаёт | Пример «Корректор» | Пример «Факт-чекер» | | --- | --- | --- | --- | | **instructions** | фрагмент системного промпта (персона/поведение) | «Исправляй только орфографию, пунктуацию и грамматику. Никогда не меняй смысл, факты, тон и структуру текста. Используй точечную правку текста» | «Проверяй фактические утверждения страницы по авторитетным веб-источникам. Цитируй ссылки. Помечай сомнительные места комментарием. Не редактируй текст страницы без явной просьбы» | | **model (опц.)** | модель ≠ дефолт воркспейса | дешёвая модель | сильная модель | | **presentation** | имя, emoji, описание | 🔤 «Корректор» | 🔎 «Факт-чекер» | Чего роль в v1 **не** задаёт (по зафиксированным решениям): набор инструментов, выбор конкретных внешних MCP-серверов, владельца (роли только общие/админские), снапшот конфигурации на чат. **Привязка чата к роли** — нулевое поле `ai_chats.role_id`. Чат «помнит», с какой ролью создан; роль применяется на каждом ходу. Чат без роли (`role_id IS NULL`) — обычный универсальный ассистент (текущее поведение). ## Модель данных (миграции) Соглашение: `apps/server/src/database/migrations/YYYYMMDDThhmmss-description.ts`. Только **добавляем** таблицы/столбцы (никогда не трогаем данные Docmost). Timestamp новой миграции должен сортироваться **после** последней применённой; на момент написания последняя — `20260618T160000-ai-stt-credentials.ts`, значит брать `20260619T...`. После миграции — `pnpm --filter server migration:codegen` для регенерации [db.d.ts](../apps/server/src/database/types/db.d.ts). Образец стиля — [20260617T130000-ai-mcp-servers.ts](../apps/server/src/database/migrations/20260617T130000-ai-mcp-servers.ts). **Миграция — таблица ролей + привязка чата:** ```sql CREATE TABLE ai_agent_roles ( id uuid PRIMARY KEY DEFAULT gen_uuid_v7(), workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, creator_id uuid REFERENCES users(id) ON DELETE SET NULL, -- кто создал (аудит) name varchar NOT NULL, -- "Корректор" emoji varchar, -- presentation description text, instructions text NOT NULL, -- фрагмент system prompt model_config jsonb, -- { driver?, chatModel } | NULL = дефолт воркспейса enabled boolean NOT NULL DEFAULT true, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), deleted_at timestamptz -- soft delete (как у ai_chats) ); CREATE INDEX idx_ai_agent_roles_workspace_id ON ai_agent_roles (workspace_id); -- привязка чата к роли ALTER TABLE ai_chats ADD COLUMN role_id uuid REFERENCES ai_agent_roles(id) ON DELETE SET NULL; ``` Заметки: - `creator_id ON DELETE SET NULL` — роль общая и переживает удаление автора (в отличие от `ai_chats.creator_id`, который `NOT NULL`); это только аудит. - `ai_chats.role_id ON DELETE SET NULL` — если роль жёстко удалят, чат деградирует к универсальному поведению, а не ломается (см. edge-cases). В сочетании с `deleted_at` основной путь удаления роли — **soft delete**: старые чаты тогда продолжают видеть инструкции через JOIN с учётом `deleted_at` (решение по поведению при удалении — в «Открытых вопросах»). - `model_config jsonb` — `{ chatModel }` либо `{ driver, chatModel }`. Пусто/`NULL` → модель воркспейса. По образцу `publicShareChatModel` из [public-share-assistant-plan.md](./public-share-assistant-plan.md): креды (`apiKey`/`baseUrl`) берутся от провайдера соответствующего драйвера из `ai_provider_credentials`, отдельные креды на роль не нужны. Типы: добавить `AiAgentRoles` в `db.interface.ts` (или поднять через codegen), `role_id` появится в `AiChats` автоматически после codegen. ## Бэкенд ### 1. Слой инструкций роли в системном промпте В [ai-chat.prompt.ts](../apps/server/src/core/ai-chat/ai-chat.prompt.ts) добавить вход `roleInstructions` в `buildSystemPrompt()`. Приоритет персоны: ```text effectivePersona = roleInstructions?.trim() || adminPrompt?.trim() || DEFAULT_PROMPT return `${effectivePersona}${context}\n${SAFETY_FRAMEWORK}` ``` Ключевое: **`SAFETY_FRAMEWORK` по-прежнему добавляется всегда и не отключается ролью.** Роль задаёт только персону; контекст (воркспейс, открытая страница) и safety-блок остаются как есть. Решение «роль заменяет, а не дополняет admin-промпт» выбрано намеренно: для узкой роли вроде «Корректора» нужно, чтобы её инструкция доминировала, а не конкурировала с общим промптом воркспейса. (Альтернатива «конкатенировать admin-промпт + роль» — в «Открытых вопросах».) ### 2. Применение роли в стриме В [ai-chat.service.ts](../apps/server/src/core/ai-chat/ai-chat.service.ts) (метод `stream()`), где сейчас резолвится `system` и `model`: - Загрузить роль по `ai_chats.role_id` (если задан и не удалён). - Передать `role.instructions` в `buildSystemPrompt({ ..., roleInstructions })`. - Если у роли есть `model_config` — резолвить модель с override (см. п. 3). - Набор инструментов **не меняется** (по решению). Важно: `role_id` сервер берёт **из строки `ai_chats`, а не из тела запроса** на каждом ходу — роль нельзя подменить пораздачно. Клиент сообщает `roleId` только при создании чата (первое сообщение), сервер сохраняет его в `ai_chats.role_id`. ### 3. Override модели `AiService.getChatModel(workspaceId)` ([integrations/ai/ai.service.ts](../apps/server/src/integrations/ai/ai.service.ts)) получает опциональный аргумент override модели (паттерн из [public-share-assistant-plan.md](./public-share-assistant-plan.md) §5): - `model_config.chatModel` — id модели вместо `chatModel` воркспейса; - `model_config.driver` (опц.) — если указан другой драйвер, берём его креды из `ai_provider_credentials`; если кредов нет → `AiNotConfiguredException` (503) с **внятным сообщением** («для роли X выбран провайдер Y, но он не настроен»), согласно конвенции об ошибках (никаких «Something went wrong»). - Пусто → текущее поведение (модель воркспейса). Резолв модели делать **до** hijack ответа, чтобы ненастроенный провайдер вернул 503, а не падал в середине стрима (как уже сделано в контроллере для воркспейс-модели). ### 4. CRUD ролей (админский модуль) Новый модуль `core/ai-chat/roles/` рядом с `external-mcp/`: `ai-agent-roles.controller.ts` + `ai-agent-roles.service.ts` + repo (`database/repos/ai-agent-roles/`). Эндпоинты под `/api/ai-chat/roles` (или `/api/ai-settings/roles` — рядом с MCP-серверами; выбрать единообразно с существующим размещением, см. «Открытые вопросы»): | Метод | Доступ | Назначение | | --- | --- | --- | | `list` | **любой участник воркспейса** | получить список ролей для пикера при создании чата | | `create` / `update` / `delete` | **только админ** | управление ролями (как `ai-settings`) | Нюанс CASL: создание/правка/удаление — под админской абилити (как [ai-settings.controller.ts](../apps/server/src/core/.../ai-settings.controller.ts) управляет провайдером и MCP-серверами), но **list должен быть доступен всем участникам**, иначе обычный пользователь не сможет выбрать роль при заведении чата. Все запросы строго скоупятся по `workspace_id` (мультитенант по хосту). Валидация при create/update: непустые `name` и `instructions`; если задан `model_config.driver` — он из числа поддерживаемых (`openai`/`gemini`/`ollama`). ## Клиент ### 1. Пикер роли при создании чата В зоне «New chat» / композере ([ai-chat-window.tsx](../apps/client/src/features/ai-chat/components/ai-chat-window.tsx), [chat-input.tsx](../apps/client/src/features/ai-chat/components/chat-input.tsx)) — селектор роли (Mantine `Select`/`SegmentedControl`), дефолт «Универсальный ассистент» (без роли). Выбранный `roleId` хранится в новом Jotai-атоме рядом с [atoms/ai-chat-atom.ts](../apps/client/src/features/ai-chat/atoms/ai-chat-atom.ts) и уходит в теле **первого** запроса на `/stream` (расширить `prepareSendMessagesRequest` в `chat-thread.tsx`: добавить `roleId`). После того как сервер создал чат с ролью, пикер для этого чата фиксируется (роль чата неизменна; смена роли = новый чат — простое и предсказуемое поведение для v1). ### 2. Бейдж роли Показывать emoji+имя роли в шапке окна чата и в строке списка ([conversation-list.tsx](../apps/client/src/features/ai-chat/components/conversation-list.tsx)), чтобы было видно, «с кем» разговор. `role_id`/денормализованное имя+emoji роли добавить в выдачу списка чатов и тип `IAiChat` ([types/ai-chat.types.ts](../apps/client/src/features/ai-chat/types/ai-chat.types.ts)). ### 3. Управление ролями в настройках Новая секция «Роли агента» в Settings → AI ([pages/settings/workspace/ai-settings.tsx](../apps/client/src/pages/settings/workspace/ai-settings.tsx)), рядом с «External tools». Переиспользовать паттерн add/edit/delete-модалки из [ai-mcp-servers.tsx](../apps/client/src/features/workspace/components/settings/components/ai-mcp-servers.tsx). Форма роли: имя, emoji, описание, **instructions** (textarea — как редактор системного сообщения в [ai-provider-settings.tsx](../apps/client/src/features/workspace/components/settings/components/ai-provider-settings.tsx)), опциональный override модели. Подпись-напоминание под полем instructions: «встроенный safety-фреймворк добавляется автоматически» (как у системного сообщения). ### 4. Слой запросов Новые TanStack Query хуки в [queries/ai-chat-query.ts](../apps/client/src/features/ai-chat/queries/ai-chat-query.ts) (или отдельный файл): `useAiRolesQuery()` (list), `useCreate/Update/DeleteAiRoleMutation()` + функции в [services/ai-chat-service.ts](../apps/client/src/features/ai-chat/services/ai-chat-service.ts). Тип `IAiRole` зеркалит серверную схему. ## Поток одного хода (с ролью) 1. Создание чата: клиент шлёт первое сообщение + `roleId` → `/ai-chat/stream`; сервер создаёт `ai_chats` с `role_id`. 2. Последующие ходы: сервер читает `role_id` из строки чата (не из тела запроса). 3. Резолв: загрузить роль (если не удалена) → `instructions` + `model_config`. 4. `buildSystemPrompt({ workspace, adminPrompt, roleInstructions, openedPage })` → персона роли + контекст + несъёмный `SAFETY_FRAMEWORK`. 5. `getChatModel(workspaceId, role.model_config)` → модель роли или дефолт. 6. `streamText({ model, system, messages, tools, stopWhen: stepCountIs(8) })` — **набор инструментов полный, как у обычного чата**. ## Edge-cases (главное) - **Роль удалена/выключена, а чаты на неё ссылаются.** При hard-delete `ON DELETE SET NULL` обнуляет `ai_chats.role_id` → чат продолжает работать как универсальный. Основной путь — soft-delete (`deleted_at`)/`enabled=false`: тогда роль исчезает из пикера, но старые чаты могут продолжать применять её инструкции (резолв учитывает `deleted_at` — точное поведение в «Открытых вопросах»). - **Роль отредактировали после создания чатов.** В v1 без снапшота правка применяется «вживую» — старые чаты подхватывают новые инструкции на следующем ходу. Приемлемо для кейсов «Корректор/Факт-чекер»; снапшот конфигурации на чат — возможное расширение. - **Safety не переопределяется.** `SAFETY_FRAMEWORK` добавляется всегда, что бы ни написали в `instructions` роли (включая попытку «игнорируй прежние инструкции»). - **Override модели на ненастроенный провайдер** → 503 с конкретным сообщением, а не молчаливый фолбэк (конвенция об ошибках). Решить, делать ли мягкий фолбэк на модель воркспейса (в «Открытых вопросах»). - **Пустые `instructions`** недопустимы при создании (валидация); но если роль как-то оказалась с пустыми инструкциями — персона падает на admin-промпт/дефолт. - **Заголовок чата** генерируется фоново (`generateText`) — оставить на модели воркспейса, чтобы экзотический override роли не ломал автозаголовок (мелочь). - **Мультитенант.** Все операции с ролями скоупятся по `workspace_id`; роль из чужого воркспейса не видна и не применима. - **MCP-зеркало схемы** ([packages/mcp](../packages/mcp)) фичу не затрагивает — роли живут только во встроенном AI-чате, не в standalone MCP. ## Безопасность - **Граница безопасности не меняется.** Агент по-прежнему ходит в API через персональный loopback-JWT (`AiChatToolsService.forUser`), и CASL ограничивает его ровно правами текущего пользователя. Роль — это слой *формирования промпта и выбора модели*, он не выдаёт и не отнимает прав. - **Следствие решения «без гейтинга» (осознанный компромисс):** - Роль «Корректор» инструкцией просят не менять смысл, но технически у чата остаются все write-инструменты — модель *могла бы* отредактировать/удалить (под soft-delete и CASL, т. е. обратимо и в пределах прав пользователя). Это мягкая граница (промпт), а не жёсткая. - Роль «Факт-чекер» полагается на то, что админ глобально подключил веб-MCP (Tavily); тогда веб-инструменты доступны *всем* чатам, а не только этой роли. Жёсткие границы возможностей по ролям — отдельная будущая фаза (см. ниже). - **Instructions — доверенный контент:** их пишет админ воркспейса, они попадают только в системный промпт чатов этого воркспейса и исполняются под правами конкретного пользователя. Эскалации нет. - **Внешние MCP** остаются под SSRF-guard; роли логику подключения MCP не трогают. ## Явные non-goals (v1) - Нет гейтинга/ограничения инструментов по ролям (роль не сужает тулсет). - Нет личных ролей (только общие админские). - Нет выбора конкретных внешних MCP-серверов на роль (все включённые доступны всем). - Нет снапшота конфигурации роли на чат (правка роли применяется вживую). - Нет per-role параметров генерации сверх модели (temperature и т. п.). - Нет композиции «скиллов» поверх роли (см. «Связь со „скиллами“»). ## Связь со «скиллами» В терминах Anthropic Skills (подгружаемый по требованию пакет инструкций + ресурсов/скриптов) текущая роль = MVP-«скилл»: только текстовая инструкция + выбор модели. Естественная эволюция — сделать «скиллы» композируемыми (несколько скиллов на одну роль), привязывать к роли эталонные страницы/файлы как контекст, и — главное — добавить **жёсткий гейтинг инструментов** (тогда «Корректор» физически не сможет удалять, а «Факт-чекер» получит веб ровно тогда, когда роль это разрешает). Всё это — следующие итерации, вне scope v1. ## Развилки (зафиксированные решения) | Развилка | Решение | Альтернативы (отклонены / отложены) | | --- | --- | --- | | Владение ролями | **Только админские, общие на воркспейс** | личные роли; личные + общие | | Ограничение инструментов | **Нет (только промпт + модель)** | крупные группы возможностей; тонкий per-tool allowlist | | Выбор MCP-серверов на роль | **Нет (все включённые доступны всем)** | мультиселект MCP-серверов на роль | | Привязка чата к роли | **Поле `ai_chats.role_id`, неизменно после создания** | смена роли внутри чата; роль в теле каждого запроса | | Персона роли vs admin-промпт | **Роль заменяет персону** (safety всегда добавляется) | конкатенация admin-промпт + роль | | Снапшот конфигурации | **Нет (правка вживую)** | снапшот конфигурации роли на чат | ## Открытые вопросы (не блокируют дизайн) 1. **Размещение CRUD-эндпоинтов и UI:** `/ai-chat/roles` (рядом с чатом) или `/ai-settings/roles` (рядом с MCP-серверами). Предлагаю в одном месте с MCP — там уже живут админские AI-настройки. 2. **Поведение при удалении роли:** soft-delete с сохранением инструкций для старых чатов vs hard-delete + `SET NULL` (старые чаты деградируют к универсальным). Предлагаю soft-delete (`deleted_at`) — консистентно с `ai_chats`. 3. **Override модели на ненастроенный драйвер:** жёсткий 503 с внятным сообщением vs мягкий фолбэк на модель воркспейса. Предлагаю 503 (явность важнее). 4. **Стартовые пресеты:** поставлять ли «Корректор» и «Факт-чекер» как преднастроенные роли-шаблоны (seed) при включении фичи, чтобы админ не писал инструкции с нуля. Предлагаю — да, как необязательный «вставить пример». 5. **Денормализация для бейджа:** хранить имя/emoji роли только в `ai_agent_roles` и джойнить, либо денормализовать на `ai_chats` для дешёвого списка. Предлагаю джойн (простота; список чатов не горячий путь). ## Объём работ Бэкенд: 1 миграция (`ai_agent_roles` + `ai_chats.role_id`) + codegen типов; новый CRUD-модуль ролей (controller/service/repo) под CASL; правка `buildSystemPrompt()` (слой `roleInstructions`); правка `AiChatService.stream()` (загрузка роли, передача инструкций и override модели); опциональный override модели в `AiService.getChatModel()`. Клиент: пикер роли при создании чата + атом + проброс `roleId` в первый запрос; бейдж роли в шапке и списке; секция управления ролями в Settings → AI (модалка add/edit/delete по образцу MCP-серверов); хуки запросов/мутаций. **Без изменений в `packages/mcp`. Набор инструментов агента не трогаем.**