< Summary

Information
Class: NexusLabs.Needlr.AgentFramework.Testing.ToolInvocationRunner
Assembly: NexusLabs.Needlr.AgentFramework.Testing
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework.Testing/ToolInvocationRunner.cs
Line coverage
75%
Covered lines: 113
Uncovered lines: 36
Coverable lines: 149
Total lines: 490
Line coverage: 75.8%
Branch coverage
61%
Covered branches: 32
Total branches: 52
Branch coverage: 61.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
.ctor(...)100%11100%
CreateFor(...)100%22100%
Create(...)100%22100%
get_IsGeneratedProviderAvailable()100%11100%
AssertGeneratedProviderAvailable()50%4225%
WithExecutionContext(...)100%11100%
WithWorkspace(...)100%11100%
WithWorkspace(...)100%11100%
LimitToTools(...)100%22100%
GetFunction(...)100%11100%
GetFunction(...)42.85%521442.3%
GetFunctionAllowingReflection(...)100%11100%
GetFunctionAllowingReflection(...)63.63%232288.46%
InvokeAsync(...)100%11100%
InvokeAsync(...)0%620%
InvokeAsync()50%4472.22%
BeginBootstrapScopeIfLimited()100%2271.42%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework.Testing/ToolInvocationRunner.cs

#LineLine coverage
 1using System.Diagnostics;
 2using System.Diagnostics.CodeAnalysis;
 3using System.Reflection;
 4
 5using Microsoft.Extensions.AI;
 6using Microsoft.Extensions.DependencyInjection;
 7
 8using NexusLabs.Needlr.AgentFramework.Context;
 9using NexusLabs.Needlr.AgentFramework.Workspace;
 10
 11namespace NexusLabs.Needlr.AgentFramework.Testing;
 12
 13/// <summary>
 14/// Test harness for invoking <c>[AgentFunction]</c>-decorated tool methods through their
 15/// source-generated <see cref="AIFunction"/> wrapper, with the same plumbing
 16/// <see cref="Microsoft.Extensions.AI.FunctionInvokingChatClient"/> uses in production.
 17/// </summary>
 18/// <remarks>
 19/// <para>
 20/// The runner exists to remove the boilerplate consumers face when testing tools they wrote
 21/// for Needlr's Agent Framework integration: build a service provider, register the tool, look
 22/// up the source-generated <see cref="IAIFunctionProvider"/>, find the right
 23/// <see cref="AIFunction"/> by name, build <see cref="AIFunctionArguments"/>, establish an
 24/// ambient <see cref="IAgentExecutionContext"/> so the tool can read
 25/// <see cref="IAgentExecutionContextAccessor.Current"/>, and invoke.
 26/// </para>
 27/// <para>
 28/// By default the runner resolves only via the source-generated <see cref="IAIFunctionProvider"/>
 29/// registered by the <c>[ModuleInitializer]</c> the Needlr Agent Framework generator emits in
 30/// the consuming assembly. This is the same path production agents take. To exercise the
 31/// reflection-based <see cref="AIFunctionFactory"/> path instead, call
 32/// <see cref="GetFunctionAllowingReflection{TTool}(string)"/> explicitly — that method carries
 33/// <see cref="RequiresUnreferencedCodeAttribute"/> annotations because it is incompatible with
 34/// NativeAOT.
 35/// </para>
 36/// <para>
 37/// Instances are immutable: each <c>With*</c> method returns a new runner with the configuration
 38/// applied. Each <see cref="InvokeAsync{TTool}(string, Action{AIFunctionArguments}?, CancellationToken)"/>
 39/// call creates a fresh <see cref="IServiceScope"/> (when an <see cref="IServiceScopeFactory"/>
 40/// is available) so tools with scoped dependencies behave correctly across invocations.
 41/// </para>
 42/// </remarks>
 43/// <example>
 44/// Minimal test, full bring-your-own service provider:
 45/// <code>
 46/// var sp = new ServiceCollection()
 47///     .AddAgentFrameworkAccessors()
 48///     .AddSingleton&lt;GrepTool&gt;()
 49///     .BuildServiceProvider();
 50///
 51/// var runner = new ToolInvocationRunner(sp)
 52///     .WithWorkspace(ws =&gt; ws.TryWriteFile("a.txt", "hi"));
 53///
 54/// var result = await runner.InvokeAsync&lt;GrepTool&gt;("grep_files", a =&gt;
 55/// {
 56///     a["pattern"] = "hi";
 57///     a["path"]    = "/";
 58/// });
 59///
 60/// result.AssertSuccess();
 61/// result.AssertResultContains("a.txt");
 62/// </code>
 63/// </example>
 64[DoNotAutoRegister]
 65public sealed class ToolInvocationRunner
 66{
 67    private readonly IServiceProvider _serviceProvider;
 68    private readonly Action<AgentExecutionContextBuilder>? _contextConfigurator;
 69    private readonly IReadOnlyList<Type>? _limitedToolTypes;
 70
 71    /// <summary>
 72    /// Creates a runner over an already-built <see cref="IServiceProvider"/>. Use this when the
 73    /// test fixture builds its own DI container (typical for Syringe-based test fixtures, or for
 74    /// tests that need to share a provider across multiple invocations).
 75    /// </summary>
 76    /// <param name="serviceProvider">
 77    /// Provider that has the tool type and its dependencies registered. The Needlr accessors
 78    /// (<see cref="IAgentExecutionContextAccessor"/>, <see cref="Diagnostics.IAgentDiagnosticsAccessor"/>)
 79    /// must also be registered — call
 80    /// <see cref="AgentFrameworkAccessorServiceCollectionExtensions.AddAgentFrameworkAccessors"/>
 81    /// or use the broader <c>UsingAgentFramework()</c> Syringe extension.
 82    /// </param>
 83    public ToolInvocationRunner(IServiceProvider serviceProvider)
 7784        : this(serviceProvider, contextConfigurator: null, limitedToolTypes: null)
 85    {
 7686    }
 87
 8688    private ToolInvocationRunner(
 8689        IServiceProvider serviceProvider,
 8690        Action<AgentExecutionContextBuilder>? contextConfigurator,
 8691        IReadOnlyList<Type>? limitedToolTypes)
 92    {
 8693        ArgumentNullException.ThrowIfNull(serviceProvider);
 8594        _serviceProvider = serviceProvider;
 8595        _contextConfigurator = contextConfigurator;
 8596        _limitedToolTypes = limitedToolTypes;
 8597    }
 98
 99    /// <summary>
 100    /// Creates a runner over a fresh service provider with the Needlr accessors and
 101    /// <typeparamref name="TTool"/> registered as a singleton. Additional services can be added
 102    /// via the optional <paramref name="configureServices"/> callback.
 103    /// </summary>
 104    /// <typeparam name="TTool">The tool class to register.</typeparam>
 105    /// <param name="configureServices">
 106    /// Optional hook to register additional dependencies (e.g. <c>IHttpClientFactory</c>, options,
 107    /// fakes for scoped services).
 108    /// </param>
 109    public static ToolInvocationRunner CreateFor<TTool>(
 110        Action<IServiceCollection>? configureServices = null)
 111        where TTool : class
 112    {
 71113        var services = new ServiceCollection().AddAgentFrameworkAccessors();
 71114        services.AddSingleton<TTool>();
 71115        configureServices?.Invoke(services);
 71116        return new ToolInvocationRunner(services.BuildServiceProvider());
 117    }
 118
 119    /// <summary>
 120    /// Creates a runner over a fresh service provider with only the Needlr accessors registered.
 121    /// Use this overload when registering multiple tools or when you want full control over the
 122    /// service collection setup.
 123    /// </summary>
 124    /// <param name="configureServices">
 125    /// Hook to register the tools and any dependencies they need.
 126    /// </param>
 127    public static ToolInvocationRunner Create(
 128        Action<IServiceCollection>? configureServices = null)
 129    {
 4130        var services = new ServiceCollection().AddAgentFrameworkAccessors();
 4131        configureServices?.Invoke(services);
 4132        return new ToolInvocationRunner(services.BuildServiceProvider());
 133    }
 134
 135    /// <summary>
 136    /// Whether a source-generated <see cref="IAIFunctionProvider"/> is currently registered with
 137    /// <see cref="AgentFrameworkGeneratedBootstrap"/>. Useful as a precondition assertion in
 138    /// tests that require the source generator to have run for at least one assembly in the
 139    /// process.
 140    /// </summary>
 141    /// <remarks>
 142    /// This property tells you only whether <em>any</em> generated provider exists globally; it
 143    /// does not tell you whether <em>your specific tool</em> is resolvable. Call
 144    /// <see cref="GetFunction{TTool}(string)"/> if you need to verify a specific function — the
 145    /// error message it throws when a type or method is missing is the canonical signal.
 146    /// </remarks>
 147    public bool IsGeneratedProviderAvailable
 59148        => AgentFrameworkGeneratedBootstrap.TryGetAIFunctionProvider(out _);
 149
 150    /// <summary>
 151    /// Throws when no source-generated <see cref="IAIFunctionProvider"/> is registered.
 152    /// Use this as a fail-fast guard in tests that depend on the generator output for at least
 153    /// one assembly in the process.
 154    /// </summary>
 155    /// <remarks>
 156    /// In practice this rarely throws because the Needlr Agent Framework assembly itself emits a
 157    /// (possibly empty) provider via <c>[ModuleInitializer]</c>, which registers as soon as
 158    /// <c>NexusLabs.Needlr.AgentFramework.dll</c> loads. The check is still useful as a sanity
 159    /// guard in environments where modules may not have initialized yet (e.g. some custom
 160    /// AssemblyLoadContext scenarios).
 161    /// </remarks>
 162    /// <exception cref="InvalidOperationException">
 163    /// Thrown when no generated provider is available, with guidance on how to enable it.
 164    /// </exception>
 165    public void AssertGeneratedProviderAvailable()
 166    {
 58167        if (!IsGeneratedProviderAvailable)
 168        {
 0169            throw new InvalidOperationException(
 0170                "No source-generated IAIFunctionProvider is registered. " +
 0171                "The Needlr Agent Framework source generator did not run for the assembly under test. " +
 0172                "Add a project reference to NexusLabs.Needlr.AgentFramework.Generators with " +
 0173                "OutputItemType=\"Analyzer\" and ReferenceOutputAssembly=\"false\", or call " +
 0174                "GetFunctionAllowingReflection<T>() to opt into the reflection fallback.");
 175        }
 58176    }
 177
 178    /// <summary>
 179    /// Returns a new runner that establishes an <see cref="IAgentExecutionContext"/> built by
 180    /// <paramref name="configure"/> for the duration of each <c>InvokeAsync</c> call.
 181    /// </summary>
 182    /// <remarks>
 183    /// Successive calls replace the previously-configured context (immutable copy semantics).
 184    /// </remarks>
 185    public ToolInvocationRunner WithExecutionContext(Action<AgentExecutionContextBuilder> configure)
 186    {
 5187        ArgumentNullException.ThrowIfNull(configure);
 5188        return new ToolInvocationRunner(_serviceProvider, configure, _limitedToolTypes);
 189    }
 190
 191    /// <summary>
 192    /// Convenience: returns a new runner that creates a fresh <see cref="InMemoryWorkspace"/>,
 193    /// runs <paramref name="seed"/> against it, and attaches it to the execution context.
 194    /// </summary>
 195    public ToolInvocationRunner WithWorkspace(Action<IWorkspace> seed)
 196    {
 2197        ArgumentNullException.ThrowIfNull(seed);
 3198        return WithExecutionContext(c => c.WithWorkspace(seed));
 199    }
 200
 201    /// <summary>
 202    /// Convenience: returns a new runner that attaches the supplied <paramref name="workspace"/>
 203    /// to the execution context.
 204    /// </summary>
 205    public ToolInvocationRunner WithWorkspace(IWorkspace workspace)
 206    {
 1207        ArgumentNullException.ThrowIfNull(workspace);
 2208        return WithExecutionContext(c => c.WithWorkspace(workspace));
 209    }
 210
 211    /// <summary>
 212    /// Returns a new runner that scopes
 213    /// <see cref="AgentFrameworkGeneratedBootstrap"/> to expose only
 214    /// <paramref name="toolTypes"/> during function resolution. Useful in consumer test
 215    /// projects that contain many <c>[AgentFunction]</c> types and want to assert behavior
 216    /// against a specific subset without disturbing other tests.
 217    /// </summary>
 218    /// <param name="toolTypes">
 219    /// The set of tool types visible to the source-generated provider during this runner's
 220    /// invocations. Must contain at least one type.
 221    /// </param>
 222    /// <exception cref="ArgumentException">
 223    /// Thrown when <paramref name="toolTypes"/> is empty.
 224    /// </exception>
 225    public ToolInvocationRunner LimitToTools(params Type[] toolTypes)
 226    {
 5227        ArgumentNullException.ThrowIfNull(toolTypes);
 5228        if (toolTypes.Length == 0)
 229        {
 1230            throw new ArgumentException(
 1231                "At least one tool type must be supplied. To clear a previous LimitToTools, " +
 1232                "construct a new runner instead.",
 1233                nameof(toolTypes));
 234        }
 4235        return new ToolInvocationRunner(_serviceProvider, _contextConfigurator, toolTypes);
 236    }
 237
 238    /// <summary>
 239    /// Resolves an <see cref="AIFunction"/> for <typeparamref name="TTool"/> by method name via
 240    /// the source-generated <see cref="IAIFunctionProvider"/>.
 241    /// </summary>
 242    /// <typeparam name="TTool">The tool class declaring the <c>[AgentFunction]</c> method.</typeparam>
 243    /// <param name="methodName">
 244    /// The function name as exposed to the LLM (defaults to the C# method name when the
 245    /// source generator emits the wrapper).
 246    /// </param>
 247    /// <exception cref="InvalidOperationException">
 248    /// Thrown when no generated provider is registered, or when no function with that name is
 249    /// found for the type.
 250    /// </exception>
 251    public AIFunction GetFunction<TTool>(string methodName) where TTool : class
 59252        => GetFunction(typeof(TTool), methodName);
 253
 254    /// <summary>
 255    /// Resolves an <see cref="AIFunction"/> for <paramref name="toolType"/> by method name via
 256    /// the source-generated <see cref="IAIFunctionProvider"/>.
 257    /// </summary>
 258    public AIFunction GetFunction(Type toolType, string methodName)
 259    {
 66260        ArgumentNullException.ThrowIfNull(toolType);
 65261        ArgumentException.ThrowIfNullOrWhiteSpace(methodName);
 262
 64263        using var bootstrapScope = BeginBootstrapScopeIfLimited();
 264
 64265        if (!AgentFrameworkGeneratedBootstrap.TryGetAIFunctionProvider(out var provider))
 266        {
 0267            throw new InvalidOperationException(
 0268                $"Cannot resolve [AgentFunction] '{methodName}' on '{toolType.Name}': " +
 0269                "no source-generated IAIFunctionProvider is registered. " +
 0270                "The Needlr Agent Framework source generator did not run for the assembly " +
 0271                $"containing '{toolType.Name}'. Either add a project reference to " +
 0272                "NexusLabs.Needlr.AgentFramework.Generators (Analyzer, no output assembly), " +
 0273                "or call GetFunctionAllowingReflection<T>() to opt into the reflection fallback.");
 274        }
 275
 64276        using var serviceScope = _serviceProvider.GetService<IServiceScopeFactory>()?.CreateScope();
 64277        var resolutionServices = serviceScope?.ServiceProvider ?? _serviceProvider;
 278
 64279        if (!provider.TryGetFunctions(toolType, resolutionServices, out var functions))
 280        {
 5281            throw new InvalidOperationException(
 5282                $"The source-generated IAIFunctionProvider has no functions for '{toolType.Name}'. " +
 5283                "Ensure the type is decorated with [AgentFunctionGroup] (or referenced by a " +
 5284                "[NeedlrAiAgent] FunctionTypes argument) so the generator picks it up.");
 285        }
 286
 140287        var function = functions.FirstOrDefault(f => f.Name == methodName);
 59288        if (function is null)
 289        {
 0290            var available = string.Join(", ", functions!.Select(f => $"'{f.Name}'"));
 0291            throw new InvalidOperationException(
 0292                $"No [AgentFunction] named '{methodName}' on '{toolType.Name}'. " +
 0293                $"Available functions on this type: {(available.Length == 0 ? "<none>" : available)}.");
 294        }
 295
 59296        return function;
 59297    }
 298
 299    /// <summary>
 300    /// Resolves an <see cref="AIFunction"/> for <typeparamref name="TTool"/> by method name,
 301    /// preferring the source-generated provider but falling back to reflection-based discovery
 302    /// via <see cref="AIFunctionFactory.Create(MethodInfo, object?, AIFunctionFactoryOptions?)"/>
 303    /// when the generator output is not available.
 304    /// </summary>
 305    /// <remarks>
 306    /// This method is incompatible with NativeAOT because the reflection branch dynamically
 307    /// generates marshalling code. Tests targeting AOT must use <see cref="GetFunction{TTool}(string)"/>
 308    /// instead.
 309    /// </remarks>
 310    [RequiresUnreferencedCode("Reflection-based AIFunction discovery requires unreferenced code access.")]
 311    [RequiresDynamicCode("Reflection-based AIFunction discovery requires dynamic code generation.")]
 312    public AIFunction GetFunctionAllowingReflection<TTool>(string methodName) where TTool : class
 3313        => GetFunctionAllowingReflection(typeof(TTool), methodName);
 314
 315    /// <summary>
 316    /// Resolves an <see cref="AIFunction"/> for <paramref name="toolType"/> by method name,
 317    /// preferring the source-generated provider but falling back to reflection-based discovery
 318    /// via <see cref="AIFunctionFactory.Create(MethodInfo, object?, AIFunctionFactoryOptions?)"/>
 319    /// when the generator output is not available.
 320    /// </summary>
 321    [RequiresUnreferencedCode("Reflection-based AIFunction discovery requires unreferenced code access.")]
 322    [RequiresDynamicCode("Reflection-based AIFunction discovery requires dynamic code generation.")]
 323    public AIFunction GetFunctionAllowingReflection(Type toolType, string methodName)
 324    {
 3325        ArgumentNullException.ThrowIfNull(toolType);
 3326        ArgumentException.ThrowIfNullOrWhiteSpace(methodName);
 327
 3328        using var bootstrapScope = BeginBootstrapScopeIfLimited();
 329
 3330        if (AgentFrameworkGeneratedBootstrap.TryGetAIFunctionProvider(out var provider))
 331        {
 3332            using var serviceScope = _serviceProvider.GetService<IServiceScopeFactory>()?.CreateScope();
 3333            var resolutionServices = serviceScope?.ServiceProvider ?? _serviceProvider;
 334
 3335            if (provider.TryGetFunctions(toolType, resolutionServices, out var generated))
 336            {
 0337                var generatedFn = generated.FirstOrDefault(f => f.Name == methodName);
 0338                if (generatedFn is not null)
 339                {
 0340                    return generatedFn;
 341                }
 342            }
 343        }
 344
 3345        var isStatic = toolType.IsAbstract && toolType.IsSealed;
 3346        var bindingFlags = isStatic
 3347            ? BindingFlags.Public | BindingFlags.Static
 3348            : BindingFlags.Public | BindingFlags.Instance;
 349
 3350        var method = toolType.GetMethods(bindingFlags)
 3351            .FirstOrDefault(m =>
 7352                m.Name == methodName &&
 7353                m.IsDefined(typeof(AgentFunctionAttribute), inherit: true))
 3354            ?? throw new InvalidOperationException(
 3355                $"Reflection fallback could not find a method named '{methodName}' decorated " +
 3356                $"with [AgentFunction] on '{toolType.Name}'.");
 357
 2358        object? instance = isStatic
 2359            ? null
 2360            : ActivatorUtilities.CreateInstance(_serviceProvider, toolType);
 361
 2362        return AIFunctionFactory.Create(method, target: instance);
 2363    }
 364
 365    /// <summary>
 366    /// Resolves the source-generated <see cref="AIFunction"/> for the given tool method, builds
 367    /// an <see cref="AIFunctionArguments"/> via <paramref name="configureArgs"/>, establishes
 368    /// the configured execution context, and invokes the function. Captures any thrown exception
 369    /// into the returned <see cref="ToolInvocationResult"/> rather than propagating.
 370    /// </summary>
 371    /// <typeparam name="TTool">The tool class declaring the <c>[AgentFunction]</c> method.</typeparam>
 372    /// <param name="methodName">The function name as exposed to the LLM.</param>
 373    /// <param name="configureArgs">
 374    /// Optional hook to populate <see cref="AIFunctionArguments"/>. Pass <see langword="null"/> to
 375    /// invoke with no arguments.
 376    /// </param>
 377    /// <param name="cancellationToken">
 378    /// Cancellation token forwarded to <see cref="AIFunction.InvokeAsync"/>. Tools that accept a
 379    /// <see cref="CancellationToken"/> parameter receive this token via the source-generated wrapper.
 380    /// </param>
 381    public Task<ToolInvocationResult> InvokeAsync<TTool>(
 382        string methodName,
 383        Action<AIFunctionArguments>? configureArgs = null,
 384        CancellationToken cancellationToken = default)
 385        where TTool : class
 4386        => InvokeAsync(typeof(TTool), methodName, configureArgs, cancellationToken);
 387
 388    /// <summary>
 389    /// Same as <see cref="InvokeAsync{TTool}(string, Action{AIFunctionArguments}?, CancellationToken)"/>
 390    /// but accepts a pre-built dictionary of arguments. Convenient when the test already has a
 391    /// <see cref="IReadOnlyDictionary{TKey, TValue}"/> of args ready (e.g. captured from a prior
 392    /// run or shared across multiple invocations).
 393    /// </summary>
 394    public Task<ToolInvocationResult> InvokeAsync<TTool>(
 395        string methodName,
 396        IReadOnlyDictionary<string, object?> arguments,
 397        CancellationToken cancellationToken = default)
 398        where TTool : class
 399    {
 0400        ArgumentNullException.ThrowIfNull(arguments);
 0401        return InvokeAsync<TTool>(
 0402            methodName,
 0403            args =>
 0404            {
 0405                foreach (var pair in arguments)
 0406                {
 0407                    args[pair.Key] = pair.Value;
 0408                }
 0409            },
 0410            cancellationToken);
 411    }
 412
 413    /// <summary>
 414    /// Resolves and invokes a function for <paramref name="toolType"/>. Identical to the generic
 415    /// overload but accepts the type as a parameter for callers who only have a runtime
 416    /// <see cref="Type"/>.
 417    /// </summary>
 418    public async Task<ToolInvocationResult> InvokeAsync(
 419        Type toolType,
 420        string methodName,
 421        Action<AIFunctionArguments>? configureArgs = null,
 422        CancellationToken cancellationToken = default)
 423    {
 5424        ArgumentNullException.ThrowIfNull(toolType);
 5425        ArgumentException.ThrowIfNullOrWhiteSpace(methodName);
 426
 5427        var contextBuilder = new AgentExecutionContextBuilder();
 5428        _contextConfigurator?.Invoke(contextBuilder);
 5429        var executionContext = contextBuilder.Build();
 5430        var workspace = contextBuilder.Workspace;
 431
 432        AIFunction function;
 433        try
 434        {
 5435            function = GetFunction(toolType, methodName);
 1436        }
 4437        catch (Exception resolutionException)
 438        {
 4439            return new ToolInvocationResult(
 4440                ReturnValue: null,
 4441                Exception: resolutionException,
 4442                FunctionSource: ToolFunctionSource.Generated,
 4443                Workspace: workspace,
 4444                Duration: TimeSpan.Zero);
 445        }
 446
 1447        var args = new AIFunctionArguments();
 1448        configureArgs?.Invoke(args);
 449
 1450        var accessor = _serviceProvider.GetRequiredService<IAgentExecutionContextAccessor>();
 1451        var stopwatch = Stopwatch.StartNew();
 1452        Exception? invocationException = null;
 1453        object? returnValue = null;
 454
 1455        using (accessor.BeginScope(executionContext))
 456        {
 457            try
 458            {
 1459                returnValue = await function.InvokeAsync(args, cancellationToken).ConfigureAwait(false);
 1460            }
 0461            catch (Exception ex)
 462            {
 0463                invocationException = ex;
 0464            }
 1465        }
 466
 1467        stopwatch.Stop();
 468
 1469        return new ToolInvocationResult(
 1470            ReturnValue: returnValue,
 1471            Exception: invocationException,
 1472            FunctionSource: ToolFunctionSource.Generated,
 1473            Workspace: workspace,
 1474            Duration: stopwatch.Elapsed);
 5475    }
 476
 477    private IDisposable? BeginBootstrapScopeIfLimited()
 478    {
 67479        if (_limitedToolTypes is null)
 480        {
 65481            return null;
 482        }
 483
 2484        var types = _limitedToolTypes;
 2485        return AgentFrameworkGeneratedBootstrap.BeginTestScope(
 0486            functionTypes: () => types,
 0487            groupTypes: static () => new Dictionary<string, IReadOnlyList<Type>>(),
 2488            agentTypes: static () => []);
 489    }
 490}