From f27417fc40b33b60614cd25dff33124640d9bb39 Mon Sep 17 00:00:00 2001 From: Andrey <3605840+Diaspar4u@users.noreply.github.com> Date: Mon, 31 Aug 2026 20:12:58 -0400 Subject: [PATCH] fix(agent): re-anchor after post-tool compression --- agent/turn_preflight.py | 7 ++- agent/turn_tool_round.py | 8 ++- ...est_post_tool_compression_turn_boundary.py | 56 +++++++++++++++++++ 3 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 tests/agent/test_post_tool_compression_turn_boundary.py diff --git a/agent/turn_preflight.py b/agent/turn_preflight.py index 3a71db1916..7361f048f1 100644 --- a/agent/turn_preflight.py +++ b/agent/turn_preflight.py @@ -22,7 +22,7 @@ from agent.conversation_compression import ( from agent.turn_context import _review_fork_first_request_pending from agent.turn_context_compaction import ( _apply_grown_window, _blocked_compress_reason, _clear_overflow_warn, _refund_api_call, - _reset_retry_state_after_compaction, + _reanchor, _reset_retry_state_after_compaction, ) logger = logging.getLogger("agent.conversation_loop") @@ -238,13 +238,14 @@ class PostToolCompressionVerdict: compression_attempts: int final_response: Any turn_exit_reason: Any + current_turn_user_idx: int def compress_after_tool_results( agent: Any, *, messages: List[Dict[str, Any]], system_message: Any, user_message: Any, active_system_prompt: Any, conversation_history: Any, compression_attempts: int, max_compression_attempts: int, effective_task_id: Any, final_response: Any, - turn_exit_reason: Any, + turn_exit_reason: Any, current_turn_user_idx: int, ) -> PostToolCompressionVerdict: """Post-tool-call compression decision. Pressure comes from API-reported ``prompt_tokens`` (a tight lower bound; thinking models inflate completion tokens), @@ -263,6 +264,7 @@ def compress_after_tool_results( end_turn=end_turn, messages=messages, active_system_prompt=active_system_prompt, conversation_history=conversation_history, compression_attempts=compression_attempts, final_response=final_response, turn_exit_reason=turn_exit_reason, + current_turn_user_idx=current_turn_user_idx, ) _compressor = agent.context_compressor @@ -355,6 +357,7 @@ def compress_after_tool_results( final_response = _HANDOFF_SKIP_FINAL_RESPONSE turn_exit_reason = "compaction_handoff_not_actionable" return _verdict(True) + current_turn_user_idx = _reanchor(agent, messages, user_message) elif agent.compression_enabled: # Over threshold but compression blocked (cooldown/anti-thrash): deduped # warning so context can't silently overflow. ``attempts_spent`` names the diff --git a/agent/turn_tool_round.py b/agent/turn_tool_round.py index 62e5f306c2..10211a8895 100644 --- a/agent/turn_tool_round.py +++ b/agent/turn_tool_round.py @@ -39,6 +39,7 @@ class ToolRoundVerdict: failed: Any _turn_exit_reason: Any truncated_tool_call_retries: Any + current_turn_user_idx: Any result: Optional[Dict[str, Any]] = None @@ -47,7 +48,7 @@ def run_tool_round( conversation_history: Any, api_call_count: Any, effective_task_id: Any, user_message: Any, system_message: Any, active_system_prompt: Any, compression_attempts: Any, max_compression_attempts: Any, final_response: Any, failed: Any, _turn_exit_reason: Any, - truncated_tool_call_retries: Any, + truncated_tool_call_retries: Any, current_turn_user_idx: Any, ) -> ToolRoundVerdict: """Execute one tool round in the exact original order. Persist-before-execute is a durability invariant: resume must see the executed block if a destructive tool restarts @@ -60,7 +61,8 @@ def run_tool_round( action=action, messages=messages, conversation_history=conversation_history, active_system_prompt=active_system_prompt, compression_attempts=compression_attempts, final_response=final_response, failed=failed, _turn_exit_reason=_turn_exit_reason, - truncated_tool_call_retries=truncated_tool_call_retries, result=result, + truncated_tool_call_retries=truncated_tool_call_retries, + current_turn_user_idx=current_turn_user_idx, result=result, ) if not agent.quiet_mode: @@ -191,6 +193,7 @@ def run_tool_round( compression_attempts=compression_attempts, max_compression_attempts=max_compression_attempts, effective_task_id=effective_task_id, final_response=final_response, turn_exit_reason=_turn_exit_reason, + current_turn_user_idx=current_turn_user_idx, ) messages = _ptc.messages active_system_prompt = _ptc.active_system_prompt @@ -198,6 +201,7 @@ def run_tool_round( compression_attempts = _ptc.compression_attempts final_response = _ptc.final_response _turn_exit_reason = _ptc.turn_exit_reason + current_turn_user_idx = _ptc.current_turn_user_idx if _ptc.end_turn: return _verdict("break") diff --git a/tests/agent/test_post_tool_compression_turn_boundary.py b/tests/agent/test_post_tool_compression_turn_boundary.py new file mode 100644 index 0000000000..b100cc1388 --- /dev/null +++ b/tests/agent/test_post_tool_compression_turn_boundary.py @@ -0,0 +1,56 @@ +from types import SimpleNamespace + +from agent.turn_preflight import compress_after_tool_results + + +def test_post_tool_compression_reanchors_the_active_user_boundary(monkeypatch): + compressed = [ + {"role": "user", "content": "compressed history"}, + {"role": "user", "content": "current ask"}, + {"role": "assistant", "tool_calls": [{"id": "2"}]}, + {"role": "tool", "content": "fresh result", "tool_call_id": "2"}, + ] + + class Compressor: + last_prompt_tokens = 100 + threshold_tokens = 50 + + @staticmethod + def should_compress(_tokens): + return True + + agent = SimpleNamespace( + context_compressor=Compressor(), + compression_enabled=True, + _clear_context_overflow_warn=lambda: None, + _safe_print=lambda *_args: None, + _compress_context=lambda *_args, **_kwargs: (compressed, "system"), + _persist_user_message_idx=4, + ) + monkeypatch.setattr( + "agent.turn_preflight.conversation_history_after_compression", + lambda _agent, _messages, _history: [], + ) + monkeypatch.setattr( + "agent.conversation_loop._should_skip_model_call_for_reference_handoff", + lambda _messages, _user_message: False, + ) + + verdict = compress_after_tool_results( + agent, + messages=[{"role": "user", "content": "current ask"}], + system_message="system", + user_message="current ask", + active_system_prompt="system", + conversation_history=[], + compression_attempts=0, + max_compression_attempts=1, + effective_task_id="task", + final_response="", + turn_exit_reason=None, + current_turn_user_idx=0, + ) + + assert verdict.messages is compressed + assert verdict.current_turn_user_idx == 1 + assert agent._persist_user_message_idx == 1