surfaces/docs/reports/2026-04-22-platform-streaming-final-bug-report-ru.md

16 KiB
Raw Permalink Blame History

Финальный баг-репорт: потеря начала ответа и сбои streaming/image path в platform-agent

Статус

Это финальный отчёт после полного аудита интеграции surfaces -> platform-agent_api -> platform-agent.

Итог:

  • текущая реализация surfaces рабочая, но проблемная из-за upstream-дефектов платформы
  • баг с пропадающим началом ответа на текущем состоянии не локализуется в surfaces
  • в воспроизведённом сценарии повреждённый первый текстовый chunk рождается уже внутри platform-agent
  • помимо этого подтверждены ещё два независимых platform-side дефекта:
    • duplicate END
    • некорректная обработка больших изображений (data-uri > 10 MB, WS 1009)

Версии и состояние кода

Рантайм воспроизводился на vendored upstream-репозиториях без локальных platform patchей:

  • platform-agent: 5e7c2df954cc3cd2f5bf8ae688e10a20038dde61
  • platform-agent_api: aa480bbec5bbf8e006284dd03aed1c2754e9bbee

Со стороны surfaces transport layer был предварительно очищен:

  • убрана локальная stream-semantics из sdk/agent_api_wrapper.py
  • sdk/real.py переведён на pinned upstream platform-agent_api.AgentApi
  • больше нет локального post-END drain, custom listener и wrapper-owned reclassification events

Это важно: баг воспроизводился после удаления наших транспортных костылей.

Контекст интеграции

  • поверхность: Matrix
  • транспорт к платформе: WebSocket через upstream platform-agent_api.AgentApi
  • chat_id на платформу: стабильный числовой surrogate id, выдаваемый со стороны surfaces
  • файловый контракт: shared /workspace, вложения передаются как относительные пути в attachments

Пользовательские симптомы

Наблюдались несколько классов сбоев:

  1. Начало ответа может пропасть
  • ожидалось: Моя ошибка: ...

  • фактически: оя ошибка: ...

  • ожидалось: На двух изображениях: ...

  • фактически: двух изображениях: ...

  1. После tool/file flow ответы могут вести себя нестабильно
  • следующий ответ стартует с середины фразы
  • в некоторых сценариях после image/tool path платформа отвечает ошибкой или замолкает
  1. На больших изображениях image path падает совсем
  • provider error Exceeded limit on max bytes per data-uri item : 10485760
  • websocket закрывается с 1009 (message too big)

Что было проверено на стороне surfaces

Ниже перечислено, что именно было перепроверено в нашем коде и почему это не выглядит корнем проблемы.

1. Мы больше не режем и не переклассифицируем stream локально

В текущем surfaces:

  • sdk/agent_api_wrapper.py — thin construction/factory shim над upstream AgentApi
  • sdk/real.py — просто итерирует upstream events и склеивает MsgEventTextChunk.text
  • adapter/matrix/bot.py — отправляет OutgoingMessage.text в Matrix без strip/lstrip

Наблюдение:

  • в текущем коде не осталось места, где строка могла бы превратиться из Моя ошибка в оя ошибка через локальный slicing

2. Сборка ответа у нас линейная и тупая

sdk/real.py делает только следующее:

  • если пришёл MsgEventTextChunk — добавляет event.text в response_parts
  • если пришёл MsgEventSendFile — превращает его в Attachment
  • не пытается “восстанавливать” поток после END

Следствие:

  • если начало уже отсутствует в event.text, мы его не можем ни потерять, ни вернуть

3. Matrix sender не модифицирует текст

adapter/matrix/bot.py передаёт текст дальше как есть.

Следствие:

  • Matrix renderer не является объяснением пропажи первого куска

Что было проверено в platform-agent_api

Upstream client всё ещё имеет спорную queue-архитектуру:

  • одна активная _current_queue
  • MsgEventEnd съедается внутри send_message()
  • в finally очередь отвязывается и дренится orphan messages

Это архитектурно хрупко и может быть источником других boundary bugs.

Но в конкретном воспроизведении этот слой не был точкой порчи текста.

Почему:

  • в raw logs клиент получил ровно тот же первый text chunk, который сервер уже отправил
  • queue/dequeue не изменили его содержимое

Что удалось доказать по raw logs

Для финальной проверки была временно добавлена точечная диагностика в:

  • external/platform-agent/src/agent/service.py
  • external/platform-agent/src/api/external.py
  • external/platform-agent_api/lambda_agent_api/agent_api.py

Эта диагностика не входила в рабочую реализацию и использовалась только для локализации бага.

Ключевое наблюдение

На проблемном запросе после tool/file flow сервер сам yieldил уже обрезанный первый chunk:

platform-agent-1  | [raw-stream][server-yield] chat=1 event=TEXT text=' двух изображениях:\n\n**Первое изображение'
platform-agent-1  | [raw-stream][ws-send] chat=1 event=AGENT_EVENT_TEXT_CHUNK text=' двух изображениях:\n\n**Первое изображение' path=None
matrix-bot-1      | [raw-stream][client-listen] agent=matrix-bot chat=1 queue_active=True AGENT_EVENT_TEXT_CHUNK text=' двух изображениях:\n\n**Первое изображение'
matrix-bot-1      | [raw-stream][client-dequeue] agent=matrix-bot chat=1 request=2 AGENT_EVENT_TEXT_CHUNK text=' двух изображениях:\n\n**Первое изображение'

Это означает:

  • порча произошла до websocket-клиента
  • surfaces transport layer не является источником именно этого дефекта
  • platform-agent_api не исказил этот конкретный chunk по дороге

Дополнительно тот же паттерн виден и вне image-сценария:

platform-agent-1  | [raw-stream][server-yield] chat=1 event=TEXT text='сё работает напрямую'
...
matrix-bot-1      | [raw-stream][client-dequeue] ... text='сё работает напрямую'

То есть сервер уже выдаёт сё, а не Всё.

Наиболее вероятный root cause

Главный подозреваемый — external/platform-agent/src/agent/service.py.

Сейчас он делает следующее:

  • читает self._agent.astream_events(...)
  • обрабатывает только kind == "on_chat_model_stream"
  • берёт chunk = event["data"]["chunk"]
  • если chunk.content, форвардит MsgEventTextChunk(text=chunk.content)

Проблема в том, что это очень грубое преобразование raw event stream в пользовательский текст.

Почему именно это место выглядит корнем

  1. Первый битый chunk уже рождается на server-side
  • это подтверждено логами выше
  1. Код берёт только chunk.content
  • если начало ответа приходит в другой форме, поле или raw event, оно просто теряется
  1. Код не учитывает ns / source
  • после tool/vision flow у Deep Agents / LangChain может быть более сложная структура потока
  • текущий adapter flattenит её слишком агрессивно
  1. Код никак не валидирует, что наружу уходит именно main assistant output

Итоговая гипотеза:

После tool/file/vision flow platform-agent неправильно адаптирует astream_events() в MsgEventTextChunk. Начало итогового пользовательского ответа может находиться не в том raw event, который сейчас читается через chunk.content, либо теряться из-за упрощённой фильтрации потока.

Подтверждённый отдельный баг: duplicate END

Это отдельный platform-side дефект.

Сейчас:

  • external/platform-agent/src/agent/service.py уже делает yield MsgEventEnd(...)
  • external/platform-agent/src/api/external.py после завершения цикла дополнительно отправляет ещё один MsgEventEnd(...)

По логам это выглядит так:

platform-agent-1  | [raw-stream][server-yield] chat=1 event=END
platform-agent-1  | [raw-stream][ws-send] chat=1 event=AGENT_EVENT_END text=None path=None
platform-agent-1  | [raw-stream][ws-send] chat=1 event=AGENT_EVENT_END duplicate_end=true
matrix-bot-1      | ... AGENT_EVENT_END tokens_used=0
matrix-bot-1      | ... AGENT_EVENT_END tokens_used=0

Независимая оценка:

  • duplicate END — реальный баг платформы
  • он делает границу ответа менее надёжной
  • но в текущем воспроизведении он не объясняет сам факт потери первого text chunk

То есть это важный, но вторичный дефект.

Подтверждённый отдельный баг: большие изображения ломают image path

В отдельном воспроизведении платформа падала на анализе изображений с provider error:

Exceeded limit on max bytes per data-uri item : 10485760

И параллельно websocket рвался с:

received 1009 (message too big); then sent 1009 (message too big)

Это означает:

  • image path отправляет в provider oversized data: URI
  • безопасной предвалидации / деградации нет
  • failure scenario сопровождается разрывом websocket-соединения

Независимая оценка:

  • это отдельный platform-side bug
  • он не объясняет потерю первого чанка в текстовом сценарии напрямую
  • но подтверждает, что path tool/file/image -> platform stream в целом сейчас нестабилен

Что мы считаем исключённым

С достаточной уверенностью можно исключить:

  1. Локальный slicing текста в surfaces
  2. Локальную “умную” реконструкцию потока, потому что она была удалена
  3. Matrix sender как источник потери первого чанка
  4. platform-agent_api queue/dequeue как primary root cause именно в этом воспроизведении

Финальная независимая оценка

Текущая оценка вероятностей:

  • 75% — ошибка в platform-agent, в адаптере astream_events() -> MsgEventTextChunk
  • 15% — provider/model stream приносит начало ответа в другой raw event/field, а platform-agent его некорректно интерпретирует
  • 10% — вторичные platform-side boundary defects (duplicate END, queue semantics и т.д.)
  • ~0-5% — ошибка в surfaces

Итоговый вывод:

На текущем состоянии кода баг с пропадающим началом ответа следует считать platform-side дефектом. Воспроизведение после cleanup transport layer показывает, что первый повреждённый text chunk формируется уже внутри platform-agent до отправки в websocket.

Что нужно исправить в платформе

Обязательно

  1. Убрать duplicate END
  • один ответ должен завершаться ровно одним MsgEventEnd
  1. Перепроверить адаптацию astream_events() в service.py
  • логировать и проанализировать raw event["event"]
  • проверить event.get("name")
  • смотреть event.get("ns")
  • сравнить chunk.content с тем, что реально лежит в chunk.text / raw chunk repr
  1. Форвардить наружу только финальный main assistant output
  • не flattenить весь поток без учёта ns/source

Желательно

  1. Сделать image path устойчивым к oversized payload
  • preflight check размера
  • resize/compress или controlled error без разрыва WS
  1. Улучшить client/server protocol boundary
  • более строгая корреляция запроса и ответа
  • более однозначная semantics конца ответа

Что мы сделали со своей стороны

Со стороны surfaces уже выполнено следующее:

  • transport layer очищен до thin adapter над upstream AgentApi
  • локальные stream-workaroundы удалены
  • рабочая интеграция сохранена
  • known issue задокументирован

То есть текущая интеграция не “идеальна”, но её поведение теперь достаточно чистое, чтобы platform bug было можно локализовать без смешения ответственности.

Приложение: короткий диагноз

Если нужна самая короткая формулировка для issue tracker:

После cleanup transport layer в surfaces и воспроизведения на clean upstream platform repos видно, что platform-agent иногда сам порождает первый MsgEventTextChunk уже обрезанным, особенно после tool/file flow. Дополнительно подтверждены duplicate END и отдельный image-path failure на больших data: URI.