diff --git a/docs/superpowers/specs/2026-03-31-matrix-adapter-design.md b/docs/superpowers/specs/2026-03-31-matrix-adapter-design.md new file mode 100644 index 0000000..c7552a1 --- /dev/null +++ b/docs/superpowers/specs/2026-03-31-matrix-adapter-design.md @@ -0,0 +1,263 @@ +# Matrix Adapter Design + +**Date:** 2026-03-31 +**Status:** Approved — ready for implementation +**Scope:** `adapter/matrix/` + +--- + +## Контекст + +Matrix-адаптер — внутренняя поверхность для команды Lambda Lab: разработчики, тестировщики, авторы скиллов. UX ориентирован на удобство работы, не на онбординг. + +Адаптер конвертирует matrix-nio события в `IncomingEvent` (core protocol) и отправляет `OutgoingEvent` обратно. Бизнес-логика — в `core/`, адаптер только переводит форматы и управляет Matrix API. + +Клиент: Element (web/desktop). Стек: matrix-nio (async), Python 3.11+, SQLite. + +--- + +## Онбординг — DM как первый чат (ленивый Space) + +**Решение:** DM-комната с ботом = Чат #1. Space создаётся только при первом `!new`. + +### Флоу + +1. Пользователь инвайтит бота в личные сообщения +2. Бот принимает инвайт, регистрирует DM-комнату как `chat_room` с `chat_id = C1` +3. Бот пишет приветствие в DM — пользователь сразу пишет +4. При первом `!new` — бот создаёт Space `Lambda — {display_name}`, добавляет DM-комнату в Space через `m.space.child`, создаёт новую комнату-чат + +### Почему не Space сразу + +Создание Space при инвайте порождает 3 инвайта подряд (Space + Settings + Чат 1) до первого сообщения. DM-first убирает этот шум, сохраняя такой же UX как Telegram. + +### Приветствие + +``` +Привет, {display_name}! Пиши — я здесь. + +Команды: !new · !chats · !rename · !archive · !skills +``` + +--- + +## Архитектура — Room-type routing + +При получении события адаптер сначала определяет тип комнаты (`chat` / `settings`), затем маршрутизирует в соответствующий обработчик. + +``` +adapter/matrix/ + bot.py — matrix-nio клиент, sync loop + converter.py — RoomEvent → IncomingEvent, OutgoingEvent → Matrix API + room_router.py — определяет тип комнаты: chat | settings + states.py — FSM состояния (per room_id, SQLite) + + handlers/ + auth.py — invite → onboarding + chat.py — сообщения, !new, !chats, !rename, !archive + settings.py — !skills, !connectors, !soul, !safety, !plan, !status, !whoami + confirm.py — реакции 👍/❌ и команды !yes / !no + + reactions.py — helpers: add_reaction, remove_reactions, parse_reaction_event +``` + +--- + +## FSM состояния (per room_id) + +```python +class RoomState(StatesGroup): + idle = State() # ждём сообщения + waiting_response = State() # запрос ушёл на платформу + confirm_pending = State() # ждём !yes/!no или реакцию 👍/❌ + settings_active = State() # Settings-комната (не чат) +``` + +`room_type` хранится в SQLite. `room_router.py` читает его при каждом событии. + +--- + +## Команды + +Все команды на английском. Работают в любой комнате Space. + +| Команда | Действие | +|---------|---------| +| `!new [name]` | Создать чат. При первом вызове — создаёт Space, переносит DM | +| `!chats` | Список чатов с текущим активным | +| `!rename ` | Переименовать текущую комнату | +| `!archive` | Вывести комнату из Space (не удалять) | +| `!skills` | Список скиллов — реакции как тумблеры | +| `!connectors` | Коннекторы (OAuth заглушки) | +| `!soul` | Личность агента | +| `!safety` | Настройки безопасности | +| `!plan` | Подписка и токены | +| `!status` | Состояние платформы и чатов | +| `!whoami` | Текущий аккаунт | +| `!yes` / `!no` | Подтверждение / отмена действия агента | + +--- + +## Settings room + +Создаётся при первом `!new` вместе со Space. Закреплена вверху Space. + +### Скиллы — реакции как тумблеры + +`!skills` → бот отправляет список. Каждый скилл пронумерован. Реакция 1️⃣–N️⃣ переключает соответствующий скилл (toggle). Несколько скиллов могут быть включены одновременно. Бот редактирует своё сообщение через `m.replace` после каждого переключения. + +``` +✅ 1 web-search — поиск в интернете +✅ 2 fetch-url — чтение веб-страниц +✅ 3 email — чтение почты +❌ 4 browser — управление браузером +❌ 5 image-gen — генерация изображений +✅ 6 files — работа с файлами + +Реакция 1️⃣–6️⃣ = переключить скилл +``` + +### Остальные настройки + +`!connectors`, `!soul`, `!safety`, `!plan`, `!status`, `!whoami` — текстовые ответы, без интерактивных элементов. Поля задаются аргументами команды: `!soul name Lambda`, `!soul style brief`, `!safety on email-send`. + +--- + +## Подтверждение действий агента + +Агент запрашивает подтверждение → бот отправляет сообщение с описанием действия. Пользователь подтверждает **реакцией или командой** — оба способа работают. + +``` +🤖 Lambda: +Отправить письмо azamat@lambda.lab? +Тема: «Отчёт за неделю» + +👍 подтвердить · ❌ отменить +!yes — подтвердить · !no — отменить + +Истекает через 5 минут +``` + +После ответа: бот убирает реакции с сообщения, редактирует статус (`m.replace`), переходит в `idle`. + +FSM: `waiting_response` → `confirm_pending` → `idle` + +--- + +## Долгие задачи — треды + +Если задача занимает больше одного хода — бот создаёт тред от своего первого сообщения. + +``` +🤖 Lambda (основной поток): +Начинаю исследование «AI агенты 2025» — займёт 2-3 минуты. + 🧵 Прогресс (в треде): + └── ✅ Ищу источники... (12 найдено) + └── ✅ Анализирую статьи... + └── ⏳ Формирую отчёт... + └── ○ Финальная проверка +``` + +Основной поток не засоряется. Финальный результат — отдельным сообщением в основной поток. + +--- + +## Typing indicator + +`m.typing` — отправлять перед запросом к платформе. Если запрос > 5 сек — возобновлять каждые 25 сек (Matrix typing живёт ~30 сек). + +--- + +## Converter + +`adapter/matrix/converter.py` — конвертация в обе стороны. + +### matrix-nio → IncomingEvent + +```python +def from_room_message(event: RoomMessageText, room_id: str, chat_id: str) -> IncomingMessage: + return IncomingMessage( + user_id=event.sender, # @user:matrix.org + platform="matrix", + chat_id=chat_id, # C1, C2... из rooms таблицы + text=event.body, + attachments=[], + reply_to=event.replyto_event_id, + ) + +def from_reaction(event: ReactionEvent, room_id: str) -> IncomingCallback | None: + # Парсит m.reaction → IncomingCallback(action="toggle_skill" | "confirm" | "cancel") + ... + +def from_command(body: str, sender: str, room_id: str, chat_id: str) -> IncomingCommand | None: + # Парсит !new, !skills, !yes, !no и т.д. → IncomingCommand + ... +``` + +### OutgoingEvent → Matrix + +```python +async def send_outgoing(client: AsyncClient, room_id: str, event: OutgoingEvent) -> None: + if isinstance(event, OutgoingMessage): + await client.room_send(room_id, "m.room.message", {"msgtype": "m.text", "body": event.text}) + + elif isinstance(event, OutgoingUI): + # Confirmation request — текст + подсказка по реакциям/командам + body = f"{event.text}\n\n👍 подтвердить · ❌ отменить\n!yes — подтвердить · !no — отменить" + await client.room_send(room_id, "m.room.message", {"msgtype": "m.text", "body": body}) + await client.room_send(room_id, "m.reaction", {...}) # добавить 👍 и ❌ на сообщение + + elif isinstance(event, OutgoingTyping): + await client.room_typing(room_id, event.is_typing, timeout=25000) +``` + +--- + +## БД схема + +```sql +CREATE TABLE matrix_users ( + matrix_user_id TEXT PRIMARY KEY, -- @user:matrix.org + platform_user_id TEXT NOT NULL, -- из MockPlatformClient + display_name TEXT, + space_id TEXT, -- NULL до первого !new + settings_room_id TEXT, -- NULL до первого !new + created_at TIMESTAMP +); + +CREATE TABLE rooms ( + room_id TEXT PRIMARY KEY, -- room_id Matrix + matrix_user_id TEXT NOT NULL, + room_type TEXT NOT NULL, -- 'chat' | 'settings' + chat_id TEXT, -- C1, C2... (NULL для settings) + display_name TEXT, + created_at TIMESTAMP, + archived_at TIMESTAMP, + FOREIGN KEY(matrix_user_id) REFERENCES matrix_users(matrix_user_id) +); +``` + +`StateStore` из `core/store.py` (`SQLiteStore`) — для FSM per room_id. + +--- + +## Что НЕ реализуем в прототипе + +- Webhook от платформы (используем sync `send_message`) +- E2E encryption (nio поддерживает, но усложняет прототип) +- Экспорт истории +- `!rename`, `!archive` — добавить после основного флоу + +--- + +## Порядок реализации + +1. `bot.py` — AsyncClient, sync loop, middleware для platform client +2. `states.py` — RoomState +3. `room_router.py` — определение типа комнаты +4. `converter.py` — from_room_message, from_reaction, from_command +5. `handlers/auth.py` — invite → onboarding +6. `handlers/chat.py` — сообщения + !new + !chats +7. `reactions.py` — helpers для работы с реакциями +8. `handlers/confirm.py` — реакции 👍/❌ + !yes/!no +9. `handlers/settings.py` — !skills с m.replace + остальные команды