surfaces/docs/superpowers/specs/2026-04-22-transport-layer-thin-adapter-design.md

318 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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