docs: add thin transport adapter design

This commit is contained in:
Mikhail Putilovskij 2026-04-22 00:11:20 +03:00
parent 7a2ad86b88
commit 3a3fcdc695

View file

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