fix(gateway): linearize tool-progress bubbles with content messages (#17280)
After PR #7885 (97b0cd51e) added content-side segment breaks for
natural mid-turn assistant messages, the tool-progress task in
gateway/run.py was not updated to match. progress_msg_id and
progress_lines persisted for the whole run, so after a tool batch
produced bubble B1 followed by content bubble C1, the next tool.started
kept editing the OLD bubble B1 above C1 — making the chat appear out
of order on Telegram, Discord, and Slack.
Add on_new_message callback to GatewayStreamConsumer, fired at the
four sites where a fresh content bubble lands on the platform:
- _send_or_edit first-send branch (NOT edits)
- _send_commentary
- _send_new_chunk (overflow split)
- each successful chunk of _send_fallback_final
Gateway supplies a lambda that enqueues ('__reset__',) into the
progress_queue. send_progress_messages() handles the marker in both
the main loop and the CancelledError drain path, clearing
progress_msg_id, progress_lines, and the dedup state so the next
tool.started opens a fresh bubble below the new content.
Result: each tool batch appears in chronological order below the
preceding content. When no content appears between tool batches,
tools still group in one bubble (CLI-style compactness).
Co-authored-by: teknium1 <teknium@users.noreply.github.com>
This commit is contained in:
@@ -91,11 +91,20 @@ class GatewayStreamConsumer:
|
||||
chat_id: str,
|
||||
config: Optional[StreamConsumerConfig] = None,
|
||||
metadata: Optional[dict] = None,
|
||||
on_new_message: Optional[callable] = None,
|
||||
):
|
||||
self.adapter = adapter
|
||||
self.chat_id = chat_id
|
||||
self.cfg = config or StreamConsumerConfig()
|
||||
self.metadata = metadata
|
||||
# Fired whenever a fresh content bubble is created on the platform
|
||||
# (first-send of a new message, commentary, overflow chunk, or
|
||||
# fallback continuation). The gateway uses this to linearize the
|
||||
# tool-progress bubble: when content resumes after a tool batch,
|
||||
# the next tool.started should open a NEW progress bubble below
|
||||
# the content, not edit the old bubble above it.
|
||||
# Called with no arguments. Exceptions are swallowed.
|
||||
self._on_new_message = on_new_message
|
||||
self._queue: queue.Queue = queue.Queue()
|
||||
self._accumulated = ""
|
||||
self._message_id: Optional[str] = None
|
||||
@@ -146,6 +155,16 @@ class GatewayStreamConsumer:
|
||||
if text:
|
||||
self._queue.put((_COMMENTARY, text))
|
||||
|
||||
def _notify_new_message(self) -> None:
|
||||
"""Fire the on_new_message callback, swallowing any errors."""
|
||||
cb = self._on_new_message
|
||||
if cb is None:
|
||||
return
|
||||
try:
|
||||
cb()
|
||||
except Exception:
|
||||
logger.debug("on_new_message callback error", exc_info=True)
|
||||
|
||||
def _reset_segment_state(self, *, preserve_no_edit: bool = False) -> None:
|
||||
if preserve_no_edit and self._message_id == "__no_edit__":
|
||||
return
|
||||
@@ -529,6 +548,9 @@ class GatewayStreamConsumer:
|
||||
self._message_id = str(result.message_id)
|
||||
self._already_sent = True
|
||||
self._last_sent_text = text
|
||||
# Fresh content bubble — close off any stale tool bubble
|
||||
# above so the next tool starts a new bubble below.
|
||||
self._notify_new_message()
|
||||
return str(result.message_id)
|
||||
else:
|
||||
self._edit_supported = False
|
||||
@@ -661,6 +683,9 @@ class GatewayStreamConsumer:
|
||||
sent_any_chunk = True
|
||||
last_successful_chunk = chunk
|
||||
last_message_id = result.message_id or last_message_id
|
||||
# Each fallback chunk is a fresh platform message — notify
|
||||
# so any stale tool-progress bubble gets closed off.
|
||||
self._notify_new_message()
|
||||
|
||||
self._message_id = last_message_id
|
||||
self._already_sent = True
|
||||
@@ -744,6 +769,11 @@ class GatewayStreamConsumer:
|
||||
# tool..."), not the final response. Setting already_sent would cause
|
||||
# the final response to be incorrectly suppressed when there are
|
||||
# multiple tool calls. See: https://github.com/NousResearch/hermes-agent/issues/10454
|
||||
if result.success:
|
||||
# Commentary counts as fresh content — close off any
|
||||
# stale tool bubble above it so the next tool starts a
|
||||
# new bubble below.
|
||||
self._notify_new_message()
|
||||
return result.success
|
||||
except Exception as e:
|
||||
logger.error("Commentary send error: %s", e)
|
||||
@@ -973,6 +1003,11 @@ class GatewayStreamConsumer:
|
||||
# every delta/tool boundary when platforms accept a
|
||||
# message but do not return an editable message id.
|
||||
self._message_id = "__no_edit__"
|
||||
# Notify the gateway that a fresh content bubble was
|
||||
# created so any accumulated tool-progress bubble above
|
||||
# gets closed off — the next tool fires into a new
|
||||
# bubble below, preserving chronological order.
|
||||
self._notify_new_message()
|
||||
return True
|
||||
else:
|
||||
# Initial send failed — disable streaming for this session
|
||||
|
||||
Reference in New Issue
Block a user