// Copyright (c) Microsoft. All rights reserved. using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization.Metadata; using Microsoft.Agents.AI.DurableTask.State; using Microsoft.DurableTask; using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Worker; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.Agents.AI.DurableTask; /// /// Agent-specific extension methods for the class. /// 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 the Durable Agents services via the service collection. /// /// The service collection. /// A delegate to configure the durable agents. /// A delegate to configure the Durable Task worker. /// A delegate to configure the Durable Task client. /// The service collection. public static IServiceCollection ConfigureDurableAgents( this IServiceCollection services, Action configure, Action? workerBuilder = null, Action? clientBuilder = null) { ArgumentNullException.ThrowIfNull(configure); DurableAgentsOptions options = services.ConfigureDurableAgents(configure); // A worker is required to run the agent entities services.AddDurableTaskWorker(builder => { workerBuilder?.Invoke(builder); builder.AddTasks(registry => { foreach (string name in options.GetAgentFactories().Keys) { registry.AddEntity(AgentSessionId.ToEntityName(name)); } }); }); // The client is needed to send notifications to the agent entities from non-orchestrator code if (clientBuilder != null) { services.AddDurableTaskClient(clientBuilder); } services.AddSingleton(); return services; } // This is internal because it's also used by Microsoft.Azure.Functions.DurableAgents, which is a friend assembly project. internal static DurableAgentsOptions ConfigureDurableAgents( this IServiceCollection services, Action configure) { DurableAgentsOptions options = new(); configure(options); var agents = options.GetAgentFactories(); // The agent dictionary contains the real agent factories, which is used by the agent entities. services.AddSingleton(agents); // The keyed services are used to resolve durable agent *proxy* instances for external clients. foreach (var factory in agents) { services.AddKeyedSingleton(factory.Key, (sp, _) => factory.Value(sp).AsDurableAgentProxy(sp)); } // A custom data converter is needed because the default chat client uses camel case for JSON properties, // which is not the default behavior for the Durable Task SDK. services.AddSingleton(); return options; } private sealed class DefaultDataConverter : DataConverter { // Use durable agent options (web defaults + camel case by default) with case-insensitive matching. // We clone to apply naming/casing tweaks while retaining source-generated metadata where available. private static readonly JsonSerializerOptions s_options = new(DurableAgentJsonUtilities.DefaultOptions) { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true, }; [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Fallback path uses reflection when metadata unavailable.")] [UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050", Justification = "Fallback path uses reflection when metadata unavailable.")] public override object? Deserialize(string? data, Type targetType) { if (data is null) { return null; } if (targetType == typeof(DurableAgentState)) { return JsonSerializer.Deserialize(data, DurableAgentStateJsonContext.Default.DurableAgentState); } JsonTypeInfo? typeInfo = s_options.GetTypeInfo(targetType); if (typeInfo is JsonTypeInfo typedInfo) { return JsonSerializer.Deserialize(data, typedInfo); } // Fallback (may trigger trimming/AOT warnings for unsupported dynamic types). return JsonSerializer.Deserialize(data, targetType, s_options); } [return: NotNullIfNotNull(nameof(value))] [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Fallback path uses reflection when metadata unavailable.")] [UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050", Justification = "Fallback path uses reflection when metadata unavailable.")] public override string? Serialize(object? value) { if (value is null) { return null; } if (value is DurableAgentState durableAgentState) { return JsonSerializer.Serialize(durableAgentState, DurableAgentStateJsonContext.Default.DurableAgentState); } JsonTypeInfo? typeInfo = s_options.GetTypeInfo(value.GetType()); if (typeInfo is JsonTypeInfo typedInfo) { return JsonSerializer.Serialize(value, typedInfo); } return JsonSerializer.Serialize(value, s_options); } } }