# 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 вариант для текущего состояния проекта.