// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows;
namespace Microsoft.Agents.AI.DurableTask.Workflows;
///
/// Analyzes workflow structure to extract executor metadata and build graph information
/// for message-driven execution.
///
internal static class WorkflowAnalyzer
{
private const string AgentExecutorTypeName = "AIAgentHostExecutor";
private const string AgentAssemblyPrefix = "Microsoft.Agents.AI";
private const string ExecutorTypePrefix = "Executor";
///
/// Analyzes a workflow instance and returns a list of executors with their metadata.
///
/// The workflow instance to analyze.
/// A list of executor information in workflow order.
internal static List GetExecutorsFromWorkflowInOrder(Workflow workflow)
{
ArgumentNullException.ThrowIfNull(workflow);
return workflow.ReflectExecutors()
.Select(kvp => CreateExecutorInfo(kvp.Key, kvp.Value))
.ToList();
}
///
/// Builds the workflow graph information needed for message-driven execution.
///
///
///
/// Extracts routing information including successors, predecessors, edge conditions,
/// and output types. Supports cyclic workflows through message-driven superstep execution.
///
///
/// The returned is consumed by DurableEdgeMap
/// to build the runtime routing layer:
/// Successors become IDurableEdgeRouter instances,
/// Predecessors become fan-in counts, and
/// EdgeConditions / ExecutorOutputTypes are passed into
/// DurableDirectEdgeRouter for conditional routing with typed deserialization.
///
///
/// The workflow instance to analyze.
/// A graph info object containing routing information.
internal static WorkflowGraphInfo BuildGraphInfo(Workflow workflow)
{
ArgumentNullException.ThrowIfNull(workflow);
Dictionary executors = workflow.ReflectExecutors();
WorkflowGraphInfo graphInfo = new()
{
StartExecutorId = workflow.StartExecutorId
};
InitializeExecutorMappings(graphInfo, executors);
PopulateGraphFromEdges(graphInfo, workflow.Edges);
return graphInfo;
}
///
/// Determines whether the specified executor type is an agentic executor.
///
/// The executor type to check.
/// true if the executor is an agentic executor; otherwise, false.
internal static bool IsAgentExecutorType(Type executorType)
{
string typeName = executorType.FullName ?? executorType.Name;
string assemblyName = executorType.Assembly.GetName().Name ?? string.Empty;
return typeName.Contains(AgentExecutorTypeName, StringComparison.OrdinalIgnoreCase)
&& assemblyName.Contains(AgentAssemblyPrefix, StringComparison.OrdinalIgnoreCase);
}
///
/// Creates a from an executor binding.
///
/// The unique identifier of the executor.
/// The executor binding containing type and configuration information.
/// A new instance with extracted metadata.
private static WorkflowExecutorInfo CreateExecutorInfo(string executorId, ExecutorBinding binding)
{
bool isAgentic = IsAgentExecutorType(binding.ExecutorType);
RequestPort? requestPort = (binding is RequestPortBinding rpb) ? rpb.Port : null;
Workflow? subWorkflow = (binding is SubworkflowBinding swb) ? swb.WorkflowInstance : null;
return new WorkflowExecutorInfo(executorId, isAgentic, requestPort, subWorkflow);
}
///
/// Initializes the graph info with empty collections for each executor.
///
/// The graph info to initialize.
/// The dictionary of executor bindings.
private static void InitializeExecutorMappings(WorkflowGraphInfo graphInfo, Dictionary executors)
{
foreach ((string executorId, ExecutorBinding binding) in executors)
{
graphInfo.Successors[executorId] = [];
graphInfo.Predecessors[executorId] = [];
graphInfo.ExecutorOutputTypes[executorId] = GetExecutorOutputType(binding.ExecutorType);
}
}
///
/// Populates the graph info with successor/predecessor relationships and edge conditions.
///
/// The graph info to populate.
/// The dictionary of edges grouped by source executor ID.
private static void PopulateGraphFromEdges(WorkflowGraphInfo graphInfo, Dictionary> edges)
{
foreach ((string sourceId, HashSet edgeSet) in edges)
{
List successors = graphInfo.Successors[sourceId];
foreach (Edge edge in edgeSet)
{
AddSuccessorsFromEdge(graphInfo, sourceId, edge, successors);
TryAddEdgeCondition(graphInfo, edge);
}
}
}
///
/// Adds successor relationships from an edge to the graph info.
///
/// The graph info to update.
/// The source executor ID.
/// The edge containing connection information.
/// The list of successors to append to.
private static void AddSuccessorsFromEdge(
WorkflowGraphInfo graphInfo,
string sourceId,
Edge edge,
List successors)
{
foreach (string sinkId in edge.Data.Connection.SinkIds)
{
if (!graphInfo.Successors.ContainsKey(sinkId))
{
continue;
}
successors.Add(sinkId);
graphInfo.Predecessors[sinkId].Add(sourceId);
}
}
///
/// Extracts and adds an edge condition to the graph info if present.
///
/// The graph info to update.
/// The edge that may contain a condition.
private static void TryAddEdgeCondition(WorkflowGraphInfo graphInfo, Edge edge)
{
DirectEdgeData? directEdge = edge.DirectEdgeData;
if (directEdge?.Condition is not null)
{
graphInfo.EdgeConditions[(directEdge.SourceId, directEdge.SinkId)] = directEdge.Condition;
}
}
///
/// Extracts the output type from an executor type by walking the inheritance chain.
///
/// The executor type to analyze.
///
/// The TOutput type for Executor<TInput, TOutput>,
/// or null for Executor<TInput> (void output) or non-executor types.
///
private static Type? GetExecutorOutputType(Type executorType)
{
Type? currentType = executorType;
while (currentType is not null)
{
Type? outputType = TryExtractOutputTypeFromGeneric(currentType);
if (outputType is not null || IsVoidExecutorType(currentType))
{
return outputType;
}
currentType = currentType.BaseType;
}
return null;
}
///
/// Attempts to extract the output type from a generic executor type.
///
/// The type to inspect.
/// The TOutput type if this is an Executor<TInput, TOutput>; otherwise, null.
private static Type? TryExtractOutputTypeFromGeneric(Type type)
{
if (!type.IsGenericType)
{
return null;
}
Type genericDefinition = type.GetGenericTypeDefinition();
Type[] genericArgs = type.GetGenericArguments();
bool isExecutorType = genericDefinition.Name.StartsWith(ExecutorTypePrefix, StringComparison.Ordinal);
if (!isExecutorType)
{
return null;
}
// Executor - return TOutput
if (genericArgs.Length == 2)
{
return genericArgs[1];
}
return null;
}
///
/// Determines whether the type is a void-returning executor (Executor<TInput>).
///
/// The type to check.
/// true if this is an Executor with a single type parameter; otherwise, false.
private static bool IsVoidExecutorType(Type type)
{
if (!type.IsGenericType)
{
return false;
}
Type genericDefinition = type.GetGenericTypeDefinition();
Type[] genericArgs = type.GetGenericArguments();
// Executor with 1 type parameter indicates void return
return genericArgs.Length == 1
&& genericDefinition.Name.StartsWith(ExecutorTypePrefix, StringComparison.Ordinal);
}
}