@@ -83,11 +83,13 @@ async def test_truncation_reducer_truncation(chat_messages):
8383 reducer = ChatHistoryTruncationReducer (target_count = 2 )
8484 reducer .messages = chat_messages
8585 result = await reducer .reduce ()
86- # We expect only 2 messages remain after truncation
86+ # We expect 2 messages: system message is preserved + 1 conversation message
87+ # (target_count=2 includes the system message, matching .NET SDK behavior)
8788 assert result is not None
8889 assert len (result ) == 2
89- # They should be the last 4 messages while tool result is not orphaned
90- assert result [0 ] == chat_messages [- 2 ]
90+ # System message should be first, followed by the last conversation message
91+ assert result [0 ] == chat_messages [0 ] # System message preserved
92+ assert result [0 ].role == AuthorRole .SYSTEM
9193 assert result [1 ] == chat_messages [- 1 ]
9294
9395
@@ -96,12 +98,89 @@ async def test_truncation_reducer_truncation_with_tools(chat_messages_with_tools
9698 reducer = ChatHistoryTruncationReducer (target_count = 3 , threshold_count = 0 )
9799 reducer .messages = chat_messages_with_tools
98100 result = await reducer .reduce ()
99- # We expect 4 messages remain after truncation
100- # Tool results are not orphaned, so we expect to keep them
101+ # We expect 3 messages: system message + last 2 conversation messages
102+ # (target_count=3 includes the system message, matching .NET SDK behavior)
101103 assert result is not None
102- assert len (result ) == 4
103- # They should be the last 4 messages
104- assert result [0 ] == chat_messages_with_tools [- 4 ]
105- assert result [1 ] == chat_messages_with_tools [- 3 ]
106- assert result [2 ] == chat_messages_with_tools [- 2 ]
107- assert result [3 ] == chat_messages_with_tools [- 1 ]
104+ assert len (result ) == 3
105+ # System message preserved, followed by last user-assistant pair
106+ assert result [0 ] == chat_messages_with_tools [0 ] # System message
107+ assert result [0 ].role == AuthorRole .SYSTEM
108+ assert result [1 ] == chat_messages_with_tools [- 2 ] # User message 2
109+ assert result [2 ] == chat_messages_with_tools [- 1 ] # Assistant message 2
110+
111+
112+ async def test_truncation_preserves_system_message ():
113+ """Verify that the system message is preserved after truncation (issue #12612)."""
114+ reducer = ChatHistoryTruncationReducer (
115+ target_count = 2 ,
116+ system_message = "a system message" ,
117+ )
118+ reducer .add_message (ChatMessageContent (role = AuthorRole .USER , content = "user prompt 1" ))
119+ reducer .add_message (ChatMessageContent (role = AuthorRole .ASSISTANT , content = "response 1" ))
120+ reducer .add_message (ChatMessageContent (role = AuthorRole .USER , content = "user prompt 2" ))
121+ reducer .add_message (ChatMessageContent (role = AuthorRole .ASSISTANT , content = "response 2" ))
122+ reducer .add_message (ChatMessageContent (role = AuthorRole .USER , content = "user prompt 3" ))
123+ reducer .add_message (ChatMessageContent (role = AuthorRole .ASSISTANT , content = "response 3" ))
124+ reducer .add_message (ChatMessageContent (role = AuthorRole .USER , content = "user prompt 4" ))
125+ reducer .add_message (ChatMessageContent (role = AuthorRole .ASSISTANT , content = "response 4" ))
126+
127+ result = await reducer .reduce ()
128+ assert result is not None
129+
130+ # System message must be present after reduction
131+ roles = [msg .role for msg in result .messages ]
132+ assert AuthorRole .SYSTEM in roles , "System message was deleted by the history reducer"
133+ assert result .messages [0 ].role == AuthorRole .SYSTEM
134+ assert result .messages [0 ].content == "a system message"
135+
136+
137+ async def test_truncation_preserves_developer_message ():
138+ """Verify that developer messages are preserved after truncation."""
139+ msgs = [
140+ ChatMessageContent (role = AuthorRole .DEVELOPER , content = "Developer instructions." ),
141+ ChatMessageContent (role = AuthorRole .USER , content = "User message 1" ),
142+ ChatMessageContent (role = AuthorRole .ASSISTANT , content = "Assistant message 1" ),
143+ ChatMessageContent (role = AuthorRole .USER , content = "User message 2" ),
144+ ChatMessageContent (role = AuthorRole .ASSISTANT , content = "Assistant message 2" ),
145+ ]
146+ reducer = ChatHistoryTruncationReducer (target_count = 2 )
147+ reducer .messages = msgs
148+ result = await reducer .reduce ()
149+ assert result is not None
150+ assert len (result ) == 2
151+ # Developer message should be first
152+ assert result [0 ].role == AuthorRole .DEVELOPER
153+ assert result [0 ].content == "Developer instructions."
154+
155+
156+ async def test_truncation_target_count_1_with_system_message ():
157+ """Verify target_count=1 with system message does not crash (edge case from review)."""
158+ reducer = ChatHistoryTruncationReducer (target_count = 1 , system_message = "System prompt" )
159+ reducer .add_message (ChatMessageContent (role = AuthorRole .USER , content = "Hello" ))
160+ reducer .add_message (ChatMessageContent (role = AuthorRole .ASSISTANT , content = "Hi" ))
161+ reducer .add_message (ChatMessageContent (role = AuthorRole .USER , content = "How are you?" ))
162+ reducer .add_message (ChatMessageContent (role = AuthorRole .ASSISTANT , content = "Good" ))
163+
164+ # Should not crash. Either returns None (no reduction possible)
165+ # or returns just the system message.
166+ result = await reducer .reduce ()
167+ if result is not None :
168+ # System message must be preserved
169+ assert any (m .role == AuthorRole .SYSTEM for m in result .messages )
170+
171+
172+ async def test_truncation_without_system_message ():
173+ """Verify truncation works correctly when there is no system message."""
174+ msgs = [
175+ ChatMessageContent (role = AuthorRole .USER , content = "User message 1" ),
176+ ChatMessageContent (role = AuthorRole .ASSISTANT , content = "Assistant message 1" ),
177+ ChatMessageContent (role = AuthorRole .USER , content = "User message 2" ),
178+ ChatMessageContent (role = AuthorRole .ASSISTANT , content = "Assistant message 2" ),
179+ ]
180+ reducer = ChatHistoryTruncationReducer (target_count = 2 )
181+ reducer .messages = msgs
182+ result = await reducer .reduce ()
183+ assert result is not None
184+ assert len (result ) == 2
185+ assert result [0 ] == msgs [- 2 ]
186+ assert result [1 ] == msgs [- 1 ]
0 commit comments