Files
agent-framework/dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableActivityExecutor.cs
T
2026-02-17 14:41:11 -08:00

153 lines
5.6 KiB
C#

// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Agents.AI.Workflows.Checkpointing;
namespace Microsoft.Agents.AI.DurableTask.Workflows;
/// <summary>
/// Executes workflow activities by invoking executor bindings and handling serialization.
/// </summary>
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Workflow and executor types are registered at startup.")]
[UnconditionalSuppressMessage("Trimming", "IL2057", Justification = "Workflow and executor types are registered at startup.")]
[UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Workflow and executor types are registered at startup.")]
internal static class DurableActivityExecutor
{
/// <summary>
/// Executes an activity using the provided executor binding.
/// </summary>
/// <param name="binding">The executor binding to invoke.</param>
/// <param name="input">The serialized input string.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>The serialized activity output.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="binding"/> is null.</exception>
/// <exception cref="InvalidOperationException">Thrown when the executor factory is not configured.</exception>
internal static async Task<string> ExecuteAsync(
ExecutorBinding binding,
string input,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(binding);
if (binding.FactoryAsync is null)
{
throw new InvalidOperationException($"Executor binding for '{binding.Id}' does not have a factory configured.");
}
DurableActivityInput? inputWithState = TryDeserializeActivityInput(input);
string executorInput = inputWithState?.Input ?? input;
Dictionary<string, string> sharedState = inputWithState?.State ?? [];
Executor executor = await binding.FactoryAsync(binding.Id).ConfigureAwait(false);
Type inputType = ResolveInputType(inputWithState?.InputTypeName, executor.InputTypes);
object typedInput = DeserializeInput(executorInput, inputType);
DurableActivityContext workflowContext = new(sharedState, executor);
object? result = await executor.ExecuteAsync(
typedInput,
new TypeId(inputType),
workflowContext,
cancellationToken).ConfigureAwait(false);
return SerializeActivityOutput(result, workflowContext);
}
private static string SerializeActivityOutput(object? result, DurableActivityContext context)
{
DurableActivityOutput output = new()
{
Result = SerializeResult(result),
StateUpdates = context.StateUpdates,
ClearedScopes = [.. context.ClearedScopes],
Events = context.Events.ConvertAll(SerializeEvent),
SentMessages = context.SentMessages,
HaltRequested = context.HaltRequested
};
return JsonSerializer.Serialize(output, DurableWorkflowJsonContext.Default.DurableActivityOutput);
}
/// <summary>
/// Serializes a workflow event with type information for proper deserialization.
/// </summary>
private static string SerializeEvent(WorkflowEvent evt)
{
Type eventType = evt.GetType();
TypedPayload wrapper = new()
{
TypeName = eventType.AssemblyQualifiedName,
Data = JsonSerializer.Serialize(evt, eventType, DurableSerialization.Options)
};
return JsonSerializer.Serialize(wrapper, DurableWorkflowJsonContext.Default.TypedPayload);
}
private static string SerializeResult(object? result)
{
if (result is null)
{
return string.Empty;
}
if (result is string str)
{
return str;
}
return JsonSerializer.Serialize(result, result.GetType(), DurableSerialization.Options);
}
private static DurableActivityInput? TryDeserializeActivityInput(string input)
{
try
{
return JsonSerializer.Deserialize(input, DurableWorkflowJsonContext.Default.DurableActivityInput);
}
catch (JsonException)
{
return null;
}
}
private static object DeserializeInput(string input, Type targetType)
{
if (targetType == typeof(string))
{
return input;
}
return JsonSerializer.Deserialize(input, targetType, DurableSerialization.Options)
?? throw new InvalidOperationException($"Failed to deserialize input to type '{targetType.Name}'.");
}
private static Type ResolveInputType(string? inputTypeName, ISet<Type> supportedTypes)
{
if (string.IsNullOrEmpty(inputTypeName))
{
return supportedTypes.FirstOrDefault() ?? typeof(string);
}
Type? matchedType = supportedTypes.FirstOrDefault(t =>
t.AssemblyQualifiedName == inputTypeName ||
t.FullName == inputTypeName ||
t.Name == inputTypeName);
if (matchedType is not null)
{
return matchedType;
}
Type? loadedType = Type.GetType(inputTypeName);
// Fall back if type is string but executor doesn't support string
if (loadedType == typeof(string) && !supportedTypes.Contains(typeof(string)))
{
return supportedTypes.FirstOrDefault() ?? typeof(string);
}
return loadedType ?? supportedTypes.FirstOrDefault() ?? typeof(string);
}
}