// Copyright (c) Microsoft. All rights reserved.
#pragma warning disable CS0618 // Type or member is obsolete - Internal use of obsolete types for backward compatibility
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Reflection;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI.Workflows;
///
/// Provides a base class for executors that maintain and manage state across multiple message handling operations.
///
/// The type of state associated with this Executor.
public abstract class StatefulExecutor : Executor
{
private readonly Func _initialStateFactory;
private TState? _stateCache;
///
/// Initializes the executor with a unique id and an initial value for the state.
///
/// The unique identifier for this executor instance. Cannot be null or empty.
/// A factory to initialize the state value to be used by the executor.
/// Optional configuration settings for the executor. If null, default options are used.
/// true to declare that the executor's state can be shared across multiple runs; otherwise, false.
protected StatefulExecutor(string id,
Func initialStateFactory,
StatefulExecutorOptions? options = null,
bool declareCrossRunShareable = false)
: base(id, options ?? new StatefulExecutorOptions(), declareCrossRunShareable)
{
this.Options = (StatefulExecutorOptions)base.Options;
this._initialStateFactory = Throw.IfNull(initialStateFactory);
}
///
protected new StatefulExecutorOptions Options { get; }
private string DefaultStateKey => $"{this.GetType().Name}.State";
///
/// Gets the key used to identify the executor's state.
///
protected string StateKey => this.Options.StateKey ?? this.DefaultStateKey;
///
/// Reads the state associated with this executor. If it is not initialized, it will be set to the initial state.
///
/// The workflow context in which the executor executes.
/// Ignore the cached value, if any. State is not cached when running in Cross-Run Shareable
/// mode.
/// The to monitor for cancellation requests.
/// The default is .
///
protected async ValueTask ReadStateAsync(IWorkflowContext context, bool skipCache = false, CancellationToken cancellationToken = default)
{
if (!skipCache && this._stateCache is not null)
{
return this._stateCache;
}
TState? state = await context.ReadOrInitStateAsync(this.StateKey, this._initialStateFactory, this.Options.ScopeName, cancellationToken)
.ConfigureAwait(false);
if (!context.ConcurrentRunsEnabled)
{
this._stateCache = state;
}
return state;
}
///
/// Queues up an update to the executor's state.
///
/// The new value of state.
/// The workflow context in which the executor executes.
/// The to monitor for cancellation requests.
/// The default is .
///
protected ValueTask QueueStateUpdateAsync(TState state, IWorkflowContext context, CancellationToken cancellationToken = default)
{
if (!context.ConcurrentRunsEnabled)
{
this._stateCache = state;
}
return context.QueueStateUpdateAsync(this.StateKey, state, this.Options.ScopeName, cancellationToken);
}
///
/// Invokes an asynchronous operation that reads, updates, and persists workflow state associated with the specified
/// key.
///
/// A delegate that receives the current state, workflow context, and cancellation token,
/// and returns the updated state asynchronously.
/// The workflow context in which the executor executes.
/// Ignore the cached value, if any. State is not cached when running in Cross-Run Shareable
/// mode.
/// The to monitor for cancellation requests.
/// The default is .
/// A ValueTask that represents the asynchronous operation.
protected async ValueTask InvokeWithStateAsync(
Func> invocation,
IWorkflowContext context,
bool skipCache = false,
CancellationToken cancellationToken = default)
{
if (!skipCache && !context.ConcurrentRunsEnabled)
{
TState newState = await invocation(this._stateCache ?? this._initialStateFactory(),
context,
cancellationToken).ConfigureAwait(false)
?? this._initialStateFactory();
await context.QueueStateUpdateAsync(this.StateKey,
newState,
this.Options.ScopeName,
cancellationToken).ConfigureAwait(false);
this._stateCache = newState;
}
else
{
await context.InvokeWithStateAsync(invocation,
this.StateKey,
this._initialStateFactory,
this.Options.ScopeName,
cancellationToken)
.ConfigureAwait(false);
}
}
///
protected ValueTask ResetAsync()
{
this._stateCache = this._initialStateFactory();
return default;
}
}
///
/// Provides a simple executor implementation that uses a single message handler function to process incoming messages,
/// and maintain state across invocations.
///
/// The type of state associated with this Executor.
/// The type of input message.
/// A unique identifier for the executor.
/// A factory to initialize the state value to be used by the executor.
/// Configuration options for the executor. If null, default options will be used.
/// Declare that this executor may be used simultaneously by multiple runs safely.
public abstract class StatefulExecutor(string id, Func initialStateFactory, StatefulExecutorOptions? options = null, bool declareCrossRunShareable = false)
: StatefulExecutor(id, initialStateFactory, options, declareCrossRunShareable), IMessageHandler
{
///
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
routeBuilder.AddHandler(this.HandleAsync);
///
public abstract ValueTask HandleAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken = default);
}
///
/// Provides a simple executor implementation that uses a single message handler function to process incoming messages,
/// and maintain state across invocations.
///
/// The type of state associated with this Executor.
/// The type of input message.
/// The type of output message.
/// A unique identifier for the executor.
/// A factory to initialize the state value to be used by the executor.
/// Configuration options for the executor. If null, default options will be used.
/// Declare that this executor may be used simultaneously by multiple runs safely.
public abstract class StatefulExecutor(string id, Func initialStateFactory, StatefulExecutorOptions? options = null, bool declareCrossRunShareable = false)
: StatefulExecutor(id, initialStateFactory, options, declareCrossRunShareable), IMessageHandler
{
///
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
routeBuilder.AddHandler(this.HandleAsync);
///
public abstract ValueTask HandleAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken = default);
}