fix(agent): explain abnormal turn endings instead of blank/partial reply
When a turn ends abnormally after substantive tool calls (empty content after retries, a partial/truncated stream, exhausted retries, or an iteration/budget limit), the CLI/TUI response area was left blank or showed only a fragment (e.g. "The") with no consolidated reason. The internal turn_exit_reason values (empty_response_exhausted, partial_stream_recovery, etc.) were never surfaced to the user. Add a turn-completion explainer that mirrors the existing file-mutation verifier footer: at turn end, map an abnormal turn_exit_reason to a short, actionable message and either replace the bare "(empty)" sentinel or append the reason after a partial fragment. Normal text_response exits (e.g. a terse "Done.") stay quiet. Gated by display.turn_completion_explainer (default on) with HERMES_TURN_COMPLETION_EXPLAINER env override, matching the file-mutation verifier seam. Closes #34452
This commit is contained in:
@@ -3046,7 +3046,11 @@ class TestRunConversation:
|
||||
|
||||
mock_compress.assert_not_called() # no compression triggered
|
||||
assert result["completed"] is True
|
||||
assert result["final_response"] == "(empty)"
|
||||
# #34452: the bare "(empty)" sentinel is now replaced by a
|
||||
# user-visible end-of-turn explanation so the failure isn't silent.
|
||||
assert result["final_response"] != "(empty)"
|
||||
assert "without a usable reply" in result["final_response"]
|
||||
assert result["turn_exit_reason"] == "empty_response_exhausted"
|
||||
assert result["api_calls"] == 6 # 1 original + 2 prefill + 3 retries
|
||||
|
||||
def test_reasoning_only_response_prefill_then_empty(self, agent):
|
||||
@@ -3066,7 +3070,9 @@ class TestRunConversation:
|
||||
):
|
||||
result = agent.run_conversation("answer me")
|
||||
assert result["completed"] is True
|
||||
assert result["final_response"] == "(empty)"
|
||||
# #34452: explanation replaces the bare "(empty)" sentinel.
|
||||
assert result["final_response"] != "(empty)"
|
||||
assert "without a usable reply" in result["final_response"]
|
||||
assert result["api_calls"] == 6 # 1 original + 2 prefill + 3 retries
|
||||
|
||||
def test_reasoning_only_prefill_succeeds_on_continuation(self, agent):
|
||||
@@ -3113,7 +3119,9 @@ class TestRunConversation:
|
||||
):
|
||||
result = agent.run_conversation("answer me")
|
||||
assert result["completed"] is True
|
||||
assert result["final_response"] == "(empty)"
|
||||
# #34452: explanation replaces the bare "(empty)" sentinel.
|
||||
assert result["final_response"] != "(empty)"
|
||||
assert "without a usable reply" in result["final_response"]
|
||||
assert result["api_calls"] == 4 # 1 original + 3 retries
|
||||
|
||||
def test_truly_empty_response_succeeds_on_nudge(self, agent):
|
||||
@@ -3209,7 +3217,9 @@ class TestRunConversation:
|
||||
):
|
||||
result = agent.run_conversation("answer me")
|
||||
assert result["completed"] is True
|
||||
assert result["final_response"] == "(empty)"
|
||||
# #34452: explanation replaces the bare "(empty)" sentinel.
|
||||
assert result["final_response"] != "(empty)"
|
||||
assert "without a usable reply" in result["final_response"]
|
||||
|
||||
def test_empty_response_emits_status_for_gateway(self, agent):
|
||||
"""_emit_status is called during empty retries so gateway users see feedback."""
|
||||
@@ -3235,7 +3245,10 @@ class TestRunConversation:
|
||||
):
|
||||
result = agent.run_conversation("answer me")
|
||||
|
||||
assert result["final_response"] == "(empty)"
|
||||
# #34452: explanation replaces the bare "(empty)" sentinel, but the
|
||||
# status emissions during retries are unchanged.
|
||||
assert result["final_response"] != "(empty)"
|
||||
assert "without a usable reply" in result["final_response"]
|
||||
# Should have emitted retry statuses (3 retries) + final failure
|
||||
retry_msgs = [m for m in status_messages if "retrying" in m.lower()]
|
||||
assert len(retry_msgs) == 3, f"Expected 3 retry status messages, got {len(retry_msgs)}: {status_messages}"
|
||||
|
||||
Reference in New Issue
Block a user