Skip to content

Commit 38adf27

Browse files
giulio-leonegiulio-leone
authored andcommitted
Python: fix: ChatHistoryTruncationReducer orphans TOOL role messages
1 parent 86d7a49 commit 38adf27

2 files changed

Lines changed: 46 additions & 1 deletion

File tree

python/semantic_kernel/contents/history_reducer/chat_history_reducer_utils.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,5 +220,12 @@ def extract_range(
220220

221221
@experimental
222222
def contains_function_call_or_result(msg: ChatMessageContent) -> bool:
223-
"""Return True if the message has any function call or function result."""
223+
"""Return True if the message has any function call or function result.
224+
225+
Also returns True for TOOL role messages, which are always responses to
226+
a preceding assistant message with tool_calls and must not be separated
227+
from it.
228+
"""
229+
if msg.role == AuthorRole.TOOL:
230+
return True
224231
return any(isinstance(item, (FunctionCallContent, FunctionResultContent)) for item in msg.items)

python/tests/unit/contents/test_chat_history_reducer_utils.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)