refactor: use thin upstream transport adapter
This commit is contained in:
parent
569824ead1
commit
0c2884c2b1
8 changed files with 420 additions and 255 deletions
|
|
@ -0,0 +1,294 @@
|
|||
# Финальный баг-репорт: потеря начала ответа и сбои 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue