// 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);
}
}
}