// Copyright (c) Microsoft. All rights reserved. // How WorkflowGraphInfo maps to DurableEdgeMap at runtime. // For a workflow like below: // // [A] ──► [B] ──► [C] ──► [E] // │ ▲ // └──► [D] ──────┘ // (condition: x => x.NeedsReview) // // WorkflowGraphInfo DurableEdgeMap // ┌──────────────────────────┐ ┌──────────────────────────────────────┐ // │ Successors: │ │ _routersBySource: │ // │ A → [B] │──constructs──►│ A → [DirectRouter(A→B)] │ // │ B → [C, D] │ │ B → [FanOutRouter([C, D])] │ // │ C → [E] │ │ C → [DirectRouter(C→E)] │ // │ D → [E] │ │ D → [DirectRouter(D→E)] │ // └──────────────────────────┘ │ │ // ┌──────────────────────────┐ │ _predecessorCounts: │ // │ Predecessors: │ │ A → 0 │ // │ E → [C, D] (fan-in!) │──constructs──►│ B → 1, C → 1, D → 1 │ // └──────────────────────────┘ │ E → 2 ◄── IsFanInExecutor = true │ // └──────────────────────────────────────┘ // // Usage during superstep execution (continuing the example): // // 1. EnqueueInitialInput(msg) ──► MessageQueues["A"].Enqueue(envelope) // // 2. After B completes, RouteMessage("B", resultB) ──► _routersBySource["B"] // │ // ▼ // FanOutRouter (B has 2 successors) // ├─► DirectRouter(B→C) ──► no condition ──► enqueue to C // └─► DirectRouter(B→D) ──► evaluate x => x.NeedsReview ──► enqueue to D (or skip) // // 3. Before superstep 4, IsFanInExecutor("E") returns true (count=2) // → CollectExecutorInputs aggregates C and D results into ["resultC","resultD"] using Microsoft.Extensions.Logging; namespace Microsoft.Agents.AI.DurableTask.Workflows.EdgeRouters; /// /// Manages message routing through workflow edges for durable orchestrations. /// /// /// /// This is the durable equivalent of EdgeMap in the in-process runner. /// It is constructed from (produced by ) /// and converts the static graph structure into an active routing layer used during superstep execution. /// /// /// What it stores: /// /// /// _routersBySource — For each source executor, a list of instances /// that know how to deliver messages to successor executors. When a source has multiple successors, a single /// wraps the individual instances. /// _predecessorCounts — The number of predecessors for each executor, used to detect /// fan-in points where multiple incoming messages should be aggregated before execution. /// _startExecutorId — The entry-point executor that receives the initial workflow input. /// /// /// How it is used during execution: /// /// /// seeds the start executor's queue before the first superstep. /// After each superstep, DurableWorkflowRunner.RouteOutputToSuccessors calls /// which looks up the routers for the completed executor and forwards the /// result to successor queues. Each router may evaluate an edge condition before enqueueing. /// is checked during input collection to decide whether /// to aggregate multiple queued messages into a single JSON array before dispatching. /// /// internal sealed class DurableEdgeMap { private readonly Dictionary> _routersBySource = []; private readonly Dictionary _predecessorCounts = []; private readonly string _startExecutorId; /// /// Initializes a new instance of from workflow graph info. /// /// The workflow graph information containing routing structure. internal DurableEdgeMap(WorkflowGraphInfo graphInfo) { ArgumentNullException.ThrowIfNull(graphInfo); this._startExecutorId = graphInfo.StartExecutorId; // Build edge routers for each source executor foreach (KeyValuePair> entry in graphInfo.Successors) { string sourceId = entry.Key; List successorIds = entry.Value; if (successorIds.Count == 0) { continue; } graphInfo.ExecutorOutputTypes.TryGetValue(sourceId, out Type? sourceOutputType); List routers = []; foreach (string sinkId in successorIds) { graphInfo.EdgeConditions.TryGetValue((sourceId, sinkId), out Func? condition); routers.Add(new DurableDirectEdgeRouter(sourceId, sinkId, condition, sourceOutputType)); } // If multiple successors, wrap in a fan-out router if (routers.Count > 1) { this._routersBySource[sourceId] = [new DurableFanOutEdgeRouter(sourceId, routers)]; } else { this._routersBySource[sourceId] = routers; } } // Store predecessor counts for fan-in detection foreach (KeyValuePair> entry in graphInfo.Predecessors) { this._predecessorCounts[entry.Key] = entry.Value.Count; } } /// /// Routes a message from a source executor to its successors. /// /// /// Called by DurableWorkflowRunner.RouteOutputToSuccessors after each superstep. /// Wraps the message in a and delegates to the /// appropriate (s) for the source executor. Each router /// may evaluate an edge condition and, if satisfied, enqueue the envelope into the /// target executor's message queue for the next superstep. /// /// The source executor ID. /// The serialized message to route. /// The type name of the message. /// The message queues to enqueue messages into. /// The logger for tracing. internal void RouteMessage( string sourceId, string message, string? inputTypeName, Dictionary> messageQueues, ILogger logger) { if (!this._routersBySource.TryGetValue(sourceId, out List? routers)) { return; } DurableMessageEnvelope envelope = DurableMessageEnvelope.Create(message, inputTypeName, sourceId); foreach (IDurableEdgeRouter router in routers) { router.RouteMessage(envelope, messageQueues, logger); } } /// /// Enqueues the initial workflow input to the start executor. /// /// The serialized initial input message. /// The message queues to enqueue into. /// /// This method is used only at workflow startup to provide input to the first executor. /// No input type hint is required because the start executor determines its expected input type from its own InputTypes configuration. /// internal void EnqueueInitialInput( string message, Dictionary> messageQueues) { DurableMessageEnvelope envelope = DurableMessageEnvelope.Create(message, inputTypeName: null); EnqueueMessage(messageQueues, this._startExecutorId, envelope); } /// /// Determines if an executor is a fan-in point (has multiple predecessors). /// /// The executor ID to check. /// true if the executor has multiple predecessors; otherwise, false. internal bool IsFanInExecutor(string executorId) { return this._predecessorCounts.TryGetValue(executorId, out int count) && count > 1; } private static void EnqueueMessage( Dictionary> queues, string executorId, DurableMessageEnvelope envelope) { if (!queues.TryGetValue(executorId, out Queue? queue)) { queue = new Queue(); queues[executorId] = queue; } queue.Enqueue(envelope); } }