diff --git a/run_agent.py b/run_agent.py index d7d1249be..1676d2f5a 100644 --- a/run_agent.py +++ b/run_agent.py @@ -7881,6 +7881,7 @@ class AIAgent: self._thinking_prefill_retries = 0 self._post_tool_empty_retried = False self._last_content_with_tools = None + self._last_content_tools_all_housekeeping = False self._mute_post_response = False self._unicode_sanitization_passes = 0 @@ -8068,6 +8069,7 @@ class AIAgent: self._empty_content_retries = 0 self._thinking_prefill_retries = 0 self._last_content_with_tools = None + self._last_content_tools_all_housekeeping = False self._mute_post_response = False # Re-estimate after compression _preflight_tokens = estimate_request_tokens_rough( @@ -10130,6 +10132,7 @@ class AIAgent: tc.function.name in _HOUSEKEEPING_TOOLS for tc in assistant_message.tool_calls ) + self._last_content_tools_all_housekeeping = _all_housekeeping if _all_housekeeping and self._has_stream_consumers(): self._mute_post_response = True elif self.quiet_mode: @@ -10312,15 +10315,22 @@ class AIAgent: break # If the previous turn already delivered real content alongside - # tool calls (e.g. "You're welcome!" + memory save), the model - # has nothing more to say. Use the earlier content immediately - # instead of wasting API calls on retries that won't help. + # HOUSEKEEPING tool calls (e.g. "You're welcome!" + memory save), + # the model has nothing more to say. Use the earlier content + # immediately instead of wasting API calls on retries. + # NOTE: Only use this shortcut when ALL tools in that turn were + # housekeeping (memory, todo, etc.). When substantive tools + # were called (terminal, search_files, etc.), the content was + # likely mid-task narration ("I'll scan the directory...") and + # the empty follow-up means the model choked — let the + # post-tool nudge below handle that instead of exiting early. fallback = getattr(self, '_last_content_with_tools', None) - if fallback: + if fallback and getattr(self, '_last_content_tools_all_housekeeping', False): _turn_exit_reason = "fallback_prior_turn_content" logger.info("Empty follow-up after tool calls — using prior turn content as final response") self._emit_status("↻ Empty response after tool calls — using earlier content as final answer") self._last_content_with_tools = None + self._last_content_tools_all_housekeeping = False self._empty_content_retries = 0 # Do NOT modify the assistant message content — the # old code injected "Calling the X tools..." which @@ -10331,13 +10341,18 @@ class AIAgent: break # ── Post-tool-call empty response nudge ─────────── - # The model returned empty after executing tool calls - # but there's no prior-turn content to fall back on. + # The model returned empty after executing tool calls. + # This covers two cases: + # (a) No prior-turn content at all — model went silent + # (b) Prior turn had content + SUBSTANTIVE tools (the + # fallback above was skipped because the content + # was mid-task narration, not a final answer) # Instead of giving up, nudge the model to continue by # appending a user-level hint. This is the #9400 case: - # weaker models (GLM-5, etc.) sometimes return empty - # after tool results instead of continuing to the next - # step. One retry with a nudge usually fixes it. + # weaker models (mimo-v2-pro, GLM-5, etc.) sometimes + # return empty after tool results instead of continuing + # to the next step. One retry with a nudge usually + # fixes it. _prior_was_tool = any( m.get("role") == "tool" for m in messages[-5:] # check recent messages @@ -10347,6 +10362,10 @@ class AIAgent: and not getattr(self, "_post_tool_empty_retried", False) ): self._post_tool_empty_retried = True + # Clear stale narration so it doesn't resurface + # on a later empty response after the nudge. + self._last_content_with_tools = None + self._last_content_tools_all_housekeeping = False logger.info( "Empty response after tool calls — nudging model " "to continue processing"