@@ -149,6 +149,93 @@ public async Task ToolCalls_EmptyMessageId_GeneratesFallbackParentMessageIdAsync
149149 "ParentMessageId should have a generated fallback for empty provider MessageId" ) ;
150150 }
151151
152+ /// <summary>
153+ /// Tool results are separate tool-role messages, so their fallback IDs must not
154+ /// collide with the assistant message that requested the tool call.
155+ /// </summary>
156+ [ Fact ]
157+ public async Task ToolResults_NullMessageId_GeneratesDistinctMessageIdAsync ( )
158+ {
159+ FunctionCallContent functionCall = new ( "call_abc123" , "GetWeather" )
160+ {
161+ Arguments = new Dictionary < string , object ? > { [ "location" ] = "San Francisco" }
162+ } ;
163+
164+ List < ChatResponseUpdate > providerUpdates =
165+ [
166+ new ChatResponseUpdate ( ChatRole . Assistant , "Checking the weather" ) ,
167+ new ChatResponseUpdate
168+ {
169+ Role = ChatRole . Assistant ,
170+ Contents = [ functionCall ]
171+ } ,
172+ new ChatResponseUpdate ( ChatRole . Tool , [ new FunctionResultContent ( "call_abc123" , "72F and sunny" ) ] )
173+ ] ;
174+
175+ List < BaseEvent > aguiEvents = [ ] ;
176+ await foreach ( BaseEvent evt in providerUpdates . ToAsyncEnumerableAsync ( )
177+ . AsAGUIEventStreamAsync ( "thread-1" , "run-1" , AGUIJsonSerializerContext . Default . Options ) )
178+ {
179+ aguiEvents . Add ( evt ) ;
180+ }
181+
182+ TextMessageStartEvent textStart = Assert . Single ( aguiEvents . OfType < TextMessageStartEvent > ( ) ) ;
183+ ToolCallStartEvent toolCallStart = Assert . Single ( aguiEvents . OfType < ToolCallStartEvent > ( ) ) ;
184+ ToolCallResultEvent toolCallResult = Assert . Single ( aguiEvents . OfType < ToolCallResultEvent > ( ) ) ;
185+
186+ Assert . Equal ( textStart . MessageId , toolCallStart . ParentMessageId ) ;
187+ Assert . Equal ( "call_abc123" , toolCallResult . ToolCallId ) ;
188+ Assert . False ( string . IsNullOrEmpty ( toolCallResult . MessageId ) ) ;
189+ Assert . NotEqual ( textStart . MessageId , toolCallResult . MessageId ) ;
190+ }
191+
192+ [ Fact ]
193+ public async Task ToolResults_WithTextContent_GeneratesDistinctMessageIdAsync ( )
194+ {
195+ FunctionCallContent functionCall = new ( "call_abc123" , "GetWeather" )
196+ {
197+ Arguments = new Dictionary < string , object ? > { [ "location" ] = "San Francisco" }
198+ } ;
199+
200+ List < ChatResponseUpdate > providerUpdates =
201+ [
202+ new ChatResponseUpdate ( ChatRole . Assistant , "Checking the weather" ) ,
203+ new ChatResponseUpdate
204+ {
205+ Role = ChatRole . Assistant ,
206+ Contents = [ functionCall ]
207+ } ,
208+ new ChatResponseUpdate
209+ {
210+ Role = ChatRole . Tool ,
211+ Contents =
212+ [
213+ new TextContent ( "Tool says: " ) ,
214+ new FunctionResultContent ( "call_abc123" , "72F and sunny" )
215+ ]
216+ }
217+ ] ;
218+
219+ List < BaseEvent > aguiEvents = [ ] ;
220+ await foreach ( BaseEvent evt in providerUpdates . ToAsyncEnumerableAsync ( )
221+ . AsAGUIEventStreamAsync ( "thread-1" , "run-1" , AGUIJsonSerializerContext . Default . Options ) )
222+ {
223+ aguiEvents . Add ( evt ) ;
224+ }
225+
226+ TextMessageStartEvent [ ] textStarts = aguiEvents . OfType < TextMessageStartEvent > ( ) . ToArray ( ) ;
227+ TextMessageContentEvent toolText = Assert . Single (
228+ aguiEvents . OfType < TextMessageContentEvent > ( ) ,
229+ content => content . Delta == "Tool says: " ) ;
230+ ToolCallStartEvent toolCallStart = Assert . Single ( aguiEvents . OfType < ToolCallStartEvent > ( ) ) ;
231+ ToolCallResultEvent toolCallResult = Assert . Single ( aguiEvents . OfType < ToolCallResultEvent > ( ) ) ;
232+
233+ Assert . Equal ( textStarts [ 0 ] . MessageId , toolCallStart . ParentMessageId ) ;
234+ Assert . NotEqual ( textStarts [ 0 ] . MessageId , toolCallResult . MessageId ) ;
235+ Assert . Equal ( toolCallResult . MessageId , toolText . MessageId ) ;
236+ Assert . Equal ( textStarts [ ^ 1 ] . MessageId , toolCallResult . MessageId ) ;
237+ }
238+
152239 /// <summary>
153240 /// When a provider properly sets MessageId (e.g., OpenAI), the AGUI pipeline
154241 /// produces valid events with correct messageId values.
0 commit comments