docs: add thin transport adapter design
This commit is contained in:
parent
7a2ad86b88
commit
3a3fcdc695
1 changed files with 318 additions and 0 deletions
|
|
@ -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 вариант для текущего состояния проекта.
|
||||
Loading…
Add table
Add a link
Reference in a new issue