From 367dda813ce47ff6bb56dac15badd65d1a319fec Mon Sep 17 00:00:00 2001 From: Ken Weiner Date: Thu, 2 Jul 2026 17:35:03 +0000 Subject: [PATCH] fix: print transform_llm_output appended content after CLI streaming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the CLI streams a response token-by-token, it marks the response as already displayed and skips re-printing after the tool loop. This means any content appended by a transform_llm_output plugin fires after streaming — the appended text is in the final response and stored in history, but never shown to the user. Fix by tracking the pre-transform response in finalize_turn() and including it in the result dict as pre_transform_response. The CLI then checks whether the response was transformed and, if so, prints only the appended suffix. Previously the already_streamed branch was a no-op pass. Now it detects post-stream plugin additions and outputs them without re-printing the streamed body. --- agent/turn_finalizer.py | 3 +++ cli.py | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/agent/turn_finalizer.py b/agent/turn_finalizer.py index 3b62028f23c82..79f4397fc6a5c 100644 --- a/agent/turn_finalizer.py +++ b/agent/turn_finalizer.py @@ -547,6 +547,7 @@ def finalize_turn( logger.debug("turn-completion explainer failed: %s", _exp_err) _response_transformed = False + _pre_transform_response = None # Plugin hook: transform_llm_output # Fired once per turn after the tool-calling loop completes. @@ -564,6 +565,7 @@ def finalize_turn( ) for _hook_result in _transform_results: if isinstance(_hook_result, str) and _hook_result: + _pre_transform_response = final_response final_response = _hook_result _response_transformed = True break # First non-empty string wins @@ -648,6 +650,7 @@ def finalize_turn( "partial": False, # True only when stopped due to invalid tool calls "interrupted": interrupted, "response_transformed": _response_transformed, + "pre_transform_response": _pre_transform_response, "response_previewed": getattr(agent, "_response_was_previewed", False), "model": agent.model, "provider": agent.provider, diff --git a/cli.py b/cli.py index 0861fcffe398e..1081c8163ab3a 100644 --- a/cli.py +++ b/cli.py @@ -14445,7 +14445,13 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin): elif already_streamed: # Response was already streamed token-by-token with box framing; # _flush_stream() already closed the box. Skip Rich Panel. - pass + # If a plugin appended content (shout, trace) after streaming, + # print only the appended portion now. + if result and result.get("response_transformed"): + _pre = result.get("pre_transform_response") or "" + _appended = response[len(_pre):] + if _appended.strip(): + _cprint(_appended) else: _chat_console = ChatConsole() _chat_console.print(Panel(