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