diff --git a/agent/context_compressor.py b/agent/context_compressor.py index b8af28a1a7..1965bd5b71 100644 --- a/agent/context_compressor.py +++ b/agent/context_compressor.py @@ -4192,13 +4192,15 @@ Write only the summary body. Do not include any preamble or prefix.""" from agent.conversation_loop import ( _CODEX_ACK_CONTINUATION_NUDGE, _CODEX_INCOMPLETE_NUDGE, _DEGENERATE_FINAL_NUDGE, _DROPPED_TOOLCALL_NUDGE_CONTENT, _EMPTY_TOOL_RESPONSE_NUDGE, _LENGTH_CONTINUATION_DROPPED_TOOLS_PREFIX, - _LENGTH_CONTINUATION_NETWORK_STUB, _LENGTH_CONTINUATION_OUTPUT_LIMIT, + _LEGACY_LENGTH_CONTINUATION_NETWORK_STUB, _LENGTH_CONTINUATION_NETWORK_STUB, + _LENGTH_CONTINUATION_OUTPUT_LIMIT, ) return text in { COMPRESSION_CONTINUATION_USER_CONTENT, _LEGACY_COMPRESSION_CONTINUATION_USER_CONTENT, MAX_ITERATIONS_SUMMARY_REQUEST, _CODEX_INCOMPLETE_NUDGE, _CODEX_ACK_CONTINUATION_NUDGE, _DEGENERATE_FINAL_NUDGE, _DROPPED_TOOLCALL_NUDGE_CONTENT, _EMPTY_TOOL_RESPONSE_NUDGE, - _LENGTH_CONTINUATION_NETWORK_STUB, _LENGTH_CONTINUATION_OUTPUT_LIMIT, + _LENGTH_CONTINUATION_NETWORK_STUB, _LEGACY_LENGTH_CONTINUATION_NETWORK_STUB, + _LENGTH_CONTINUATION_OUTPUT_LIMIT, } or text.startswith(( _BACKGROUND_PROCESS_NOTIFICATION_PREFIX, TODO_INJECTION_HEADER + "\n", _LENGTH_CONTINUATION_DROPPED_TOOLS_PREFIX, )) diff --git a/agent/conversation_loop.py b/agent/conversation_loop.py index 69fa98296d..635f9a14d7 100644 --- a/agent/conversation_loop.py +++ b/agent/conversation_loop.py @@ -892,6 +892,11 @@ _LENGTH_CONTINUATION_OUTPUT_LIMIT = ( "[System: Your previous response was truncated by the output length limit. Continue exactly " "where you left off. Do not restart or repeat prior text. Finish the answer directly.]" ) +# Pre-#74990 wording; kept so crash-persisted nudges from older sessions are still recognized. +_LEGACY_LENGTH_CONTINUATION_NETWORK_STUB = ( + "[System: The previous response was cut off by a network error mid-stream. Continue exactly " + "where you left off. Do not restart or repeat prior text. Finish the answer directly.]" +) # The dropped-tools variant interpolates tool names; matched by prefix. _LENGTH_CONTINUATION_DROPPED_TOOLS_PREFIX = "[System: Your previous tool call " @@ -904,7 +909,8 @@ def _get_continuation_prompt(is_partial_stub: bool, dropped_tools: Optional[List "the stream timed out before it could be delivered. Do NOT retry the same tool call " "with the same large content. Instead, break the content into multiple smaller tool " "calls (e.g. use multiple patch calls or write smaller files). Each tool call's " - "arguments must be under ~8K tokens to avoid stream timeouts.]" + "arguments must be under ~8K tokens to avoid stream timeouts. The cut was a transport " + "interruption, not a capability change — your tools remain fully available.]" ) return _LENGTH_CONTINUATION_NETWORK_STUB if is_partial_stub else _LENGTH_CONTINUATION_OUTPUT_LIMIT diff --git a/tests/agent/test_partial_stream_continuation_prompt.py b/tests/agent/test_partial_stream_continuation_prompt.py new file mode 100644 index 0000000000..0699debc77 --- /dev/null +++ b/tests/agent/test_partial_stream_continuation_prompt.py @@ -0,0 +1,18 @@ +"""After a mid-stream transport cut the continuation must keep tools on the table (#74990).""" +import pytest + +from agent import conversation_loop as cl +from agent.context_compressor import ContextCompressor + + +@pytest.mark.parametrize("dropped", [None, ["write_file"]]) +def test_partial_stub_continuation_says_tools_remain_available(dropped): + prompt = cl._get_continuation_prompt(True, dropped).lower() + assert "tools" in prompt and "available" in prompt + assert "finish the answer directly" not in prompt + + +@pytest.mark.parametrize("dropped", [None, ["write_file"]]) +def test_new_and_legacy_stub_nudges_are_recognized_as_synthetic(dropped): + for text in (cl._get_continuation_prompt(True, dropped), cl._LEGACY_LENGTH_CONTINUATION_NETWORK_STUB): + assert ContextCompressor._is_synthetic_compression_user_turn({"role": "user", "content": text})