Skip to content
Closed
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
23 changes: 12 additions & 11 deletions gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -1079,6 +1079,7 @@ def _normalize_empty_agent_response(
response: str,
*,
history_len: int = 0,
allow_user_visible_diagnostics: bool = True,
) -> str:
"""Normalize empty/None agent responses into user-facing messages.

Expand All @@ -1089,6 +1090,9 @@ def _normalize_empty_agent_response(
if response:
return response

if not allow_user_visible_diagnostics:
return response

if agent_result.get("failed"):
error_detail = agent_result.get("error", "unknown error")
error_str = str(error_detail).lower()
Expand Down Expand Up @@ -7552,17 +7556,11 @@ async def _handle_message_with_agent(self, event, source, _quick_key: str, run_g

response = agent_result.get("final_response") or ""

# Convert the agent's internal "(empty)" sentinel into a
# user-friendly message. "(empty)" means the model failed to
# produce visible content after exhausting all retries (nudge,
# prefill, empty-retry, fallback). Sending the raw sentinel
# looks like a bug; a short explanation is more helpful.
# Chat gateways treat silent outcomes as a valid no-op. Keep the
# agent's internal empty sentinel out of user-visible channels and
# let the adapter no-op on the empty final response.
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."
)
response = ""
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 @@ -7594,7 +7592,10 @@ async def _handle_message_with_agent(self, event, source, _quick_key: str, run_g
# Normalize empty responses: surface errors, partial failures, and
# 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),
agent_result,
response,
history_len=len(history),
allow_user_visible_diagnostics=False,
)

# If the agent's session_id changed during compression, update
Expand Down
43 changes: 43 additions & 0 deletions tests/test_lazy_session_regressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,49 @@ def test_failed_generic_error(self):
assert "500 Internal Server Error" in response
assert "/reset" in response

def test_gateway_mode_keeps_failed_empty_response_silent(self):
"""Gateway turns should not leak backend failures into chat channels."""
from gateway.run import _normalize_empty_agent_response

agent_result = {
"final_response": None,
"api_calls": 0,
"failed": True,
"error": "500 Internal Server Error",
}

response = agent_result.get("final_response") or ""
response = _normalize_empty_agent_response(
agent_result,
response,
history_len=5,
allow_user_visible_diagnostics=False,
)

assert response == ""

def test_gateway_mode_keeps_partial_empty_response_silent(self):
"""Gateway turns should stay silent for empty partial/tool failures."""
from gateway.run import _normalize_empty_agent_response

agent_result = {
"final_response": None,
"api_calls": 5,
"partial": True,
"interrupted": False,
"error": "Model generated invalid tool call: nonexistent_tool",
}

response = agent_result.get("final_response") or ""
response = _normalize_empty_agent_response(
agent_result,
response,
history_len=10,
allow_user_visible_diagnostics=False,
)

assert response == ""

def test_nonempty_response_passes_through(self):
"""Non-empty response is returned unchanged."""
from gateway.run import _normalize_empty_agent_response
Expand Down
Loading