// 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 - the DataConverter handles serialization/deserialization registry.AddOrchestratorFunc, DurableWorkflowResult>( registration.OrchestrationName, (context, input) => RunWorkflowOrchestrationAsync(context, input, durableOptions)); foreach (ActivityRegistrationInfo activity in registration.Activities) { ExecutorBinding binding = activity.Binding; registry.AddActivityFunc( activity.ActivityName, (context, input) => DurableActivityExecutor.ExecuteAsync(binding, input)); } } // Register agent entities foreach (string agentName in agentFactories.Keys) { registry.AddEntity(AgentSessionId.ToEntityName(agentName)); } } private static void BuildWorkflowRegistrationRecursive( Workflow workflow, DurableWorkflowOptions workflowOptions, List registrations, HashSet registeredActivities, HashSet registeredOrchestrations) { string orchestrationName = WorkflowNamingHelper.ToOrchestrationFunctionName(workflow.Name!); if (!registeredOrchestrations.Add(orchestrationName)) { return; } registrations.Add(BuildWorkflowRegistration(workflow, registeredActivities)); // Process subworkflows recursively to register them as separate orchestrations foreach (SubworkflowBinding subworkflowBinding in workflow.ReflectExecutors() .Select(e => e.Value) .OfType()) { Workflow subWorkflow = subworkflowBinding.WorkflowInstance; workflowOptions.AddWorkflow(subWorkflow); BuildWorkflowRegistrationRecursive( subWorkflow, workflowOptions, registrations, registeredActivities, registeredOrchestrations); } } private static WorkflowRegistrationInfo BuildWorkflowRegistration( Workflow workflow, HashSet registeredActivities) { string orchestrationName = WorkflowNamingHelper.ToOrchestrationFunctionName(workflow.Name!); Dictionary executorBindings = workflow.ReflectExecutors(); List activities = []; foreach (KeyValuePair entry in executorBindings .Where(e => IsActivityBinding(e.Value))) { string executorName = WorkflowNamingHelper.GetExecutorName(entry.Key); string activityName = WorkflowNamingHelper.ToOrchestrationFunctionName(executorName); if (registeredActivities.Add(activityName)) { activities.Add(new ActivityRegistrationInfo(activityName, entry.Value)); } } return new WorkflowRegistrationInfo(orchestrationName, activities); } /// /// Returns for bindings that should be registered as Durable Task activities. /// (Durable Entities), (sub-orchestrations), /// and (human-in-the-loop via external events) use specialized dispatch /// and are excluded. /// private static bool IsActivityBinding(ExecutorBinding binding) => binding is not AIAgentBinding and not SubworkflowBinding and not RequestPortBinding; private static async Task RunWorkflowOrchestrationAsync( TaskOrchestrationContext context, DurableWorkflowInput workflowInput, DurableOptions durableOptions) { ILogger logger = context.CreateReplaySafeLogger("DurableWorkflow"); DurableWorkflowRunner runner = new(durableOptions); // ConfigureAwait(true) is required in orchestration code for deterministic replay. return await runner.RunWorkflowOrchestrationAsync(context, workflowInput, logger).ConfigureAwait(true); } private sealed record WorkflowRegistrationInfo(string OrchestrationName, List Activities); private sealed record ActivityRegistrationInfo(string ActivityName, ExecutorBinding Binding); /// /// Validates that an agent with the specified name has been registered. /// /// The service provider. /// The name of the agent to validate. /// /// Thrown when the agent dictionary is not registered in the service provider. /// /// /// Thrown when the agent with the specified name has not been registered. /// internal static void ValidateAgentIsRegistered(IServiceProvider services, string agentName) { IReadOnlyDictionary>? agents = services.GetService>>() ?? throw new InvalidOperationException( $"Durable agents have not been configured. Ensure {nameof(ConfigureDurableAgents)} has been called on the service collection."); if (!agents.ContainsKey(agentName)) { throw new AgentNotRegisteredException(agentName); } } }