Tool-using responses silently dropped on Discord (no Sending response log after response ready)
Environment
- Hermes Agent commit:
1c4d3216d3855848a632847306c96f4c3090b259
- macOS Intel
- Profile-based gateway:
hermes --profile jenny gateway run --replace
- Discord platform adapter
- Model:
google/gemini-2.5-flash via OpenRouter
- Tools enabled: web, browser, terminal, file, code_execution, etc.
Summary
When an agent response involves at least one tool call (api_calls >= 2), the gateway logs response ready with a non-zero char count but never emits the corresponding [Discord] Sending response (...) log line. The session JSON contains the full final assistant message, but it never reaches the user's Discord channel.
Responses with api_calls == 1 (no tool call) deliver fine. The bug is consistent for multi-turn tool-using replies in our setup.
Reproduction
- Set up a Discord profile with
web tool enabled.
- Ask the agent something that requires a real-time lookup (e.g. "最近台北到巴黎機票最便宜的是?").
- Agent generates a preamble announcing it will use
web_search.
- Tool executes successfully (verified — KAYAK data returned in session).
- Agent generates a final 1000+ char answer with prices.
- No Discord message appears for the final answer.
Observed log pattern
~/.hermes/profiles/<profile>/logs/agent.log:
22:14:12,807 response ready: ... api_calls=1 response=179 chars
22:14:12,817 [Discord] Sending response (179 chars) to ... ← delivered ✓
22:29:31,526 response ready: ... api_calls=1 response=288 chars
22:29:31,529 [Discord] Sending response (288 chars) to ... ← delivered ✓
22:30:02,568 response ready: ... api_calls=2 response=1132 chars
(no Sending response log) ← lost ✗
22:16:28,366 response ready: ... api_calls=4 response=372 chars
(no Sending response log) ← lost ✗
Every entry with api_calls >= 2 is missing the corresponding send. No errors are logged in errors.log or gateway.log for the affected timestamps.
Hypothesis
In gateway/platforms/base.py around line 1699:
if text_content:
logger.info("[%s] Sending response (%d chars) to %s", ...)
result = await self._send_with_retry(...)
The send is silently skipped when text_content is falsy. Between the response = await self._message_handler(event) at line 1621 (which returned 1132 chars) and the if text_content: guard, the response flows through:
extract_media (line 1646)
extract_images (line 1649)
- Manual strips for
[[audio_as_voice]] and MEDIA: (lines 1651–1652)
extract_local_files (line 1658)
One of these pipelines is presumably reducing tool-using responses to an empty string. Likely candidates:
extract_local_files regex matching URL fragments that look like local file paths (e.g. when responses contain markdown with paths-like strings)
extract_images mishandling certain markdown link patterns
- Some other transformation specific to multi-LLM-call responses
It would help to add a logger.debug (or warning when response was non-empty but text_content is now empty) right before line 1699 so this category of silent failure is observable.
Session evidence
The full final assistant message exists in the session JSON at ~/.hermes/profiles/<profile>/sessions/session_<timestamp>.json. Happy to share a redacted copy if useful.
Suggested fix
At minimum, log a WARNING when response was non-empty but text_content ends up empty:
if not text_content and response:
logger.warning(
"[%s] Response stripped to empty after extract pipeline (orig=%d chars). "
"Sending raw response as fallback.",
self.name, len(response),
)
text_content = response # or some safe variant
Or, narrower: ensure extract_local_files and extract_images never reduce non-empty content to an empty string — clamp to a minimum of original response if stripping is aggressive.
Workaround (current)
Profile SOUL.md updated to instruct the agent to never call web_search for flight lookups, and instead emit pre-filled search-engine URLs in a single LLM turn so api_calls == 1 and the response reliably delivers.
Tool-using responses silently dropped on Discord (no
Sending responselog afterresponse ready)Environment
1c4d3216d3855848a632847306c96f4c3090b259hermes --profile jenny gateway run --replacegoogle/gemini-2.5-flashvia OpenRouterSummary
When an agent response involves at least one tool call (
api_calls >= 2), the gateway logsresponse readywith a non-zero char count but never emits the corresponding[Discord] Sending response (...)log line. The session JSON contains the full final assistant message, but it never reaches the user's Discord channel.Responses with
api_calls == 1(no tool call) deliver fine. The bug is consistent for multi-turn tool-using replies in our setup.Reproduction
webtool enabled.web_search.Observed log pattern
~/.hermes/profiles/<profile>/logs/agent.log:Every entry with
api_calls >= 2is missing the corresponding send. No errors are logged inerrors.logorgateway.logfor the affected timestamps.Hypothesis
In
gateway/platforms/base.pyaround line 1699:The send is silently skipped when
text_contentis falsy. Between theresponse = await self._message_handler(event)at line 1621 (which returned 1132 chars) and theif text_content:guard, the response flows through:extract_media(line 1646)extract_images(line 1649)[[audio_as_voice]]andMEDIA:(lines 1651–1652)extract_local_files(line 1658)One of these pipelines is presumably reducing tool-using responses to an empty string. Likely candidates:
extract_local_filesregex matching URL fragments that look like local file paths (e.g. when responses contain markdown with paths-like strings)extract_imagesmishandling certain markdown link patternsIt would help to add a
logger.debug(orwarningwhenresponsewas non-empty buttext_contentis now empty) right before line 1699 so this category of silent failure is observable.Session evidence
The full final assistant message exists in the session JSON at
~/.hermes/profiles/<profile>/sessions/session_<timestamp>.json. Happy to share a redacted copy if useful.Suggested fix
At minimum, log a
WARNINGwhenresponsewas non-empty buttext_contentends up empty:Or, narrower: ensure
extract_local_filesandextract_imagesnever reduce non-empty content to an empty string — clamp to a minimum of originalresponseif stripping is aggressive.Workaround (current)
Profile SOUL.md updated to instruct the agent to never call
web_searchfor flight lookups, and instead emit pre-filled search-engine URLs in a single LLM turn soapi_calls == 1and the response reliably delivers.