mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: [Feature Branch] Add basic durable workflow support (#3648)
* Add basic durable workflow support. * PR feedback fixes * Add conditional edge sample. * PR feedback fixes. * Minor cleanup. * Minor cleanup * Minor formatting improvements. * Improve comments/documentation on the execution flow.
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
// 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>
|
||||
/// Shared JSON options that match the DurableDataConverter settings.
|
||||
/// </summary>
|
||||
private static readonly JsonSerializerOptions s_jsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
/// <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),
|
||||
SentMessages = context.SentMessages.ConvertAll(m => new SentMessageInfo
|
||||
{
|
||||
Message = m.Message,
|
||||
TypeName = m.TypeName
|
||||
})
|
||||
};
|
||||
|
||||
return JsonSerializer.Serialize(output, DurableWorkflowJsonContext.Default.DurableActivityOutput);
|
||||
}
|
||||
|
||||
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(), s_jsonOptions);
|
||||
}
|
||||
|
||||
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, s_jsonOptions)
|
||||
?? 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user