From 2ef06a04bbd2398ec9667167d148f6ed846315d3 Mon Sep 17 00:00:00 2001 From: Test Date: Fri, 20 Mar 2026 09:52:47 -0700 Subject: [PATCH] =?UTF-8?q?fix:=20streaming=20display=20=E2=80=94=20show?= =?UTF-8?q?=20tool=20feed=20lines=20and=20fix=20response=20box=20framing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two display bugs when streaming is enabled in the interactive CLI: 1. Tool feed lines (┊ 📖 read, ┊ 💻 $, etc.) were invisible during streaming sessions. The sequential tool execution path at line 4840 had a guard `not self._has_stream_consumers()` that skipped the entire spinner + cute message display when a stream_delta_callback was registered. But no tokens are streaming during tool execution — the _executing_tools flag already handles this for _vprint. Removed the unnecessary guard. 2. Response box (╭─ ⚕ Hermes ─╮) could wrap intermediate tool-calling turns instead of only the final response. Content tokens arrive before tool_call tokens during streaming, so the box opens before the agent knows tool calls are coming. Now sends a None sentinel through the stream callback when tool_calls are confirmed, which tells the CLI to close any open box and reset stream state. Only the actual final response gets the Hermes border. Live tested: reasoning blocks → tool feed → reasoning → response box now display in the correct sequence. --- cli.py | 12 ++++++++++++ run_agent.py | 10 ++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/cli.py b/cli.py index af8ac4efc247..171c4d43c69a 100755 --- a/cli.py +++ b/cli.py @@ -1511,10 +1511,22 @@ class HermesCLI: partial lines and emits complete lines via _cprint to work reliably with prompt_toolkit's patch_stdout. + A ``None`` sentinel signals that an intermediate turn (with tool + calls) just completed — close any open response/reasoning box + and reset so the final response gets a clean frame. + Reasoning/thinking blocks (, , etc.) are suppressed during streaming since they'd display raw XML tags. The agent strips them from the final response anyway. """ + if text is None: + # Intermediate turn ended — discard any open box and reset + self._close_reasoning_box() + if self._stream_box_opened: + w = shutil.get_terminal_size().columns + _cprint(f"{_GOLD}╰{'─' * (w - 2)}╯{_RST}") + self._reset_stream_state() + return if not text: return diff --git a/run_agent.py b/run_agent.py index 78948a78270a..d3b74b7e2d3e 100644 --- a/run_agent.py +++ b/run_agent.py @@ -4837,7 +4837,7 @@ class AIAgent: spinner.stop(cute_msg) elif self.quiet_mode: self._vprint(f" {cute_msg}") - elif self.quiet_mode and not self._has_stream_consumers(): + elif self.quiet_mode: face = random.choice(KawaiiSpinner.KAWAII_WAITING) emoji = _get_tool_emoji(function_name) preview = _build_tool_preview(function_name, function_args) or function_name @@ -6552,7 +6552,13 @@ class AIAgent: ) assistant_msg = self._build_assistant_message(assistant_message, finish_reason) - + + # Signal streaming display that this was an intermediate turn. + # Content may have been streamed (opening the response box) before + # tool_calls were detected. The None sentinel tells the CLI to + # close any open box and reset so the final response gets a clean frame. + self._fire_stream_delta(None) + # If this turn has both content AND tool_calls, capture the content # as a fallback final response. Common pattern: model delivers its # answer and calls memory/skill tools as a side-effect in the same