// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.DurableTask.Workflows;
using Microsoft.Agents.AI.Workflows;
using Microsoft.DurableTask;
using Microsoft.DurableTask.Client;
using Microsoft.DurableTask.Worker;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
namespace Microsoft.Agents.AI.DurableTask;
///
/// Extension methods for configuring durable agents and workflows with dependency injection.
///
public static class ServiceCollectionExtensions
{
///
/// Gets a durable agent proxy by name.
///
/// The service provider.
/// The name of the agent.
/// The durable agent proxy.
/// Thrown if the agent proxy is not found.
public static AIAgent GetDurableAgentProxy(this IServiceProvider services, string name)
{
return services.GetKeyedService(name)
?? throw new KeyNotFoundException($"A durable agent with name '{name}' has not been registered.");
}
///
/// Configures durable agents, automatically registering agent entities.
///
///
///
/// This method provides an agent-focused configuration experience.
/// If you need to configure both agents and workflows, consider using
/// instead.
///
///
/// Multiple calls to this method are supported and configurations are composed additively.
///
///
/// The service collection.
/// A delegate to configure the durable agents.
/// Optional delegate to configure the Durable Task worker.
/// Optional delegate to configure the Durable Task client.
/// The service collection for chaining.
public static IServiceCollection ConfigureDurableAgents(
this IServiceCollection services,
Action configure,
Action? workerBuilder = null,
Action? clientBuilder = null)
{
return services.ConfigureDurableOptions(
options => configure(options.Agents),
workerBuilder,
clientBuilder);
}
///
/// Configures durable workflows, automatically registering orchestrations and activities.
///
///
///
/// This method provides a workflow-focused configuration experience.
/// If you need to configure both agents and workflows, consider using
/// instead.
///
///
/// Multiple calls to this method are supported and configurations are composed additively.
///
///
/// The service collection to configure.
/// A delegate to configure the workflow options.
/// Optional delegate to configure the durable task worker.
/// Optional delegate to configure the durable task client.
/// The service collection for chaining.
public static IServiceCollection ConfigureDurableWorkflows(
this IServiceCollection services,
Action configure,
Action? workerBuilder = null,
Action? clientBuilder = null)
{
return services.ConfigureDurableOptions(
options => configure(options.Workflows),
workerBuilder,
clientBuilder);
}
///
/// Configures durable agents and workflows, automatically registering orchestrations, activities, and agent entities.
///
///
///
/// This is the recommended entry point for configuring durable functionality. It provides unified configuration
/// for both agents and workflows through a single instance, ensuring agents
/// referenced in workflows are automatically registered.
///
///
/// Multiple calls to this method (or to
/// and ) are supported and configurations are composed additively.
///
///
/// The service collection to configure.
/// A delegate to configure the durable options for both agents and workflows.
/// Optional delegate to configure the durable task worker.
/// Optional delegate to configure the durable task client.
/// The service collection for chaining.
///
///
/// services.ConfigureDurableOptions(options =>
/// {
/// // Register agents not part of workflows
/// options.Agents.AddAIAgent(standaloneAgent);
///
/// // Register workflows - agents in workflows are auto-registered
/// options.Workflows.AddWorkflow(myWorkflow);
/// },
/// workerBuilder: builder => builder.UseDurableTaskScheduler(connectionString),
/// clientBuilder: builder => builder.UseDurableTaskScheduler(connectionString));
///
///
public static IServiceCollection ConfigureDurableOptions(
this IServiceCollection services,
Action configure,
Action? workerBuilder = null,
Action? clientBuilder = null)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configure);
// Get or create the shared DurableOptions instance for configuration
DurableOptions sharedOptions = GetOrCreateSharedOptions(services);
// Apply the configuration immediately to capture agent names for keyed service registration
configure(sharedOptions);
// Register keyed services for any new agents
RegisterAgentKeyedServices(services, sharedOptions);
// Register core services only once
EnsureDurableServicesRegistered(services, sharedOptions, workerBuilder, clientBuilder);
return services;
}
private static DurableOptions GetOrCreateSharedOptions(IServiceCollection services)
{
// Look for an existing DurableOptions registration
ServiceDescriptor? existingDescriptor = services.FirstOrDefault(
d => d.ServiceType == typeof(DurableOptions) && d.ImplementationInstance is not null);
if (existingDescriptor?.ImplementationInstance is DurableOptions existing)
{
return existing;
}
// Create a new shared options instance
DurableOptions options = new();
services.AddSingleton(options);
return options;
}
private static void RegisterAgentKeyedServices(IServiceCollection services, DurableOptions options)
{
foreach (KeyValuePair> factory in options.Agents.GetAgentFactories())
{
// Only add if not already registered (to support multiple Configure* calls)
if (!services.Any(d => d.ServiceType == typeof(AIAgent) && d.IsKeyedService && Equals(d.ServiceKey, factory.Key)))
{
services.AddKeyedSingleton(factory.Key, (sp, _) => factory.Value(sp).AsDurableAgentProxy(sp));
}
}
}
///
/// Ensures that the core durable services are registered only once, regardless of how many
/// times the configuration methods are called.
///
private static void EnsureDurableServicesRegistered(
IServiceCollection services,
DurableOptions sharedOptions,
Action? workerBuilder,
Action? clientBuilder)
{
// Use a marker to ensure we only register core services once
if (services.Any(d => d.ServiceType == typeof(DurableServicesMarker)))
{
return;
}
services.AddSingleton();
services.TryAddSingleton();
// Configure Durable Task Worker - capture sharedOptions reference in closure.
// The options object is populated by all Configure* calls before the worker starts.
if (workerBuilder is not null)
{
services.AddDurableTaskWorker(builder =>
{
workerBuilder?.Invoke(builder);
builder.AddTasks(registry => RegisterTasksFromOptions(registry, sharedOptions));
});
}
// Configure Durable Task Client
if (clientBuilder is not null)
{
services.AddDurableTaskClient(clientBuilder);
services.TryAddSingleton();
services.TryAddSingleton();
}
// Register workflow and agent services
services.TryAddSingleton();
// Register agent factories resolver - returns factories from the shared options
services.TryAddSingleton(
sp => sp.GetRequiredService().Agents.GetAgentFactories());
// Register DurableAgentsOptions resolver
services.TryAddSingleton(sp => sp.GetRequiredService().Agents);
}
private static void RegisterTasksFromOptions(DurableTaskRegistry registry, DurableOptions durableOptions)
{
// Build registrations for all workflows including sub-workflows
List registrations = [];
HashSet registeredActivities = [];
HashSet registeredOrchestrations = [];
DurableWorkflowOptions workflowOptions = durableOptions.Workflows;
foreach (Workflow workflow in workflowOptions.Workflows.Values.ToList())
{
BuildWorkflowRegistrationRecursive(
workflow,
workflowOptions,
registrations,
registeredActivities,
registeredOrchestrations);
}
IReadOnlyDictionary> agentFactories =
durableOptions.Agents.GetAgentFactories();
// Register orchestrations and activities
foreach (WorkflowRegistrationInfo registration in registrations)
{
// Register with DurableWorkflowInput