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