// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Agents.Workflows.Checkpointing; using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.Workflows; /// /// A class that represents a workflow that can be executed. /// public class Workflow { /// /// A dictionary of executor providers, keyed by executor ID. /// internal Dictionary Registrations { get; init; } = []; internal Dictionary> Edges { get; init; } = []; internal HashSet OutputExecutors { get; init; } = []; /// /// Gets the collection of edges grouped by their source node identifier. /// public Dictionary> ReflectEdges() { return this.Edges.Keys.ToDictionary( keySelector: key => key, elementSelector: key => new HashSet(this.Edges[key].Select(RepresentationExtensions.ToEdgeInfo)) ); } internal Dictionary Ports { get; init; } = []; /// /// Gets the collection of external request ports, keyed by their ID. /// /// /// Each port has a corresponding entry in the dictionary. /// public Dictionary ReflectPorts() { return this.Ports.Keys.ToDictionary( keySelector: key => key, elementSelector: key => this.Ports[key].ToPortInfo() ); } /// /// Gets the identifier of the starting executor of the workflow. /// public string StartExecutorId { get; } /// /// Initializes a new instance of the class with the specified starting executor identifier /// and input type. /// /// The unique identifier of the starting executor for the workflow. Cannot be null. internal Workflow(string startExecutorId) { this.StartExecutorId = Throw.IfNull(startExecutorId); } /// /// Attempts to promote the current workflow to a type pre-checked instance that can handle input of type . /// /// The desired input type. /// A type-parametrized workflow definitely able to process input of type or /// if the workflow does not accept that type of input. /// internal async ValueTask?> TryPromoteAsync() { // Grab the start node, and make sure it has the right type? if (!this.Registrations.TryGetValue(this.StartExecutorId, out ExecutorRegistration? startRegistration)) { // TODO: This should never be able to be hit throw new InvalidOperationException($"Start executor with ID '{this.StartExecutorId}' is not bound."); } // TODO: Can we cache this somehow to avoid having to instantiate a new one when running? // Does that break some user expectations? Executor startExecutor = await startRegistration.ProviderAsync().ConfigureAwait(false); if (!startExecutor.InputTypes.Any(t => t.IsAssignableFrom(typeof(TInput)))) { // We have no handlers for the input type T, which means the built workflow will not be able to // process messages of the desired type return null; } return new Workflow(this.StartExecutorId) { Registrations = this.Registrations, Edges = this.Edges, Ports = this.Ports, OutputExecutors = this.OutputExecutors }; } private bool _needsReset; private bool IsResettable => this.Registrations.Values.All(registration => !registration.IsUnresettableSharedInstance); private async ValueTask TryResetExecutorRegistrationsAsync() { if (this.IsResettable) { foreach (ExecutorRegistration registration in this.Registrations.Values) { if (!await registration.TryResetAsync().ConfigureAwait(false)) { return false; } } this._needsReset = false; return true; } return false; } private object? _ownerToken; internal void TakeOwnership(object ownerToken) { object? maybeToken = Interlocked.CompareExchange(ref this._ownerToken, ownerToken, null); if (maybeToken == null && this._needsReset) { // There is no owner, but the workflow failed to reset on ownership release (because there are // shared executors). throw new InvalidOperationException( "Cannot reuse Workflow with shared Executor instances that do not implement IResettableExecutor." ); } if (maybeToken != null && !ReferenceEquals(maybeToken, ownerToken)) { // Someone else owns the workflow Debug.Assert(maybeToken != null); throw new InvalidOperationException("Cannot use a Workflow in multiple simultaneous (Streaming)Runs."); } this._needsReset = true; } [System.Diagnostics.CodeAnalysis.SuppressMessage("Maintainability", "CA1513:Use ObjectDisposedException throw helper", Justification = "Does not exist in NetFx 4.7.2")] internal async ValueTask ReleaseOwnershipAsync(object ownerToken) { if (this._ownerToken == null) { throw new InvalidOperationException("Attempting to release ownership of a Workflow that is not owned."); } if (!ReferenceEquals(this._ownerToken, this._ownerToken)) { throw new InvalidOperationException("Attempt to release ownership of a Workflow by non-owner."); } await this.TryResetExecutorRegistrationsAsync().ConfigureAwait(false); Interlocked.CompareExchange(ref this._ownerToken, null, ownerToken); this._ownerToken = null; } } /// /// Represents a workflow that operates on data of type . /// /// The type of input to the workflow. public class Workflow : Workflow { /// /// Initializes a new instance of the class with the specified starting executor identifier /// /// The unique identifier of the starting executor for the workflow. Cannot be null. public Workflow(string startExecutorId) : base(startExecutorId) { } /// /// Gets the type of input expected by the starting executor of the workflow. /// public Type InputType => typeof(T); }