12 KiB
tags
| tags | ||||
|---|---|---|---|---|
|
Hermes Gateway Stream Consumer — потеря текста ответов
Ключевой вывод (2026-06-19): DeepSeek не возвращает interim commentary между tool calls Причина отсутствия комментариев в Zulip между тулами — не в gateway, а в том что DeepSeek (с cache hit 95-100%) при каждом tool call возвращает только
tool_calls, безcontent.interim_assistant_callbackвызывается только когда есть текст между тулами. DeepSeek этого текста не выдаёт.
UPDATED FINDINGS: Ответы ДОХОДЯТ в Zulip. Проблема не в полной потере, а в:
- Дублирование текста —
_accumulatedне очищается после_send_or_editсgot_done=True, затемgot_doneблок (строки 607-619) снова отправляет то же самое - Подавление финального send —
content_delivered=Trueустанавливается после first-send, gateway подавляет нормальную отправку (но first-send уже отправил текст, так что ответ доходит) - Partial на
\nотправляет пустые чанки — первый\nв начале текста триггерит send пустой строки
Что не так:
- В no-edit ветке,
got_done=True:_should_send=True→_send_or_edit(accumulated)— отправляет весь текст- Строка 596:
if _sent_len and not got_done→ False (got_done=True) → аккумулятор НЕ очищен - Строка 607:
if self._accumulated:→ True (старый текст всё ещё там) - Строка 618-619:
_final_content_delivered = True - Gateway видит content_delivered и подавляет fallback send — но текст уже отправлен
Почему пользователь видит "только tool calls":
- В webhook режиме user может видеть tool calls мгновенно, а текстовый ответ приходит пачкой позже — потому что весь текст накапливается до
got_done - Между partial sends и tool calls нет синхронизации — текст может прийти после следующего tool call
Симптом
При ответе на webhook-сообщение (Zulip → zulip-router → webhook POST на whale gateway → обработка → ответ) часть или весь текстовый ответ агента не доставляется в чат. Пользователь видит только tool calls, approval запросы, и иногда первые несколько символов текста. Полноценный текстовый ответ пропадает.
Детектированная проблема
Механизм
Gateway настроен со streaming для webhook:
display.platforms.webhook.streaming: true(из конфига)adapter_supports_edit=Falseдля webhook (задано кодом)
Stream consumer (no-edit branch) должен накапливать текст и отправлять частями по \n с rate-limit, затем на got_done — отправить остаток целиком.
Что происходит на самом деле
Паттерн из лога (все ответы):
first-send: text_len=N text=[...полный текст ответа...]— stream consumer получает весь текстSuppressing normal final send ... streamed=True previewed=False content_delivered=True— gateway подавляет финальную отправку- В Zulip приходит только обрезанная версия или ничего
Ключевая строка лога:
2026-06-19 17:17:20,668 INFO gateway.run: Suppressing normal final send for session ...: streamed=True previewed=False content_delivered=True
content_delivered=True устанавливается СЛИШКОМ РАНО — до того как весь текст отправлен.
Root Cause
В stream_consumer.py в no-edit branch:
- Первый
\nв начале ответа (пустая строка перед началом текста) триггеритsendчерез_send_or_edit - Этот send отправляет
\n\n(пустоту или пару символов) в webhook - После отправки через webhook, stream consumer помечает
content_delivered=True - Когда приходит
got_done— остальной огромный текст тоже отправляется, НО - Gateway в
run.pyвидитstreamed=True AND content_delivered=Trueи ПОДАВЛЯЕТ свою нормальную финальную отправку, думая что контент уже доставлен стримером
В итоге: первый пустой/короткий \n чанк — единственное что доставляется. Весь реальный контент теряется в подавлении.
Почему раньше не было видно
Ранее гейтвей по умолчанию не имел streaming для webhook. Когда streaming включили (для фикса tool progress), стали видны эти проблемы:
- streaming активируется
- первый \n отправляет пустоту
- content_delivered=True
- got_done подавлен
- текст пропадает
Подтверждение из лога
Все ответы после последнего рестарта (17:13:06, 17:16:55):
17:13:44→ first-send text_len=44 →Suppressing ... content_delivered=True→ Ты не видел моего сообщения17:14:09→ first-send text_len=71 →Suppressing ... content_delivered=True→ Видел обрывок17:14:22→ first-send text_len=223 →Suppressing ... content_delivered=True→ Видел17:15:13→ first-send text_len=335 →Suppressing ... content_delivered=True→ Видел17:16:30→ first-send text_len=439 →Suppressing ... content_delivered=True→ Видел17:17:20→ first-send text_len=356 →Suppressing ... content_delivered=True→ Обрезано17:18:16→ first-send text_len=785 →Suppressing ... content_delivered=True→ Не дошло17:19:02→ first-send text_len=780 →Suppressing ... content_delivered=True→ Не дошло17:21:10→ first-send text_len=1314 →Suppressing ... content_delivered=True→ Не дошло
Из лога: все большие ответы (>300 символов) подавляются. Маленькие (<200 символов) проходят.
Сравнение Zulip DB с логом
Последние сообщения Кит в треде (из Zulip DB):
- msg 70116: "✅ Command approved" — это approval ответ, маленький
- msg 70115-70107: tool calls (терминал, read_file)
- msg 70106: read_file
Ни одного текстового ответа больше 100 символов! Все они были подавлены гейтвеем.
Фикс
Два варианта:
Вариант A: Не подавлять финальную отправку для webhook (no-edit адаптер)
В run.py, где проверяется content_delivered:
# Не подавлять, если адаптер не поддерживает редактирование
# (webhook не может отправлять partials — только один финальный ответ)
if streamed and content_delivered and adapter_supports_edit:
suppress = True
Либо: не устанавливать content_delivered=True для no-edit адаптера вообще — пусть всегда проходит через нормальный send.
Вариант B: Не слать первый \n чанк если текст пустой/минимальный
# Не отправлять partial если это просто \n без реального контента
if _has_nl and len(self._accumulated.strip()) < 3:
# Пропустить — это просто форматирование, не настоящий контент
continue
Но вариант A надёжнее — он не полагается на эвристику.
DeepSeek commentary — руководство по диагностике (2026-06-19)
Когда текст агента не доходит между tool calls
ПЕРВОЕ что проверять: какой провайдер используется.
DeepSeek (особенно с cache hit 95-100%) не генерирует .content между tool calls — он возвращает только tool_calls. Gateway вызывает interim_assistant_callback ТОЛЬКО когда в ответе есть .content. Если content пустой — commentary не шлётся.
Это особенность модели, не баг gateway.
Где смотреть в логе
Лог: ~/.hermes/hermes-whale/logs/gateway.log
Искать размер ответа у каждого API call:
grep -n 'Received' ~/.hermes/hermes-whale/logs/gateway.log | tail -20
Пример — commentary отправлен (out > 0):
Received 31 additional tokens (total 31, out=31) in session ...
Пример — только tool call (out < 200, cache 100%):
Received 153 tokens (total 3787, out=153, 100% cache) ...
Received 4 tokens (total 7811, out=4, 100% cache) ...
Эвристика:
out > 300— commentary + tool calls (есть текст)out < 200+100% cache— чистый tool call, commentary не было
Как проверить в Zulip DB
SELECT id, content, sender_id FROM zulip_message
WHERE subject = 'Название треда'
ORDER BY id DESC LIMIT 20;
Сопоставить: сколько API calls → сколько сообщений в Zulip.
Где смотреть в коде
| Файл | Что искать |
|---|---|
gateway/run.py:~16960 |
interim_assistant_callback — вызывается только если .content is not None |
gateway/run.py:~16997 |
source.platform != Platform.WEBHOOK — был гард для webhook (исправлен) |
gateway/stream_consumer.py |
_send_or_edit и _has_nl — отправка commentary (исправлено подавление) |
gateway/adapters/webhook.py |
_delivery_info инстанс-переменная — терялась при рестарте (исправлена) |
Сводка исправлений (2026-06-19)
| Файл | Исправление |
|---|---|
gateway/run.py:16997 |
Убран source.platform != Platform.WEBHOOK — interim messages разрешены для webhook |
gateway/stream_consumer.py |
Commentary отправляется независимо от \n в no-edit ветке |
gateway/adapters/webhook.py |
delivery_info восстанавливается из route config при рестарте |
Важно: все эти фиксы не влияют на DeepSeek commentary — DeepSeek не шлёт .content между tool calls независимо от gateway.
Если проблема появится с другим провайдером (Claude, GPT) — надо смотреть interim_assistant_callback в run.py.