@@ -354,6 +354,264 @@ public async Task FunctionInvokerDelegateOverridesHandlingAsync()
354354 await InvokeAndAssertStreamingAsync ( options , plan , configurePipeline : configure ) ;
355355 }
356356
357+ [ Theory ]
358+ [ InlineData ( false ) ]
359+ [ InlineData ( true ) ]
360+ public async Task FunctionReturningFunctionResultContentWithMatchingCallId_UsesItDirectly ( bool streaming )
361+ {
362+ FunctionResultContent ? returnedFrc = null ;
363+
364+ var options = new ChatOptions
365+ {
366+ Tools =
367+ [
368+ AIFunctionFactory . Create ( ( ) => "Result 1" , "Func1" ) ,
369+ ]
370+ } ;
371+
372+ using var innerClient = new TestChatClient
373+ {
374+ GetResponseAsyncCallback = ( msgs , opts , ct ) =>
375+ {
376+ var toolMessage = msgs . FirstOrDefault ( m => m . Role == ChatRole . Tool ) ;
377+ if ( toolMessage is null )
378+ {
379+ return Task . FromResult ( new ChatResponse (
380+ new ChatMessage ( ChatRole . Assistant , [ new FunctionCallContent ( "callId1" , "Func1" ) ] ) ) ) ;
381+ }
382+ else
383+ {
384+ return Task . FromResult ( new ChatResponse ( new ChatMessage ( ChatRole . Assistant , "done" ) ) ) ;
385+ }
386+ } ,
387+ GetStreamingResponseAsyncCallback = ( msgs , opts , ct ) =>
388+ {
389+ var toolMessage = msgs . FirstOrDefault ( m => m . Role == ChatRole . Tool ) ;
390+ if ( toolMessage is null )
391+ {
392+ return YieldAsync ( new ChatResponse (
393+ new ChatMessage ( ChatRole . Assistant , [ new FunctionCallContent ( "callId1" , "Func1" ) ] ) ) . ToChatResponseUpdates ( ) ) ;
394+ }
395+ else
396+ {
397+ return YieldAsync ( new ChatResponse ( new ChatMessage ( ChatRole . Assistant , "done" ) ) . ToChatResponseUpdates ( ) ) ;
398+ }
399+ }
400+ } ;
401+
402+ using var client = new FunctionInvokingChatClient ( innerClient )
403+ {
404+ FunctionInvoker = ( ctx , cancellationToken ) =>
405+ {
406+ returnedFrc = new FunctionResultContent ( ctx . CallContent . CallId , "Custom result from function" )
407+ {
408+ RawRepresentation = "CustomRaw"
409+ } ;
410+ return new ValueTask < object ? > ( returnedFrc ) ;
411+ }
412+ } ;
413+
414+ var messages = new List < ChatMessage >
415+ {
416+ new ChatMessage ( ChatRole . User , "hello" ) ,
417+ } ;
418+
419+ ChatResponse response ;
420+ if ( streaming )
421+ {
422+ response = await client . GetStreamingResponseAsync ( messages , options ) . ToChatResponseAsync ( ) ;
423+ }
424+ else
425+ {
426+ response = await client . GetResponseAsync ( messages , options ) ;
427+ }
428+
429+ // Verify that the FunctionResultContent was used directly (same reference)
430+ var toolMessage = response . Messages . First ( m => m . Role == ChatRole . Tool ) ;
431+ var capturedFrc = Assert . Single ( toolMessage . Contents . OfType < FunctionResultContent > ( ) ) ;
432+ Assert . Same ( returnedFrc , capturedFrc ) ;
433+ Assert . Equal ( "Custom result from function" , capturedFrc . Result ) ;
434+ Assert . Equal ( "CustomRaw" , capturedFrc . RawRepresentation ) ;
435+ Assert . Equal ( "callId1" , capturedFrc . CallId ) ;
436+ }
437+
438+ [ Theory ]
439+ [ InlineData ( false ) ]
440+ [ InlineData ( true ) ]
441+ public async Task FunctionReturningFunctionResultContentWithMismatchedCallId_WrapsIt ( bool streaming )
442+ {
443+ FunctionResultContent ? returnedFrc = null ;
444+
445+ var options = new ChatOptions
446+ {
447+ Tools =
448+ [
449+ AIFunctionFactory . Create ( ( ) => "Result 1" , "Func1" ) ,
450+ ]
451+ } ;
452+
453+ using var innerClient = new TestChatClient
454+ {
455+ GetResponseAsyncCallback = ( msgs , opts , ct ) =>
456+ {
457+ var toolMessage = msgs . FirstOrDefault ( m => m . Role == ChatRole . Tool ) ;
458+ if ( toolMessage is null )
459+ {
460+ return Task . FromResult ( new ChatResponse (
461+ new ChatMessage ( ChatRole . Assistant , [ new FunctionCallContent ( "callId1" , "Func1" ) ] ) ) ) ;
462+ }
463+ else
464+ {
465+ return Task . FromResult ( new ChatResponse ( new ChatMessage ( ChatRole . Assistant , "done" ) ) ) ;
466+ }
467+ } ,
468+ GetStreamingResponseAsyncCallback = ( msgs , opts , ct ) =>
469+ {
470+ var toolMessage = msgs . FirstOrDefault ( m => m . Role == ChatRole . Tool ) ;
471+ if ( toolMessage is null )
472+ {
473+ return YieldAsync ( new ChatResponse (
474+ new ChatMessage ( ChatRole . Assistant , [ new FunctionCallContent ( "callId1" , "Func1" ) ] ) ) . ToChatResponseUpdates ( ) ) ;
475+ }
476+ else
477+ {
478+ return YieldAsync ( new ChatResponse ( new ChatMessage ( ChatRole . Assistant , "done" ) ) . ToChatResponseUpdates ( ) ) ;
479+ }
480+ }
481+ } ;
482+
483+ using var client = new FunctionInvokingChatClient ( innerClient )
484+ {
485+ FunctionInvoker = ( ctx , cancellationToken ) =>
486+ {
487+ // Return a FunctionResultContent with a different CallId
488+ returnedFrc = new FunctionResultContent ( "differentCallId" , "Result from function" ) ;
489+ return new ValueTask < object ? > ( returnedFrc ) ;
490+ }
491+ } ;
492+
493+ var messages = new List < ChatMessage >
494+ {
495+ new ChatMessage ( ChatRole . User , "hello" ) ,
496+ } ;
497+
498+ ChatResponse response ;
499+ if ( streaming )
500+ {
501+ response = await client . GetStreamingResponseAsync ( messages , options ) . ToChatResponseAsync ( ) ;
502+ }
503+ else
504+ {
505+ response = await client . GetResponseAsync ( messages , options ) ;
506+ }
507+
508+ // Verify the result is wrapped - the outer FunctionResultContent has the correct CallId
509+ // and the inner one is reference-equal to what was returned
510+ var toolMessage = response . Messages . First ( m => m . Role == ChatRole . Tool ) ;
511+ var frc = Assert . Single ( toolMessage . Contents . OfType < FunctionResultContent > ( ) ) ;
512+ Assert . Equal ( "callId1" , frc . CallId ) ;
513+ Assert . Same ( returnedFrc , frc . Result ) ;
514+ var innerFrc = ( FunctionResultContent ) frc . Result ! ;
515+ Assert . Equal ( "differentCallId" , innerFrc . CallId ) ;
516+ Assert . Equal ( "Result from function" , innerFrc . Result ) ;
517+ }
518+
519+ [ Theory ]
520+ [ InlineData ( false ) ]
521+ [ InlineData ( true ) ]
522+ public async Task FunctionReturningDerivedFunctionResultContent_PropagatesInstanceToInnerClient ( bool streaming )
523+ {
524+ DerivedFunctionResultContent ? returnedFrc = null ;
525+
526+ var options = new ChatOptions
527+ {
528+ Tools =
529+ [
530+ AIFunctionFactory . Create ( ( ) => "Result 1" , "Func1" ) ,
531+ ]
532+ } ;
533+
534+ using var innerClient = new TestChatClient
535+ {
536+ GetResponseAsyncCallback = ( msgs , opts , ct ) =>
537+ {
538+ var toolMessage = msgs . FirstOrDefault ( m => m . Role == ChatRole . Tool ) ;
539+ if ( toolMessage is null )
540+ {
541+ return Task . FromResult ( new ChatResponse (
542+ new ChatMessage ( ChatRole . Assistant , [ new FunctionCallContent ( "callId1" , "Func1" ) ] ) ) ) ;
543+ }
544+ else
545+ {
546+ return Task . FromResult ( new ChatResponse ( new ChatMessage ( ChatRole . Assistant , "done" ) ) ) ;
547+ }
548+ } ,
549+ GetStreamingResponseAsyncCallback = ( msgs , opts , ct ) =>
550+ {
551+ var toolMessage = msgs . FirstOrDefault ( m => m . Role == ChatRole . Tool ) ;
552+ if ( toolMessage is null )
553+ {
554+ return YieldAsync ( new ChatResponse (
555+ new ChatMessage ( ChatRole . Assistant , [ new FunctionCallContent ( "callId1" , "Func1" ) ] ) ) . ToChatResponseUpdates ( ) ) ;
556+ }
557+ else
558+ {
559+ return YieldAsync ( new ChatResponse ( new ChatMessage ( ChatRole . Assistant , "done" ) ) . ToChatResponseUpdates ( ) ) ;
560+ }
561+ }
562+ } ;
563+
564+ using var client = new FunctionInvokingChatClient ( innerClient )
565+ {
566+ FunctionInvoker = ( ctx , cancellationToken ) =>
567+ {
568+ // Return a derived FunctionResultContent
569+ returnedFrc = new DerivedFunctionResultContent ( ctx . CallContent . CallId , "Derived result" )
570+ {
571+ CustomProperty = "CustomValue"
572+ } ;
573+ return new ValueTask < object ? > ( returnedFrc ) ;
574+ }
575+ } ;
576+
577+ var messages = new List < ChatMessage >
578+ {
579+ new ChatMessage ( ChatRole . User , "hello" ) ,
580+ } ;
581+
582+ ChatResponse response ;
583+ if ( streaming )
584+ {
585+ response = await client . GetStreamingResponseAsync ( messages , options ) . ToChatResponseAsync ( ) ;
586+ }
587+ else
588+ {
589+ response = await client . GetResponseAsync ( messages , options ) ;
590+ }
591+
592+ // Verify that the derived FunctionResultContent instance was propagated to the inner client
593+ // and is reference-equal to what was returned
594+ var toolMessage = response . Messages . First ( m => m . Role == ChatRole . Tool ) ;
595+ var capturedFrc = Assert . Single ( toolMessage . Contents . OfType < FunctionResultContent > ( ) ) ;
596+ Assert . Same ( returnedFrc , capturedFrc ) ;
597+ Assert . IsType < DerivedFunctionResultContent > ( capturedFrc ) ;
598+ var derivedFrc = ( DerivedFunctionResultContent ) capturedFrc ;
599+ Assert . Equal ( "callId1" , derivedFrc . CallId ) ;
600+ Assert . Equal ( "Derived result" , derivedFrc . Result ) ;
601+ Assert . Equal ( "CustomValue" , derivedFrc . CustomProperty ) ;
602+ }
603+
604+ /// <summary>A derived FunctionResultContent for testing purposes.</summary>
605+ private sealed class DerivedFunctionResultContent : FunctionResultContent
606+ {
607+ public DerivedFunctionResultContent ( string callId , object ? result )
608+ : base ( callId , result )
609+ {
610+ }
611+
612+ public string ? CustomProperty { get ; set ; }
613+ }
614+
357615 [ Fact ]
358616 public async Task ContinuesWithSuccessfulCallsUntilMaximumIterations ( )
359617 {
0 commit comments