mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
76fe7319e0
* feat: Refactor Handoff Orchestration and add HITL support * Change HandoffAgentExecutor to use factory-based instantiation * Extract shared request collection logic in AIAgentUnservicedRequestsCollector * Refactor HandoffAgentExecutor to use the "ContinueTurn" pattern as in AIAgentHostExecutor * fix: Remove '$' from exception strings
226 lines
11 KiB
C#
226 lines
11 KiB
C#
// 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.Collections.Generic;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Agents.AI.Workflows.Reflection;
|
|
using Microsoft.Shared.Diagnostics;
|
|
|
|
namespace Microsoft.Agents.AI.Workflows;
|
|
|
|
/// <summary>
|
|
/// Provides a base class for executors that maintain and manage state across multiple message handling operations.
|
|
/// </summary>
|
|
/// <typeparam name="TState">The type of state associated with this Executor.</typeparam>
|
|
public abstract class StatefulExecutor<TState> : Executor
|
|
{
|
|
private readonly Func<TState> _initialStateFactory;
|
|
|
|
private TState? _stateCache;
|
|
|
|
/// <summary>
|
|
/// Initializes the executor with a unique id and an initial value for the state.
|
|
/// </summary>
|
|
/// <param name="id">The unique identifier for this executor instance. Cannot be null or empty.</param>
|
|
/// <param name="initialStateFactory">A factory to initialize the state value to be used by the executor.</param>
|
|
/// <param name="options">Optional configuration settings for the executor. If null, default options are used.</param>
|
|
/// <param name="declareCrossRunShareable">true to declare that the executor's state can be shared across multiple runs; otherwise, false.</param>
|
|
protected StatefulExecutor(string id,
|
|
Func<TState> initialStateFactory,
|
|
StatefulExecutorOptions? options = null,
|
|
bool declareCrossRunShareable = false)
|
|
: base(id, options ?? new StatefulExecutorOptions(), declareCrossRunShareable)
|
|
{
|
|
this.Options = (StatefulExecutorOptions)base.Options;
|
|
this._initialStateFactory = Throw.IfNull(initialStateFactory);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
protected new StatefulExecutorOptions Options { get; }
|
|
|
|
private string DefaultStateKey => $"{this.GetType().Name}.State";
|
|
|
|
/// <summary>
|
|
/// Gets the key used to identify the executor's state.
|
|
/// </summary>
|
|
protected string StateKey => this.Options.StateKey ?? this.DefaultStateKey;
|
|
|
|
/// <summary>
|
|
/// Reads the state associated with this executor. If it is not initialized, it will be set to the initial state.
|
|
/// </summary>
|
|
/// <param name="context">The workflow context in which the executor executes.</param>
|
|
/// <param name="skipCache">Ignore the cached value, if any. State is not cached when running in Cross-Run Shareable
|
|
/// mode.</param>
|
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
|
|
/// The default is <see cref="CancellationToken.None"/>.</param>
|
|
/// <returns></returns>
|
|
protected async ValueTask<TState> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Queues up an update to the executor's state.
|
|
/// </summary>
|
|
/// <param name="state">The new value of state.</param>
|
|
/// <param name="context">The workflow context in which the executor executes.</param>
|
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
|
|
/// The default is <see cref="CancellationToken.None"/>.</param>
|
|
/// <returns></returns>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invokes an asynchronous operation that reads, updates, and persists workflow state associated with the specified
|
|
/// key.
|
|
/// </summary>
|
|
/// <param name="invocation">A delegate that receives the current state, workflow context, and cancellation token,
|
|
/// and returns the updated state asynchronously.</param>
|
|
/// <param name="context">The workflow context in which the executor executes.</param>
|
|
/// <param name="skipCache">Ignore the cached value, if any. State is not cached when running in Cross-Run Shareable
|
|
/// mode.</param>
|
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
|
|
/// The default is <see cref="CancellationToken.None"/>.</param>
|
|
/// <returns>A ValueTask that represents the asynchronous operation.</returns>
|
|
protected async ValueTask InvokeWithStateAsync(
|
|
Func<TState, IWorkflowContext, CancellationToken, ValueTask<TState?>> invocation,
|
|
IWorkflowContext context,
|
|
bool skipCache = false,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (!skipCache && !context.ConcurrentRunsEnabled)
|
|
{
|
|
if (this._stateCache is null)
|
|
{
|
|
this._stateCache = await context.ReadOrInitStateAsync(this.StateKey, this._initialStateFactory, this.Options.ScopeName, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc cref="IResettableExecutor.ResetAsync"/>
|
|
protected virtual ValueTask ResetAsync()
|
|
{
|
|
this._stateCache = this._initialStateFactory();
|
|
|
|
return default;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Provides a simple executor implementation that uses a single message handler function to process incoming messages,
|
|
/// and maintain state across invocations.
|
|
/// </summary>
|
|
/// <typeparam name="TState">The type of state associated with this Executor.</typeparam>
|
|
/// <typeparam name="TInput">The type of input message.</typeparam>
|
|
/// <param name="id">A unique identifier for the executor.</param>
|
|
/// <param name="initialStateFactory">A factory to initialize the state value to be used by the executor.</param>
|
|
/// <param name="options">Configuration options for the executor. If <c>null</c>, default options will be used.</param>
|
|
/// <param name="sentMessageTypes">Message types sent by the handler. Defaults to empty, and will filter out non-matching messages.</param>
|
|
/// <param name="outputTypes">Message types yielded as output by the handler. Defaults to empty.</param>
|
|
/// <param name="declareCrossRunShareable">Declare that this executor may be used simultaneously by multiple runs safely.</param>
|
|
public abstract class StatefulExecutor<TState, TInput>(string id,
|
|
Func<TState> initialStateFactory,
|
|
StatefulExecutorOptions? options = null,
|
|
IEnumerable<Type>? sentMessageTypes = null,
|
|
IEnumerable<Type>? outputTypes = null,
|
|
bool declareCrossRunShareable = false)
|
|
: StatefulExecutor<TState>(id, initialStateFactory, options, declareCrossRunShareable), IMessageHandler<TInput>
|
|
{
|
|
/// <inheritdoc/>
|
|
protected override ProtocolBuilder ConfigureProtocol(ProtocolBuilder protocolBuilder)
|
|
{
|
|
Func<TInput, IWorkflowContext, CancellationToken, ValueTask> handlerDelegate = this.HandleAsync;
|
|
|
|
return protocolBuilder.ConfigureRoutes(routeBuilder => routeBuilder.AddHandler(handlerDelegate))
|
|
.AddMethodAttributeTypes(handlerDelegate.Method)
|
|
.AddClassAttributeTypes(this.GetType())
|
|
.SendsMessageTypes(sentMessageTypes ?? [])
|
|
.YieldsOutputTypes(outputTypes ?? []);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public abstract ValueTask HandleAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken = default);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Provides a simple executor implementation that uses a single message handler function to process incoming messages,
|
|
/// and maintain state across invocations.
|
|
/// </summary>
|
|
/// <typeparam name="TState">The type of state associated with this Executor.</typeparam>
|
|
/// <typeparam name="TInput">The type of input message.</typeparam>
|
|
/// <typeparam name="TOutput">The type of output message.</typeparam>
|
|
/// <param name="id">A unique identifier for the executor.</param>
|
|
/// <param name="initialStateFactory">A factory to initialize the state value to be used by the executor.</param>
|
|
/// <param name="options">Configuration options for the executor. If <c>null</c>, default options will be used.</param>
|
|
/// <param name="sentMessageTypes">Message types sent by the handler. Defaults to empty, and will filter out non-matching messages.</param>
|
|
/// <param name="outputTypes">Message types yielded as output by the handler. Defaults to empty.</param>
|
|
/// <param name="declareCrossRunShareable">Declare that this executor may be used simultaneously by multiple runs safely.</param>
|
|
public abstract class StatefulExecutor<TState, TInput, TOutput>(string id,
|
|
Func<TState> initialStateFactory,
|
|
StatefulExecutorOptions? options = null,
|
|
IEnumerable<Type>? sentMessageTypes = null,
|
|
IEnumerable<Type>? outputTypes = null,
|
|
bool declareCrossRunShareable = false)
|
|
: StatefulExecutor<TState>(id, initialStateFactory, options, declareCrossRunShareable), IMessageHandler<TInput, TOutput>
|
|
where TOutput : notnull
|
|
{
|
|
/// <inheritdoc/>
|
|
protected override ProtocolBuilder ConfigureProtocol(ProtocolBuilder protocolBuilder)
|
|
{
|
|
Func<TInput, IWorkflowContext, CancellationToken, ValueTask<TOutput>> handlerDelegate = this.HandleAsync;
|
|
return protocolBuilder.ConfigureRoutes(routeBuilder => routeBuilder.AddHandler(handlerDelegate))
|
|
.AddMethodAttributeTypes(handlerDelegate.Method)
|
|
.AddClassAttributeTypes(this.GetType())
|
|
.SendsMessageTypes(sentMessageTypes ?? [])
|
|
.YieldsOutputTypes(outputTypes ?? []);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public abstract ValueTask<TOutput> HandleAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken = default);
|
|
}
|