Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -220,5 +220,12 @@ def extract_range(

@experimental
def contains_function_call_or_result(msg: ChatMessageContent) -> bool:
"""Return True if the message has any function call or function result."""
"""Return True if the message has any function call or function result.

Also returns True for TOOL role messages, which are always responses to
a preceding assistant message with tool_calls and must not be separated
from it.
"""
if msg.role == AuthorRole.TOOL:
return True
return any(isinstance(item, (FunctionCallContent, FunctionResultContent)) for item in msg.items)
38 changes: 38 additions & 0 deletions python/tests/unit/contents/test_chat_history_reducer_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,41 @@ def test_locate_safe_reduction_index_high_offset(chat_messages_with_pairs):
else:
# It's fine if it returns None, meaning no valid safe reduction was found.
pass


def test_locate_safe_reduction_index_tool_role_without_function_result_content():
"""Regression test: TOOL role messages without FunctionResultContent in items
must still be recognized as part of a tool call/result pair.

This prevents orphaning tool results when the TOOL message only contains
text content (no FunctionResultContent item).
"""
msgs = [
ChatMessageContent(role=AuthorRole.USER, content="Hello"),
]
# Assistant with tool call
msg_call = ChatMessageContent(role=AuthorRole.ASSISTANT, content="")
msg_call.items.append(FunctionCallContent(id="call1", function_name="myTool", arguments={"x": 1}))
msgs.append(msg_call)

# Tool result as role=TOOL but with plain text content only
msgs.append(ChatMessageContent(role=AuthorRole.TOOL, content="Tool result here"))

msgs.append(ChatMessageContent(role=AuthorRole.USER, content="Thanks"))
msgs.append(ChatMessageContent(role=AuthorRole.ASSISTANT, content="You are welcome"))

idx = locate_safe_reduction_index(msgs, target_count=3, threshold_count=0)
assert idx is not None

# The tool call (index 1) must be included if tool result (index 2) is included
kept_indices = list(range(idx, len(msgs)))
has_tool_role = any(msgs[i].role == AuthorRole.TOOL for i in kept_indices)
has_tool_call = any(
any(isinstance(it, FunctionCallContent) for it in msgs[i].items) for i in kept_indices
)

if has_tool_role:
assert has_tool_call, (
f"Tool result at index 2 was kept but tool call at index 1 was dropped. "
f"Kept indices: {kept_indices}, reduction index: {idx}"
)