# Transport Layer Thin Adapter Design

## Цель

Упростить transport layer между Matrix surface и `platform-agent` до максимально production-like вида:

- использовать upstream `platform-agent_api.AgentApi` почти как есть
- убрать из surface-side клиента собственную интерпретацию stream semantics
- оставить в нашем коде только integration concerns:
  - per-chat lifecycle
  - per-chat serialization
  - attachment path forwarding
  - exception mapping в `PlatformError`

Это нужно, чтобы:

- восстановить чёткую границу ответственности между `surfaces` и платформой
- убрать из диагностики ложные факторы, внесённые нашей кастомной обёрткой
- получить честную картину реальных platform bugs до добавления любых policy-надстроек

## Контекст

Сейчас transport path состоит из:

- [sdk/agent_api_wrapper.py](/Users/a/MAI/sem2/lambda/surfaces-bot/sdk/agent_api_wrapper.py)
- [sdk/real.py](/Users/a/MAI/sem2/lambda/surfaces-bot/sdk/real.py)

Изначально `AgentApiWrapper` был создан по разумным причинам:

- поддержка переходного периода между разными версиями `platform-agent_api`
- унификация `base_url/url`
- создание per-chat client instances через `for_chat()`
- локальный учёт `tokens_used`

Позже в этот слой были добавлены уже не compatibility-функции, а собственные transport semantics:

- custom `_listen()`
- custom `send_message()`
- post-END drain window
- custom idle timeout
- event-kind reclassification

После этого `surfaces` перестал быть тонким клиентом платформы и начал вести себя как альтернативная реализация transport layer. Это делает диагностику platform bugs нечистой.

## Принципы дизайна

### 1. Transport должен быть скучным

Transport layer не должен:

- спасать поздние chunks
- лечить duplicate `END`
- придумывать собственные правила границы ответа
- по-своему классифицировать stream events сверх upstream client behavior

Если upstream stream protocol повреждён, мы должны видеть это как platform issue, а не скрывать его кастомной очередью.

### 2. Policy и transport разделяются

Transport:

- говорит с upstream API
- доставляет события
- закрывает соединение

Policy:

- решает, что считать recoverable failure
- нужна ли повторная попытка
- как сообщать ошибку пользователю
- нужно ли сбрасывать chat session

На первом этапе policy не расширяется. Мы сначала приводим transport к тонкому адаптеру, потом заново оцениваем реальные проблемы.

### 3. Session lifecycle остаётся на нашей стороне

Даже в thin-adapter модели `surfaces` по-прежнему отвечает за:

- кеширование client per chat
- один send lock на chat
- сброс мёртвой chat session после failure
- mapping upstream exceptions в `PlatformError`

Это не transport semantics, а integration lifecycle.

## Варианты

### Вариант A. Оставить текущий кастомный wrapper

Плюсы:

- уже работает на части сценариев
- содержит built-in mitigations против observed failures

Минусы:

- нарушает границу ответственности
- усложняет диагностику
- делает platform bug reports спорными
- содержит symptom-fix логику в transport layer

Вердикт: не подходит как production-like target.

### Вариант B. Thin upstream adapter

Плюсы:

- чистая архитектура
- честная диагностика upstream проблем
- минимальная собственная магия

Минусы:

- локальные mitigations исчезают
- если upstream client несовершенен, это сразу проявится

Вердикт: правильный первый этап.

### Вариант C. Thin adapter сейчас, outer policy layer потом

Плюсы:

- даёт production-like эволюцию
- не смешивает transport и resilience policy
- позволяет сначала увидеть реальные проблемы, потом адресовать только нужные

Минусы:

- требует двух фаз вместо одной

Вердикт: рекомендуемый путь.

## Рекомендуемая архитектура

### Слой 1. Upstream client

Источник истины:

- [external/platform-agent_api/lambda_agent_api/agent_api.py](/Users/a/MAI/sem2/lambda/surfaces-bot/external/platform-agent_api/lambda_agent_api/agent_api.py)

Мы принимаем его stream semantics как authoritative behavior.

### Слой 2. Thin adapter

Файл:

- [sdk/agent_api_wrapper.py](/Users/a/MAI/sem2/lambda/surfaces-bot/sdk/agent_api_wrapper.py)

После cleanup он должен содержать только:

- создание клиента через modern constructor
- `base_url` normalization, если это действительно нужно для наших env
- `for_chat(chat_id)` как factory convenience
- опционально thin storage для `last_tokens_used`, если это можно сделать без переопределения stream semantics

Он не должен переопределять:

- `_listen()`
- `send_message()`
- queue lifecycle
- post-END behavior
- timeout behavior

### Слой 3. Integration/session layer

Файл:

- [sdk/real.py](/Users/a/MAI/sem2/lambda/surfaces-bot/sdk/real.py)

Ответственность:

- кешировать chat client instances
- сериализовать sends по chat lock
- вызывать `disconnect_chat(chat_id)` после transport failure
- превращать upstream exceptions в `PlatformError`
- форвардить `attachments` как relative workspace paths
- собирать `MessageResponse` / `MessageChunk` для остального приложения

Этот слой не должен заниматься:

- исправлением broken stream boundaries
- custom post-END reconstruction
- поздним дренированием очереди

## Что удаляем

Из [sdk/agent_api_wrapper.py](/Users/a/MAI/sem2/lambda/surfaces-bot/sdk/agent_api_wrapper.py):

- custom `_listen()`
- custom `send_message()`
- `_drain_post_end_events()`
- `_event_kind()`
- `_is_kind()`
- `_is_text_event()`
- `_is_end_event()`
- `_is_send_file_event()`
- `_POST_END_DRAIN_MS`
- `_STREAM_IDLE_TIMEOUT_MS`
- debug logging, завязанное на наш собственный queue lifecycle

## Что оставляем

В thin adapter:

- `__init__()` для modern `base_url/chat_id`
- `_normalize_base_url()` только если нужен стабильный env input
- `for_chat(chat_id)`

В [sdk/real.py](/Users/a/MAI/sem2/lambda/surfaces-bot/sdk/real.py):

- `_get_chat_api()`
- `_get_chat_send_lock()`
- `_attachment_paths()`
- `disconnect_chat()`
- `_handle_chat_api_failure()`
- `send_message()`
- `stream_message()`

## Дополнительное упрощение

Если после cleanup мы считаем pinned upstream API обязательным, то из [sdk/real.py](/Users/a/MAI/sem2/lambda/surfaces-bot/sdk/real.py) можно убрать legacy-minded probing:

- `inspect.signature(send_message)`
- conditional fallback на старый `send_message(text)` без `attachments`

В этом случае `RealPlatformClient` всегда использует современный контракт:

- `send_message(text, attachments=...)`

Это ещё сильнее уменьшит ambiguity.

## Этапы миграции

### Этап 1. Cleanup до thin adapter

Делаем:

- сжимаем `sdk/agent_api_wrapper.py` до thin shim
- переносим всю допустимую resilience logic только в `sdk/real.py`
- удаляем тесты, которые закрепляют наши кастомные transport semantics

### Этап 2. Повторная верификация

Заново прогоняем:

- text-only flow
- staged attachments flow
- large image failure
- duplicate `END` behavior
- behavior after transport disconnect

На этом этапе мы честно увидим, что реально делает upstream transport.

### Этап 3. Опциональный outer policy layer

Только если после Этапа 2 это действительно нужно, добавляем policy поверх transport:

- request timeout целиком
- retry policy
- circuit-breaker-like behavior

Но это должно жить не в client wrapper, а выше, в integration layer.

## Тестовая стратегия

### Удаляем как нецелевые тесты

Больше не считаем нормой:

- post-END drain behavior
- recovery late chunks после `END`
- idle timeout внутри wrapper как часть client contract

### Оставляем и добавляем

Нужные guarantees:

1. создаётся отдельный client per chat
2. один chat сериализуется через lock
3. разные чаты не делят client instance
4. attachment paths уходят в `send_message(..., attachments=...)`
5. transport failure приводит к `disconnect_chat(chat_id)`
6. следующий запрос после failure открывает новую chat session
7. upstream exception превращается в `PlatformError`

## Риски

### 1. Может снова проявиться реальный upstream bug

Это не regression дизайна, а полезный результат cleanup.

### 2. Может исчезнуть локальная защита от зависших стримов

Это допустимо на первом этапе.
Если она действительно нужна, она должна вернуться как outer policy, а не как переписанный client transport.

### 3. Может выясниться, что даже thin wrapper не нужен

Если modern upstream `AgentApi` уже полностью покрывает наш use case, файл `sdk/agent_api_wrapper.py` можно будет заменить на маленький factory helper или убрать совсем.

## Критерии успеха

Результат считается успешным, если:

- transport layer в `surfaces` перестаёт иметь собственную stream semantics
- platform bug reports снова можно формулировать без сильного caveat про кастомный клиент
- Matrix real backend продолжает работать на text-only и attachments scenarios
- failure handling остаётся контролируемым, но больше не маскирует transport behavior платформы

## Решение

Принять путь:

- `Thin upstream adapter now`
- `Observe real behavior`
- `Add outer policy later only if needed`

Это наиболее близкий к production best practice вариант для текущего состояния проекта.
