# Surface Protocol — унификация поверхностей

## Идея

Любая поверхность (Telegram, Matrix, будущий Discord, голос, web) делает одно
и то же: принимает события от пользователя и отправляет ответы обратно.

Разница только в том как событие выглядит снаружи: aiogram `Message`,
matrix-nio `RoomMessageText`, Discord `Interaction`. Внутри — одно и то же.

Surface Protocol — это общий язык между адаптерами поверхностей и ядром.
Адаптер конвертирует нативные события в протокольные структуры и обратно.
Ядро работает только с протокольными структурами и ничего не знает о транспорте.

**Добавить новую поверхность = написать один адаптер-конвертер.**
Ядро не трогается.

---

## Структура проекта

```
surfaces-bot/
  core/
    protocol.py      — все унифицированные структуры данных
    handler.py       — EventDispatcher: IncomingEvent → OutgoingEvent
    handlers/        — обработчики по типам событий (start, message, chat, settings, callback)
    store.py         — StateStore Protocol + InMemoryStore + SQLiteStore
    chat.py          — ChatManager: метаданные чатов C1/C2/C3
    auth.py          — AuthManager, AuthFlow
    settings.py      — SettingsManager, SettingsAction

  adapter/
    telegram/        — aiogram адаптер
      converter.py   — aiogram Event → IncomingEvent, OutgoingEvent → aiogram API
      bot.py         — точка входа, роутер
    matrix/          — matrix-nio адаптер
      converter.py   — matrix-nio Event → IncomingEvent, OutgoingEvent → Matrix API
      bot.py         — точка входа, клиент

  sdk/
    interface.py     — Protocol: PlatformClient (контракт к SDK)
    real.py          — RealPlatformClient (через AgentApi)
    mock.py          — MockPlatformClient (для локальных тестов)
```

---

## Входящие события (Incoming)

Всё что пользователь делает конвертируется в одно из трёх:

### IncomingMessage
Обычное сообщение — текст, файл, голос.

```python
@dataclass
class IncomingMessage:
    user_id: str           # "tg_123456" | "@user:matrix.org"
    platform: str          # "telegram" | "matrix"
    chat_id: str           # "C1" | "C2" — ID чата в воркспейсе платформы
    text: str
    attachments: list[Attachment]
    reply_to: str | None   # message_id если это ответ на сообщение
```

### IncomingCommand
Команда управления — `/start`, `!new`, `/settings`.

```python
@dataclass
class IncomingCommand:
    user_id: str
    platform: str
    chat_id: str
    command: str           # "start" | "new" | "settings" | "archive" ...
    args: list[str]        # аргументы после команды
```

### IncomingCallback
Нажатие кнопки или реакции — подтверждение, переключение скилла.

```python
@dataclass
class IncomingCallback:
    user_id: str
    platform: str
    chat_id: str
    action: str            # "confirm" | "cancel" | "toggle_skill" | "connect" ...
    payload: dict          # {"skill": "browser"} | {"action_id": "abc123"} ...
```

### Attachment
Вложение, нормализованное из любого источника.

```python
@dataclass
class Attachment:
    type: str              # "image" | "document" | "audio" | "video"
    url: str | None        # ссылка если доступна
    content: bytes | None  # содержимое если скачано
    filename: str | None
    mime_type: str | None
```

---

## Исходящие события (Outgoing)

Всё что ядро хочет показать пользователю:

### OutgoingMessage
Ответ агента или системное сообщение.

```python
@dataclass
class OutgoingMessage:
    chat_id: str
    text: str
    parse_mode: str        # "markdown" | "plain"
    attachments: list[Attachment]
    reply_to: str | None
```

### OutgoingUI
Интерактивные элементы — кнопки, меню, переключатели.

```python
@dataclass
class OutgoingUI:
    chat_id: str
    text: str
    buttons: list[UIButton]

@dataclass
class UIButton:
    label: str
    action: str            # action для IncomingCallback
    payload: dict
    style: str             # "primary" | "danger" | "secondary"
```

Telegram рендерит это как InlineKeyboard.
Matrix рендерит как текст (в MVP).

### OutgoingNotification
Асинхронное уведомление — агент закончил долгую задачу.

```python
@dataclass
class OutgoingNotification:
    chat_id: str
    text: str
    level: str             # "info" | "warning" | "success" | "error"
```

### OutgoingTyping
Индикатор что агент думает.

```python
@dataclass
class OutgoingTyping:
    chat_id: str
    is_typing: bool
```

---

## Жизненный цикл (Lifecycle)

Унифицированные события для управления чатами и подключением.

### ChatContext
Метаданные чата — общие для всех поверхностей. Хранятся ботом, lifecycle контейнера управляет платформа (Master).

```python
@dataclass
class ChatContext:
    chat_id: str           # "C1" | "C2" — ID чата в воркспейсе платформы
    display_name: str      # «Чат 1» | «Анализ рынка»
    platform: str
    surface_ref: str       # room_id в Matrix | topic_id в Telegram
    created_at: datetime
    is_archived: bool
```

### AuthFlow
Флоу аутентификации — одинаков для всех поверхностей.

```python
@dataclass
class AuthFlow:
    user_id: str
    platform: str
    state: str             # "pending" | "code_sent" | "confirmed" | "failed"
    platform_user_id: str | None
```

### ConfirmationRequest
Запрос подтверждения опасного действия — от агента к пользователю.

```python
@dataclass
class ConfirmationRequest:
    action_id: str
    chat_id: str
    description: str       # «Отправить письмо на vasya@mail.ru»
    risk_level: str        # "low" | "medium" | "high"
    expires_at: datetime
```

Telegram показывает как Inline-кнопки.
Matrix показывает как запрос для `!yes` / `!no`.
Ядро не знает как именно — только получает `IncomingCallback` с `action: "confirm"`.

---

## SettingsAction

Унифицированные действия в настройках — одинаковы для Telegram и Matrix.

```python
@dataclass
class SettingsAction:
    action: str            # "connect" | "disconnect" | "toggle_skill" | ...
    payload: dict

# Примеры:
SettingsAction(action="connect", payload={"service": "gmail"})
SettingsAction(action="toggle_skill", payload={"skill": "browser", "enabled": True})
SettingsAction(action="set_soul", payload={"field": "name", "value": "Лямбда"})
SettingsAction(action="set_safety", payload={"trigger": "email-send", "enabled": True})
```

Ядро обрабатывает `SettingsAction` одинаково. Откуда пришло — не важно.

---

## PaymentEvent

Заглушка для биллинга — реализует другая команда.

```python
@dataclass
class PaymentRequired:
    user_id: str
    reason: str            # "limit_reached" | "feature_locked"
    current_plan: str
```

Поверхность получила `PaymentRequired` → показала заглушку.
Содержимое заглушки потом наполняет команда биллинга.

---

## Как написать новую поверхность

Скопируй `adapter/_template.py` и реализуй три метода:

```python
class MySurfaceAdapter:

    def parse_incoming(self, raw_event) -> IncomingMessage | IncomingCommand | IncomingCallback:
        """Конвертировать нативное событие в протокольную структуру."""
        ...

    def render_outgoing(self, event: OutgoingMessage | OutgoingUI | OutgoingNotification) -> any:
        """Конвертировать протокольную структуру в нативный формат."""
        ...

    async def send(self, rendered) -> None:
        """Отправить нативным способом."""
        ...
```

Дальше подключи адаптер в точку входа и передай `core.handler.handle` как callback.
Всё остальное уже работает.

---

## Что ядро даёт бесплатно

Любая новая поверхность получает без дополнительного кода:

- управление чатами (`ChatContext`, C1/C2/C3)
- аутентификацию (`AuthFlow`)
- подтверждение действий (`ConfirmationRequest`)
- все настройки (коннекторы, скиллы, SOUL, безопасность, подписка)
- интеграцию с платформой через `PlatformClient`
- обработку ошибок платформы

---

## Замена MockPlatformClient на реальный SDK

Вся работа с платформой идёт через `PlatformClient` протокол:

```python
class PlatformClient(Protocol):
    async def get_or_create_user(self, external_id: str, platform: str,
                                 display_name: str | None = None) -> User: ...
    async def send_message(self, user_id: str, chat_id: str, text: str,
                           attachments: list | None = None) -> MessageResponse: ...
    async def get_settings(self, user_id: str) -> UserSettings: ...
    async def update_settings(self, user_id: str, action: Any) -> None: ...
```

Бот **не управляет lifecycle контейнеров** агентов. Запуск/перезапуск агентов — ответственность платформы.
Бот передаёт `user_id` + `chat_id` + текст.

`MockPlatformClient` реализует этот протокол для локальных тестов.
Реальный SDK используется через `RealPlatformClient` (`sdk/real.py`), который подключается к `AgentApi` по WebSocket.
Адаптеры поверхностей и ядро не меняются вообще, привязка идёт через `config/matrix-agents.yaml`.
