12 KiB
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 состоит из:
Изначально 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
Источник истины:
Мы принимаем его stream semantics как authoritative behavior.
Слой 2. Thin adapter
Файл:
После cleanup он должен содержать только:
- создание клиента через modern constructor
base_urlnormalization, если это действительно нужно для наших envfor_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
Файл:
Ответственность:
- кешировать 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
- поздним дренированием очереди
Что удаляем
- 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__()для modernbase_url/chat_id_normalize_base_url()только если нужен стабильный env inputfor_chat(chat_id)
В 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 можно убрать 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
ENDbehavior - 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:
- создаётся отдельный client per chat
- один chat сериализуется через lock
- разные чаты не делят client instance
- attachment paths уходят в
send_message(..., attachments=...) - transport failure приводит к
disconnect_chat(chat_id) - следующий запрос после failure открывает новую chat session
- 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 nowObserve real behaviorAdd outer policy later only if needed
Это наиболее близкий к production best practice вариант для текущего состояния проекта.