Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion gateway/display_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@
# live, just cleaned up after success so the chat doesn't fill up with
# stale breadcrumbs. Failed runs leave bubbles in place as breadcrumbs.
"cleanup_progress": False,
# When true, suppresses retry/empty-response/thinking-only/fallback status
# bubbles and the "(empty)" sentinel substitution. Intended for deployments
# where agent personas can legitimately produce empty responses (e.g. "stay
# silent when addressed to someone else") that the generic retry logic would
# otherwise misinterpret as failures. Off by default.
"suppress_retry_status": False,
}

# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -194,7 +200,7 @@ def _normalise(setting: str, value: Any) -> Any:
if isinstance(value, str):
return value.lower() in {"true", "1", "yes", "on"}
return bool(value)
if setting == "cleanup_progress":
if setting in {"cleanup_progress", "suppress_retry_status"}:
if isinstance(value, str):
return value.lower() in {"true", "1", "yes", "on"}
return bool(value)
Expand Down
38 changes: 33 additions & 5 deletions gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -1073,11 +1073,33 @@ def _format_gateway_process_notification(evt: dict) -> "str | None":
_gateway_runner_ref: _weakref.ref = lambda: None


def _resolve_suppress_retry_status(platform_key: str) -> bool:
"""Resolve the ``display.platforms.<platform>.suppress_retry_status`` flag.

Returns True when the active platform has ``suppress_retry_status`` set to a
truthy value in the display config. Used to silence the ⚠️ empty-response
substitution and the generic "no response was generated" warning for
deployments where empty model responses are a legitimate outcome (e.g. an
agent persona that stays silent when a message is addressed to another
participant).
"""
try:
from gateway.display_config import resolve_display_setting
from hermes_cli.config import load_config as _load_cfg
cfg = _load_cfg() or {}
return bool(
resolve_display_setting(cfg, platform_key, "suppress_retry_status", False)
)
except Exception:
return False


def _normalize_empty_agent_response(
agent_result: dict,
response: str,
*,
history_len: int = 0,
platform_key: str = "",
) -> str:
"""Normalize empty/None agent responses into user-facing messages.

Expand Down Expand Up @@ -1111,6 +1133,8 @@ def _normalize_empty_agent_response(
if agent_result.get("partial"):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This suppresses the diagnostic for every blank post-API response, including genuine degraded failures. Current main distinguishes intentional silence via exact NO_REPLY/[SILENT] markers in gateway/response_filters.py; preserving that distinction avoids silently hiding malformed or failed model output.

err = agent_result.get("error", "processing incomplete")
return f"⚠️ Processing stopped: {str(err)[:200]}. Try again."
if _resolve_suppress_retry_status(platform_key):
return response
return (
"⚠️ Processing completed but no response was generated. "
"This may be a transient error — try sending your message again."
Expand Down Expand Up @@ -7644,11 +7668,14 @@ async def _handle_message_with_agent(self, event, source, _quick_key: str, run_g
# prefill, empty-retry, fallback). Sending the raw sentinel
# looks like a bug; a short explanation is more helpful.
if response == "(empty)":
response = (
"⚠️ The model returned no response after processing tool "
"results. This can happen with some models — try again or "
"rephrase your question."
)
if _resolve_suppress_retry_status(_platform_name):
response = ""
else:
response = (
"⚠️ The model returned no response after processing tool "
"results. This can happen with some models — try again or "
"rephrase your question."
)
agent_messages = agent_result.get("messages", [])
_response_time = time.time() - _msg_start_time
_api_calls = agent_result.get("api_calls", 0)
Expand Down Expand Up @@ -7681,6 +7708,7 @@ async def _handle_message_with_agent(self, event, source, _quick_key: str, run_g
# the case where agent did work but returned no text. Fix for #18765.
response = _normalize_empty_agent_response(
agent_result, response, history_len=len(history),
platform_key=_platform_name,
)

# If the agent's session_id changed during compression, update
Expand Down
46 changes: 39 additions & 7 deletions run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -2884,6 +2884,38 @@ def _should_emit_quiet_tool_messages(self) -> bool:
and getattr(self, "platform", "") == "cli"
)

def _should_suppress_retry_status(self) -> bool:
"""Return True when retry/empty-response status bubbles should be suppressed.

Reads ``display.platforms.<platform>.suppress_retry_status`` from config.
Cached per agent instance.
"""
cached = getattr(self, "_suppress_retry_status_cached", None)
if cached is not None:
return cached
try:
from gateway.display_config import resolve_display_setting
from hermes_cli.config import load_config as _load_cfg
cfg = _load_cfg() or {}
platform_key = (self.platform or "").lower().strip() or "cli"
result = bool(
resolve_display_setting(cfg, platform_key, "suppress_retry_status", False)
)
except Exception:
result = False
self._suppress_retry_status_cached = result
return result

def _emit_retry_status(self, message: str) -> None:
"""Emit a retry/empty-response/fallback status bubble, if not suppressed.

No-op when ``suppress_retry_status`` is enabled for the active platform.
Otherwise identical to ``_emit_status``.
"""
if self._should_suppress_retry_status():
return
self._emit_status(message)

def _emit_status(self, message: str) -> None:
"""Emit a lifecycle status message to both CLI and gateway channels.

Expand Down Expand Up @@ -15091,7 +15123,7 @@ def _stop_spinner():
"Empty response after tool calls — nudging model "

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current main no longer executes this retry block from run_agent.py: the live empty-response transitions are now in agent/conversation_loop.py, where they call _buffer_status() and later flush centrally. Please rework this against the current buffer/flush path rather than adding another per-call emission wrapper.

"to continue processing"
)
self._emit_status(
self._emit_retry_status(
"⚠️ Model returned empty after tool calls — "
"nudging to continue"
)
Expand Down Expand Up @@ -15137,7 +15169,7 @@ def _stop_spinner():
"prefilling to continue (%d/2)",
self._thinking_prefill_retries,
)
self._emit_status(
self._emit_retry_status(
f"↻ Thinking-only response — prefilling to continue "
f"({self._thinking_prefill_retries}/2)"
)
Expand Down Expand Up @@ -15173,7 +15205,7 @@ def _stop_spinner():
"retry %d/3 (model=%s)",
self._empty_content_retries, self.model,
)
self._emit_status(
self._emit_retry_status(
f"⚠️ Empty response from model — retrying "
f"({self._empty_content_retries}/3)"
)
Expand All @@ -15192,13 +15224,13 @@ def _stop_spinner():
self._empty_content_retries, self.model,
self.provider,
)
self._emit_status(
self._emit_retry_status(
"⚠️ Model returning empty responses — "
"switching to fallback provider..."
)
if self._try_activate_fallback():
self._empty_content_retries = 0
self._emit_status(
self._emit_retry_status(
f"↻ Switched to fallback: {self.model} "
f"({self.provider})"
)
Expand Down Expand Up @@ -15232,7 +15264,7 @@ def _stop_spinner():
"after exhausting retries and fallback. "
"Reasoning: %s", reasoning_preview,
)
self._emit_status(
self._emit_retry_status(
"⚠️ Model produced reasoning but no visible "
"response after all retries. Returning empty."
)
Expand All @@ -15244,7 +15276,7 @@ def _stop_spinner():
self._empty_content_retries, self.model,
self.provider,
)
self._emit_status(
self._emit_retry_status(
"❌ Model returned no content after all retries"
+ (" and fallback attempts." if self._fallback_chain else
". No fallback providers configured.")
Expand Down