294 lines
16 KiB
Markdown
294 lines
16 KiB
Markdown
# Финальный баг-репорт: потеря начала ответа и сбои 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. Начало ответа может пропасть
|
||
- ожидалось: `Моя ошибка: ...`
|
||
- фактически: `оя ошибка: ...`
|
||
|
||
- ожидалось: `На двух изображениях: ...`
|
||
- фактически: ` двух изображениях: ...`
|
||
|
||
2. После tool/file flow ответы могут вести себя нестабильно
|
||
- следующий ответ стартует с середины фразы
|
||
- в некоторых сценариях после image/tool path платформа отвечает ошибкой или замолкает
|
||
|
||
3. На больших изображениях 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:
|
||
|
||
```text
|
||
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-сценария:
|
||
|
||
```text
|
||
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
|
||
- это подтверждено логами выше
|
||
|
||
2. Код берёт только `chunk.content`
|
||
- если начало ответа приходит в другой форме, поле или raw event, оно просто теряется
|
||
|
||
3. Код не учитывает `ns` / `source`
|
||
- после tool/vision flow у Deep Agents / LangChain может быть более сложная структура потока
|
||
- текущий adapter flatten’ит её слишком агрессивно
|
||
|
||
4. Код никак не валидирует, что наружу уходит именно 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(...)`
|
||
|
||
По логам это выглядит так:
|
||
|
||
```text
|
||
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:
|
||
|
||
```text
|
||
Exceeded limit on max bytes per data-uri item : 10485760
|
||
```
|
||
|
||
И параллельно websocket рвался с:
|
||
|
||
```text
|
||
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`
|
||
|
||
2. Перепроверить адаптацию `astream_events()` в `service.py`
|
||
- логировать и проанализировать raw `event["event"]`
|
||
- проверить `event.get("name")`
|
||
- смотреть `event.get("ns")`
|
||
- сравнить `chunk.content` с тем, что реально лежит в `chunk.text` / raw chunk repr
|
||
|
||
3. Форвардить наружу только финальный main assistant output
|
||
- не flatten’ить весь поток без учёта `ns/source`
|
||
|
||
### Желательно
|
||
|
||
4. Сделать image path устойчивым к oversized payload
|
||
- preflight check размера
|
||
- resize/compress или controlled error без разрыва WS
|
||
|
||
5. Улучшить 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.
|