Files
hermes-agent/tests/agent/test_turn_api_call_interrupt.py
teknium1 55a26c9983 fix: only drop an interrupted partial for the runaway repetition shape
is_repetition_dominated is tuned for the truncated-continuation nudge, where a
false positive merely skips a continuation. At the interrupt checkpoints the
same verdict DROPS the partial from history and tells the model the reply
degenerated, so a legitimately repetitive but correct reply - twelve distinct
INSERT rows sharing a long prefix trip the 60-char window scan - was erased and
mislabelled. Gate the two checkpoints on is_runaway_repetition: dominated AND,
when the text has line structure, at most half of its non-empty lines distinct.
Byte-identical repeated lines (the #112764 shape) still qualify; distinct batch
rows no longer do. The continuation path keeps the looser predicate.

The plain interrupt site now mirrors the redirect placeholder (empty content,
display_kind=hidden, api_content=[response interrupted]) so the bracketed
placeholder no longer surfaces as an assistant bubble in transcript replays.
2026-09-16 17:10:13 -07:00

76 lines
3.0 KiB
Python

"""Tests for ``agent/turn_api_call.py::handle_api_interrupt`` — the plain (non-redirect)
interrupt that lands mid provider call and records the streamed partial as the interrupted
assistant row."""
from __future__ import annotations
import threading
import time
from agent.agent_runtime_helpers import _INTERRUPTED_PLACEHOLDER
from agent.repetition_guard import REPETITION_LOOP_INTERRUPTED
from agent.turn_api_call import handle_api_interrupt
from agent.turn_retry_state import TurnRetryState
from run_agent import AIAgent
def _bare_agent(streamed: str) -> AIAgent:
agent = object.__new__(AIAgent)
agent._pending_redirect = None
agent._pending_redirect_lock = threading.Lock()
agent._interrupt_requested = False
agent._interrupt_message = None
agent._current_streamed_assistant_text = streamed
agent._strip_think_blocks = lambda content: content
agent.quiet_mode = True
agent.log_prefix = ""
agent.thinking_callback = None
agent._print_fn = lambda *args, **kwargs: None
agent._persist_session = lambda *args, **kwargs: None
return agent
def _interrupt(streamed: str):
messages = [{"role": "user", "content": "start"}]
verdict = handle_api_interrupt(
_bare_agent(streamed), _retry=TurnRetryState(), thinking_spinner=None, messages=messages,
conversation_history=[], api_start_time=time.time(), interrupted=False, final_response=None,
)
return messages, verdict
def test_repetition_dominated_partial_is_not_kept_as_the_interrupted_row():
"""A looped partial replayed as the interrupted assistant row re-seeds the loop on the next
turn (#112764): the row keeps the neutral placeholder and the user is told what happened."""
looped = "I. " * 1941
messages, verdict = _interrupt(looped)
# Same hidden shape as the redirect placeholder: no visible bubble in transcript replays.
assert messages[-1]["role"] == "assistant"
assert messages[-1]["content"] == ""
assert messages[-1]["display_kind"] == "hidden"
assert messages[-1]["api_content"] == _INTERRUPTED_PLACEHOLDER
assert verdict.final_response == REPETITION_LOOP_INTERRUPTED
assert "I. I. I." not in verdict.final_response
def test_distinct_batch_rows_are_not_mistaken_for_a_loop():
"""Legitimately repetitive output (distinct INSERT rows sharing a long prefix) trips the
window scan but is not a runaway loop: the partial must stay the interrupted row and must
not be relabelled as a degenerate reply."""
rows = "\n".join(
f"INSERT INTO users (id, name, email, created_at) VALUES ({i}, 'user{i}', 'user{i}@example.com', NOW());"
for i in range(12)
)
messages, verdict = _interrupt(rows)
assert (messages[-1]["role"], messages[-1]["content"]) == ("assistant", rows)
assert verdict.final_response == rows
def test_ordinary_partial_is_kept_as_the_interrupted_row():
messages, verdict = _interrupt("Visible draft.")
assert (messages[-1]["role"], messages[-1]["content"]) == ("assistant", "Visible draft.")
assert verdict.final_response == "Visible draft."