mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Add ServiceLifetime support for Hosting DI registration (#4476)
This commit is contained in:
committed by
GitHub
Unverified
parent
4bd5469798
commit
d8d6ac1c59
@@ -19,9 +19,10 @@ public static class AgentHostingServiceCollectionExtensions
|
||||
/// <param name="services">The service collection to configure.</param>
|
||||
/// <param name="name">The name of the agent.</param>
|
||||
/// <param name="instructions">The instructions for the agent.</param>
|
||||
/// <param name="lifetime">The DI service lifetime for the agent registration. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param>
|
||||
/// <returns>The same <see cref="IServiceCollection"/> instance so that additional calls can be chained.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/> or <paramref name="name"/> is <see langword="null"/>.</exception>
|
||||
public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, string name, string? instructions)
|
||||
public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, string name, string? instructions, ServiceLifetime lifetime = ServiceLifetime.Singleton)
|
||||
{
|
||||
Throw.IfNull(services);
|
||||
Throw.IfNullOrEmpty(name);
|
||||
@@ -30,7 +31,7 @@ public static class AgentHostingServiceCollectionExtensions
|
||||
var chatClient = sp.GetRequiredService<IChatClient>();
|
||||
var tools = sp.GetKeyedServices<AITool>(name).ToList();
|
||||
return new ChatClientAgent(chatClient, instructions, key, tools: tools);
|
||||
});
|
||||
}, lifetime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -40,9 +41,10 @@ public static class AgentHostingServiceCollectionExtensions
|
||||
/// <param name="name">The name of the agent.</param>
|
||||
/// <param name="instructions">The instructions for the agent.</param>
|
||||
/// <param name="chatClient">The chat client which the agent will use for inference.</param>
|
||||
/// <param name="lifetime">The DI service lifetime for the agent registration. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param>
|
||||
/// <returns>The same <see cref="IServiceCollection"/> instance so that additional calls can be chained.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/> or <paramref name="name"/> is <see langword="null"/>.</exception>
|
||||
public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, string name, string? instructions, IChatClient chatClient)
|
||||
public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, string name, string? instructions, IChatClient chatClient, ServiceLifetime lifetime = ServiceLifetime.Singleton)
|
||||
{
|
||||
Throw.IfNull(services);
|
||||
Throw.IfNullOrEmpty(name);
|
||||
@@ -50,7 +52,7 @@ public static class AgentHostingServiceCollectionExtensions
|
||||
{
|
||||
var tools = sp.GetKeyedServices<AITool>(name).ToList();
|
||||
return new ChatClientAgent(chatClient, instructions, key, tools: tools);
|
||||
});
|
||||
}, lifetime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -60,9 +62,10 @@ public static class AgentHostingServiceCollectionExtensions
|
||||
/// <param name="name">The name of the agent.</param>
|
||||
/// <param name="instructions">The instructions for the agent.</param>
|
||||
/// <param name="chatClientServiceKey">The key to use when resolving the chat client from the service provider. If <see langword="null"/>, a non-keyed service will be resolved.</param>
|
||||
/// <param name="lifetime">The DI service lifetime for the agent registration. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param>
|
||||
/// <returns>The same <see cref="IServiceCollection"/> instance so that additional calls can be chained.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/> or <paramref name="name"/> is <see langword="null"/>.</exception>
|
||||
public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, string name, string? instructions, object? chatClientServiceKey)
|
||||
public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, string name, string? instructions, object? chatClientServiceKey, ServiceLifetime lifetime = ServiceLifetime.Singleton)
|
||||
{
|
||||
Throw.IfNull(services);
|
||||
Throw.IfNullOrEmpty(name);
|
||||
@@ -71,7 +74,7 @@ public static class AgentHostingServiceCollectionExtensions
|
||||
var chatClient = chatClientServiceKey is null ? sp.GetRequiredService<IChatClient>() : sp.GetRequiredKeyedService<IChatClient>(chatClientServiceKey);
|
||||
var tools = sp.GetKeyedServices<AITool>(name).ToList();
|
||||
return new ChatClientAgent(chatClient, instructions, key, tools: tools);
|
||||
});
|
||||
}, lifetime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -82,9 +85,10 @@ public static class AgentHostingServiceCollectionExtensions
|
||||
/// <param name="instructions">The instructions for the agent.</param>
|
||||
/// <param name="description">A description of the agent.</param>
|
||||
/// <param name="chatClientServiceKey">The key to use when resolving the chat client from the service provider. If <see langword="null"/>, a non-keyed service will be resolved.</param>
|
||||
/// <param name="lifetime">The DI service lifetime for the agent registration. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param>
|
||||
/// <returns>The same <see cref="IServiceCollection"/> instance so that additional calls can be chained.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/> or <paramref name="name"/> is <see langword="null"/>.</exception>
|
||||
public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, string name, string? instructions, string? description, object? chatClientServiceKey)
|
||||
public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, string name, string? instructions, string? description, object? chatClientServiceKey, ServiceLifetime lifetime = ServiceLifetime.Singleton)
|
||||
{
|
||||
Throw.IfNull(services);
|
||||
Throw.IfNullOrEmpty(name);
|
||||
@@ -93,7 +97,7 @@ public static class AgentHostingServiceCollectionExtensions
|
||||
var chatClient = chatClientServiceKey is null ? sp.GetRequiredService<IChatClient>() : sp.GetRequiredKeyedService<IChatClient>(chatClientServiceKey);
|
||||
var tools = sp.GetKeyedServices<AITool>(name).ToList();
|
||||
return new ChatClientAgent(chatClient, instructions: instructions, name: key, description: description, tools: tools);
|
||||
});
|
||||
}, lifetime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -102,15 +106,16 @@ public static class AgentHostingServiceCollectionExtensions
|
||||
/// <param name="services">The service collection to configure.</param>
|
||||
/// <param name="name">The name of the agent.</param>
|
||||
/// <param name="createAgentDelegate">A factory delegate that creates the AI agent instance. The delegate receives the service provider and agent key as parameters.</param>
|
||||
/// <param name="lifetime">The DI service lifetime for the agent registration. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param>
|
||||
/// <returns>The same <see cref="IServiceCollection"/> instance so that additional calls can be chained.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/>, <paramref name="name"/>, or <paramref name="createAgentDelegate"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="InvalidOperationException">Thrown when the agent factory delegate returns <see langword="null"/> or an agent whose <see cref="AIAgent.Name"/> does not match <paramref name="name"/>.</exception>
|
||||
public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, string name, Func<IServiceProvider, string, AIAgent> createAgentDelegate)
|
||||
public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, string name, Func<IServiceProvider, string, AIAgent> createAgentDelegate, ServiceLifetime lifetime = ServiceLifetime.Singleton)
|
||||
{
|
||||
Throw.IfNull(services);
|
||||
Throw.IfNull(name);
|
||||
Throw.IfNull(createAgentDelegate);
|
||||
services.AddKeyedSingleton(name, (sp, key) =>
|
||||
services.AddKeyedService(name, (sp, key) =>
|
||||
{
|
||||
Throw.IfNull(key);
|
||||
var keyString = key as string;
|
||||
@@ -122,8 +127,18 @@ public static class AgentHostingServiceCollectionExtensions
|
||||
}
|
||||
|
||||
return agent;
|
||||
});
|
||||
}, lifetime);
|
||||
|
||||
return new HostedAgentBuilder(name, services);
|
||||
return new HostedAgentBuilder(name, services, lifetime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a keyed service with the specified lifetime.
|
||||
/// </summary>
|
||||
internal static void AddKeyedService<T>(this IServiceCollection services, object? serviceKey, Func<IServiceProvider, object?, T> factory, ServiceLifetime lifetime)
|
||||
where T : class
|
||||
{
|
||||
var descriptor = new ServiceDescriptor(typeof(T), serviceKey, (sp, key) => factory(sp, key), lifetime);
|
||||
services.Add(descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
using System;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
@@ -18,12 +19,13 @@ public static class HostApplicationBuilderAgentExtensions
|
||||
/// <param name="builder">The host application builder to configure.</param>
|
||||
/// <param name="name">The name of the agent.</param>
|
||||
/// <param name="instructions">The instructions for the agent.</param>
|
||||
/// <param name="lifetime">The DI service lifetime for the agent registration. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param>
|
||||
/// <returns>The configured host application builder.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/>, <paramref name="name"/>, or <paramref name="instructions"/> is null.</exception>
|
||||
public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions)
|
||||
public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions, ServiceLifetime lifetime = ServiceLifetime.Singleton)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
return builder.Services.AddAIAgent(name, instructions);
|
||||
return builder.Services.AddAIAgent(name, instructions, lifetime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -33,13 +35,14 @@ public static class HostApplicationBuilderAgentExtensions
|
||||
/// <param name="name">The name of the agent.</param>
|
||||
/// <param name="instructions">The instructions for the agent.</param>
|
||||
/// <param name="chatClient">The chat client which the agent will use for inference.</param>
|
||||
/// <param name="lifetime">The DI service lifetime for the agent registration. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param>
|
||||
/// <returns>The configured host application builder.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/>, <paramref name="name"/>, or <paramref name="instructions"/> is null.</exception>
|
||||
public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions, IChatClient chatClient)
|
||||
public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions, IChatClient chatClient, ServiceLifetime lifetime = ServiceLifetime.Singleton)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
Throw.IfNullOrEmpty(name);
|
||||
return builder.Services.AddAIAgent(name, instructions, chatClient);
|
||||
return builder.Services.AddAIAgent(name, instructions, chatClient, lifetime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -50,13 +53,14 @@ public static class HostApplicationBuilderAgentExtensions
|
||||
/// <param name="instructions">The instructions for the agent.</param>
|
||||
/// <param name="description">A description of the agent.</param>
|
||||
/// <param name="chatClientServiceKey">The key to use when resolving the chat client from the service provider. If null, a non-keyed service will be resolved.</param>
|
||||
/// <param name="lifetime">The DI service lifetime for the agent registration. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param>
|
||||
/// <returns>The configured host application builder.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/>, <paramref name="name"/>, or <paramref name="instructions"/> is null.</exception>
|
||||
public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions, string? description, object? chatClientServiceKey)
|
||||
public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions, string? description, object? chatClientServiceKey, ServiceLifetime lifetime = ServiceLifetime.Singleton)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
Throw.IfNullOrEmpty(name);
|
||||
return builder.Services.AddAIAgent(name, instructions, description, chatClientServiceKey);
|
||||
return builder.Services.AddAIAgent(name, instructions, description, chatClientServiceKey, lifetime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -66,12 +70,13 @@ public static class HostApplicationBuilderAgentExtensions
|
||||
/// <param name="name">The name of the agent.</param>
|
||||
/// <param name="instructions">The instructions for the agent.</param>
|
||||
/// <param name="chatClientServiceKey">The key to use when resolving the chat client from the service provider. If null, a non-keyed service will be resolved.</param>
|
||||
/// <param name="lifetime">The DI service lifetime for the agent registration. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param>
|
||||
/// <returns>The configured host application builder.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/>, <paramref name="name"/>, or <paramref name="instructions"/> is null.</exception>
|
||||
public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions, object? chatClientServiceKey)
|
||||
public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions, object? chatClientServiceKey, ServiceLifetime lifetime = ServiceLifetime.Singleton)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
return builder.Services.AddAIAgent(name, instructions, chatClientServiceKey);
|
||||
return builder.Services.AddAIAgent(name, instructions, chatClientServiceKey, lifetime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -80,12 +85,13 @@ public static class HostApplicationBuilderAgentExtensions
|
||||
/// <param name="builder">The host application builder to configure.</param>
|
||||
/// <param name="name">The name of the agent.</param>
|
||||
/// <param name="createAgentDelegate">A factory delegate that creates the AI agent instance. The delegate receives the service provider and agent key as parameters.</param>
|
||||
/// <param name="lifetime">The DI service lifetime for the agent registration. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param>
|
||||
/// <returns>The configured host application builder.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/>, <paramref name="name"/>, or <paramref name="createAgentDelegate"/> is null.</exception>
|
||||
/// <exception cref="InvalidOperationException">Thrown when the agent factory delegate returns null or an invalid AI agent instance.</exception>
|
||||
public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, Func<IServiceProvider, string, AIAgent> createAgentDelegate)
|
||||
public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, Func<IServiceProvider, string, AIAgent> createAgentDelegate, ServiceLifetime lifetime = ServiceLifetime.Singleton)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
return builder.Services.AddAIAgent(name, createAgentDelegate);
|
||||
return builder.Services.AddAIAgent(name, createAgentDelegate, lifetime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,19 +19,20 @@ public static class HostApplicationBuilderWorkflowExtensions
|
||||
/// <param name="builder">The <see cref="IHostApplicationBuilder"/> to configure.</param>
|
||||
/// <param name="name">The unique name for the workflow.</param>
|
||||
/// <param name="createWorkflowDelegate">A factory function that creates the <see cref="Workflow"/> instance. The function receives the service provider and workflow name as parameters.</param>
|
||||
/// <param name="lifetime">The DI service lifetime for the workflow registration. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param>
|
||||
/// <returns>An <see cref="IHostedWorkflowBuilder"/> that can be used to further configure the workflow.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/>, <paramref name="name"/>, or <paramref name="createWorkflowDelegate"/> is null.</exception>
|
||||
/// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is empty.</exception>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown when the factory delegate returns null or a workflow with a name that doesn't match the expected name.
|
||||
/// </exception>
|
||||
public static IHostedWorkflowBuilder AddWorkflow(this IHostApplicationBuilder builder, string name, Func<IServiceProvider, string, Workflow> createWorkflowDelegate)
|
||||
public static IHostedWorkflowBuilder AddWorkflow(this IHostApplicationBuilder builder, string name, Func<IServiceProvider, string, Workflow> createWorkflowDelegate, ServiceLifetime lifetime = ServiceLifetime.Singleton)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
Throw.IfNull(name);
|
||||
Throw.IfNull(createWorkflowDelegate);
|
||||
|
||||
builder.Services.AddKeyedSingleton(name, (sp, key) =>
|
||||
builder.Services.AddKeyedService(name, (sp, key) =>
|
||||
{
|
||||
Throw.IfNull(key);
|
||||
var keyString = key as string;
|
||||
@@ -43,7 +44,7 @@ public static class HostApplicationBuilderWorkflowExtensions
|
||||
}
|
||||
|
||||
return workflow;
|
||||
});
|
||||
}, lifetime);
|
||||
|
||||
return new HostedWorkflowBuilder(name, builder);
|
||||
}
|
||||
|
||||
@@ -9,15 +9,17 @@ internal sealed class HostedAgentBuilder : IHostedAgentBuilder
|
||||
{
|
||||
public string Name { get; }
|
||||
public IServiceCollection ServiceCollection { get; }
|
||||
public ServiceLifetime Lifetime { get; }
|
||||
|
||||
public HostedAgentBuilder(string name, IHostApplicationBuilder builder)
|
||||
: this(name, builder.Services)
|
||||
public HostedAgentBuilder(string name, IHostApplicationBuilder builder, ServiceLifetime lifetime = ServiceLifetime.Singleton)
|
||||
: this(name, builder.Services, lifetime)
|
||||
{
|
||||
}
|
||||
|
||||
public HostedAgentBuilder(string name, IServiceCollection serviceCollection)
|
||||
public HostedAgentBuilder(string name, IServiceCollection serviceCollection, ServiceLifetime lifetime = ServiceLifetime.Singleton)
|
||||
{
|
||||
this.Name = name;
|
||||
this.ServiceCollection = serviceCollection;
|
||||
this.Lifetime = lifetime;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,17 +42,19 @@ public static class HostedAgentBuilderExtensions
|
||||
/// <param name="builder">The host agent builder to configure.</param>
|
||||
/// <param name="createAgentSessionStore">A factory function that creates an agent session store instance using the provided service provider and agent
|
||||
/// name.</param>
|
||||
/// <param name="lifetime">The DI service lifetime for the session store registration. Defaults to <see cref="ServiceLifetime.Singleton"/>
|
||||
/// because session stores persist conversation state across requests and are consumed independently of the agent's lifetime.</param>
|
||||
/// <returns>The same host agent builder instance, enabling further configuration.</returns>
|
||||
public static IHostedAgentBuilder WithSessionStore(this IHostedAgentBuilder builder, Func<IServiceProvider, string, AgentSessionStore> createAgentSessionStore)
|
||||
public static IHostedAgentBuilder WithSessionStore(this IHostedAgentBuilder builder, Func<IServiceProvider, string, AgentSessionStore> createAgentSessionStore, ServiceLifetime lifetime = ServiceLifetime.Singleton)
|
||||
{
|
||||
builder.ServiceCollection.AddKeyedSingleton(builder.Name, (sp, key) =>
|
||||
builder.ServiceCollection.AddKeyedService(builder.Name, (sp, key) =>
|
||||
{
|
||||
Throw.IfNull(key);
|
||||
var keyString = key as string;
|
||||
Throw.IfNullOrEmpty(keyString);
|
||||
return createAgentSessionStore(sp, keyString) ??
|
||||
throw new InvalidOperationException($"The agent session store factory did not return a valid {nameof(AgentSessionStore)} instance for key '{keyString}'.");
|
||||
});
|
||||
}, lifetime);
|
||||
return builder;
|
||||
}
|
||||
|
||||
@@ -98,13 +100,39 @@ public static class HostedAgentBuilderExtensions
|
||||
/// </summary>
|
||||
/// <param name="builder">The hosted agent builder.</param>
|
||||
/// <param name="factory">A factory function that creates a AI tool using the provided service provider.</param>
|
||||
public static IHostedAgentBuilder WithAITool(this IHostedAgentBuilder builder, Func<IServiceProvider, AITool> factory)
|
||||
/// <param name="lifetime">The DI service lifetime for the tool registration. If <see langword="null"/>, the agent's lifetime is used.</param>
|
||||
/// <returns>The same <see cref="IHostedAgentBuilder"/> instance so that additional calls can be chained.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/> or <paramref name="factory"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown when the effective tool lifetime is shorter than the agent's lifetime, which would cause a captive dependency.
|
||||
/// For example, a singleton agent cannot use scoped or transient tools.
|
||||
/// </exception>
|
||||
public static IHostedAgentBuilder WithAITool(this IHostedAgentBuilder builder, Func<IServiceProvider, AITool> factory, ServiceLifetime? lifetime = null)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
Throw.IfNull(factory);
|
||||
|
||||
builder.ServiceCollection.AddKeyedSingleton(builder.Name, (sp, name) => factory(sp));
|
||||
var effectiveLifetime = lifetime ?? builder.Lifetime;
|
||||
ValidateToolLifetime(builder.Lifetime, effectiveLifetime);
|
||||
|
||||
builder.ServiceCollection.AddKeyedService(builder.Name, (sp, name) => factory(sp), effectiveLifetime);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that the tool lifetime is compatible with the agent lifetime.
|
||||
/// A tool's lifetime must be at least as long as the agent's lifetime to prevent captive dependency issues.
|
||||
/// </summary>
|
||||
internal static void ValidateToolLifetime(ServiceLifetime agentLifetime, ServiceLifetime toolLifetime)
|
||||
{
|
||||
// ServiceLifetime enum: Singleton=0, Scoped=1, Transient=2
|
||||
// A higher value means a shorter lifetime.
|
||||
if (toolLifetime > agentLifetime)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"A tool with lifetime '{toolLifetime}' cannot be registered for an agent with lifetime '{agentLifetime}'. " +
|
||||
"The tool's lifetime must be at least as long as the agent's lifetime to avoid captive dependency issues.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,22 +14,24 @@ public static class HostedWorkflowBuilderExtensions
|
||||
/// Registers the workflow as an AI agent in the dependency injection container.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IHostedWorkflowBuilder"/> instance to extend.</param>
|
||||
/// <param name="lifetime">The DI service lifetime for the agent registration. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param>
|
||||
/// <returns>An <see cref="IHostedAgentBuilder"/> that can be used to further configure the agent.</returns>
|
||||
public static IHostedAgentBuilder AddAsAIAgent(this IHostedWorkflowBuilder builder)
|
||||
=> builder.AddAsAIAgent(name: null);
|
||||
public static IHostedAgentBuilder AddAsAIAgent(this IHostedWorkflowBuilder builder, ServiceLifetime lifetime = ServiceLifetime.Singleton)
|
||||
=> builder.AddAsAIAgent(name: null, lifetime: lifetime);
|
||||
|
||||
/// <summary>
|
||||
/// Registers the workflow as an AI agent in the dependency injection container.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IHostedWorkflowBuilder"/> instance to extend.</param>
|
||||
/// <param name="name">The optional name for the AI agent. If not specified, the workflow name is used.</param>
|
||||
/// <param name="lifetime">The DI service lifetime for the agent registration. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param>
|
||||
/// <returns>An <see cref="IHostedAgentBuilder"/> that can be used to further configure the agent.</returns>
|
||||
public static IHostedAgentBuilder AddAsAIAgent(this IHostedWorkflowBuilder builder, string? name)
|
||||
public static IHostedAgentBuilder AddAsAIAgent(this IHostedWorkflowBuilder builder, string? name, ServiceLifetime lifetime = ServiceLifetime.Singleton)
|
||||
{
|
||||
var workflowName = builder.Name;
|
||||
var agentName = name ?? workflowName;
|
||||
|
||||
return builder.HostApplicationBuilder.AddAIAgent(agentName, (sp, key) =>
|
||||
sp.GetRequiredKeyedService<Workflow>(workflowName).AsAIAgent(name: key));
|
||||
sp.GetRequiredKeyedService<Workflow>(workflowName).AsAIAgent(name: key), lifetime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,4 +18,9 @@ public interface IHostedAgentBuilder
|
||||
/// Gets the service collection for configuration.
|
||||
/// </summary>
|
||||
IServiceCollection ServiceCollection { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the DI service lifetime used for the agent registration.
|
||||
/// </summary>
|
||||
ServiceLifetime Lifetime { get; }
|
||||
}
|
||||
|
||||
+91
-1
@@ -105,7 +105,7 @@ public class AgentHostingServiceCollectionExtensionsTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddAIAgent registers the agent as a keyed singleton service.
|
||||
/// Verifies that AddAIAgent registers the agent as a keyed singleton service by default.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddAIAgent_RegistersKeyedSingleton()
|
||||
@@ -203,4 +203,94 @@ public class AgentHostingServiceCollectionExtensionsTests
|
||||
d.ServiceType == typeof(AIAgent));
|
||||
Assert.NotNull(descriptor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddAIAgent registers with the specified scoped lifetime.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddAIAgent_WithScopedLifetime_RegistersKeyedScoped()
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection();
|
||||
var mockAgent = new Mock<AIAgent>();
|
||||
const string AgentName = "scopedAgent";
|
||||
|
||||
// Act
|
||||
var result = services.AddAIAgent(AgentName, (sp, key) => mockAgent.Object, ServiceLifetime.Scoped);
|
||||
|
||||
// Assert
|
||||
var descriptor = services.FirstOrDefault(
|
||||
d => (d.ServiceKey as string) == AgentName &&
|
||||
d.ServiceType == typeof(AIAgent));
|
||||
|
||||
Assert.NotNull(descriptor);
|
||||
Assert.Equal(ServiceLifetime.Scoped, descriptor.Lifetime);
|
||||
Assert.Equal(ServiceLifetime.Scoped, result.Lifetime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddAIAgent registers with the specified transient lifetime.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddAIAgent_WithTransientLifetime_RegistersKeyedTransient()
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection();
|
||||
var mockAgent = new Mock<AIAgent>();
|
||||
const string AgentName = "transientAgent";
|
||||
|
||||
// Act
|
||||
var result = services.AddAIAgent(AgentName, (sp, key) => mockAgent.Object, ServiceLifetime.Transient);
|
||||
|
||||
// Assert
|
||||
var descriptor = services.FirstOrDefault(
|
||||
d => (d.ServiceKey as string) == AgentName &&
|
||||
d.ServiceType == typeof(AIAgent));
|
||||
|
||||
Assert.NotNull(descriptor);
|
||||
Assert.Equal(ServiceLifetime.Transient, descriptor.Lifetime);
|
||||
Assert.Equal(ServiceLifetime.Transient, result.Lifetime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the builder exposes the correct lifetime for default registration.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddAIAgent_DefaultLifetime_BuilderExposesSingleton()
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection();
|
||||
var mockAgent = new Mock<AIAgent>();
|
||||
|
||||
// Act
|
||||
var result = services.AddAIAgent("agentName", (sp, key) => mockAgent.Object);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(ServiceLifetime.Singleton, result.Lifetime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddAIAgent with instructions overload respects the lifetime parameter.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(ServiceLifetime.Singleton)]
|
||||
[InlineData(ServiceLifetime.Scoped)]
|
||||
[InlineData(ServiceLifetime.Transient)]
|
||||
public void AddAIAgent_InstructionsOverload_RespectsLifetime(ServiceLifetime lifetime)
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection();
|
||||
|
||||
// Act
|
||||
var result = services.AddAIAgent("agent", "instructions", lifetime);
|
||||
|
||||
// Assert
|
||||
var descriptor = services.FirstOrDefault(
|
||||
d => (d.ServiceKey as string) == "agent" &&
|
||||
d.ServiceType == typeof(AIAgent));
|
||||
|
||||
Assert.NotNull(descriptor);
|
||||
Assert.Equal(lifetime, descriptor.Lifetime);
|
||||
Assert.Equal(lifetime, result.Lifetime);
|
||||
}
|
||||
}
|
||||
|
||||
+74
-1
@@ -127,7 +127,7 @@ public class HostApplicationBuilderAgentExtensionsTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddAIAgent registers the agent as a keyed singleton service.
|
||||
/// Verifies that AddAIAgent registers the agent as a keyed singleton service by default.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddAIAgent_RegistersKeyedSingleton()
|
||||
@@ -235,4 +235,77 @@ public class HostApplicationBuilderAgentExtensionsTests
|
||||
d.ServiceType == typeof(AIAgent));
|
||||
Assert.NotNull(descriptor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddAIAgent registers with the specified scoped lifetime via the host builder.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddAIAgent_WithScopedLifetime_RegistersKeyedScoped()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new HostApplicationBuilder();
|
||||
var mockAgent = new Mock<AIAgent>();
|
||||
const string AgentName = "scopedAgent";
|
||||
|
||||
// Act
|
||||
var result = builder.AddAIAgent(AgentName, (sp, key) => mockAgent.Object, ServiceLifetime.Scoped);
|
||||
|
||||
// Assert
|
||||
var descriptor = builder.Services.FirstOrDefault(
|
||||
d => (d.ServiceKey as string) == AgentName &&
|
||||
d.ServiceType == typeof(AIAgent));
|
||||
|
||||
Assert.NotNull(descriptor);
|
||||
Assert.Equal(ServiceLifetime.Scoped, descriptor.Lifetime);
|
||||
Assert.Equal(ServiceLifetime.Scoped, result.Lifetime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddAIAgent registers with the specified transient lifetime via the host builder.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddAIAgent_WithTransientLifetime_RegistersKeyedTransient()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new HostApplicationBuilder();
|
||||
var mockAgent = new Mock<AIAgent>();
|
||||
const string AgentName = "transientAgent";
|
||||
|
||||
// Act
|
||||
var result = builder.AddAIAgent(AgentName, (sp, key) => mockAgent.Object, ServiceLifetime.Transient);
|
||||
|
||||
// Assert
|
||||
var descriptor = builder.Services.FirstOrDefault(
|
||||
d => (d.ServiceKey as string) == AgentName &&
|
||||
d.ServiceType == typeof(AIAgent));
|
||||
|
||||
Assert.NotNull(descriptor);
|
||||
Assert.Equal(ServiceLifetime.Transient, descriptor.Lifetime);
|
||||
Assert.Equal(ServiceLifetime.Transient, result.Lifetime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddAIAgent with instructions overload respects the lifetime parameter via the host builder.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(ServiceLifetime.Singleton)]
|
||||
[InlineData(ServiceLifetime.Scoped)]
|
||||
[InlineData(ServiceLifetime.Transient)]
|
||||
public void AddAIAgent_InstructionsOverload_RespectsLifetime(ServiceLifetime lifetime)
|
||||
{
|
||||
// Arrange
|
||||
var builder = new HostApplicationBuilder();
|
||||
|
||||
// Act
|
||||
var result = builder.AddAIAgent("agent", "instructions", lifetime);
|
||||
|
||||
// Assert
|
||||
var descriptor = builder.Services.FirstOrDefault(
|
||||
d => (d.ServiceKey as string) == "agent" &&
|
||||
d.ServiceType == typeof(AIAgent));
|
||||
|
||||
Assert.NotNull(descriptor);
|
||||
Assert.Equal(lifetime, descriptor.Lifetime);
|
||||
Assert.Equal(lifetime, result.Lifetime);
|
||||
}
|
||||
}
|
||||
|
||||
+72
-1
@@ -63,7 +63,7 @@ public class HostApplicationBuilderWorkflowExtensionsTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddWorkflow registers the workflow as a keyed singleton service.
|
||||
/// Verifies that AddWorkflow registers the workflow as a keyed singleton service by default.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddWorkflow_RegistersKeyedSingleton()
|
||||
@@ -328,6 +328,77 @@ public class HostApplicationBuilderWorkflowExtensionsTests
|
||||
Assert.NotNull(agentDescriptor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddWorkflow registers with the specified scoped lifetime.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddWorkflow_WithScopedLifetime_RegistersKeyedScoped()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new HostApplicationBuilder();
|
||||
const string WorkflowName = "scopedWorkflow";
|
||||
|
||||
// Act
|
||||
builder.AddWorkflow(WorkflowName, (sp, key) => CreateTestWorkflow(key), ServiceLifetime.Scoped);
|
||||
|
||||
// Assert
|
||||
var descriptor = builder.Services.FirstOrDefault(
|
||||
d => (d.ServiceKey as string) == WorkflowName &&
|
||||
d.ServiceType == typeof(Workflow));
|
||||
|
||||
Assert.NotNull(descriptor);
|
||||
Assert.Equal(ServiceLifetime.Scoped, descriptor.Lifetime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddWorkflow registers with the specified transient lifetime.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddWorkflow_WithTransientLifetime_RegistersKeyedTransient()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new HostApplicationBuilder();
|
||||
const string WorkflowName = "transientWorkflow";
|
||||
|
||||
// Act
|
||||
builder.AddWorkflow(WorkflowName, (sp, key) => CreateTestWorkflow(key), ServiceLifetime.Transient);
|
||||
|
||||
// Assert
|
||||
var descriptor = builder.Services.FirstOrDefault(
|
||||
d => (d.ServiceKey as string) == WorkflowName &&
|
||||
d.ServiceType == typeof(Workflow));
|
||||
|
||||
Assert.NotNull(descriptor);
|
||||
Assert.Equal(ServiceLifetime.Transient, descriptor.Lifetime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddAsAIAgent respects the lifetime parameter.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(ServiceLifetime.Singleton)]
|
||||
[InlineData(ServiceLifetime.Scoped)]
|
||||
[InlineData(ServiceLifetime.Transient)]
|
||||
public void AddAsAIAgent_RespectsLifetime(ServiceLifetime lifetime)
|
||||
{
|
||||
// Arrange
|
||||
var builder = new HostApplicationBuilder();
|
||||
const string WorkflowName = "testWorkflow";
|
||||
var workflowBuilder = builder.AddWorkflow(WorkflowName, (sp, key) => CreateTestWorkflow(key));
|
||||
|
||||
// Act
|
||||
var agentBuilder = workflowBuilder.AddAsAIAgent("agent", lifetime);
|
||||
|
||||
// Assert
|
||||
var descriptor = builder.Services.FirstOrDefault(
|
||||
d => (d.ServiceKey as string) == "agent" &&
|
||||
d.ServiceType == typeof(AIAgent));
|
||||
|
||||
Assert.NotNull(descriptor);
|
||||
Assert.Equal(lifetime, descriptor.Lifetime);
|
||||
Assert.Equal(lifetime, agentBuilder.Lifetime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to create a simple test workflow with a given name.
|
||||
/// </summary>
|
||||
|
||||
+174
@@ -7,6 +7,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Moq;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.UnitTests;
|
||||
|
||||
@@ -250,6 +251,179 @@ public sealed class HostedAgentBuilderToolsExtensionsTests
|
||||
Assert.Contains(factoryTool, agentTools);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that WithAITool factory method defaults to the agent's lifetime when no explicit lifetime is specified.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(ServiceLifetime.Singleton)]
|
||||
[InlineData(ServiceLifetime.Scoped)]
|
||||
[InlineData(ServiceLifetime.Transient)]
|
||||
public void WithAIToolFactory_DefaultsToAgentLifetime(ServiceLifetime agentLifetime)
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection();
|
||||
var builder = services.AddAIAgent("test-agent", (sp, key) => new Mock<AIAgent>().Object, agentLifetime);
|
||||
|
||||
// Act
|
||||
builder.WithAITool(_ => new DummyAITool());
|
||||
|
||||
// Assert
|
||||
var toolDescriptor = services.FirstOrDefault(
|
||||
d => (d.ServiceKey as string) == "test-agent" &&
|
||||
d.ServiceType == typeof(AITool));
|
||||
|
||||
Assert.NotNull(toolDescriptor);
|
||||
Assert.Equal(agentLifetime, toolDescriptor.Lifetime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that WithAITool factory method accepts an explicit lifetime override.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WithAIToolFactory_ExplicitLifetimeOverridesDefault()
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection();
|
||||
var builder = services.AddAIAgent("test-agent", (sp, key) => new Mock<AIAgent>().Object, ServiceLifetime.Transient);
|
||||
|
||||
// Act - Transient agent with Singleton tool is valid (longer-lived dependency)
|
||||
builder.WithAITool(_ => new DummyAITool(), ServiceLifetime.Singleton);
|
||||
|
||||
// Assert
|
||||
var toolDescriptor = services.FirstOrDefault(
|
||||
d => (d.ServiceKey as string) == "test-agent" &&
|
||||
d.ServiceType == typeof(AITool));
|
||||
|
||||
Assert.NotNull(toolDescriptor);
|
||||
Assert.Equal(ServiceLifetime.Singleton, toolDescriptor.Lifetime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that WithAITool factory throws for singleton agent with scoped tool (captive dependency).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WithAIToolFactory_SingletonAgentWithScopedTool_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection();
|
||||
var builder = services.AddAIAgent("test-agent", (sp, key) => new Mock<AIAgent>().Object, ServiceLifetime.Singleton);
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
builder.WithAITool(_ => new DummyAITool(), ServiceLifetime.Scoped));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that WithAITool factory throws for singleton agent with transient tool (captive dependency).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WithAIToolFactory_SingletonAgentWithTransientTool_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection();
|
||||
var builder = services.AddAIAgent("test-agent", (sp, key) => new Mock<AIAgent>().Object, ServiceLifetime.Singleton);
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
builder.WithAITool(_ => new DummyAITool(), ServiceLifetime.Transient));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that WithAITool factory throws for scoped agent with transient tool (captive dependency).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WithAIToolFactory_ScopedAgentWithTransientTool_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection();
|
||||
var builder = services.AddAIAgent("test-agent", (sp, key) => new Mock<AIAgent>().Object, ServiceLifetime.Scoped);
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
builder.WithAITool(_ => new DummyAITool(), ServiceLifetime.Transient));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies all valid tool lifetime combinations do not throw.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(ServiceLifetime.Singleton, ServiceLifetime.Singleton)]
|
||||
[InlineData(ServiceLifetime.Scoped, ServiceLifetime.Singleton)]
|
||||
[InlineData(ServiceLifetime.Scoped, ServiceLifetime.Scoped)]
|
||||
[InlineData(ServiceLifetime.Transient, ServiceLifetime.Singleton)]
|
||||
[InlineData(ServiceLifetime.Transient, ServiceLifetime.Scoped)]
|
||||
[InlineData(ServiceLifetime.Transient, ServiceLifetime.Transient)]
|
||||
public void WithAIToolFactory_ValidLifetimeCombinations_DoNotThrow(ServiceLifetime agentLifetime, ServiceLifetime toolLifetime)
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection();
|
||||
var builder = services.AddAIAgent("test-agent", (sp, key) => new Mock<AIAgent>().Object, agentLifetime);
|
||||
|
||||
// Act & Assert - should not throw
|
||||
builder.WithAITool(_ => new DummyAITool(), toolLifetime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that ValidateToolLifetime correctly identifies all invalid combinations.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(ServiceLifetime.Singleton, ServiceLifetime.Scoped)]
|
||||
[InlineData(ServiceLifetime.Singleton, ServiceLifetime.Transient)]
|
||||
[InlineData(ServiceLifetime.Scoped, ServiceLifetime.Transient)]
|
||||
public void ValidateToolLifetime_InvalidCombinations_Throw(ServiceLifetime agentLifetime, ServiceLifetime toolLifetime)
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
HostedAgentBuilderExtensions.ValidateToolLifetime(agentLifetime, toolLifetime));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the WithSessionStore factory method defaults to Singleton regardless of agent lifetime.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(ServiceLifetime.Singleton)]
|
||||
[InlineData(ServiceLifetime.Scoped)]
|
||||
[InlineData(ServiceLifetime.Transient)]
|
||||
public void WithSessionStoreFactory_DefaultsToSingleton(ServiceLifetime agentLifetime)
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection();
|
||||
var builder = services.AddAIAgent("test-agent", (sp, key) => new Mock<AIAgent>().Object, agentLifetime);
|
||||
|
||||
// Act
|
||||
builder.WithSessionStore((sp, name) => new InMemoryAgentSessionStore());
|
||||
|
||||
// Assert
|
||||
var storeDescriptor = services.FirstOrDefault(
|
||||
d => (d.ServiceKey as string) == "test-agent" &&
|
||||
d.ServiceType == typeof(AgentSessionStore));
|
||||
|
||||
Assert.NotNull(storeDescriptor);
|
||||
Assert.Equal(ServiceLifetime.Singleton, storeDescriptor.Lifetime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the WithSessionStore factory method accepts an explicit lifetime override.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WithSessionStoreFactory_ExplicitLifetimeOverridesDefault()
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection();
|
||||
var builder = services.AddAIAgent("test-agent", (sp, key) => new Mock<AIAgent>().Object, ServiceLifetime.Transient);
|
||||
|
||||
// Act
|
||||
builder.WithSessionStore((sp, name) => new InMemoryAgentSessionStore(), ServiceLifetime.Singleton);
|
||||
|
||||
// Assert
|
||||
var storeDescriptor = services.FirstOrDefault(
|
||||
d => (d.ServiceKey as string) == "test-agent" &&
|
||||
d.ServiceType == typeof(AgentSessionStore));
|
||||
|
||||
Assert.NotNull(storeDescriptor);
|
||||
Assert.Equal(ServiceLifetime.Singleton, storeDescriptor.Lifetime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dummy AITool implementation for testing.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user