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.
76 lines
3.0 KiB
Python
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."
|