mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-28 18:19:28 +00:00
fix: close orphaned code fences on all send paths
Adds `ensure_closed_code_fences()` helper to detect text with an odd count of triple-backtick markers (indicating an unclosed code block) and append a closing fence. Applies the fix to all four identified gap paths: G1: truncate_message early-break path for final chunk G2: _send_or_edit streaming edit path (most commonly hit) G3: overflow split first chunk (covered by G2's fix) G4: _send_fallback_final fallback send path Closes: #TBD
This commit is contained in:
parent
9ed5aef1e6
commit
3e2fe3fcab
2 changed files with 58 additions and 7 deletions
|
|
@ -5931,7 +5931,26 @@ class BasePlatformAdapter(ABC):
|
|||
|
||||
# Everything remaining fits in one final chunk
|
||||
if _len(prefix) + _len(remaining) <= max_length - INDICATOR_RESERVE:
|
||||
chunks.append(prefix + remaining)
|
||||
final_chunk = prefix + remaining
|
||||
# Check fence balance: if carry_lang was set, the chunk
|
||||
# starts with an opening fence. Walk the remaining text
|
||||
# to see if the code block was closed; if not, close it.
|
||||
_final_in_code = carry_lang is not None
|
||||
_final_lang = carry_lang or ""
|
||||
if _final_in_code:
|
||||
for _line in remaining.split("\n"):
|
||||
_stripped = _line.strip()
|
||||
if _stripped.startswith("```"):
|
||||
if _final_in_code:
|
||||
_final_in_code = False
|
||||
_final_lang = ""
|
||||
else:
|
||||
_final_in_code = True
|
||||
_tag = _stripped[3:].strip()
|
||||
_final_lang = _tag.split()[0] if _tag else ""
|
||||
if _final_in_code:
|
||||
final_chunk += FENCE_CLOSE
|
||||
chunks.append(final_chunk)
|
||||
break
|
||||
|
||||
# Find a natural split point (prefer newlines, then spaces).
|
||||
|
|
|
|||
|
|
@ -42,13 +42,7 @@ logger = logging.getLogger("gateway.stream_consumer")
|
|||
|
||||
# Sentinel to signal the stream is complete
|
||||
_DONE = object()
|
||||
|
||||
# Sentinel to signal a tool boundary — finalize current message and start a
|
||||
# new one so that subsequent text appears below tool progress messages.
|
||||
_NEW_SEGMENT = object()
|
||||
|
||||
# Queue marker for a completed assistant commentary message emitted between
|
||||
# API/tool iterations (for example: "I'll inspect the repo first.").
|
||||
_COMMENTARY = object()
|
||||
|
||||
# Queue marker for a synchronous flush barrier. Enqueued as
|
||||
|
|
@ -79,6 +73,34 @@ def escape_code_fences_for_display(text: str) -> str:
|
|||
return text.replace("```", "\\`\\`\\`")
|
||||
|
||||
|
||||
def ensure_closed_code_fences(text: str) -> str:
|
||||
"""Append a closing `` ``` `` fence if the text has an odd number of
|
||||
triple-backtick markers.
|
||||
|
||||
When model output is truncated mid-code-block (e.g. by token limits
|
||||
or a finish_reason="length"), the resulting message has an unclosed
|
||||
code fence. On Discord, Slack, and other platforms this causes
|
||||
everything after the orphaned fence to render as a single code block.
|
||||
|
||||
The fix is trivial: count `` ``` `` occurrences. If odd, append a
|
||||
closing fence on its own line. This is safe because nested
|
||||
triple-backtick fences (e.g. a literal `` ``` `` inside a code block)
|
||||
are exceedingly rare in model output and, when they do appear, the
|
||||
extra closing fence just creates a brief empty code block at the end
|
||||
of the message — far less harmful than the entire message being one
|
||||
giant code block.
|
||||
|
||||
Returns:
|
||||
The input text with a closing fence appended if needed, or the
|
||||
input text unchanged.
|
||||
"""
|
||||
if not isinstance(text, str) or not text:
|
||||
return text
|
||||
if text.count("```") % 2 == 1:
|
||||
return text.rstrip("\n") + "\n```"
|
||||
return text
|
||||
|
||||
|
||||
@dataclass
|
||||
class StreamConsumerConfig:
|
||||
"""Runtime config for a single stream consumer instance."""
|
||||
|
|
@ -1119,6 +1141,10 @@ class GatewayStreamConsumer:
|
|||
Retries each chunk once on flood-control failures with a short delay.
|
||||
"""
|
||||
final_text = self._clean_for_display(text)
|
||||
# Ensure balanced code fences before computing continuation,
|
||||
# so the closing fence reaches the user even when the fallback
|
||||
# only delivers the tail after mid-stream edits failed.
|
||||
final_text = ensure_closed_code_fences(final_text)
|
||||
continuation = self._continuation_text(final_text)
|
||||
self._fallback_final_send = False
|
||||
if not continuation.strip():
|
||||
|
|
@ -1776,6 +1802,12 @@ class GatewayStreamConsumer:
|
|||
# Media files are delivered as native attachments after the stream
|
||||
# finishes (via _deliver_media_from_response in gateway/run.py).
|
||||
text = self._clean_for_display(text)
|
||||
# Ensure code fences are balanced before send/edit. Model output
|
||||
# truncated mid-code-block (e.g. finish_reason="length") leaves an
|
||||
# orphaned ``` which, on Discord/Slack/Matrix, causes the entire
|
||||
# remaining output to render as a single code block. This covers
|
||||
# the streaming edit path (G2) and first-send path alike.
|
||||
text = ensure_closed_code_fences(text)
|
||||
# A bare streaming cursor is not meaningful user-visible content and
|
||||
# can render as a stray tofu/white-box message on some clients.
|
||||
visible_without_cursor = text
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue