< Summary

Information
Class: NexusLabs.Needlr.AgentFramework.Workflows.GraphTopologyProvider
Assembly: NexusLabs.Needlr.AgentFramework.Workflows
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework.Workflows/GraphTopologyProvider.cs
Line coverage
97%
Covered lines: 82
Uncovered lines: 2
Coverable lines: 84
Total lines: 158
Line coverage: 97.6%
Branch coverage
100%
Covered branches: 40
Total branches: 40
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor()100%11100%
GetTopology(...)100%11100%
DiscoverTopology(...)100%404097.56%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework.Workflows/GraphTopologyProvider.cs

#LineLine coverage
 1using System.Collections.Concurrent;
 2using System.Reflection;
 3
 4using NexusLabs.Needlr.AgentFramework;
 5
 6namespace NexusLabs.Needlr.AgentFramework.Workflows;
 7
 8/// <summary>
 9/// Discovers and caches <see cref="GraphTopology"/> from attributes declared
 10/// on agent types. Cached per graph name for the lifetime of the provider.
 11/// </summary>
 12internal sealed class GraphTopologyProvider
 13{
 4014    private readonly ConcurrentDictionary<string, GraphTopology> _cache = new();
 15
 16    /// <summary>
 17    /// Gets the topology for the named graph, scanning assemblies on first access
 18    /// and caching the result for subsequent calls.
 19    /// </summary>
 20    public GraphTopology GetTopology(string graphName) =>
 6221        _cache.GetOrAdd(graphName, static name => DiscoverTopology(name));
 22
 23    private static GraphTopology DiscoverTopology(string graphName)
 24    {
 3125        Type? entryType = null;
 3126        GraphRoutingMode graphRoutingMode = GraphRoutingMode.Deterministic;
 3127        var edgeDetails = new List<GraphEdgeDetail>();
 3128        var joinModes = new Dictionary<Type, GraphJoinMode>();
 3129        var allTypes = new HashSet<Type>();
 3130        Func<IReadOnlyList<string>, string>? reducerFunc = null;
 3131        Type? reducerType = null;
 32
 572833        foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
 34        {
 35            Type[] types;
 566636            try { types = assembly.GetTypes(); }
 037            catch (ReflectionTypeLoadException) { continue; }
 038            catch (FileNotFoundException) { continue; }
 39
 107170440            foreach (var type in types)
 41            {
 106783642                foreach (var attr in type.GetCustomAttributes<AgentGraphEntryAttribute>())
 43                {
 89944                    if (string.Equals(attr.GraphName, graphName, StringComparison.Ordinal))
 45                    {
 3146                        entryType = type;
 3147                        graphRoutingMode = attr.RoutingMode;
 3148                        allTypes.Add(type);
 49                    }
 50                }
 51
 107161852                foreach (var attr in type.GetCustomAttributes<AgentGraphEdgeAttribute>())
 53                {
 279054                    if (string.Equals(attr.GraphName, graphName, StringComparison.Ordinal))
 55                    {
 10356                        edgeDetails.Add(new GraphEdgeDetail(
 10357                            type,
 10358                            attr.TargetAgentType,
 10359                            attr.Condition,
 10360                            attr.IsRequired,
 10361                            attr.HasNodeRoutingMode ? attr.NodeRoutingMode : null));
 10362                        allTypes.Add(type);
 10363                        allTypes.Add(attr.TargetAgentType);
 64                    }
 65                }
 66
 106734067                foreach (var attr in type.GetCustomAttributes<AgentGraphNodeAttribute>())
 68                {
 65169                    if (string.Equals(attr.GraphName, graphName, StringComparison.Ordinal))
 70                    {
 2671                        joinModes[type] = attr.JoinMode;
 72                    }
 73                }
 74
 106628675                foreach (var attr in type.GetCustomAttributes<AgentGraphReducerAttribute>())
 76                {
 12477                    if (!string.Equals(attr.GraphName, graphName, StringComparison.Ordinal))
 78                        continue;
 579                    if (string.IsNullOrWhiteSpace(attr.ReducerMethod))
 80                        continue;
 81
 582                    var method = type.GetMethod(
 583                        attr.ReducerMethod,
 584                        BindingFlags.Public | BindingFlags.Static,
 585                        null,
 586                        [typeof(IReadOnlyList<string>)],
 587                        null);
 88
 589                    if (method is not null && method.ReturnType == typeof(string))
 90                    {
 591                        reducerType = type;
 592                        var captured = method;
 1093                        reducerFunc = inputs => (string)captured.Invoke(null, [inputs])!;
 94                    }
 95                }
 96            }
 97        }
 98
 3199        var incomingTypes = new Dictionary<Type, List<Type>>();
 31100        var inboundEdges = new Dictionary<Type, List<Type>>();
 31101        var outboundEdges = new Dictionary<Type, List<Type>>();
 102
 290103        foreach (var type in allTypes)
 104        {
 114105            incomingTypes[type] = [];
 114106            inboundEdges[type] = [];
 114107            outboundEdges[type] = [];
 108        }
 109
 268110        foreach (var edge in edgeDetails)
 111        {
 103112            incomingTypes[edge.Target].Add(edge.Source);
 103113            inboundEdges[edge.Target].Add(edge.Source);
 103114            outboundEdges[edge.Source].Add(edge.Target);
 115        }
 116
 31117        var outgoingEdgesBySource = new Dictionary<Type, List<GraphEdgeDetail>>();
 268118        foreach (var edge in edgeDetails)
 119        {
 103120            if (!outgoingEdgesBySource.TryGetValue(edge.Source, out var list))
 121            {
 75122                list = [];
 75123                outgoingEdgesBySource[edge.Source] = list;
 124            }
 125
 103126            list.Add(edge);
 127        }
 128
 31129        var effectiveRoutingModes = new Dictionary<Type, GraphRoutingMode>();
 212130        foreach (var (sourceType, sourceEdges) in outgoingEdgesBySource)
 131        {
 75132            var nodeOverride = sourceEdges
 102133                .Select(e => e.NodeRoutingModeOverride)
 177134                .FirstOrDefault(m => m is not null);
 75135            effectiveRoutingModes[sourceType] = nodeOverride ?? graphRoutingMode;
 136        }
 137
 31138        var edgeIsRequired = new Dictionary<(Type Source, Type Target), bool>();
 268139        foreach (var edge in edgeDetails)
 140        {
 103141            edgeIsRequired[(edge.Source, edge.Target)] = edge.IsRequired;
 142        }
 143
 31144        return new GraphTopology(
 31145            entryType,
 31146            allTypes,
 31147            joinModes,
 31148            incomingTypes,
 228149            inboundEdges.ToDictionary(kv => kv.Key, kv => (IReadOnlyList<Type>)kv.Value),
 228150            outboundEdges.ToDictionary(kv => kv.Key, kv => (IReadOnlyList<Type>)kv.Value),
 31151            graphRoutingMode,
 31152            outgoingEdgesBySource,
 31153            effectiveRoutingModes,
 31154            edgeIsRequired,
 31155            reducerFunc,
 31156            reducerType);
 157    }
 158}