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

12 KiB
Raw Permalink Blame History

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_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

Файл:

Ответственность:

  • кешировать 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:

  • 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:

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