@@ -194,3 +194,41 @@ def test_locate_safe_reduction_index_high_offset(chat_messages_with_pairs):
194194 else :
195195 # It's fine if it returns None, meaning no valid safe reduction was found.
196196 pass
197+
198+
199+ def test_locate_safe_reduction_index_tool_role_without_function_result_content ():
200+ """Regression test: TOOL role messages without FunctionResultContent in items
201+ must still be recognized as part of a tool call/result pair.
202+
203+ This prevents orphaning tool results when the TOOL message only contains
204+ text content (no FunctionResultContent item).
205+ """
206+ msgs = [
207+ ChatMessageContent (role = AuthorRole .USER , content = "Hello" ),
208+ ]
209+ # Assistant with tool call
210+ msg_call = ChatMessageContent (role = AuthorRole .ASSISTANT , content = "" )
211+ msg_call .items .append (FunctionCallContent (id = "call1" , function_name = "myTool" , arguments = {"x" : 1 }))
212+ msgs .append (msg_call )
213+
214+ # Tool result as role=TOOL but with plain text content only
215+ msgs .append (ChatMessageContent (role = AuthorRole .TOOL , content = "Tool result here" ))
216+
217+ msgs .append (ChatMessageContent (role = AuthorRole .USER , content = "Thanks" ))
218+ msgs .append (ChatMessageContent (role = AuthorRole .ASSISTANT , content = "You are welcome" ))
219+
220+ idx = locate_safe_reduction_index (msgs , target_count = 3 , threshold_count = 0 )
221+ assert idx is not None
222+
223+ # The tool call (index 1) must be included if tool result (index 2) is included
224+ kept_indices = list (range (idx , len (msgs )))
225+ has_tool_role = any (msgs [i ].role == AuthorRole .TOOL for i in kept_indices )
226+ has_tool_call = any (
227+ any (isinstance (it , FunctionCallContent ) for it in msgs [i ].items ) for i in kept_indices
228+ )
229+
230+ if has_tool_role :
231+ assert has_tool_call , (
232+ f"Tool result at index 2 was kept but tool call at index 1 was dropped. "
233+ f"Kept indices: { kept_indices } , reduction index: { idx } "
234+ )
0 commit comments