From cf9cc4b39e7063a437d74ed57c3e84551081a4f1 Mon Sep 17 00:00:00 2001 From: Julio Cesar Suastegui Date: Fri, 27 Mar 2026 13:47:16 -0600 Subject: [PATCH] Fix infinite loop in fix_message_list when assistant has multiple tool_calls When an assistant message has multiple tool_calls and the tool responses arrive out of order, the second pass of fix_message_list enters an infinite loop. This happens because the validity check only looks at the immediately preceding message (which may be a sibling tool response) rather than walking backward past sibling tool messages to find the parent assistant message. The fix walks backward past sibling tool messages to locate the nearest assistant message before checking whether the sequence is valid. This correctly handles the case where multiple tool responses follow the same assistant message. Fixes #410 --- src/cai/util.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/cai/util.py b/src/cai/util.py index b5acca68c..85a2c3e99 100644 --- a/src/cai/util.py +++ b/src/cai/util.py @@ -1244,9 +1244,19 @@ def fix_message_list(messages): # pylint: disable=R0914,R0915,R0912 # If this isn't the first message, check if the previous message is a matching assistant message if i > 0: - prev_msg = processed_messages[i - 1] - - # Check if the previous message is an assistant message with matching tool_call_id + # Walk backward past sibling tool messages to find the nearest + # assistant. This avoids an infinite loop when an assistant has + # multiple tool_calls and their responses arrive out of order: + # the previous message may be a sibling tool response rather + # than the parent assistant message, which is still valid. + k = i - 1 + while k >= 0 and processed_messages[k].get("role") == "tool": + k -= 1 + + prev_msg = processed_messages[k] if k >= 0 else {} + + # Check if the nearest non-tool ancestor is an assistant message + # with a matching tool_call_id is_valid_sequence = ( prev_msg.get("role") == "assistant" and prev_msg.get("tool_calls")