Minor cleanups

This commit is contained in:
Shyju Krishnankutty
2026-01-24 17:04:17 -08:00
parent fcd7aa0b77
commit ebeeeeb421
27 changed files with 295 additions and 751 deletions
@@ -0,0 +1,34 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
namespace Microsoft.Agents.AI.DurableTask;
/// <summary>
/// Represents the result of a durable workflow orchestration execution.
/// </summary>
public sealed class DurableWorkflowRunResult
{
/// <summary>
/// Initializes a new instance of the <see cref="DurableWorkflowRunResult"/> class.
/// </summary>
/// <param name="workflowName">The name of the workflow that was executed.</param>
/// <param name="output">The output from the workflow execution.</param>
public DurableWorkflowRunResult(string workflowName, string output)
{
this.WorkflowName = workflowName;
this.Output = output;
}
/// <summary>
/// Gets the name of the workflow that was executed.
/// </summary>
[JsonPropertyName("workflowName")]
public string WorkflowName { get; }
/// <summary>
/// Gets the output from the workflow execution.
/// </summary>
[JsonPropertyName("output")]
public string Output { get; }
}
@@ -43,29 +43,32 @@ public class DurableWorkflowRunner
/// Runs a workflow orchestration.
/// </summary>
/// <param name="context">The task orchestration context.</param>
/// <param name="request">The workflow run request containing workflow name and input.</param>
/// <param name="input">The workflow run input containing workflow name and input.</param>
/// <param name="logger">The replay-safe logger for orchestration logging.</param>
/// <returns>A list containing the workflow execution result.</returns>
public async Task<List<string>> RunWorkflowOrchestrationAsync(
/// <returns>The result of the workflow execution.</returns>
/// <exception cref="InvalidOperationException">Thrown when the specified workflow is not found.</exception>
public async Task<string> RunWorkflowOrchestrationAsync(
TaskOrchestrationContext context,
DurableWorkflowRunRequest request,
string input,
ILogger logger)
{
ArgumentNullException.ThrowIfNull(context);
ArgumentNullException.ThrowIfNull(request);
ArgumentNullException.ThrowIfNull(input);
if (!this.Options.Workflows.TryGetValue(request.WorkflowName, out Workflow? workflow))
string orchestrationName = context.Name;
string workflowName = WorkflowNamingHelper.ToWorkflowName(orchestrationName);
if (!this.Options.Workflows.TryGetValue(workflowName, out Workflow? workflow))
{
throw new InvalidOperationException($"Workflow '{request.WorkflowName}' not found.");
throw new InvalidOperationException($"Workflow '{workflowName}' not found.");
}
logger.LogRunningWorkflow(workflow.Name);
string result = await this.ExecuteWorkflowLevelsAsync(context, workflow, request.Input, logger).ConfigureAwait(true);
string result = await this.ExecuteWorkflowLevelsAsync(context, workflow, input, logger).ConfigureAwait(true);
await CleanupWorkflowStateAsync(context).ConfigureAwait(true);
return [result];
return result;
}
/// <summary>
@@ -87,36 +90,23 @@ public class DurableWorkflowRunner
/// <returns>The extracted executor name.</returns>
protected static string ParseExecutorName(string activityFunctionName)
{
const string Prefix = "dafx-";
if (!activityFunctionName.StartsWith(Prefix, StringComparison.Ordinal))
if (!activityFunctionName.StartsWith(WorkflowNamingHelper.OrchestrationFunctionPrefix, StringComparison.Ordinal))
{
throw new InvalidOperationException(
$"Activity function name '{activityFunctionName}' does not start with '{Prefix}' prefix.");
$"Activity function name '{activityFunctionName}' does not start with '{WorkflowNamingHelper.OrchestrationFunctionPrefix}' prefix.");
}
string executorName = activityFunctionName[Prefix.Length..];
string executorName = activityFunctionName[WorkflowNamingHelper.OrchestrationFunctionPrefix.Length..];
if (string.IsNullOrEmpty(executorName))
{
throw new InvalidOperationException(
$"Activity function name '{activityFunctionName}' is not in the expected format '{Prefix}{{executorName}}'.");
$"Activity function name '{activityFunctionName}' is not in the expected format '{WorkflowNamingHelper.OrchestrationFunctionPrefix}{{executorName}}'.");
}
return executorName;
}
/// <summary>
/// Gets the base name from an executor ID by removing any GUID suffix.
/// </summary>
/// <param name="executorId">The executor ID.</param>
/// <returns>The base name without the GUID suffix.</returns>
protected static string GetBaseName(string executorId)
{
int underscoreIndex = executorId.IndexOf('_');
return underscoreIndex > 0 ? executorId[..underscoreIndex] : executorId;
}
/// <summary>
/// Serializes a list of strings to JSON.
/// </summary>
@@ -240,7 +230,8 @@ public class DurableWorkflowRunner
{
if (!executorInfo.IsAgenticExecutor)
{
string triggerName = $"dafx-{GetBaseName(executorInfo.ExecutorId)}";
string executorName = WorkflowNamingHelper.GetExecutorName(executorInfo.ExecutorId);
string triggerName = WorkflowNamingHelper.ToOrchestrationFunctionName(executorName);
return await context.CallActivityAsync<string>(triggerName, input).ConfigureAwait(true);
}
@@ -253,7 +244,7 @@ public class DurableWorkflowRunner
string input,
ILogger logger)
{
string agentName = GetBaseName(executorInfo.ExecutorId);
string agentName = WorkflowNamingHelper.GetExecutorName(executorInfo.ExecutorId);
DurableAIAgent agent = context.GetAgent(agentName);
if (agent is null)
@@ -0,0 +1,127 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Agents.AI.DurableTask;
/// <summary>
/// Provides helper methods for workflow naming conventions used in durable orchestrations.
/// </summary>
public static class WorkflowNamingHelper
{
/// <summary>
/// The prefix used for durable workflow orchestration function names.
/// </summary>
public const string OrchestrationFunctionPrefix = "dafx-";
/// <summary>
/// Converts a workflow name to its corresponding orchestration function name.
/// </summary>
/// <param name="workflowName">The workflow name.</param>
/// <returns>The orchestration function name.</returns>
/// <exception cref="ArgumentException">Thrown when the workflow name is null or empty.</exception>
public static string ToOrchestrationFunctionName(string workflowName)
{
ArgumentException.ThrowIfNullOrEmpty(workflowName);
return $"{OrchestrationFunctionPrefix}{workflowName}";
}
/// <summary>
/// Converts an orchestration function name back to its workflow name.
/// </summary>
/// <param name="orchestrationFunctionName">The orchestration function name.</param>
/// <returns>The workflow name.</returns>
/// <exception cref="ArgumentException">Thrown when the orchestration function name is null, empty, or doesn't have the expected prefix.</exception>
public static string ToWorkflowName(string orchestrationFunctionName)
{
ArgumentException.ThrowIfNullOrEmpty(orchestrationFunctionName);
if (!orchestrationFunctionName.StartsWith(OrchestrationFunctionPrefix, StringComparison.Ordinal))
{
throw new ArgumentException(
$"Orchestration function name '{orchestrationFunctionName}' does not start with the expected '{OrchestrationFunctionPrefix}' prefix.",
nameof(orchestrationFunctionName));
}
string workflowName = orchestrationFunctionName[OrchestrationFunctionPrefix.Length..];
if (string.IsNullOrEmpty(workflowName))
{
throw new ArgumentException(
$"Orchestration function name '{orchestrationFunctionName}' does not contain a workflow name after the prefix.",
nameof(orchestrationFunctionName));
}
return workflowName;
}
/// <summary>
/// Tries to convert an orchestration function name back to its workflow name.
/// </summary>
/// <param name="orchestrationFunctionName">The orchestration function name.</param>
/// <param name="workflowName">When this method returns, contains the workflow name if the conversion succeeded, or null if it failed.</param>
/// <returns><c>true</c> if the conversion succeeded; otherwise, <c>false</c>.</returns>
public static bool TryGetWorkflowName(string? orchestrationFunctionName, out string? workflowName)
{
workflowName = null;
if (string.IsNullOrEmpty(orchestrationFunctionName))
{
return false;
}
if (!orchestrationFunctionName.StartsWith(OrchestrationFunctionPrefix, StringComparison.Ordinal))
{
return false;
}
workflowName = orchestrationFunctionName[OrchestrationFunctionPrefix.Length..];
return !string.IsNullOrEmpty(workflowName);
}
/// <summary>
/// The suffix separator used when the workflow builder appends a GUID to executor IDs.
/// </summary>
/// <remarks>
/// For agentic executors, the workflow builder appends a GUID suffix to ensure uniqueness.
/// For example: "Physicist_8884e71021334ce49517fa2b17b1695b".
/// </remarks>
private const char ExecutorIdSuffixSeparator = '_';
/// <summary>
/// Extracts the executor name from an executor ID.
/// </summary>
/// <remarks>
/// <para>
/// For non-agentic executors, the executor ID is the same as the executor name (e.g., "OrderParser").
/// </para>
/// <para>
/// For agentic executors, the workflow builder appends a GUID suffix separated by an underscore
/// (e.g., "Physicist_8884e71021334ce49517fa2b17b1695b"). This method extracts just the name portion.
/// </para>
/// </remarks>
/// <param name="executorId">The executor ID, which may contain a GUID suffix.</param>
/// <returns>The executor name without any GUID suffix.</returns>
/// <exception cref="ArgumentException">Thrown when the executor ID is null or empty.</exception>
public static string GetExecutorName(string executorId)
{
ArgumentException.ThrowIfNullOrEmpty(executorId);
int separatorIndex = executorId.IndexOf(ExecutorIdSuffixSeparator);
return separatorIndex > 0 ? executorId[..separatorIndex] : executorId;
}
/// <summary>
/// Determines whether the executor ID contains a GUID suffix.
/// </summary>
/// <param name="executorId">The executor ID to check.</param>
/// <returns><c>true</c> if the executor ID contains a suffix; otherwise, <c>false</c>.</returns>
public static bool HasExecutorIdSuffix(string? executorId)
{
if (string.IsNullOrEmpty(executorId))
{
return false;
}
int separatorIndex = executorId.IndexOf(ExecutorIdSuffixSeparator);
return separatorIndex > 0 && separatorIndex < executorId.Length - 1;
}
}
@@ -75,10 +75,10 @@ internal static class BuiltInFunctions
[DurableClient] DurableTaskClient client,
FunctionContext context)
{
var workflowName = context.FunctionDefinition.Name.Replace("http-", "");
var orchestrationFunctionName = $"dafx-{workflowName}";
var workflowName = context.FunctionDefinition.Name.Replace(HttpPrefix, string.Empty);
var orchestrationFunctionName = WorkflowNamingHelper.ToOrchestrationFunctionName(workflowName);
var inputMessage = await req.ReadAsStringAsync();
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(orchestrationFunctionName, new DurableWorkflowRunRequest { WorkflowName = workflowName, Input = inputMessage! }); //OrchFunction"); // dafx-MyTestWorkflow");
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(orchestrationFunctionName, inputMessage);
HttpResponseData response = req.CreateResponse(HttpStatusCode.Accepted);
await response.WriteStringAsync($"InvokeWorkflowOrechstrtationAsync is invoked for {workflowName}. Orchestration instanceId: {instanceId}");
@@ -221,7 +221,7 @@ internal static class BuiltInFunctions
}
#pragma warning disable DURTASK001 // Durable analyzer complained
public static Task<List<string>> WorkflowRunnerOrchestrationAsync(TaskOrchestrationContext context, DurableWorkflowRunRequest input)
public static Task<DurableWorkflowRunResult> WorkflowRunnerOrchestrationAsync(TaskOrchestrationContext context, DurableWorkflowRunRequest input)
{
ArgumentNullException.ThrowIfNull(context);
@@ -236,7 +236,7 @@ internal static class BuiltInFunctions
var workFlowName = input.WorkflowName;
return Task.FromResult(new List<string>() { workFlowName });
return Task.FromResult(new DurableWorkflowRunResult(workFlowName, workFlowName));
}
#pragma warning restore DURTASK001
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.DurableTask;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Azure.Functions.Worker.Core.FunctionMetadata;
using Microsoft.Extensions.DependencyInjection;
@@ -39,7 +40,12 @@ internal static class CoreAgentConfigurationExtensions
/// <returns>The functions application builder for method chaining.</returns>
internal static FunctionsApplicationBuilder RegisterWorkflowServices(this FunctionsApplicationBuilder builder)
{
// Register FunctionsWorkflowRunner as a singleton
builder.Services.TryAddSingleton<FunctionsWorkflowRunner>();
// Also register it as DurableWorkflowRunner so orchestrations can resolve it by base type
builder.Services.TryAddSingleton<DurableWorkflowRunner>(sp => sp.GetRequiredService<FunctionsWorkflowRunner>());
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IFunctionMetadataTransformer, DurableWorkflowFunctionMetadataTransformer>());
return builder;
@@ -81,25 +81,24 @@ public static class DurableOptionsExtensions
private static void ConfigureWorkflowOrchestrations(FunctionsApplicationBuilder builder, DurableWorkflowOptions workflows)
{
// Registering orchestration functions and the workflow state entity.
builder.ConfigureDurableWorker().AddTasks(tasks =>
{
// Register the workflow state entity for durable state management
// Each orchestration instance gets its own entity keyed by instance ID
// Register the workflow state entity for shared state management within workflows.
tasks.AddEntity<WorkflowSharedStateEntity>(WorkflowSharedStateEntity.EntityName);
foreach (string workflowName in workflows.Workflows.Select(kp => kp.Key))
{
tasks.AddOrchestratorFunc<DurableWorkflowRunRequest, List<string>>(
$"dafx-{workflowName}",
string orchestrationFunctionName = WorkflowNamingHelper.ToOrchestrationFunctionName(workflowName);
tasks.AddOrchestratorFunc<string, string>(
orchestrationFunctionName,
async (orchestrationContext, request) =>
{
FunctionContext functionContext = orchestrationContext.GetFunctionContext()
?? throw new InvalidOperationException("FunctionContext is not available in the orchestration context.");
DurableWorkflowRunner runner = functionContext.InstanceServices.GetRequiredService<DurableWorkflowRunner>();
ILogger logger = orchestrationContext.CreateReplaySafeLogger($"dafx-orchestration-{workflowName}");
ILogger logger = orchestrationContext.CreateReplaySafeLogger(orchestrationFunctionName);
return await runner.RunWorkflowOrchestrationAsync(orchestrationContext, request, logger).ConfigureAwait(true);
});
@@ -61,7 +61,8 @@ internal sealed class DurableWorkflowFunctionMetadataTransformer : IFunctionMeta
{
if (executorInfos.TryGetValue(executorId, out ExecutorInfo? executorInfo))
{
string functionName = $"dafx-{executorId.Split("_")[0]}";
string executorName = WorkflowNamingHelper.GetExecutorName(executorId);
string functionName = WorkflowNamingHelper.ToOrchestrationFunctionName(executorName);
// Check if the executor type is an agent-related type
if (WorkflowHelper.IsAgentExecutorType(executorInfo.ExecutorType))