// 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; /// /// Executes workflow activities by invoking executor bindings and handling serialization. /// [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 { /// /// Executes an activity using the provided executor binding. /// /// The executor binding to invoke. /// The serialized input string. /// A token to cancel the operation. /// The serialized activity output. /// Thrown when is null. /// Thrown when the executor factory is not configured. internal static async Task 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 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); } /// /// Serializes a workflow event with type information for proper deserialization. /// 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 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); } }