From f9c2ad48c29168a168a1e5895afe8a34a0454078 Mon Sep 17 00:00:00 2001 From: MacroAnarchy Date: Sun, 22 Mar 2026 08:32:21 +0100 Subject: [PATCH] fix: defer streaming iteration linebreak to prevent blank line stacking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to 669c60a6 (cherry-pick of PR #2187, fixes #2177). The original fix emits a "\n\n" delta immediately after every _execute_tool_calls() invocation. When the model runs multiple consecutive tool iterations before producing text (common with search → read → analyze flows), each iteration appends its own paragraph break, resulting in 4-6+ blank lines before the actual response. Replace the immediate delta with a deferred flag (_stream_needs_break). _fire_stream_delta() checks the flag and prepends a single "\n\n" only when the first real text delta arrives, so multiple back-to-back tool iterations still produce exactly one paragraph break. --- run_agent.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/run_agent.py b/run_agent.py index a2353c053..da8c8293b 100644 --- a/run_agent.py +++ b/run_agent.py @@ -670,6 +670,9 @@ class AIAgent: # Internal stream callback (set during streaming TTS). # Initialized here so _vprint can reference it before run_conversation. self._stream_callback = None + # Deferred paragraph break flag — set after tool iterations so a + # single "\n\n" is prepended to the next real text delta. + self._stream_needs_break = False # Optional current-turn user-message override used when the API-facing # user message intentionally differs from the persisted transcript @@ -3471,6 +3474,13 @@ class AIAgent: def _fire_stream_delta(self, text: str) -> None: """Fire all registered stream delta callbacks (display + TTS).""" + # If a tool iteration set the break flag, prepend a single paragraph + # break before the first real text delta. This prevents the original + # problem (text concatenation across tool boundaries) without stacking + # blank lines when multiple tool iterations run back-to-back. + if getattr(self, "_stream_needs_break", False) and text and text.strip(): + self._stream_needs_break = False + text = "\n\n" + text for cb in (self.stream_delta_callback, self._stream_callback): if cb is not None: try: @@ -6756,8 +6766,13 @@ class AIAgent: _msg_count_before_tools = len(messages) self._execute_tool_calls(assistant_message, messages, effective_task_id, api_call_count) - # Signal iteration boundary to stream consumers to prevent text concatenation - self._fire_stream_delta("\n\n") + # Signal that a paragraph break is needed before the next + # streamed text. We don't emit it immediately because + # multiple consecutive tool iterations would stack up + # redundant blank lines. Instead, _fire_stream_delta() + # will prepend a single "\n\n" the next time real text + # arrives. + self._stream_needs_break = True # Refund the iteration if the ONLY tool(s) called were # execute_code (programmatic tool calling). These are