@@ -195,78 +195,210 @@ public void KernelFunctionsCloneReturnsCorrectClone()
195195 }
196196
197197 [ Fact ]
198- public void FunctionChoiceBehaviorAutoConvertsToAutoInvokeKernelFunctions ( )
198+ public void FunctionChoiceBehaviorAutoConvertsToEnabledFunctionsWithAllKernelFunctions ( )
199199 {
200200 // Arrange
201+ var kernel = CreateKernelWithFunctions ( "FunctionA" , "FunctionB" ) ;
201202 var settings = new GeminiPromptExecutionSettings
202203 {
203204 FunctionChoiceBehavior = FunctionChoiceBehavior . Auto ( )
204205 } ;
205206
206207 // Act
207- var converted = GeminiPromptExecutionSettings . FromExecutionSettings ( settings ) ;
208+ var converted = GeminiPromptExecutionSettings . FromExecutionSettings ( settings , kernel ) ;
208209
209210 // Assert
210211 Assert . NotNull ( converted . ToolCallBehavior ) ;
211- Assert . IsType < GeminiToolCallBehavior . KernelFunctions > ( converted . ToolCallBehavior ) ;
212+ var enabledFunctions = Assert . IsType < GeminiToolCallBehavior . EnabledFunctions > ( converted . ToolCallBehavior ) ;
212213 Assert . True ( converted . ToolCallBehavior . MaximumAutoInvokeAttempts > 0 ) ;
214+ // The provided set is validated against the requested functions.
215+ Assert . False ( GetAllowAnyRequestedKernelFunction ( converted . ToolCallBehavior ) ) ;
216+ // Auto continues to provide the functions across follow-up requests.
217+ Assert . Equal ( int . MaxValue , converted . ToolCallBehavior . MaximumUseAttempts ) ;
218+ Assert . Equal ( new [ ] { $ "TestPlugin{ GeminiFunction . NameSeparator } FunctionA", $ "TestPlugin{ GeminiFunction . NameSeparator } FunctionB" } , GetAdvertisedFunctionNames ( enabledFunctions , kernel ) ) ;
213219 }
214220
215221 [ Fact ]
216- public void FunctionChoiceBehaviorAutoWithNoAutoInvokeConvertsToEnableKernelFunctions ( )
222+ public void FunctionChoiceBehaviorAutoWithNoAutoInvokeConvertsToEnabledFunctionsWithoutAutoInvoke ( )
217223 {
218224 // Arrange
225+ var kernel = CreateKernelWithFunctions ( "FunctionA" ) ;
219226 var settings = new GeminiPromptExecutionSettings
220227 {
221228 FunctionChoiceBehavior = FunctionChoiceBehavior . Auto ( autoInvoke : false )
222229 } ;
223230
224231 // Act
225- var converted = GeminiPromptExecutionSettings . FromExecutionSettings ( settings ) ;
232+ var converted = GeminiPromptExecutionSettings . FromExecutionSettings ( settings , kernel ) ;
226233
227234 // Assert
228235 Assert . NotNull ( converted . ToolCallBehavior ) ;
229- Assert . IsType < GeminiToolCallBehavior . KernelFunctions > ( converted . ToolCallBehavior ) ;
236+ Assert . IsType < GeminiToolCallBehavior . EnabledFunctions > ( converted . ToolCallBehavior ) ;
230237 Assert . Equal ( 0 , converted . ToolCallBehavior . MaximumAutoInvokeAttempts ) ;
231238 }
232239
233240 [ Fact ]
234- public void FunctionChoiceBehaviorRequiredConvertsToAutoInvokeKernelFunctions ( )
241+ public void FunctionChoiceBehaviorRequiredConvertsToEnabledFunctions ( )
235242 {
236243 // Arrange
244+ var kernel = CreateKernelWithFunctions ( "FunctionA" ) ;
237245 var settings = new GeminiPromptExecutionSettings
238246 {
239247 FunctionChoiceBehavior = FunctionChoiceBehavior . Required ( )
240248 } ;
241249
242250 // Act
243- var converted = GeminiPromptExecutionSettings . FromExecutionSettings ( settings ) ;
251+ var converted = GeminiPromptExecutionSettings . FromExecutionSettings ( settings , kernel ) ;
244252
245253 // Assert
246254 Assert . NotNull ( converted . ToolCallBehavior ) ;
247- Assert . IsType < GeminiToolCallBehavior . KernelFunctions > ( converted . ToolCallBehavior ) ;
248- Assert . True ( converted . ToolCallBehavior . MaximumAutoInvokeAttempts > 0 ) ;
255+ Assert . IsType < GeminiToolCallBehavior . EnabledFunctions > ( converted . ToolCallBehavior ) ;
256+ Assert . False ( GetAllowAnyRequestedKernelFunction ( converted . ToolCallBehavior ) ) ;
257+ // Required stops providing the functions after the first request so the model is not
258+ // repeatedly forced to call them on follow-up iterations. Auto-invocation is capped to the same
259+ // value so it does not outlast the advertised tools (MaximumUseAttempts >= MaximumAutoInvokeAttempts).
260+ Assert . Equal ( 1 , converted . ToolCallBehavior . MaximumUseAttempts ) ;
261+ Assert . Equal ( 1 , converted . ToolCallBehavior . MaximumAutoInvokeAttempts ) ;
262+ Assert . True ( converted . ToolCallBehavior . MaximumUseAttempts >= converted . ToolCallBehavior . MaximumAutoInvokeAttempts ) ;
249263 }
250264
251265 [ Fact ]
252- public void FunctionChoiceBehaviorNoneConvertsToEnableKernelFunctions ( )
266+ public void FunctionChoiceBehaviorNoneConvertsToEnabledFunctionsWithoutAutoInvoke ( )
253267 {
254268 // Arrange
269+ var kernel = CreateKernelWithFunctions ( "FunctionA" ) ;
255270 var settings = new GeminiPromptExecutionSettings
256271 {
257272 FunctionChoiceBehavior = FunctionChoiceBehavior . None ( )
258273 } ;
259274
260275 // Act
261- var converted = GeminiPromptExecutionSettings . FromExecutionSettings ( settings ) ;
276+ var converted = GeminiPromptExecutionSettings . FromExecutionSettings ( settings , kernel ) ;
262277
263278 // Assert
264279 Assert . NotNull ( converted . ToolCallBehavior ) ;
265- Assert . IsType < GeminiToolCallBehavior . KernelFunctions > ( converted . ToolCallBehavior ) ;
280+ Assert . IsType < GeminiToolCallBehavior . EnabledFunctions > ( converted . ToolCallBehavior ) ;
266281 // None behavior doesn't auto-invoke
267282 Assert . Equal ( 0 , converted . ToolCallBehavior . MaximumAutoInvokeAttempts ) ;
268283 }
269284
285+ [ Fact ]
286+ public void FunctionChoiceBehaviorAutoWithEmptyFunctionListDisablesFunctionCalling ( )
287+ {
288+ // Arrange
289+ // An empty function list is documented as being equivalent to disabling function calling.
290+ var kernel = CreateKernelWithFunctions ( "FunctionA" , "FunctionB" ) ;
291+ var settings = new GeminiPromptExecutionSettings
292+ {
293+ FunctionChoiceBehavior = FunctionChoiceBehavior . Auto ( functions : [ ] , autoInvoke : true )
294+ } ;
295+
296+ // Act
297+ var converted = GeminiPromptExecutionSettings . FromExecutionSettings ( settings , kernel ) ;
298+
299+ // Assert
300+ Assert . Null ( converted . ToolCallBehavior ) ;
301+ }
302+
303+ [ Fact ]
304+ public void FunctionChoiceBehaviorAutoWithSubsetProvidesOnlyThatSubset ( )
305+ {
306+ // Arrange
307+ var kernel = CreateKernelWithFunctions ( "First" , "Second" ) ;
308+ var first = kernel . Plugins . GetFunction ( "TestPlugin" , "First" ) ;
309+ var settings = new GeminiPromptExecutionSettings
310+ {
311+ FunctionChoiceBehavior = FunctionChoiceBehavior . Auto ( functions : [ first ] , autoInvoke : true )
312+ } ;
313+
314+ // Act
315+ var converted = GeminiPromptExecutionSettings . FromExecutionSettings ( settings , kernel ) ;
316+
317+ // Assert
318+ Assert . NotNull ( converted . ToolCallBehavior ) ;
319+ var enabledFunctions = Assert . IsType < GeminiToolCallBehavior . EnabledFunctions > ( converted . ToolCallBehavior ) ;
320+ Assert . False ( GetAllowAnyRequestedKernelFunction ( converted . ToolCallBehavior ) ) ;
321+ // Only the function specified in the behavior is provided to the model.
322+ Assert . Equal ( new [ ] { $ "TestPlugin{ GeminiFunction . NameSeparator } First" } , GetAdvertisedFunctionNames ( enabledFunctions , kernel ) ) ;
323+ }
324+
325+ [ Fact ]
326+ public void FunctionChoiceBehaviorConfigurationErrorsArePropagated ( )
327+ {
328+ // Arrange - auto-invocation requires the declared function to exist in the kernel; when it does
329+ // not, the behavior resolution throws and the error must surface rather than be swallowed.
330+ var kernelWithFunction = CreateKernelWithFunctions ( "First" ) ;
331+ var first = kernelWithFunction . Plugins . GetFunction ( "TestPlugin" , "First" ) ;
332+ var emptyKernel = Kernel . CreateBuilder ( ) . Build ( ) ;
333+ var settings = new GeminiPromptExecutionSettings
334+ {
335+ FunctionChoiceBehavior = FunctionChoiceBehavior . Auto ( functions : [ first ] , autoInvoke : true )
336+ } ;
337+
338+ // Act & Assert
339+ Assert . Throws < KernelException > ( ( ) => GeminiPromptExecutionSettings . FromExecutionSettings ( settings , emptyKernel ) ) ;
340+ }
341+
342+ [ Fact ]
343+ public void FromExecutionSettingsDoesNotMutateCallerSettingsWhenConvertingFunctionChoiceBehavior ( )
344+ {
345+ // Arrange
346+ var kernel = CreateKernelWithFunctions ( "FunctionA" ) ;
347+ var settings = new GeminiPromptExecutionSettings
348+ {
349+ FunctionChoiceBehavior = FunctionChoiceBehavior . Auto ( )
350+ } ;
351+
352+ // Act
353+ var converted = GeminiPromptExecutionSettings . FromExecutionSettings ( settings , kernel ) ;
354+
355+ // Assert - the caller's original settings object must remain untouched.
356+ Assert . NotSame ( settings , converted ) ;
357+ Assert . Null ( settings . ToolCallBehavior ) ;
358+ Assert . NotNull ( converted . ToolCallBehavior ) ;
359+ }
360+
361+ [ Fact ]
362+ public void FromExecutionSettingsWithFrozenSettingsDoesNotThrowWhenConvertingFunctionChoiceBehavior ( )
363+ {
364+ // Arrange
365+ var kernel = CreateKernelWithFunctions ( "FunctionA" ) ;
366+ var settings = new GeminiPromptExecutionSettings
367+ {
368+ FunctionChoiceBehavior = FunctionChoiceBehavior . Auto ( )
369+ } ;
370+ settings . Freeze ( ) ;
371+
372+ // Act - converting a frozen settings object must not attempt to mutate it.
373+ var converted = GeminiPromptExecutionSettings . FromExecutionSettings ( settings , kernel ) ;
374+
375+ // Assert
376+ Assert . NotNull ( converted . ToolCallBehavior ) ;
377+ Assert . IsType < GeminiToolCallBehavior . EnabledFunctions > ( converted . ToolCallBehavior ) ;
378+ }
379+
380+ [ Fact ]
381+ public void FromExecutionSettingsReResolvesFunctionChoiceBehaviorPerKernel ( )
382+ {
383+ // Arrange - a single reused settings object must reflect each request's own kernel.
384+ var settings = new GeminiPromptExecutionSettings
385+ {
386+ FunctionChoiceBehavior = FunctionChoiceBehavior . Auto ( )
387+ } ;
388+ var kernelA = CreateKernelWithFunctions ( "FunctionA" ) ;
389+ var kernelB = CreateKernelWithFunctions ( "FunctionB" ) ;
390+
391+ // Act
392+ var convertedA = GeminiPromptExecutionSettings . FromExecutionSettings ( settings , kernelA ) ;
393+ var convertedB = GeminiPromptExecutionSettings . FromExecutionSettings ( settings , kernelB ) ;
394+
395+ // Assert - each request reflects its own kernel.
396+ var enabledA = Assert . IsType < GeminiToolCallBehavior . EnabledFunctions > ( convertedA . ToolCallBehavior ) ;
397+ var enabledB = Assert . IsType < GeminiToolCallBehavior . EnabledFunctions > ( convertedB . ToolCallBehavior ) ;
398+ Assert . Equal ( new [ ] { $ "TestPlugin{ GeminiFunction . NameSeparator } FunctionA" } , GetAdvertisedFunctionNames ( enabledA , kernelA ) ) ;
399+ Assert . Equal ( new [ ] { $ "TestPlugin{ GeminiFunction . NameSeparator } FunctionB" } , GetAdvertisedFunctionNames ( enabledB , kernelB ) ) ;
400+ }
401+
270402 [ Fact ]
271403 public void GeminiPromptExecutionSettingsWithNoFunctionChoiceBehaviorDoesNotSetToolCallBehavior ( )
272404 {
@@ -311,6 +443,32 @@ private static KernelPlugin GetTestPlugin()
311443 return KernelPluginFactory . CreateFromFunctions ( "MyPlugin" , [ function ] ) ;
312444 }
313445
446+ private static Kernel CreateKernelWithFunctions ( params string [ ] functionNames )
447+ {
448+ var functions = functionNames
449+ . Select ( name => KernelFunctionFactory . CreateFromMethod ( ( ) => "result" , name ) )
450+ . ToArray ( ) ;
451+
452+ var kernel = Kernel . CreateBuilder ( ) . Build ( ) ;
453+ kernel . Plugins . Add ( KernelPluginFactory . CreateFromFunctions ( "TestPlugin" , functions ) ) ;
454+ return kernel ;
455+ }
456+
457+ private static bool GetAllowAnyRequestedKernelFunction ( GeminiToolCallBehavior behavior )
458+ => behavior . AllowAnyRequestedKernelFunction ;
459+
460+ private static string [ ] GetAdvertisedFunctionNames ( GeminiToolCallBehavior behavior , Kernel kernel )
461+ {
462+ var request = new GeminiRequest ( ) ;
463+ behavior . ConfigureGeminiRequest ( kernel , request ) ;
464+ if ( request . Tools is null )
465+ {
466+ return [ ] ;
467+ }
468+
469+ return request . Tools [ 0 ] . Functions . Select ( f => f . Name ) . ToArray ( ) ;
470+ }
471+
314472 private static void AssertFunctions ( GeminiRequest request )
315473 {
316474 Assert . NotNull ( request . Tools ) ;
0 commit comments