From 3a3fcdc6953b169ad4a92a70603b30390dc2e679 Mon Sep 17 00:00:00 2001 From: Mikhail Putilovskij Date: Wed, 22 Apr 2026 00:11:20 +0300 Subject: [PATCH] docs: add thin transport adapter design --- ...-22-transport-layer-thin-adapter-design.md | 318 ++++++++++++++++++ 1 file changed, 318 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-22-transport-layer-thin-adapter-design.md diff --git a/docs/superpowers/specs/2026-04-22-transport-layer-thin-adapter-design.md b/docs/superpowers/specs/2026-04-22-transport-layer-thin-adapter-design.md new file mode 100644 index 0000000..5fab5ef --- /dev/null +++ b/docs/superpowers/specs/2026-04-22-transport-layer-thin-adapter-design.md @@ -0,0 +1,318 @@ +# 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 вариант для текущего состояния проекта.