// 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); } }