@@ -99,6 +99,11 @@ public static partial class AIFunctionFactory
9999 /// <para>
100100 /// By default, return values are serialized to <see cref="JsonElement"/> using <paramref name="options"/>'s
101101 /// <see cref="AIFunctionFactoryOptions.SerializerOptions"/> if provided, or else using <see cref="AIJsonUtilities.DefaultOptions"/>.
102+ /// However, return values whose declared type is <see cref="AIContent"/>, a derived type of <see cref="AIContent"/>, or
103+ /// any type assignable from <see cref="IEnumerable{AIContent}"/> (e.g. <c>AIContent[]</c>, <c>List<AIContent></c>) are
104+ /// special-cased and are not serialized: the created function returns the original instance(s) directly to enable
105+ /// callers (such as an <c>IChatClient</c>) to perform type tests and implement specialized handling. If
106+ /// <see cref="AIFunctionFactoryOptions.MarshalResult"/> is supplied, that delegate governs the behavior instead.
102107 /// Handling of return values can be overridden via <see cref="AIFunctionFactoryOptions.MarshalResult"/>.
103108 /// </para>
104109 /// </remarks>
@@ -172,7 +177,9 @@ public static AIFunction Create(Delegate method, AIFunctionFactoryOptions? optio
172177 /// </para>
173178 /// <para>
174179 /// Return values are serialized to <see cref="JsonElement"/> using <paramref name="serializerOptions"/> if provided,
175- /// or else using <see cref="AIJsonUtilities.DefaultOptions"/>.
180+ /// or else using <see cref="AIJsonUtilities.DefaultOptions"/>. However, return values whose declared type is <see cref="AIContent"/>, a
181+ /// derived type of <see cref="AIContent"/>, or any type assignable from <see cref="IEnumerable{AIContent}"/> are not serialized;
182+ /// they are returned as-is to facilitate specialized handling.
176183 /// </para>
177184 /// </remarks>
178185 /// <exception cref="ArgumentNullException"><paramref name="method"/> is <see langword="null"/>.</exception>
@@ -262,6 +269,8 @@ public static AIFunction Create(Delegate method, string? name = null, string? de
262269 /// <para>
263270 /// By default, return values are serialized to <see cref="JsonElement"/> using <paramref name="options"/>'s
264271 /// <see cref="AIFunctionFactoryOptions.SerializerOptions"/> if provided, or else using <see cref="AIJsonUtilities.DefaultOptions"/>.
272+ /// However, return values whose declared type is <see cref="AIContent"/>, a derived type of <see cref="AIContent"/>, or
273+ /// any type assignable from <see cref="IEnumerable{AIContent}"/> are not serialized and are instead returned directly.
265274 /// Handling of return values can be overridden via <see cref="AIFunctionFactoryOptions.MarshalResult"/>.
266275 /// </para>
267276 /// </remarks>
@@ -345,7 +354,9 @@ public static AIFunction Create(MethodInfo method, object? target, AIFunctionFac
345354 /// </para>
346355 /// <para>
347356 /// Return values are serialized to <see cref="JsonElement"/> using <paramref name="serializerOptions"/> if provided,
348- /// or else using <see cref="AIJsonUtilities.DefaultOptions"/>.
357+ /// or else using <see cref="AIJsonUtilities.DefaultOptions"/>. However, return values whose declared type is <see cref="AIContent"/>, a
358+ /// derived type of <see cref="AIContent"/>, or any type assignable from <see cref="IEnumerable{AIContent}"/> are returned
359+ /// without serialization to enable specialized handling.
349360 /// </para>
350361 /// </remarks>
351362 /// <exception cref="ArgumentNullException"><paramref name="method"/> is <see langword="null"/>.</exception>
@@ -448,6 +459,8 @@ public static AIFunction Create(MethodInfo method, object? target, string? name
448459 /// <para>
449460 /// By default, return values are serialized to <see cref="JsonElement"/> using <paramref name="options"/>'s
450461 /// <see cref="AIFunctionFactoryOptions.SerializerOptions"/> if provided, or else using <see cref="AIJsonUtilities.DefaultOptions"/>.
462+ /// However, return values whose declared type is <see cref="AIContent"/>, a derived type of <see cref="AIContent"/>, or any type
463+ /// assignable from <see cref="IEnumerable{AIContent}"/> are returned directly without serialization.
451464 /// Handling of return values can be overridden via <see cref="AIFunctionFactoryOptions.MarshalResult"/>.
452465 /// </para>
453466 /// </remarks>
@@ -720,7 +733,7 @@ private ReflectionAIFunctionDescriptor(DescriptorKey key, JsonSerializerOptions
720733 Description = key . Description ?? key . Method . GetCustomAttribute < DescriptionAttribute > ( inherit : true ) ? . Description ?? string . Empty ;
721734 JsonSerializerOptions = serializerOptions ;
722735 ReturnJsonSchema = returnType is null || key . ExcludeResultSchema ? null : AIJsonUtilities . CreateJsonSchema (
723- returnType ,
736+ NormalizeReturnType ( returnType , serializerOptions ) ,
724737 description : key . Method . ReturnParameter . GetCustomAttribute < DescriptionAttribute > ( inherit : true ) ? . Description ,
725738 serializerOptions : serializerOptions ,
726739 inferenceOptions : schemaOptions ) ;
@@ -978,6 +991,7 @@ static void ThrowNullServices(string parameterName) =>
978991 MethodInfo taskResultGetter = GetMethodFromGenericMethodDefinition ( returnType , _taskGetResult ) ;
979992 returnType = taskResultGetter . ReturnType ;
980993
994+ // If a MarshalResult delegate is provided, use it.
981995 if ( marshalResult is not null )
982996 {
983997 return async ( taskObj , cancellationToken ) =>
@@ -988,6 +1002,18 @@ static void ThrowNullServices(string parameterName) =>
9881002 } ;
9891003 }
9901004
1005+ // Special-case AIContent results to not be serialized, so that IChatClients can type test and handle them
1006+ // specially, such as by returning content to the model/service in a manner appropriate to the content type.
1007+ if ( IsAIContentRelatedType ( returnType ) )
1008+ {
1009+ return async ( taskObj , cancellationToken ) =>
1010+ {
1011+ await ( ( Task ) ThrowIfNullResult ( taskObj ) ) . ConfigureAwait ( true ) ;
1012+ return ReflectionInvoke ( taskResultGetter , taskObj , null ) ;
1013+ } ;
1014+ }
1015+
1016+ // For everything else, just serialize the result as-is.
9911017 returnTypeInfo = serializerOptions . GetTypeInfo ( returnType ) ;
9921018 return async ( taskObj , cancellationToken ) =>
9931019 {
@@ -1004,6 +1030,7 @@ static void ThrowNullServices(string parameterName) =>
10041030 MethodInfo asTaskResultGetter = GetMethodFromGenericMethodDefinition ( valueTaskAsTask . ReturnType , _taskGetResult ) ;
10051031 returnType = asTaskResultGetter . ReturnType ;
10061032
1033+ // If a MarshalResult delegate is provided, use it.
10071034 if ( marshalResult is not null )
10081035 {
10091036 return async ( taskObj , cancellationToken ) =>
@@ -1015,6 +1042,19 @@ static void ThrowNullServices(string parameterName) =>
10151042 } ;
10161043 }
10171044
1045+ // Special-case AIContent results to not be serialized, so that IChatClients can type test and handle them
1046+ // specially, such as by returning content to the model/service in a manner appropriate to the content type.
1047+ if ( IsAIContentRelatedType ( returnType ) )
1048+ {
1049+ return async ( taskObj , cancellationToken ) =>
1050+ {
1051+ var task = ( Task ) ReflectionInvoke ( valueTaskAsTask , ThrowIfNullResult ( taskObj ) , null ) ! ;
1052+ await task . ConfigureAwait ( true ) ;
1053+ return ReflectionInvoke ( asTaskResultGetter , task , null ) ;
1054+ } ;
1055+ }
1056+
1057+ // For everything else, just serialize the result as-is.
10181058 returnTypeInfo = serializerOptions . GetTypeInfo ( returnType ) ;
10191059 return async ( taskObj , cancellationToken ) =>
10201060 {
@@ -1026,13 +1066,21 @@ static void ThrowNullServices(string parameterName) =>
10261066 }
10271067 }
10281068
1029- // For everything else, just serialize the result as-is .
1069+ // If a MarshalResult delegate is provided, use it .
10301070 if ( marshalResult is not null )
10311071 {
10321072 Type returnTypeCopy = returnType ;
10331073 return ( result , cancellationToken ) => marshalResult ( result , returnTypeCopy , cancellationToken ) ;
10341074 }
10351075
1076+ // Special-case AIContent results to not be serialized, so that IChatClients can type test and handle them
1077+ // specially, such as by returning content to the model/service in a manner appropriate to the content type.
1078+ if ( IsAIContentRelatedType ( returnType ) )
1079+ {
1080+ return static ( result , _ ) => new ValueTask < object ? > ( result ) ;
1081+ }
1082+
1083+ // For everything else, just serialize the result as-is.
10361084 returnTypeInfo = serializerOptions . GetTypeInfo ( returnType ) ;
10371085 return ( result , cancellationToken ) => SerializeResultAsync ( result , returnTypeInfo , cancellationToken ) ;
10381086
@@ -1069,6 +1117,41 @@ private static MethodInfo GetMethodFromGenericMethodDefinition(Type specializedT
10691117#endif
10701118 }
10711119
1120+ private static bool IsAIContentRelatedType ( Type type ) =>
1121+ typeof ( AIContent ) . IsAssignableFrom ( type ) ||
1122+ typeof ( IEnumerable < AIContent > ) . IsAssignableFrom ( type ) ;
1123+
1124+ private static Type NormalizeReturnType ( Type type , JsonSerializerOptions ? options )
1125+ {
1126+ options ??= AIJsonUtilities . DefaultOptions ;
1127+
1128+ if ( options == AIJsonUtilities . DefaultOptions && ! options . TryGetTypeInfo ( type , out _ ) )
1129+ {
1130+ // GetTypeInfo is not polymorphic, so attempts to look up derived types will fail even if the
1131+ // base type is registered. In some cases, though, we can fall back to using interfaces
1132+ // we know we have contracts for in AIJsonUtilities.DefaultOptions where the semantics of using
1133+ // that interface will be reasonable. This should really only affect situations where
1134+ // reflection-based serialization is disabled.
1135+
1136+ if ( typeof ( IEnumerable < AIContent > ) . IsAssignableFrom ( type ) )
1137+ {
1138+ return typeof ( IEnumerable < AIContent > ) ;
1139+ }
1140+
1141+ if ( typeof ( IEnumerable < ChatMessage > ) . IsAssignableFrom ( type ) )
1142+ {
1143+ return typeof ( IEnumerable < ChatMessage > ) ;
1144+ }
1145+
1146+ if ( typeof ( IEnumerable < string > ) . IsAssignableFrom ( type ) )
1147+ {
1148+ return typeof ( IEnumerable < string > ) ;
1149+ }
1150+ }
1151+
1152+ return type ;
1153+ }
1154+
10721155 private record struct DescriptorKey(
10731156 MethodInfo Method ,
10741157 string ? Name ,
0 commit comments