mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: [BREAKING] Unify ExecutorIsh and ExecutorRegistration, unify/simplify APIs (#1637)
* refactor: Unify ExecutorIsh and ExecutorRegistration => ExecutorBinding * Switch to more modern Record type-tree for Sum Types * Unify APIs for getting ExecutorBinding * Fix an issue where workflows consisting entirely of cross-run shareable executors which are not instance-resettable do not properly clear state when running non-concurrently. * feat: Simplify function-to-executor pattern * refactor: Normalize API naming
This commit is contained in:
committed by
GitHub
Unverified
parent
91c66f8a2c
commit
b25b0af49b
+3
-18
@@ -20,7 +20,9 @@ public static class Program
|
||||
private static async Task Main()
|
||||
{
|
||||
// Create the executors
|
||||
UppercaseExecutor uppercase = new();
|
||||
Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
|
||||
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
|
||||
|
||||
ReverseTextExecutor reverse = new();
|
||||
|
||||
// Build the workflow by connecting executors sequentially
|
||||
@@ -40,23 +42,6 @@ public static class Program
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// First executor: converts input text to uppercase.
|
||||
/// </summary>
|
||||
internal sealed class UppercaseExecutor() : Executor<string, string>("UppercaseExecutor")
|
||||
{
|
||||
/// <summary>
|
||||
/// Processes the input message by converting it to uppercase.
|
||||
/// </summary>
|
||||
/// <param name="message">The input text to convert</param>
|
||||
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
|
||||
/// The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>The input text converted to uppercase</returns>
|
||||
public override ValueTask<string> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default) =>
|
||||
ValueTask.FromResult(message.ToUpperInvariant()); // The return value will be sent as a message along an edge to subsequent executors
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Second executor: reverses the input text and completes the workflow.
|
||||
/// </summary>
|
||||
|
||||
@@ -40,7 +40,7 @@ public static class Program
|
||||
.Build();
|
||||
|
||||
// Step 2: Configure the sub-workflow as an executor for use in the parent workflow
|
||||
ExecutorIsh subWorkflowExecutor = subWorkflow.ConfigureSubWorkflow("TextProcessingSubWorkflow");
|
||||
ExecutorBinding subWorkflowExecutor = subWorkflow.BindAsExecutor("TextProcessingSubWorkflow");
|
||||
|
||||
// Step 3: Build a main workflow that uses the sub-workflow as an executor
|
||||
Console.WriteLine("Building main workflow that uses the sub-workflow as an executor...\n");
|
||||
|
||||
+1
-1
@@ -138,7 +138,7 @@ I cannot process this request as it appears to contain unsafe content.
|
||||
|
||||
## What You'll Learn
|
||||
|
||||
1. **How to mix executors and agents** - Understanding that both are treated as `ExecutorIsh` internally
|
||||
1. **How to mix executors and agents** - Understanding that both are treated as `ExecutorBinding` internally
|
||||
2. **When to use executors vs agents** - Executors for deterministic logic, agents for AI-powered decisions
|
||||
3. **How to process agent outputs** - Using executors to sync, format, or aggregate agent responses
|
||||
4. **Building complex pipelines** - Chaining multiple heterogeneous components together
|
||||
|
||||
+3
-3
@@ -19,12 +19,12 @@ internal sealed class WorkflowModelBuilder : IModelBuilder<Func<object?, bool>>
|
||||
Debug.WriteLine($"> CONNECT: {source.Id} => {target.Id}{(condition is null ? string.Empty : " (?)")}");
|
||||
|
||||
this.WorkflowBuilder.AddEdge(
|
||||
GetExecutorIsh(source),
|
||||
GetExecutorIsh(target),
|
||||
GetExecutorBinding(source),
|
||||
GetExecutorBinding(target),
|
||||
condition);
|
||||
}
|
||||
|
||||
private static ExecutorIsh GetExecutorIsh(IModeledAction action) =>
|
||||
private static ExecutorBinding GetExecutorBinding(IModeledAction action) =>
|
||||
action switch
|
||||
{
|
||||
RequestPortAction port => port.RequestPort,
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows.Specialized;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the workflow binding details for an AI agent, including configuration options for event emission.
|
||||
/// </summary>
|
||||
/// <param name="Agent">The AI agent.</param>
|
||||
/// <param name="EmitEvents">Specifies whether the agent should emit events. If null, the default behavior is applied.</param>
|
||||
public record AIAgentBinding(AIAgent Agent, bool EmitEvents = false)
|
||||
: ExecutorBinding(Throw.IfNull(Agent).Name ?? Throw.IfNull(Agent.Id),
|
||||
(_) => new(new AIAgentHostExecutor(Agent, EmitEvents)),
|
||||
typeof(AIAgentHostExecutor),
|
||||
Agent)
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override bool IsSharedInstance => false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool SupportsConcurrentSharedExecution => true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool SupportsResetting => false;
|
||||
}
|
||||
@@ -40,7 +40,7 @@ public static partial class AgentWorkflowBuilder
|
||||
// Create a builder that chains the agents together in sequence. The workflow simply begins
|
||||
// with the first agent in the sequence.
|
||||
WorkflowBuilder? builder = null;
|
||||
ExecutorIsh? previous = null;
|
||||
ExecutorBinding? previous = null;
|
||||
foreach (var agent in agents)
|
||||
{
|
||||
AgentRunStreamingExecutor agentExecutor = new(agent, includeInputInOutput: true);
|
||||
@@ -125,8 +125,8 @@ public static partial class AgentWorkflowBuilder
|
||||
// so that the final accumulator receives a single list of messages from each agent. Otherwise, the
|
||||
// accumulator would not be able to determine what came from what agent, as there's currently no
|
||||
// provenance tracking exposed in the workflow context passed to a handler.
|
||||
ExecutorIsh[] agentExecutors = (from agent in agents select (ExecutorIsh)new AgentRunStreamingExecutor(agent, includeInputInOutput: false)).ToArray();
|
||||
ExecutorIsh[] accumulators = [.. from agent in agentExecutors select (ExecutorIsh)new CollectChatMessagesExecutor($"Batcher/{agent.Id}")];
|
||||
ExecutorBinding[] agentExecutors = (from agent in agents select (ExecutorBinding)new AgentRunStreamingExecutor(agent, includeInputInOutput: false)).ToArray();
|
||||
ExecutorBinding[] accumulators = [.. from agent in agentExecutors select (ExecutorBinding)new CollectChatMessagesExecutor($"Batcher/{agent.Id}")];
|
||||
builder.AddFanOutEdge(start, targets: agentExecutors);
|
||||
for (int i = 0; i < agentExecutors.Length; i++)
|
||||
{
|
||||
@@ -141,7 +141,7 @@ public static partial class AgentWorkflowBuilder
|
||||
Func<string, string, ValueTask<ConcurrentEndExecutor>> endFactory =
|
||||
(string _, string __) => new(new ConcurrentEndExecutor(agentExecutors.Length, aggregator));
|
||||
|
||||
ExecutorIsh end = endFactory.ConfigureFactory(ConcurrentEndExecutor.ExecutorId);
|
||||
ExecutorBinding end = endFactory.BindExecutor(ConcurrentEndExecutor.ExecutorId);
|
||||
|
||||
builder.AddFanInEdge(end, sources: accumulators);
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ internal sealed record class ExecutorInfo(TypeId ExecutorType, string ExecutorId
|
||||
this.ExecutorType.IsMatch(executor.GetType())
|
||||
&& this.ExecutorId == executor.Id;
|
||||
|
||||
public bool IsMatch(ExecutorRegistration registration) =>
|
||||
this.ExecutorType.IsMatch(registration.ExecutorType)
|
||||
&& this.ExecutorId == registration.Id;
|
||||
public bool IsMatch(ExecutorBinding binding) =>
|
||||
this.ExecutorType.IsMatch(binding.ExecutorType)
|
||||
&& this.ExecutorId == binding.Id;
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ namespace Microsoft.Agents.AI.Workflows.Checkpointing;
|
||||
|
||||
internal static class RepresentationExtensions
|
||||
{
|
||||
public static ExecutorInfo ToExecutorInfo(this ExecutorRegistration registration)
|
||||
public static ExecutorInfo ToExecutorInfo(this ExecutorBinding binding)
|
||||
{
|
||||
Throw.IfNull(registration);
|
||||
return new ExecutorInfo(new TypeId(registration.ExecutorType), registration.Id);
|
||||
Throw.IfNull(binding);
|
||||
return new ExecutorInfo(new TypeId(binding.ExecutorType), binding.Id);
|
||||
}
|
||||
|
||||
public static EdgeInfo ToEdgeInfo(this Edge edge)
|
||||
@@ -38,8 +38,8 @@ internal static class RepresentationExtensions
|
||||
Throw.IfNull(workflow);
|
||||
|
||||
Dictionary<string, ExecutorInfo> executors =
|
||||
workflow.Registrations.Values.ToDictionary(
|
||||
keySelector: registration => registration.Id,
|
||||
workflow.ExecutorBindings.Values.ToDictionary(
|
||||
keySelector: binding => binding.Id,
|
||||
elementSelector: ToExecutorInfo);
|
||||
|
||||
Dictionary<string, List<EdgeInfo>> edges = workflow.Edges.Keys.ToDictionary(
|
||||
|
||||
@@ -47,10 +47,10 @@ internal sealed class WorkflowInfo
|
||||
}
|
||||
|
||||
// Validate the executors
|
||||
if (workflow.Registrations.Count != this.Executors.Count ||
|
||||
if (workflow.ExecutorBindings.Count != this.Executors.Count ||
|
||||
this.Executors.Keys.Any(
|
||||
executorId => workflow.Registrations.TryGetValue(executorId, out ExecutorRegistration? registration)
|
||||
&& !this.Executors[executorId].IsMatch(registration)))
|
||||
executorId => workflow.ExecutorBindings.TryGetValue(executorId, out ExecutorBinding? binding)
|
||||
&& !this.Executors[executorId].IsMatch(binding)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows;
|
||||
|
||||
// TODO: Unwrap the Configured object, just like for SubworkflowBinding
|
||||
internal record ConfiguredExecutorBinding(Configured<Executor> ConfiguredExecutor, Type ExecutorType)
|
||||
: ExecutorBinding(Throw.IfNull(ConfiguredExecutor).Id,
|
||||
ConfiguredExecutor.BoundFactoryAsync,
|
||||
ExecutorType,
|
||||
ConfiguredExecutor.Raw)
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override bool IsSharedInstance { get; } = ConfiguredExecutor.Raw is Executor;
|
||||
|
||||
protected override async ValueTask<bool> ResetCoreAsync()
|
||||
{
|
||||
if (this.ConfiguredExecutor.Raw is IResettableExecutor resettable)
|
||||
{
|
||||
await resettable.ResetAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool SupportsConcurrentSharedExecution => true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool SupportsResetting => false;
|
||||
}
|
||||
@@ -27,6 +27,8 @@ public abstract class Executor : IIdentified
|
||||
private static readonly string s_namespace = typeof(Executor).Namespace!;
|
||||
private static readonly ActivitySource s_activitySource = new(s_namespace);
|
||||
|
||||
// TODO: Add overloads for binding with a configuration/options object once the Configured<T> hierarchy goes away.
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the executor with a unique identifier
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the binding information for a workflow executor, including its identifier, factory method, type, and
|
||||
/// optional raw value.
|
||||
/// </summary>
|
||||
/// <param name="Id">The unique identifier for the executor in the workflow.</param>
|
||||
/// <param name="FactoryAsync">A factory function that creates an instance of the executor. The function accepts two string parameters and returns
|
||||
/// a ValueTask containing the created Executor instance.</param>
|
||||
/// <param name="ExecutorType">The type of the executor. Must be a type derived from Executor.</param>
|
||||
/// <param name="RawValue">An optional raw value associated with the binding.</param>
|
||||
public abstract record class ExecutorBinding(string Id, Func<string, ValueTask<Executor>>? FactoryAsync, Type ExecutorType, object? RawValue = null)
|
||||
: IIdentified,
|
||||
IEquatable<IIdentified>,
|
||||
IEquatable<string>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the binding is a placeholder (i.e., does not have a factory method defined).
|
||||
/// </summary>
|
||||
[MemberNotNullWhen(false, nameof(FactoryAsync))]
|
||||
public bool IsPlaceholder => this.FactoryAsync == null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value whether the executor created from this binding is a shared instance across all runs.
|
||||
/// </summary>
|
||||
public abstract bool IsSharedInstance { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value whether instances of the executor created from this binding can be used in concurrent runs
|
||||
/// from the same <see cref="Workflow"/> instance.
|
||||
/// </summary>
|
||||
public abstract bool SupportsConcurrentSharedExecution { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value whether instances of the executor created from this binding can be reset between subsequent
|
||||
/// runs from the same <see cref="Workflow"/> instance. This value is not relevant for executors that <see
|
||||
/// cref="SupportsConcurrentSharedExecution"/>.
|
||||
/// </summary>
|
||||
public abstract bool SupportsResetting { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() => $"{this.Id}:{(this.IsPlaceholder ? ":<unbound>" : this.ExecutorType.Name)}";
|
||||
|
||||
private Executor CheckId(Executor executor)
|
||||
{
|
||||
if (executor.Id != this.Id)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Executor ID mismatch: expected '{this.Id}', but got '{executor.Id}'.");
|
||||
}
|
||||
|
||||
return executor;
|
||||
}
|
||||
|
||||
internal async ValueTask<Executor> CreateInstanceAsync(string runId)
|
||||
=> !this.IsPlaceholder
|
||||
? this.CheckId(await this.FactoryAsync(runId).ConfigureAwait(false))
|
||||
: throw new InvalidOperationException(
|
||||
$"Cannot create executor with ID '{this.Id}': Binding ({this.GetType().Name}) is a placeholder.");
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual bool Equals(ExecutorBinding? other) =>
|
||||
other is not null && other.Id == this.Id;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(IIdentified? other) =>
|
||||
other is not null && other.Id == this.Id;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(string? other) =>
|
||||
other is not null && other == this.Id;
|
||||
|
||||
internal ValueTask<bool> TryResetAsync()
|
||||
{
|
||||
// Non-shared instances do not need resetting
|
||||
if (!this.IsSharedInstance)
|
||||
{
|
||||
return new(true);
|
||||
}
|
||||
|
||||
// If the executor supports concurrent use, then resetting is a no-op.
|
||||
if (!this.SupportsResetting)
|
||||
{
|
||||
return new(false);
|
||||
}
|
||||
|
||||
return this.ResetCoreAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the executor's shared resources to their initial state. Must be overridden by bindings that support
|
||||
/// resetting.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
protected virtual ValueTask<bool> ResetCoreAsync() => throw new InvalidOperationException("ExecutorBindings that support resetting must override ResetCoreAsync()");
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode() => this.Id.GetHashCode();
|
||||
|
||||
/// <summary>
|
||||
/// Defines an implicit conversion from an Executor to a <see cref="ExecutorBinding"/>.
|
||||
/// </summary>
|
||||
/// <param name="executor">The Executor instance to convert.</param>
|
||||
public static implicit operator ExecutorBinding(Executor executor) => executor.BindExecutor();
|
||||
|
||||
/// <summary>
|
||||
/// Defines an implicit conversion from a string identifier to an <see cref="ExecutorPlaceholder"/>.
|
||||
/// </summary>
|
||||
/// <param name="id">The string identifier to convert to a placeholder.</param>
|
||||
public static implicit operator ExecutorBinding(string id) => new ExecutorPlaceholder(id);
|
||||
|
||||
/// <summary>
|
||||
/// Defines an implicit conversion from a <see cref="RequestPort "/>to an <see cref="ExecutorBinding"/>.
|
||||
/// </summary>
|
||||
/// <param name="port">The RequestPort instance to convert.</param>
|
||||
public static implicit operator ExecutorBinding(RequestPort port) => port.BindAsExecutor();
|
||||
|
||||
/// <summary>
|
||||
/// Defines an implicit conversion from an <see cref="AIAgent"/> to an <see cref="ExecutorBinding"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="agent"></param>
|
||||
public static implicit operator ExecutorBinding(AIAgent agent) => agent.BindAsExecutor();
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for configuring executors and functions as <see cref="ExecutorBinding"/> instances.
|
||||
/// </summary>
|
||||
public static class ExecutorBindingExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures an <see cref="Executor"/> instance for use in a workflow.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Note that Executor Ids must be unique within a workflow.
|
||||
/// </remarks>
|
||||
/// <param name="executor">The executor instance.</param>
|
||||
/// <returns>An <see cref="ExecutorBinding"/> instance wrapping the specified <see cref="Executor"/>.</returns>
|
||||
public static ExecutorBinding BindExecutor(this Executor executor)
|
||||
=> new ExecutorInstanceBinding(executor);
|
||||
|
||||
/// <summary>
|
||||
/// Configures a factory method for creating an <see cref="Executor"/> of type <typeparamref name="TExecutor"/>, using the
|
||||
/// type name as the id.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Note that Executor Ids must be unique within a workflow.
|
||||
///
|
||||
/// Although this will generally result in a delay-instantiated <see cref="Executor"/> once messages are available
|
||||
/// for it, it will be instantiated if a <see cref="ProtocolDescriptor"/> for the <see cref="Workflow"/> is requested,
|
||||
/// and it is the starting executor.
|
||||
/// </remarks>
|
||||
/// <typeparam name="TExecutor">The type of the resulting executor</typeparam>
|
||||
/// <param name="factoryAsync">The factory method.</param>
|
||||
/// <returns>An <see cref="ExecutorBinding"/> instance that resolves to the result of the factory call when messages get sent to it.</returns>
|
||||
public static ExecutorBinding BindExecutor<TExecutor>(this Func<string, string, ValueTask<TExecutor>> factoryAsync)
|
||||
where TExecutor : Executor
|
||||
=> BindExecutor<TExecutor, ExecutorOptions>((config, runId) => factoryAsync(config.Id, runId), id: typeof(TExecutor).Name, options: null);
|
||||
|
||||
/// <summary>
|
||||
/// Configures a factory method for creating an <see cref="Executor"/> of type <typeparamref name="TExecutor"/>, using the
|
||||
/// type name as the id.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Note that Executor Ids must be unique within a workflow.
|
||||
///
|
||||
/// Although this will generally result in a delay-instantiated <see cref="Executor"/> once messages are available
|
||||
/// for it, it will be instantiated if a <see cref="ProtocolDescriptor"/> for the <see cref="Workflow"/> is requested,
|
||||
/// and it is the starting executor.
|
||||
/// </remarks>
|
||||
/// <typeparam name="TExecutor">The type of the resulting executor</typeparam>
|
||||
/// <param name="factoryAsync">The factory method.</param>
|
||||
/// <returns>An <see cref="ExecutorBinding"/> instance that resolves to the result of the factory call when messages get sent to it.</returns>
|
||||
[Obsolete("Use BindExecutor() instead.")]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static ExecutorBinding ConfigureFactory<TExecutor>(this Func<string, string, ValueTask<TExecutor>> factoryAsync)
|
||||
where TExecutor : Executor
|
||||
=> factoryAsync.BindExecutor();
|
||||
|
||||
/// <summary>
|
||||
/// Configures a factory method for creating an <see cref="Executor"/> of type <typeparamref name="TExecutor"/>, with
|
||||
/// the specified id.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Although this will generally result in a delay-instantiated <see cref="Executor"/> once messages are available
|
||||
/// for it, it will be instantiated if a <see cref="ProtocolDescriptor"/> for the <see cref="Workflow"/> is requested,
|
||||
/// and it is the starting executor.
|
||||
/// </remarks>
|
||||
/// <typeparam name="TExecutor">The type of the resulting executor</typeparam>
|
||||
/// <param name="factoryAsync">The factory method.</param>
|
||||
/// <param name="id">An id for the executor to be instantiated.</param>
|
||||
/// <returns>An <see cref="ExecutorBinding"/> instance that resolves to the result of the factory call when messages get sent to it.</returns>
|
||||
public static ExecutorBinding BindExecutor<TExecutor>(this Func<string, string, ValueTask<TExecutor>> factoryAsync, string id)
|
||||
where TExecutor : Executor
|
||||
=> BindExecutor<TExecutor, ExecutorOptions>((_, runId) => factoryAsync(id, runId), id, options: null);
|
||||
|
||||
/// <summary>
|
||||
/// Configures a factory method for creating an <see cref="Executor"/> of type <typeparamref name="TExecutor"/>, with
|
||||
/// the specified id.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Although this will generally result in a delay-instantiated <see cref="Executor"/> once messages are available
|
||||
/// for it, it will be instantiated if a <see cref="ProtocolDescriptor"/> for the <see cref="Workflow"/> is requested,
|
||||
/// and it is the starting executor.
|
||||
/// </remarks>
|
||||
/// <typeparam name="TExecutor">The type of the resulting executor</typeparam>
|
||||
/// <param name="factoryAsync">The factory method.</param>
|
||||
/// <param name="id">An id for the executor to be instantiated.</param>
|
||||
/// <returns>An <see cref="ExecutorBinding"/> instance that resolves to the result of the factory call when messages get sent to it.</returns>
|
||||
[Obsolete("Use BindExecutor() instead.")]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static ExecutorBinding ConfigureFactory<TExecutor>(this Func<string, string, ValueTask<TExecutor>> factoryAsync, string id)
|
||||
where TExecutor : Executor
|
||||
=> factoryAsync.BindExecutor(id);
|
||||
|
||||
/// <summary>
|
||||
/// Configures a factory method for creating an <see cref="Executor"/> of type <typeparamref name="TExecutor"/>, with
|
||||
/// the specified id and options.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Although this will generally result in a delay-instantiated <see cref="Executor"/> once messages are available
|
||||
/// for it, it will be instantiated if a <see cref="ProtocolDescriptor"/> for the <see cref="Workflow"/> is requested,
|
||||
/// and it is the starting executor.
|
||||
/// </remarks>
|
||||
/// <typeparam name="TExecutor">The type of the resulting executor</typeparam>
|
||||
/// <typeparam name="TOptions">The type of options object to be passed to the factory method.</typeparam>
|
||||
/// <param name="factoryAsync">The factory method.</param>
|
||||
/// <param name="id">An id for the executor to be instantiated.</param>
|
||||
/// <param name="options">An optional parameter specifying the options.</param>
|
||||
/// <returns>An <see cref="ExecutorBinding"/> instance that resolves to the result of the factory call when messages get sent to it.</returns>
|
||||
public static ExecutorBinding BindExecutor<TExecutor, TOptions>(this Func<Config<TOptions>, string, ValueTask<TExecutor>> factoryAsync, string id, TOptions? options = null)
|
||||
where TExecutor : Executor
|
||||
where TOptions : ExecutorOptions
|
||||
{
|
||||
Configured<TExecutor, TOptions> configured = new(factoryAsync, id, options);
|
||||
|
||||
return new ConfiguredExecutorBinding(configured.Super<TExecutor, Executor, TOptions>(), typeof(TExecutor));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures a factory method for creating an <see cref="Executor"/> of type <typeparamref name="TExecutor"/>, with
|
||||
/// the specified id and options.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Although this will generally result in a delay-instantiated <see cref="Executor"/> once messages are available
|
||||
/// for it, it will be instantiated if a <see cref="ProtocolDescriptor"/> for the <see cref="Workflow"/> is requested,
|
||||
/// and it is the starting executor.
|
||||
/// </remarks>
|
||||
/// <typeparam name="TExecutor">The type of the resulting executor</typeparam>
|
||||
/// <typeparam name="TOptions">The type of options object to be passed to the factory method.</typeparam>
|
||||
/// <param name="factoryAsync">The factory method.</param>
|
||||
/// <param name="id">An id for the executor to be instantiated.</param>
|
||||
/// <param name="options">An optional parameter specifying the options.</param>
|
||||
/// <returns>An <see cref="ExecutorBinding"/> instance that resolves to the result of the factory call when messages get sent to it.</returns>
|
||||
[Obsolete("Use BindExecutor() instead")]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static ExecutorBinding ConfigureFactory<TExecutor, TOptions>(this Func<Config<TOptions>, string, ValueTask<TExecutor>> factoryAsync, string id, TOptions? options = null)
|
||||
where TExecutor : Executor
|
||||
where TOptions : ExecutorOptions
|
||||
=> factoryAsync.BindExecutor(id, options);
|
||||
|
||||
private static ConfiguredExecutorBinding ToBinding<TInput>(this FunctionExecutor<TInput> executor, Delegate raw)
|
||||
=> new(Configured.FromInstance(executor, raw: raw)
|
||||
.Super<FunctionExecutor<TInput>, Executor>(),
|
||||
typeof(FunctionExecutor<TInput>));
|
||||
|
||||
private static ConfiguredExecutorBinding ToBinding<TInput, TOutput>(this FunctionExecutor<TInput, TOutput> executor, Delegate raw)
|
||||
=> new(Configured.FromInstance(executor, raw: raw)
|
||||
.Super<FunctionExecutor<TInput, TOutput>, Executor>(),
|
||||
typeof(FunctionExecutor<TInput, TOutput>));
|
||||
|
||||
/// <summary>
|
||||
/// Configures a sub-workflow executor for the specified workflow, using the provided identifier and options.
|
||||
/// </summary>
|
||||
/// <param name="workflow">The workflow instance to be executed as a sub-workflow. Cannot be null.</param>
|
||||
/// <param name="id">A unique identifier for the sub-workflow execution. Used to distinguish this sub-workflow instance.</param>
|
||||
/// <param name="options">Optional configuration options for the sub-workflow executor. If null, default options are used.</param>
|
||||
/// <returns>An ExecutorRegistration instance representing the configured sub-workflow executor.</returns>
|
||||
[Obsolete("Use BindAsExecutor() instead")]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static ExecutorBinding ConfigureSubWorkflow(this Workflow workflow, string id, ExecutorOptions? options = null)
|
||||
=> workflow.BindAsExecutor(id, options);
|
||||
|
||||
/// <summary>
|
||||
/// Configures a sub-workflow executor for the specified workflow, using the provided identifier and options.
|
||||
/// </summary>
|
||||
/// <param name="workflow">The workflow instance to be executed as a sub-workflow. Cannot be null.</param>
|
||||
/// <param name="id">A unique identifier for the sub-workflow execution. Used to distinguish this sub-workflow instance.</param>
|
||||
/// <param name="options">Optional configuration options for the sub-workflow executor. If null, default options are used.</param>
|
||||
/// <returns>An <see cref="ExecutorBinding"/> instance representing the configured sub-workflow executor.</returns>
|
||||
public static ExecutorBinding BindAsExecutor(this Workflow workflow, string id, ExecutorOptions? options = null)
|
||||
=> new SubworkflowBinding(workflow, id, options);
|
||||
|
||||
/// <summary>
|
||||
/// Configures a function-based asynchronous message handler as an executor with the specified identifier and
|
||||
/// options.
|
||||
/// </summary>
|
||||
/// <typeparam name="TInput">The type of input message.</typeparam>
|
||||
/// <param name="messageHandlerAsync">A delegate that defines the asynchronous function to execute for each input message.</param>
|
||||
/// <param name="id">An optional unique identifier for the executor. If <c>null</c>, will use the function argument as an id.</param>
|
||||
/// <param name="options">Configuration options for the executor. If <c>null</c>, default options will be used.</param>
|
||||
/// <param name="threadsafe">Declare that the message handler may be used simultaneously by multiple runs concurrently.</param>
|
||||
/// <returns>An <see cref="ExecutorBinding"/> instance that wraps the provided asynchronous message handler and configuration.</returns>
|
||||
public static ExecutorBinding BindAsExecutor<TInput>(this Func<TInput, IWorkflowContext, CancellationToken, ValueTask> messageHandlerAsync, string id, ExecutorOptions? options = null, bool threadsafe = false)
|
||||
=> new FunctionExecutor<TInput>(id, messageHandlerAsync, options, declareCrossRunShareable: threadsafe).ToBinding(messageHandlerAsync);
|
||||
|
||||
/// <summary>
|
||||
/// Configures a function-based asynchronous message handler as an executor with the specified identifier and
|
||||
/// options.
|
||||
/// </summary>
|
||||
/// <typeparam name="TInput">The type of input message.</typeparam>
|
||||
/// <param name="messageHandlerAsync">A delegate that defines the asynchronous function to execute for each input message.</param>
|
||||
/// <param name="id">An optional unique identifier for the executor. If <c>null</c>, will use the function argument as an id.</param>
|
||||
/// <param name="options">Configuration options for the executor. If <c>null</c>, default options will be used.</param>
|
||||
/// <param name="threadsafe">Declare that the message handler may be used simultaneously by multiple runs concurrently.</param>
|
||||
/// <returns>An <see cref="ExecutorBinding"/> instance that wraps the provided asynchronous message handler and configuration.</returns>
|
||||
public static ExecutorBinding BindAsExecutor<TInput>(this Func<TInput, ValueTask> messageHandlerAsync, string id, ExecutorOptions? options = null, bool threadsafe = false)
|
||||
=> ((Func<TInput, IWorkflowContext, CancellationToken, ValueTask>)((input, _, __) => messageHandlerAsync(input)))
|
||||
.BindAsExecutor(id, options, threadsafe);
|
||||
|
||||
/// <summary>
|
||||
/// Configures a function-based asynchronous message handler as an executor with the specified identifier and
|
||||
/// options.
|
||||
/// </summary>
|
||||
/// <typeparam name="TInput">The type of input message.</typeparam>
|
||||
/// <param name="messageHandlerAsync">A delegate that defines the asynchronous function to execute for each input message.</param>
|
||||
/// <param name="id">An optional unique identifier for the executor. If <c>null</c>, will use the function argument as an id.</param>
|
||||
/// <param name="options">Configuration options for the executor. If <c>null</c>, default options will be used.</param>
|
||||
/// <param name="threadsafe">Declare that the message handler may be used simultaneously by multiple runs concurrently.</param>
|
||||
/// <returns>An <see cref="ExecutorBinding"/> instance that wraps the provided asynchronous message handler and configuration.</returns>
|
||||
public static ExecutorBinding BindAsExecutor<TInput>(this Func<TInput, IWorkflowContext, ValueTask> messageHandlerAsync, string id, ExecutorOptions? options = null, bool threadsafe = false)
|
||||
=> ((Func<TInput, IWorkflowContext, CancellationToken, ValueTask>)((input, ctx, __) => messageHandlerAsync(input, ctx)))
|
||||
.BindAsExecutor(id, options, threadsafe);
|
||||
|
||||
/// <summary>
|
||||
/// Configures a function-based asynchronous message handler as an executor with the specified identifier and
|
||||
/// options.
|
||||
/// </summary>
|
||||
/// <typeparam name="TInput">The type of input message.</typeparam>
|
||||
/// <param name="messageHandlerAsync">A delegate that defines the asynchronous function to execute for each input message.</param>
|
||||
/// <param name="id">An optional unique identifier for the executor. If <c>null</c>, will use the function argument as an id.</param>
|
||||
/// <param name="options">Configuration options for the executor. If <c>null</c>, default options will be used.</param>
|
||||
/// <param name="threadsafe">Declare that the message handler may be used simultaneously by multiple runs concurrently.</param>
|
||||
/// <returns>An <see cref="ExecutorBinding"/> instance that wraps the provided asynchronous message handler and configuration.</returns>
|
||||
public static ExecutorBinding BindAsExecutor<TInput>(this Func<TInput, CancellationToken, ValueTask> messageHandlerAsync, string id, ExecutorOptions? options = null, bool threadsafe = false)
|
||||
=> ((Func<TInput, IWorkflowContext, CancellationToken, ValueTask>)((input, _, ct) => messageHandlerAsync(input, ct)))
|
||||
.BindAsExecutor(id, options, threadsafe);
|
||||
|
||||
/// <summary>
|
||||
/// Configures a function-based message handler as an executor with the specified identifier and
|
||||
/// options.
|
||||
/// </summary>
|
||||
/// <typeparam name="TInput">The type of input message.</typeparam>
|
||||
/// <param name="messageHandler">A delegate that defines the function to execute for each input message.</param>
|
||||
/// <param name="id">An optional unique identifier for the executor. If <c>null</c>, will use the function argument as an id.</param>
|
||||
/// <param name="options">Configuration options for the executor. If <c>null</c>, default options will be used.</param>
|
||||
/// <param name="threadsafe">Declare that the message handler may be used simultaneously by multiple runs concurrently.</param>
|
||||
/// <returns>An <see cref="ExecutorBinding"/> instance that wraps the provided asynchronous message handler and configuration.</returns>
|
||||
public static ExecutorBinding BindAsExecutor<TInput>(this Action<TInput, IWorkflowContext, CancellationToken> messageHandler, string id, ExecutorOptions? options = null, bool threadsafe = false)
|
||||
=> new FunctionExecutor<TInput>(id, messageHandler, options, declareCrossRunShareable: threadsafe).ToBinding(messageHandler);
|
||||
|
||||
/// <summary>
|
||||
/// Configures a function-based message handler as an executor with the specified identifier and
|
||||
/// options.
|
||||
/// </summary>
|
||||
/// <typeparam name="TInput">The type of input message.</typeparam>
|
||||
/// <param name="messageHandler">A delegate that defines the function to execute for each input message.</param>
|
||||
/// <param name="id">An optional unique identifier for the executor. If <c>null</c>, will use the function argument as an id.</param>
|
||||
/// <param name="options">Configuration options for the executor. If <c>null</c>, default options will be used.</param>
|
||||
/// <param name="threadsafe">Declare that the message handler may be used simultaneously by multiple runs concurrently.</param>
|
||||
/// <returns>An <see cref="ExecutorBinding"/> instance that wraps the provided asynchronous message handler and configuration.</returns>
|
||||
public static ExecutorBinding BindAsExecutor<TInput>(this Action<TInput> messageHandler, string id, ExecutorOptions? options = null, bool threadsafe = false)
|
||||
=> ((Action<TInput, IWorkflowContext, CancellationToken>)((input, _, __) => messageHandler(input)))
|
||||
.BindAsExecutor(id, options, threadsafe);
|
||||
|
||||
/// <summary>
|
||||
/// Configures a function-based message handler as an executor with the specified identifier and
|
||||
/// options.
|
||||
/// </summary>
|
||||
/// <typeparam name="TInput">The type of input message.</typeparam>
|
||||
/// <param name="messageHandler">A delegate that defines the function to execute for each input message.</param>
|
||||
/// <param name="id">An optional unique identifier for the executor. If <c>null</c>, will use the function argument as an id.</param>
|
||||
/// <param name="options">Configuration options for the executor. If <c>null</c>, default options will be used.</param>
|
||||
/// <param name="threadsafe">Declare that the message handler may be used simultaneously by multiple runs concurrently.</param>
|
||||
/// <returns>An <see cref="ExecutorBinding"/> instance that wraps the provided asynchronous message handler and configuration.</returns>
|
||||
public static ExecutorBinding BindAsExecutor<TInput>(this Action<TInput, IWorkflowContext> messageHandler, string id, ExecutorOptions? options = null, bool threadsafe = false)
|
||||
=> ((Action<TInput, IWorkflowContext, CancellationToken>)((input, ctx, __) => messageHandler(input, ctx)))
|
||||
.BindAsExecutor(id, options, threadsafe);
|
||||
|
||||
/// <summary>
|
||||
/// Configures a function-based message handler as an executor with the specified identifier and
|
||||
/// options.
|
||||
/// </summary>
|
||||
/// <typeparam name="TInput">The type of input message.</typeparam>
|
||||
/// <param name="messageHandler">A delegate that defines the function to execute for each input message.</param>
|
||||
/// <param name="id">An optional unique identifier for the executor. If <c>null</c>, will use the function argument as an id.</param>
|
||||
/// <param name="options">Configuration options for the executor. If <c>null</c>, default options will be used.</param>
|
||||
/// <param name="threadsafe">Declare that the message handler may be used simultaneously by multiple runs concurrently.</param>
|
||||
/// <returns>An <see cref="ExecutorBinding"/> instance that wraps the provided asynchronous message handler and configuration.</returns>
|
||||
public static ExecutorBinding BindAsExecutor<TInput>(this Action<TInput, CancellationToken> messageHandler, string id, ExecutorOptions? options = null, bool threadsafe = false)
|
||||
=> ((Action<TInput, IWorkflowContext, CancellationToken>)((input, _, ct) => messageHandler(input, ct)))
|
||||
.BindAsExecutor(id, options, threadsafe);
|
||||
|
||||
/// <summary>
|
||||
/// Configures a function-based asynchronous message handler as an executor with the specified identifier and
|
||||
/// options.
|
||||
/// </summary>
|
||||
/// <typeparam name="TInput">The type of input message.</typeparam>
|
||||
/// <typeparam name="TOutput">The type of output message.</typeparam>
|
||||
/// <param name="messageHandlerAsync">A delegate that defines the asynchronous function to execute for each input message.</param>
|
||||
/// <param name="id">A unique identifier for the executor.</param>
|
||||
/// <param name="options">Configuration options for the executor. If <c>null</c>, default options will be used.</param>
|
||||
/// <param name="threadsafe">Declare that the message handler may be used simultaneously by multiple runs concurrently.</param>
|
||||
/// <returns>An <see cref="ExecutorBinding"/> instance that wraps the provided asynchronous message handler and configuration.</returns>
|
||||
public static ExecutorBinding BindAsExecutor<TInput, TOutput>(this Func<TInput, IWorkflowContext, CancellationToken, ValueTask<TOutput>> messageHandlerAsync, string id, ExecutorOptions? options = null, bool threadsafe = false)
|
||||
=> new FunctionExecutor<TInput, TOutput>(Throw.IfNull(id), messageHandlerAsync, options, declareCrossRunShareable: threadsafe).ToBinding(messageHandlerAsync);
|
||||
|
||||
/// <summary>
|
||||
/// Configures a function-based asynchronous message handler as an executor with the specified identifier and
|
||||
/// options.
|
||||
/// </summary>
|
||||
/// <typeparam name="TInput">The type of input message.</typeparam>
|
||||
/// <typeparam name="TOutput">The type of output message.</typeparam>
|
||||
/// <param name="messageHandlerAsync">A delegate that defines the asynchronous function to execute for each input message.</param>
|
||||
/// <param name="id">An optional unique identifier for the executor. If <c>null</c>, will use the function argument as an id.</param>
|
||||
/// <param name="options">Configuration options for the executor. If <c>null</c>, default options will be used.</param>
|
||||
/// <param name="threadsafe">Declare that the message handler may be used simultaneously by multiple runs concurrently.</param>
|
||||
/// <returns>An <see cref="ExecutorBinding"/> instance that wraps the provided asynchronous message handler and configuration.</returns>
|
||||
public static ExecutorBinding BindAsExecutor<TInput, TOutput>(this Func<TInput, ValueTask<TOutput>> messageHandlerAsync, string id, ExecutorOptions? options = null, bool threadsafe = false)
|
||||
=> ((Func<TInput, IWorkflowContext, CancellationToken, ValueTask<TOutput>>)((input, _, __) => messageHandlerAsync(input)))
|
||||
.BindAsExecutor(id, options, threadsafe);
|
||||
|
||||
/// <summary>
|
||||
/// Configures a function-based asynchronous message handler as an executor with the specified identifier and
|
||||
/// options.
|
||||
/// </summary>
|
||||
/// <typeparam name="TInput">The type of input message.</typeparam>
|
||||
/// <typeparam name="TOutput">The type of output message.</typeparam>
|
||||
/// <param name="messageHandlerAsync">A delegate that defines the asynchronous function to execute for each input message.</param>
|
||||
/// <param name="id">An optional unique identifier for the executor. If <c>null</c>, will use the function argument as an id.</param>
|
||||
/// <param name="options">Configuration options for the executor. If <c>null</c>, default options will be used.</param>
|
||||
/// <param name="threadsafe">Declare that the message handler may be used simultaneously by multiple runs concurrently.</param>
|
||||
/// <returns>An <see cref="ExecutorBinding"/> instance that wraps the provided asynchronous message handler and configuration.</returns>
|
||||
public static ExecutorBinding BindAsExecutor<TInput, TOutput>(this Func<TInput, IWorkflowContext, ValueTask<TOutput>> messageHandlerAsync, string id, ExecutorOptions? options = null, bool threadsafe = false)
|
||||
=> ((Func<TInput, IWorkflowContext, CancellationToken, ValueTask<TOutput>>)((input, ctx, __) => messageHandlerAsync(input, ctx)))
|
||||
.BindAsExecutor(id, options, threadsafe);
|
||||
|
||||
/// <summary>
|
||||
/// Configures a function-based asynchronous message handler as an executor with the specified identifier and
|
||||
/// options.
|
||||
/// </summary>
|
||||
/// <typeparam name="TInput">The type of input message.</typeparam>
|
||||
/// <typeparam name="TOutput">The type of output message.</typeparam>
|
||||
/// <param name="messageHandlerAsync">A delegate that defines the asynchronous function to execute for each input message.</param>
|
||||
/// <param name="id">An optional unique identifier for the executor. If <c>null</c>, will use the function argument as an id.</param>
|
||||
/// <param name="options">Configuration options for the executor. If <c>null</c>, default options will be used.</param>
|
||||
/// <param name="threadsafe">Declare that the message handler may be used simultaneously by multiple runs concurrently.</param>
|
||||
/// <returns>An <see cref="ExecutorBinding"/> instance that wraps the provided asynchronous message handler and configuration.</returns>
|
||||
public static ExecutorBinding BindAsExecutor<TInput, TOutput>(this Func<TInput, CancellationToken, ValueTask<TOutput>> messageHandlerAsync, string id, ExecutorOptions? options = null, bool threadsafe = false)
|
||||
=> ((Func<TInput, IWorkflowContext, CancellationToken, ValueTask<TOutput>>)((input, _, ct) => messageHandlerAsync(input, ct)))
|
||||
.BindAsExecutor(id, options, threadsafe);
|
||||
|
||||
/// <summary>
|
||||
/// Configures a function-based message handler as an executor with the specified identifier and options.
|
||||
/// </summary>
|
||||
/// <typeparam name="TInput">The type of input message.</typeparam>
|
||||
/// <typeparam name="TOutput">The type of output message.</typeparam>
|
||||
/// <param name="messageHandler">A delegate that defines the function to execute for each input message.</param>
|
||||
/// <param name="id">An optional unique identifier for the executor. If <c>null</c>, will use the function argument as an id.</param>
|
||||
/// <param name="options">Configuration options for the executor. If <c>null</c>, default options will be used.</param>
|
||||
/// <param name="threadsafe">Declare that the message handler may be used simultaneously by multiple runs concurrently.</param>
|
||||
/// <returns>An <see cref="ExecutorBinding"/> instance that wraps the provided asynchronous message handler and configuration.</returns>
|
||||
public static ExecutorBinding BindAsExecutor<TInput, TOutput>(this Func<TInput, IWorkflowContext, CancellationToken, TOutput> messageHandler, string id, ExecutorOptions? options = null, bool threadsafe = false)
|
||||
=> new FunctionExecutor<TInput, TOutput>(id, messageHandler, options, declareCrossRunShareable: threadsafe).ToBinding(messageHandler);
|
||||
|
||||
/// <summary>
|
||||
/// Configures a function-based message handler as an executor with the specified identifier and options.
|
||||
/// </summary>
|
||||
/// <typeparam name="TInput">The type of input message.</typeparam>
|
||||
/// <typeparam name="TOutput">The type of output message.</typeparam>
|
||||
/// <param name="messageHandler">A delegate that defines the function to execute for each input message.</param>
|
||||
/// <param name="id">An optional unique identifier for the executor. If <c>null</c>, will use the function argument as an id.</param>
|
||||
/// <param name="options">Configuration options for the executor. If <c>null</c>, default options will be used.</param>
|
||||
/// <param name="threadsafe">Declare that the message handler may be used simultaneously by multiple runs concurrently.</param>
|
||||
/// <returns>An <see cref="ExecutorBinding"/> instance that wraps the provided asynchronous message handler and configuration.</returns>
|
||||
public static ExecutorBinding BindAsExecutor<TInput, TOutput>(this Func<TInput, TOutput> messageHandler, string id, ExecutorOptions? options = null, bool threadsafe = false)
|
||||
=> ((Func<TInput, IWorkflowContext, CancellationToken, TOutput>)((input, _, __) => messageHandler(input)))
|
||||
.BindAsExecutor(id, options, threadsafe);
|
||||
|
||||
/// <summary>
|
||||
/// Configures a function-based message handler as an executor with the specified identifier and options.
|
||||
/// </summary>
|
||||
/// <typeparam name="TInput">The type of input message.</typeparam>
|
||||
/// <typeparam name="TOutput">The type of output message.</typeparam>
|
||||
/// <param name="messageHandler">A delegate that defines the function to execute for each input message.</param>
|
||||
/// <param name="id">An optional unique identifier for the executor. If <c>null</c>, will use the function argument as an id.</param>
|
||||
/// <param name="options">Configuration options for the executor. If <c>null</c>, default options will be used.</param>
|
||||
/// <param name="threadsafe">Declare that the message handler may be used simultaneously by multiple runs concurrently.</param>
|
||||
/// <returns>An <see cref="ExecutorBinding"/> instance that wraps the provided asynchronous message handler and configuration.</returns>
|
||||
public static ExecutorBinding BindAsExecutor<TInput, TOutput>(this Func<TInput, IWorkflowContext, TOutput> messageHandler, string id, ExecutorOptions? options = null, bool threadsafe = false)
|
||||
=> ((Func<TInput, IWorkflowContext, CancellationToken, TOutput>)((input, ctx, __) => messageHandler(input, ctx)))
|
||||
.BindAsExecutor(id, options, threadsafe);
|
||||
|
||||
/// <summary>
|
||||
/// Configures a function-based message handler as an executor with the specified identifier and options.
|
||||
/// </summary>
|
||||
/// <typeparam name="TInput">The type of input message.</typeparam>
|
||||
/// <typeparam name="TOutput">The type of output message.</typeparam>
|
||||
/// <param name="messageHandler">A delegate that defines the function to execute for each input message.</param>
|
||||
/// <param name="id">An optional unique identifier for the executor. If <c>null</c>, will use the function argument as an id.</param>
|
||||
/// <param name="options">Configuration options for the executor. If <c>null</c>, default options will be used.</param>
|
||||
/// <param name="threadsafe">Declare that the message handler may be used simultaneously by multiple runs concurrently.</param>
|
||||
/// <returns>An <see cref="ExecutorBinding"/> instance that wraps the provided asynchronous message handler and configuration.</returns>
|
||||
public static ExecutorBinding BindAsExecutor<TInput, TOutput>(this Func<TInput, CancellationToken, TOutput> messageHandler, string id, ExecutorOptions? options = null, bool threadsafe = false)
|
||||
=> ((Func<TInput, IWorkflowContext, CancellationToken, TOutput>)((input, _, ct) => messageHandler(input, ct)))
|
||||
.BindAsExecutor(id, options, threadsafe);
|
||||
|
||||
/// <summary>
|
||||
/// Configures a function-based aggregating executor with the specified identifier and options.
|
||||
/// </summary>
|
||||
/// <typeparam name="TInput">The type of input message.</typeparam>
|
||||
/// <typeparam name="TAccumulate">The type of the accumulating object.</typeparam>
|
||||
/// <param name="aggregatorFunc">A delegate the defines the aggregation procedure</param>
|
||||
/// <param name="id">A unique identifier for the executor.</param>
|
||||
/// <param name="options">Configuration options for the executor. If <c>null</c>, default options will be used.</param>
|
||||
/// <param name="threadsafe">Declare that the message handler may be used simultaneously by multiple runs concurrently.</param>
|
||||
/// <returns>An <see cref="ExecutorBinding"/> instance that wraps the provided asynchronous message handler and configuration.</returns>
|
||||
public static ExecutorBinding BindAsExecutor<TInput, TAccumulate>(this Func<TAccumulate?, TInput, TAccumulate?> aggregatorFunc, string id, ExecutorOptions? options = null, bool threadsafe = false)
|
||||
=> new AggregatingExecutor<TInput, TAccumulate>(id, aggregatorFunc, options, declareCrossRunShareable: threadsafe);
|
||||
|
||||
/// <summary>
|
||||
/// Configure an <see cref="AIAgent"/> as an executor for use in a workflow.
|
||||
/// </summary>
|
||||
/// <param name="agent">The agent instance.</param>
|
||||
/// <param name="emitEvents">Specifies whether the agent should emit streaming events.</param>
|
||||
/// <returns>An <see cref="AIAgentBinding"/> instance that wraps the provided agent.</returns>
|
||||
public static ExecutorBinding BindAsExecutor(this AIAgent agent, bool emitEvents = false)
|
||||
=> new AIAgentBinding(agent, emitEvents);
|
||||
|
||||
/// <summary>
|
||||
/// Configure a <see cref="RequestPort"/> as an executor for use in a workflow.
|
||||
/// </summary>
|
||||
/// <param name="port">The port configuration.</param>
|
||||
/// <param name="allowWrappedRequests">Specifies whether the port should accept requests already wrapped in
|
||||
/// <see cref="ExternalRequest"/>.</param>
|
||||
/// <returns>A <see cref="RequestPortBinding"/> instance that wraps the provided port.</returns>
|
||||
public static ExecutorBinding BindAsExecutor(this RequestPort port, bool allowWrappedRequests = true)
|
||||
=> new RequestPortBinding(port, allowWrappedRequests);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the workflow binding details for a shared executor instance, including configuration options
|
||||
/// for event emission.
|
||||
/// </summary>
|
||||
/// <param name="ExecutorInstance">The executor instance to bind. Cannot be null.</param>
|
||||
public record ExecutorInstanceBinding(Executor ExecutorInstance)
|
||||
: ExecutorBinding(Throw.IfNull(ExecutorInstance).Id,
|
||||
(_) => new(ExecutorInstance),
|
||||
ExecutorInstance.GetType(),
|
||||
ExecutorInstance)
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override bool SupportsConcurrentSharedExecution => this.ExecutorInstance.IsCrossRunShareable;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool SupportsResetting => this.ExecutorInstance is IResettableExecutor;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsSharedInstance => true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async ValueTask<bool> ResetCoreAsync()
|
||||
{
|
||||
if (this.ExecutorInstance is IResettableExecutor resettable)
|
||||
{
|
||||
await resettable.ResetAsync().ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,369 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Specialized;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for configuring executors and functions as <see cref="ExecutorIsh"/> instances.
|
||||
/// </summary>
|
||||
public static class ExecutorIshConfigurationExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures a factory method for creating an <see cref="Executor"/> of type <typeparamref name="TExecutor"/>, using the
|
||||
/// type name as the id.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Note that Executor Ids must be unique within a workflow.
|
||||
///
|
||||
/// Although this will generally result in a delay-instantiated <see cref="Executor"/> once messages are available
|
||||
/// for it, it will be instantiated if a <see cref="ProtocolDescriptor"/> for the <see cref="Workflow"/> is requested,
|
||||
/// and it is the starting executor.
|
||||
/// </remarks>
|
||||
/// <typeparam name="TExecutor">The type of the resulting executor</typeparam>
|
||||
/// <param name="factoryAsync">The factory method.</param>
|
||||
/// <returns>An ExecutorIsh instance that resolves to the result of the factory call when messages get sent to it.</returns>
|
||||
public static ExecutorIsh ConfigureFactory<TExecutor>(this Func<string, string, ValueTask<TExecutor>> factoryAsync)
|
||||
where TExecutor : Executor
|
||||
=> ConfigureFactory<TExecutor, ExecutorOptions>((config, runId) => factoryAsync(config.Id, runId), typeof(TExecutor).Name, options: null);
|
||||
|
||||
/// <summary>
|
||||
/// Configures a factory method for creating an <see cref="Executor"/> of type <typeparamref name="TExecutor"/>, with
|
||||
/// the specified id.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Although this will generally result in a delay-instantiated <see cref="Executor"/> once messages are available
|
||||
/// for it, it will be instantiated if a <see cref="ProtocolDescriptor"/> for the <see cref="Workflow"/> is requested,
|
||||
/// and it is the starting executor.
|
||||
/// </remarks>
|
||||
/// <typeparam name="TExecutor">The type of the resulting executor</typeparam>
|
||||
/// <param name="factoryAsync">The factory method.</param>
|
||||
/// <param name="id">An id for the executor to be instantiated.</param>
|
||||
/// <returns>An ExecutorIsh instance that resolves to the result of the factory call when messages get sent to it.</returns>
|
||||
public static ExecutorIsh ConfigureFactory<TExecutor>(this Func<string, string, ValueTask<TExecutor>> factoryAsync, string id)
|
||||
where TExecutor : Executor
|
||||
=> ConfigureFactory<TExecutor, ExecutorOptions>((_, runId) => factoryAsync(id, runId), id, options: null);
|
||||
|
||||
/// <summary>
|
||||
/// Configures a factory method for creating an <see cref="Executor"/> of type <typeparamref name="TExecutor"/>, with
|
||||
/// the specified id and options.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Although this will generally result in a delay-instantiated <see cref="Executor"/> once messages are available
|
||||
/// for it, it will be instantiated if a <see cref="ProtocolDescriptor"/> for the <see cref="Workflow"/> is requested,
|
||||
/// and it is the starting executor.
|
||||
/// </remarks>
|
||||
/// <typeparam name="TExecutor">The type of the resulting executor</typeparam>
|
||||
/// <typeparam name="TOptions">The type of options object to be passed to the factory method.</typeparam>
|
||||
/// <param name="factoryAsync">The factory method.</param>
|
||||
/// <param name="id">An id for the executor to be instantiated.</param>
|
||||
/// <param name="options">An optional parameter specifying the options.</param>
|
||||
/// <returns>An ExecutorIsh instance that resolves to the result of the factory call when messages get sent to it.</returns>
|
||||
public static ExecutorIsh ConfigureFactory<TExecutor, TOptions>(this Func<Config<TOptions>, string, ValueTask<TExecutor>> factoryAsync, string id, TOptions? options = null)
|
||||
where TExecutor : Executor
|
||||
where TOptions : ExecutorOptions
|
||||
{
|
||||
Configured<TExecutor, TOptions> configured = new(factoryAsync, id, options);
|
||||
|
||||
return new ExecutorIsh(configured.Super<TExecutor, Executor, TOptions>(), typeof(TExecutor), ExecutorIsh.Type.Executor);
|
||||
}
|
||||
|
||||
private static ExecutorIsh ToExecutorIsh<TInput>(this FunctionExecutor<TInput> executor, Delegate raw) => new(Configured.FromInstance(executor, raw: raw)
|
||||
.Super<FunctionExecutor<TInput>, Executor>(),
|
||||
typeof(FunctionExecutor<TInput>),
|
||||
ExecutorIsh.Type.Function);
|
||||
|
||||
private static ExecutorIsh ToExecutorIsh<TInput, TOutput>(this FunctionExecutor<TInput, TOutput> executor, Delegate raw) => new(Configured.FromInstance(executor, raw: raw)
|
||||
.Super<FunctionExecutor<TInput, TOutput>, Executor>(),
|
||||
typeof(FunctionExecutor<TInput, TOutput>),
|
||||
ExecutorIsh.Type.Function);
|
||||
|
||||
/// <summary>
|
||||
/// Configures a sub-workflow executor for the specified workflow, using the provided identifier and options.
|
||||
/// </summary>
|
||||
/// <param name="workflow">The workflow instance to be executed as a sub-workflow. Cannot be null.</param>
|
||||
/// <param name="id">A unique identifier for the sub-workflow execution. Used to distinguish this sub-workflow instance.</param>
|
||||
/// <param name="options">Optional configuration options for the sub-workflow executor. If null, default options are used.</param>
|
||||
/// <returns>An ExecutorIsh instance representing the configured sub-workflow executor.</returns>
|
||||
public static ExecutorIsh ConfigureSubWorkflow(this Workflow workflow, string id, ExecutorOptions? options = null)
|
||||
{
|
||||
object ownershipToken = new();
|
||||
workflow.TakeOwnership(ownershipToken, subworkflow: true);
|
||||
|
||||
Configured<WorkflowHostExecutor, ExecutorOptions> configured = new(InitHostExecutorAsync, id, options, raw: workflow);
|
||||
return new ExecutorIsh(configured.Super<WorkflowHostExecutor, Executor, ExecutorOptions>(), typeof(WorkflowHostExecutor), ExecutorIsh.Type.Workflow);
|
||||
|
||||
ValueTask<WorkflowHostExecutor> InitHostExecutorAsync(Config<ExecutorOptions> config, string runId)
|
||||
{
|
||||
return new(new WorkflowHostExecutor(config.Id, workflow, runId, ownershipToken, config.Options));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures a function-based asynchronous message handler as an executor with the specified identifier and
|
||||
/// options.
|
||||
/// </summary>
|
||||
/// <typeparam name="TInput">The type of input message.</typeparam>
|
||||
/// <param name="messageHandlerAsync">A delegate that defines the asynchronous function to execute for each input message.</param>
|
||||
/// <param name="id">A optional unique identifier for the executor. If <c>null</c>, will use the function argument as an id.</param>
|
||||
/// <param name="options">Configuration options for the executor. If <c>null</c>, default options will be used.</param>
|
||||
/// <param name="threadsafe">Declare that the message handler may be used simultaneously by multiple runs concurrently.</param>
|
||||
/// <returns>An ExecutorIsh instance that wraps the provided asynchronous message handler and configuration.</returns>
|
||||
public static ExecutorIsh AsExecutor<TInput>(this Func<TInput, IWorkflowContext, CancellationToken, ValueTask> messageHandlerAsync, string id, ExecutorOptions? options = null, bool threadsafe = false)
|
||||
=> new FunctionExecutor<TInput>(id, messageHandlerAsync, options, declareCrossRunShareable: threadsafe).ToExecutorIsh(messageHandlerAsync);
|
||||
|
||||
/// <summary>
|
||||
/// Configures a function-based asynchronous message handler as an executor with the specified identifier and
|
||||
/// options.
|
||||
/// </summary>
|
||||
/// <typeparam name="TInput">The type of input message.</typeparam>
|
||||
/// <typeparam name="TOutput">The type of output message.</typeparam>
|
||||
/// <param name="messageHandlerAsync">A delegate that defines the asynchronous function to execute for each input message.</param>
|
||||
/// <param name="id">A unique identifier for the executor.</param>
|
||||
/// <param name="options">Configuration options for the executor. If <c>null</c>, default options will be used.</param>
|
||||
/// <param name="threadsafe">Declare that the message handler may be used simultaneously by multiple runs concurrently.</param>
|
||||
/// <returns>An ExecutorIsh instance that wraps the provided asynchronous message handler and configuration.</returns>
|
||||
public static ExecutorIsh AsExecutor<TInput, TOutput>(this Func<TInput, IWorkflowContext, CancellationToken, ValueTask<TOutput>> messageHandlerAsync, string id, ExecutorOptions? options = null, bool threadsafe = false)
|
||||
=> new FunctionExecutor<TInput, TOutput>(Throw.IfNull(id), messageHandlerAsync, options, declareCrossRunShareable: threadsafe).ToExecutorIsh(messageHandlerAsync);
|
||||
|
||||
/// <summary>
|
||||
/// Configures a function-based aggregating executor with the specified identifier and options.
|
||||
/// </summary>
|
||||
/// <typeparam name="TInput">The type of input message.</typeparam>
|
||||
/// <typeparam name="TAccumulate">The type of the accumulating object.</typeparam>
|
||||
/// <param name="aggregatorFunc">A delegate the defines the aggregation procedure</param>
|
||||
/// <param name="id">A unique identifier for the executor.</param>
|
||||
/// <param name="options">Configuration options for the executor. If <c>null</c>, default options will be used.</param>
|
||||
/// <param name="threadsafe">Declare that the message handler may be used simultaneously by multiple runs concurrently.</param>
|
||||
/// <returns>An ExecutorIsh instance that wraps the provided asynchronous message handler and configuration.</returns>
|
||||
public static ExecutorIsh AsExecutor<TInput, TAccumulate>(this Func<TAccumulate?, TInput, TAccumulate?> aggregatorFunc, string id, ExecutorOptions? options = null, bool threadsafe = false)
|
||||
=> new AggregatingExecutor<TInput, TAccumulate>(id, aggregatorFunc, options, declareCrossRunShareable: threadsafe);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A tagged union representing an object that can function like an <see cref="Executor"/> in a <see cref="Workflow"/>,
|
||||
/// or a reference to one by ID.
|
||||
/// </summary>
|
||||
public sealed class ExecutorIsh :
|
||||
IIdentified,
|
||||
IEquatable<ExecutorIsh>,
|
||||
IEquatable<IIdentified>,
|
||||
IEquatable<string>
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of the <see cref="ExecutorIsh"/>.
|
||||
/// </summary>
|
||||
public enum Type
|
||||
{
|
||||
/// <summary>
|
||||
/// An unbound executor reference, identified only by ID.
|
||||
/// </summary>
|
||||
Unbound,
|
||||
/// <summary>
|
||||
/// An actual <see cref="Executor"/> instance.
|
||||
/// </summary>
|
||||
Executor,
|
||||
/// <summary>
|
||||
/// A function delegate to be wrapped as an executor.
|
||||
/// </summary>
|
||||
Function,
|
||||
/// <summary>
|
||||
/// An <see cref="RequestPort"/> for servicing external requests.
|
||||
/// </summary>
|
||||
RequestPort,
|
||||
/// <summary>
|
||||
/// An <see cref="AIAgent"/> instance.
|
||||
/// </summary>
|
||||
Agent,
|
||||
/// <summary>
|
||||
/// A nested <see cref="Workflow"/> instance.
|
||||
/// </summary>
|
||||
Workflow,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of data contained in this <see cref="ExecutorIsh" /> instance.
|
||||
/// </summary>
|
||||
public Type ExecutorType { get; init; }
|
||||
|
||||
private readonly string? _idValue;
|
||||
|
||||
private readonly Configured<Executor>? _configuredExecutor;
|
||||
private readonly System.Type? _configuredExecutorType;
|
||||
|
||||
internal readonly RequestPort? _requestPortValue;
|
||||
private readonly AIAgent? _aiAgentValue;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExecutorIsh"/> class as an unbound reference by ID.
|
||||
/// </summary>
|
||||
/// <param name="id">A unique identifier for an <see cref="Executor"/> in the <see cref="Workflow"/></param>
|
||||
public ExecutorIsh(string id)
|
||||
{
|
||||
this.ExecutorType = Type.Unbound;
|
||||
this._idValue = Throw.IfNull(id);
|
||||
}
|
||||
|
||||
internal ExecutorIsh(Configured<Executor> configured, System.Type configuredExecutorType, Type type)
|
||||
{
|
||||
this.ExecutorType = type;
|
||||
this._configuredExecutor = configured;
|
||||
this._configuredExecutorType = configuredExecutorType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the ExecutorIsh class using the specified executor.
|
||||
/// </summary>
|
||||
/// <param name="executor">The executor instance to be wrapped.</param>
|
||||
public ExecutorIsh(Executor executor)
|
||||
{
|
||||
this.ExecutorType = Type.Executor;
|
||||
this._configuredExecutor = Configured.FromInstance(Throw.IfNull(executor));
|
||||
this._configuredExecutorType = executor.GetType();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the ExecutorIsh class using the specified input port.
|
||||
/// </summary>
|
||||
/// <param name="port">The input port to associate to be wrapped.</param>
|
||||
public ExecutorIsh(RequestPort port)
|
||||
{
|
||||
this.ExecutorType = Type.RequestPort;
|
||||
this._requestPortValue = Throw.IfNull(port);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the ExecutorIsh class using the specified AI agent.
|
||||
/// </summary>
|
||||
/// <param name="aiAgent"></param>
|
||||
public ExecutorIsh(AIAgent aiAgent)
|
||||
{
|
||||
this.ExecutorType = Type.Agent;
|
||||
this._aiAgentValue = Throw.IfNull(aiAgent);
|
||||
}
|
||||
|
||||
internal bool IsUnbound => this.ExecutorType == Type.Unbound;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Id => this.ExecutorType switch
|
||||
{
|
||||
Type.Unbound => this._idValue ?? throw new InvalidOperationException("This ExecutorIsh is unbound and has no ID."),
|
||||
Type.Executor => this._configuredExecutor!.Id,
|
||||
Type.RequestPort => this._requestPortValue!.Id,
|
||||
Type.Agent => this._aiAgentValue!.Id,
|
||||
Type.Function => this._configuredExecutor!.Id,
|
||||
Type.Workflow => this._configuredExecutor!.Id,
|
||||
_ => throw new InvalidOperationException($"Unknown ExecutorIsh type: {this.ExecutorType}")
|
||||
};
|
||||
|
||||
internal object? RawData => this.ExecutorType switch
|
||||
{
|
||||
Type.Unbound => this._idValue,
|
||||
Type.Executor => this._configuredExecutor!.Raw ?? this._configuredExecutor,
|
||||
Type.RequestPort => this._requestPortValue,
|
||||
Type.Agent => this._aiAgentValue,
|
||||
Type.Function => this._configuredExecutor!.Raw ?? this._configuredExecutor,
|
||||
Type.Workflow => this._configuredExecutor!.Raw ?? this._configuredExecutor,
|
||||
_ => throw new InvalidOperationException($"Unknown ExecutorIsh type: {this.ExecutorType}")
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets the registration details for the current executor.
|
||||
/// </summary>
|
||||
/// <remarks>The returned registration depends on the type of the executor. If the executor is unbound, an
|
||||
/// <see cref="InvalidOperationException"/> is thrown. For other executor types, the registration includes the
|
||||
/// appropriate ID, type, and provider based on the executor's configuration.</remarks>
|
||||
internal ExecutorRegistration Registration => new(this.Id, this.RuntimeType, this.ExecutorProvider, this.RawData);
|
||||
|
||||
private System.Type RuntimeType => this.ExecutorType switch
|
||||
{
|
||||
Type.Unbound => throw new InvalidOperationException($"ExecutorIsh with ID '{this.Id}' is unbound."),
|
||||
Type.Executor => this._configuredExecutorType!,
|
||||
Type.RequestPort => typeof(RequestInfoExecutor),
|
||||
Type.Agent => typeof(AIAgentHostExecutor),
|
||||
Type.Function => this._configuredExecutorType!,
|
||||
Type.Workflow => this._configuredExecutorType!,
|
||||
_ => throw new InvalidOperationException($"Unknown ExecutorIsh type: {this.ExecutorType}")
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets an <see cref="Func{Executor}"/> that can be used to obtain an <see cref="Executor"/> instance
|
||||
/// corresponding to this <see cref="ExecutorIsh"/>.
|
||||
/// </summary>
|
||||
private Func<string, ValueTask<Executor>> ExecutorProvider => this.ExecutorType switch
|
||||
{
|
||||
Type.Unbound => throw new InvalidOperationException($"Executor with ID '{this.Id}' is unbound."),
|
||||
Type.Executor => this._configuredExecutor!.BoundFactoryAsync,
|
||||
Type.RequestPort => (runId) => new(new RequestInfoExecutor(this._requestPortValue!)),
|
||||
Type.Agent => (runId) => new(new AIAgentHostExecutor(this._aiAgentValue!)),
|
||||
Type.Function => this._configuredExecutor!.BoundFactoryAsync,
|
||||
Type.Workflow => this._configuredExecutor!.BoundFactoryAsync,
|
||||
_ => throw new InvalidOperationException($"Unknown ExecutorIsh type: {this.ExecutorType}")
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Defines an implicit conversion from an <see cref="Executor"/> instance to an <see cref="ExecutorIsh"/> object.
|
||||
/// </summary>
|
||||
/// <param name="executor">The <see cref="Executor"/> instance to convert to <see cref="ExecutorIsh"/>.</param>
|
||||
public static implicit operator ExecutorIsh(Executor executor) => new(executor);
|
||||
|
||||
/// <summary>
|
||||
/// Defines an implicit conversion from an <see cref="RequestPort"/> to an <see cref="ExecutorIsh"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="inputPort">The <see cref="RequestPort"/> to convert to an <see cref="ExecutorIsh"/>.</param>
|
||||
public static implicit operator ExecutorIsh(RequestPort inputPort) => new(inputPort);
|
||||
|
||||
/// <summary>
|
||||
/// Defines an implicit conversion from an <see cref="AIAgent"/> to an <see cref="ExecutorIsh"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="aiAgent">The <see cref="AIAgent"/> to convert to an <see cref="ExecutorIsh"/>.</param>
|
||||
public static implicit operator ExecutorIsh(AIAgent aiAgent) => new(aiAgent);
|
||||
|
||||
/// <summary>
|
||||
/// Defines an implicit conversion from a string to an <see cref="ExecutorIsh"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="id">The string ID to convert to an <see cref="ExecutorIsh"/>.</param>
|
||||
public static implicit operator ExecutorIsh(string id) => new(id);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(ExecutorIsh? other) =>
|
||||
other is not null && other.Id == this.Id;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(IIdentified? other) =>
|
||||
other is not null && other.Id == this.Id;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(string? other) =>
|
||||
other is not null && other == this.Id;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj) =>
|
||||
obj switch
|
||||
{
|
||||
null => false,
|
||||
ExecutorIsh ish => this.Equals(ish),
|
||||
IIdentified identified => this.Equals(identified),
|
||||
string str => this.Equals(str),
|
||||
_ => false
|
||||
};
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode() => this.Id.GetHashCode();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() => this.ExecutorType switch
|
||||
{
|
||||
Type.Unbound => $"'{this.Id}':<unbound>",
|
||||
Type.Executor => $"'{this.Id}':{this._configuredExecutorType!.Name}",
|
||||
Type.RequestPort => $"'{this.Id}':Input({this._requestPortValue!.Request.Name}->{this._requestPortValue!.Response.Name})",
|
||||
Type.Agent => $"{this.Id}':AIAgent(@{this._aiAgentValue!.GetType().Name})",
|
||||
Type.Function => $"'{this.Id}':{this._configuredExecutorType!.Name}",
|
||||
Type.Workflow => $"'{this.Id}':{this._configuredExecutorType!.Name}",
|
||||
_ => $"'{this.Id}':<unknown[{this.ExecutorType}]>"
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a placeholder entry for an <see cref="ExecutorBinding"/>, identified by a unique ID.
|
||||
/// </summary>
|
||||
/// <param name="Id">The unique identifier for the placeholder registration.</param>
|
||||
public record ExecutorPlaceholder(string Id)
|
||||
: ExecutorBinding(Id,
|
||||
null,
|
||||
typeof(Executor),
|
||||
Id)
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override bool SupportsConcurrentSharedExecution => false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool SupportsResetting => false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsSharedInstance => false;
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
using ExecutorFactoryF = System.Func<string, System.Threading.Tasks.ValueTask<Microsoft.Agents.AI.Workflows.Executor>>;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows;
|
||||
|
||||
internal sealed class ExecutorRegistration(string id, Type executorType, ExecutorFactoryF provider, object? rawData)
|
||||
{
|
||||
public string Id { get; } = Throw.IfNullOrEmpty(id);
|
||||
public Type ExecutorType { get; } = Throw.IfNull(executorType);
|
||||
private ExecutorFactoryF ProviderAsync { get; } = Throw.IfNull(provider);
|
||||
|
||||
public bool IsSharedInstance { get; } = rawData is Executor;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value whether instances of the executor created from this registration can be reset between subsequent
|
||||
/// runs from the same <see cref="Workflow"/> instance. This value is not relevant for executors that <see
|
||||
/// cref="SupportsConcurrent"/>.
|
||||
/// </summary>
|
||||
public bool SupportsResetting { get; } = rawData is Executor &&
|
||||
// Cross-Run Shareable executors are "trivially" resettable, since they
|
||||
// have no on-object state.
|
||||
rawData is IResettableExecutor;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value whether instances of the executor created from this registration can be used in concurrent runs
|
||||
/// from the same <see cref="Workflow"/> instance.
|
||||
/// </summary>
|
||||
public bool SupportsConcurrent { get; } = rawData is not Executor executor || executor.IsCrossRunShareable;
|
||||
|
||||
internal async ValueTask<bool> TryResetAsync()
|
||||
{
|
||||
// Non-shared instances do not need resetting
|
||||
if (!this.IsSharedInstance)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Technically we definitely know this is true, since if rawData is an Executor, if it was not resettable
|
||||
// then we would have returned in the first condition, and if rawData is not an Executor, we would have
|
||||
// returned in the second condition. That only leaves the possibility of rawData is Executor and also
|
||||
// IResettableExecutor.
|
||||
if (this.RawExecutorishData is IResettableExecutor resettableExecutor)
|
||||
{
|
||||
await resettableExecutor.ResetAsync().ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal object? RawExecutorishData { get; } = rawData;
|
||||
|
||||
public override string ToString() => $"{this.ExecutorType.Name}({this.Id})";
|
||||
|
||||
private Executor CheckId(Executor executor)
|
||||
{
|
||||
if (executor.Id != this.Id)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Executor ID mismatch: expected '{this.Id}', but got '{executor.Id}'.");
|
||||
}
|
||||
|
||||
return executor;
|
||||
}
|
||||
|
||||
public async ValueTask<Executor> CreateInstanceAsync(string runId) => this.CheckId(await this.ProviderAsync(runId).ConfigureAwait(false));
|
||||
}
|
||||
@@ -38,7 +38,9 @@ public class FunctionExecutor<TInput>(string id,
|
||||
/// </summary>
|
||||
/// <param name="id">A unique identifier for the executor.</param>
|
||||
/// <param name="handlerSync">A synchronous function to execute for each input message and workflow context.</param>
|
||||
public FunctionExecutor(string id, Action<TInput, IWorkflowContext, CancellationToken> handlerSync) : this(id, WrapAction(handlerSync))
|
||||
/// <param name="options">Configuration options for the executor. If <c>null</c>, default options will be used.</param>
|
||||
/// <param name="declareCrossRunShareable">Declare that this executor may be used simultaneously by multiple runs safely.</param>
|
||||
public FunctionExecutor(string id, Action<TInput, IWorkflowContext, CancellationToken> handlerSync, ExecutorOptions? options = null, bool declareCrossRunShareable = false) : this(id, WrapAction(handlerSync), options, declareCrossRunShareable)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -76,7 +78,9 @@ public class FunctionExecutor<TInput, TOutput>(string id,
|
||||
/// </summary>
|
||||
/// <param name="id">A unique identifier for the executor.</param>
|
||||
/// <param name="handlerSync">A synchronous function to execute for each input message and workflow context.</param>
|
||||
public FunctionExecutor(string id, Func<TInput, IWorkflowContext, CancellationToken, TOutput> handlerSync) : this(id, WrapFunc(handlerSync))
|
||||
/// <param name="options">Configuration options for the executor. If <c>null</c>, default options will be used.</param>
|
||||
/// <param name="declareCrossRunShareable">Declare that this executor may be used simultaneously by multiple runs safely.</param>
|
||||
public FunctionExecutor(string id, Func<TInput, IWorkflowContext, CancellationToken, TOutput> handlerSync, ExecutorOptions? options = null, bool declareCrossRunShareable = false) : this(id, WrapFunc(handlerSync), options, declareCrossRunShareable)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,12 +50,12 @@ public sealed class GroupChatWorkflowBuilder
|
||||
public Workflow Build()
|
||||
{
|
||||
AIAgent[] agents = this._participants.ToArray();
|
||||
Dictionary<AIAgent, ExecutorIsh> agentMap = agents.ToDictionary(a => a, a => (ExecutorIsh)new AgentRunStreamingExecutor(a, includeInputInOutput: true));
|
||||
Dictionary<AIAgent, ExecutorBinding> agentMap = agents.ToDictionary(a => a, a => (ExecutorBinding)new AgentRunStreamingExecutor(a, includeInputInOutput: true));
|
||||
|
||||
Func<string, string, ValueTask<Executor>> groupChatHostFactory =
|
||||
(string id, string runId) => new(new GroupChatHost(id, agents, agentMap, this._managerFactory));
|
||||
|
||||
ExecutorIsh host = groupChatHostFactory.ConfigureFactory(nameof(GroupChatHost));
|
||||
ExecutorBinding host = groupChatHostFactory.BindExecutor(nameof(GroupChatHost));
|
||||
WorkflowBuilder builder = new(host);
|
||||
|
||||
foreach (var participant in agentMap.Values)
|
||||
|
||||
@@ -73,7 +73,7 @@ internal sealed class InProcessRunnerContext : IRunnerContext
|
||||
|
||||
async Task<Executor> CreateExecutorAsync(string id)
|
||||
{
|
||||
if (!this._workflow.Registrations.TryGetValue(executorId, out var registration))
|
||||
if (!this._workflow.ExecutorBindings.TryGetValue(executorId, out var registration))
|
||||
{
|
||||
throw new InvalidOperationException($"Executor with ID '{executorId}' is not registered.");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Specialized;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the registration details for a request port, including configuration for allowing wrapped requests.
|
||||
/// </summary>
|
||||
/// <param name="Port">The request port.</param>
|
||||
/// <param name="AllowWrapped">true to allow wrapped requests to be handled by the port; otherwise, false.
|
||||
/// The default is true.</param>
|
||||
public record RequestPortBinding(RequestPort Port, bool AllowWrapped = true)
|
||||
: ExecutorBinding(Throw.IfNull(Port).Id,
|
||||
(_) => new ValueTask<Executor>(new RequestInfoExecutor(Port, AllowWrapped)),
|
||||
typeof(RequestInfoExecutor),
|
||||
Port)
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override bool IsSharedInstance => false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool SupportsConcurrentSharedExecution => true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool SupportsResetting => false;
|
||||
}
|
||||
@@ -10,11 +10,11 @@ namespace Microsoft.Agents.AI.Workflows.Specialized;
|
||||
internal sealed class GroupChatHost(
|
||||
string id,
|
||||
AIAgent[] agents,
|
||||
Dictionary<AIAgent, ExecutorIsh> agentMap,
|
||||
Dictionary<AIAgent, ExecutorBinding> agentMap,
|
||||
Func<IReadOnlyList<AIAgent>, GroupChatManager> managerFactory) : Executor(id), IResettableExecutor
|
||||
{
|
||||
private readonly AIAgent[] _agents = agents;
|
||||
private readonly Dictionary<AIAgent, ExecutorIsh> _agentMap = agentMap;
|
||||
private readonly Dictionary<AIAgent, ExecutorBinding> _agentMap = agentMap;
|
||||
private readonly Func<IReadOnlyList<AIAgent>, GroupChatManager> _managerFactory = managerFactory;
|
||||
private readonly List<ChatMessage> _pendingMessages = [];
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Specialized;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the workflow binding details for a subworkflow, including its instance, identifier, and optional
|
||||
/// executor options.
|
||||
/// </summary>
|
||||
/// <param name="WorkflowInstance"></param>
|
||||
/// <param name="Id"></param>
|
||||
/// <param name="ExecutorOptions"></param>
|
||||
public record SubworkflowBinding(Workflow WorkflowInstance, string Id, ExecutorOptions? ExecutorOptions = null)
|
||||
: ExecutorBinding(Throw.IfNull(Id),
|
||||
CreateWorkflowExecutorFactory(WorkflowInstance, Id, ExecutorOptions),
|
||||
typeof(WorkflowHostExecutor),
|
||||
WorkflowInstance)
|
||||
{
|
||||
private static Func<string, ValueTask<Executor>> CreateWorkflowExecutorFactory(Workflow workflow, string id, ExecutorOptions? options)
|
||||
{
|
||||
object ownershipToken = new();
|
||||
workflow.TakeOwnership(ownershipToken, subworkflow: true);
|
||||
|
||||
return InitHostExecutorAsync;
|
||||
|
||||
ValueTask<Executor> InitHostExecutorAsync(string runId)
|
||||
{
|
||||
return new(new WorkflowHostExecutor(id, workflow, runId, ownershipToken, options));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsSharedInstance => false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool SupportsConcurrentSharedExecution => true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool SupportsResetting => false;
|
||||
}
|
||||
@@ -13,7 +13,7 @@ namespace Microsoft.Agents.AI.Workflows;
|
||||
/// </summary>
|
||||
public sealed class SwitchBuilder
|
||||
{
|
||||
private readonly List<ExecutorIsh> _executors = [];
|
||||
private readonly List<ExecutorBinding> _executors = [];
|
||||
private readonly Dictionary<string, int> _executorIndicies = [];
|
||||
private readonly List<(Func<object?, bool> Predicate, HashSet<int> OutgoingIndicies)> _caseMap = [];
|
||||
private readonly HashSet<int> _defaultIndicies = [];
|
||||
@@ -30,14 +30,14 @@ public sealed class SwitchBuilder
|
||||
/// <param name="executors">One or more executors to associate with the predicate. Each executor will be invoked if the predicate matches.
|
||||
/// Cannot be null.</param>
|
||||
/// <returns>The current <see cref="SwitchBuilder"/> instance, allowing for method chaining.</returns>
|
||||
public SwitchBuilder AddCase<T>(Func<T?, bool> predicate, params IEnumerable<ExecutorIsh> executors)
|
||||
public SwitchBuilder AddCase<T>(Func<T?, bool> predicate, params IEnumerable<ExecutorBinding> executors)
|
||||
{
|
||||
Throw.IfNull(predicate);
|
||||
Throw.IfNull(executors);
|
||||
|
||||
HashSet<int> indicies = [];
|
||||
|
||||
foreach (ExecutorIsh executor in executors)
|
||||
foreach (ExecutorBinding executor in executors)
|
||||
{
|
||||
if (!this._executorIndicies.TryGetValue(executor.Id, out int index))
|
||||
{
|
||||
@@ -60,11 +60,11 @@ public sealed class SwitchBuilder
|
||||
/// </summary>
|
||||
/// <param name="executors"></param>
|
||||
/// <returns></returns>
|
||||
public SwitchBuilder WithDefault(params IEnumerable<ExecutorIsh> executors)
|
||||
public SwitchBuilder WithDefault(params IEnumerable<ExecutorBinding> executors)
|
||||
{
|
||||
Throw.IfNull(executors);
|
||||
|
||||
foreach (ExecutorIsh executor in executors)
|
||||
foreach (ExecutorBinding executor in executors)
|
||||
{
|
||||
if (!this._executorIndicies.TryGetValue(executor.Id, out int index))
|
||||
{
|
||||
@@ -79,7 +79,7 @@ public sealed class SwitchBuilder
|
||||
return this;
|
||||
}
|
||||
|
||||
internal WorkflowBuilder ReduceToFanOut(WorkflowBuilder builder, ExecutorIsh source)
|
||||
internal WorkflowBuilder ReduceToFanOut(WorkflowBuilder builder, ExecutorBinding source)
|
||||
{
|
||||
List<(Func<object?, bool> Predicate, HashSet<int> OutgoingIndicies)> caseMap = this._caseMap;
|
||||
HashSet<int> defaultIndicies = this._defaultIndicies;
|
||||
|
||||
@@ -69,7 +69,7 @@ public static class WorkflowVisualizer
|
||||
lines.Add($"{indent}\"{MapId(startExecutorId)}\" [fillcolor=lightgreen, label=\"{startExecutorId}\\n(Start)\"];");
|
||||
|
||||
// Add other executor nodes
|
||||
foreach (var executorId in workflow.Registrations.Keys)
|
||||
foreach (var executorId in workflow.ExecutorBindings.Keys)
|
||||
{
|
||||
if (executorId != startExecutorId)
|
||||
{
|
||||
@@ -108,7 +108,7 @@ public static class WorkflowVisualizer
|
||||
|
||||
private static void EmitSubWorkflowsDigraph(Workflow workflow, List<string> lines, string indent)
|
||||
{
|
||||
foreach (var kvp in workflow.Registrations)
|
||||
foreach (var kvp in workflow.ExecutorBindings)
|
||||
{
|
||||
var execId = kvp.Key;
|
||||
var registration = kvp.Value;
|
||||
@@ -145,7 +145,7 @@ public static class WorkflowVisualizer
|
||||
lines.Add($"{indent}{MapId(startExecutorId)}[\"{startExecutorId} (Start)\"];");
|
||||
|
||||
// Add other executor nodes
|
||||
foreach (var executorId in workflow.Registrations.Keys)
|
||||
foreach (var executorId in workflow.ExecutorBindings.Keys)
|
||||
{
|
||||
if (executorId != startExecutorId)
|
||||
{
|
||||
@@ -264,9 +264,9 @@ public static class WorkflowVisualizer
|
||||
#endif
|
||||
}
|
||||
|
||||
private static bool TryGetNestedWorkflow(ExecutorRegistration registration, [NotNullWhen(true)] out Workflow? workflow)
|
||||
private static bool TryGetNestedWorkflow(ExecutorBinding binding, [NotNullWhen(true)] out Workflow? workflow)
|
||||
{
|
||||
if (registration.RawExecutorishData is Workflow subWorkflow)
|
||||
if (binding.RawValue is Workflow subWorkflow)
|
||||
{
|
||||
workflow = subWorkflow;
|
||||
return true;
|
||||
|
||||
@@ -19,7 +19,7 @@ public class Workflow
|
||||
/// <summary>
|
||||
/// A dictionary of executor providers, keyed by executor ID.
|
||||
/// </summary>
|
||||
internal Dictionary<string, ExecutorRegistration> Registrations { get; init; } = [];
|
||||
internal Dictionary<string, ExecutorBinding> ExecutorBindings { get; init; } = [];
|
||||
|
||||
internal Dictionary<string, HashSet<Edge>> Edges { get; init; } = [];
|
||||
internal HashSet<string> OutputExecutors { get; init; } = [];
|
||||
@@ -41,7 +41,7 @@ public class Workflow
|
||||
/// Gets the collection of external request ports, keyed by their ID.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Each port has a corresponding entry in the <see cref="Registrations"/> dictionary.
|
||||
/// Each port has a corresponding entry in the <see cref="ExecutorBindings"/> dictionary.
|
||||
/// </remarks>
|
||||
public Dictionary<string, RequestPortInfo> ReflectPorts()
|
||||
{
|
||||
@@ -66,10 +66,10 @@ public class Workflow
|
||||
/// </summary>
|
||||
public string? Description { get; internal init; }
|
||||
|
||||
internal bool AllowConcurrent => this.Registrations.Values.All(registration => registration.SupportsConcurrent);
|
||||
internal bool AllowConcurrent => this.ExecutorBindings.Values.All(registration => registration.SupportsConcurrentSharedExecution);
|
||||
|
||||
internal IEnumerable<string> NonConcurrentExecutorIds =>
|
||||
this.Registrations.Values.Where(r => !r.SupportsConcurrent).Select(r => r.Id);
|
||||
this.ExecutorBindings.Values.Where(r => !r.SupportsConcurrentSharedExecution).Select(r => r.Id);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Workflow"/> class with the specified starting executor identifier
|
||||
@@ -86,12 +86,14 @@ public class Workflow
|
||||
}
|
||||
|
||||
private bool _needsReset;
|
||||
private bool HasResettable => this.Registrations.Values.Any(registration => registration.SupportsResetting);
|
||||
private bool HasResettableExecutors =>
|
||||
this.ExecutorBindings.Values.Any(registration => registration.SupportsResetting);
|
||||
|
||||
private async ValueTask<bool> TryResetExecutorRegistrationsAsync()
|
||||
{
|
||||
if (this.HasResettable)
|
||||
if (this.HasResettableExecutors)
|
||||
{
|
||||
foreach (ExecutorRegistration registration in this.Registrations.Values)
|
||||
foreach (ExecutorBinding registration in this.ExecutorBindings.Values)
|
||||
{
|
||||
// TryResetAsync returns true if the executor does not need resetting
|
||||
if (!await registration.TryResetAsync().ConfigureAwait(false))
|
||||
@@ -158,7 +160,7 @@ public class Workflow
|
||||
});
|
||||
}
|
||||
|
||||
this._needsReset = this.HasResettable;
|
||||
this._needsReset = this.HasResettableExecutors;
|
||||
this._ownedAsSubworkflow = subworkflow;
|
||||
}
|
||||
|
||||
@@ -188,7 +190,7 @@ public class Workflow
|
||||
/// a <see cref="ProtocolDescriptor"/> the protocol this <see cref="Workflow"/> follows.</returns>
|
||||
public async ValueTask<ProtocolDescriptor> DescribeProtocolAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
ExecutorRegistration startExecutorRegistration = this.Registrations[this.StartExecutorId];
|
||||
ExecutorBinding startExecutorRegistration = this.ExecutorBindings[this.StartExecutorId];
|
||||
Executor startExecutor = await startExecutorRegistration.CreateInstanceAsync(string.Empty)
|
||||
.ConfigureAwait(false);
|
||||
return startExecutor.DescribeProtocol();
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Microsoft.Agents.AI.Workflows;
|
||||
/// <remarks>Use the WorkflowBuilder to incrementally add executors and edges, including fan-in and fan-out
|
||||
/// patterns, before building a strongly-typed workflow instance. Executors must be bound before building the workflow.
|
||||
/// All executors must be bound by calling into <see cref="BindExecutor"/> if they were intially specified as
|
||||
/// <see cref="ExecutorIsh.Type.Unbound"/>.</remarks>
|
||||
/// <see cref="ExecutorBinding.IsPlaceholder"/>.</remarks>
|
||||
public class WorkflowBuilder
|
||||
{
|
||||
private readonly record struct EdgeConnection(string SourceId, string TargetId)
|
||||
@@ -28,11 +28,11 @@ public class WorkflowBuilder
|
||||
}
|
||||
|
||||
private int _edgeCount;
|
||||
private readonly Dictionary<string, ExecutorRegistration> _executors = [];
|
||||
private readonly Dictionary<string, ExecutorBinding> _executors = [];
|
||||
private readonly Dictionary<string, HashSet<Edge>> _edges = [];
|
||||
private readonly HashSet<string> _unboundExecutors = [];
|
||||
private readonly HashSet<EdgeConnection> _conditionlessConnections = [];
|
||||
private readonly Dictionary<string, RequestPort> _inputPorts = [];
|
||||
private readonly Dictionary<string, RequestPort> _requestPorts = [];
|
||||
private readonly HashSet<string> _outputExecutors = [];
|
||||
|
||||
private readonly string _startExecutorId;
|
||||
@@ -46,57 +46,56 @@ public class WorkflowBuilder
|
||||
/// Initializes a new instance of the WorkflowBuilder class with the specified starting executor.
|
||||
/// </summary>
|
||||
/// <param name="start">The executor that defines the starting point of the workflow. Cannot be null.</param>
|
||||
public WorkflowBuilder(ExecutorIsh start)
|
||||
public WorkflowBuilder(ExecutorBinding start)
|
||||
{
|
||||
this._startExecutorId = this.Track(start).Id;
|
||||
}
|
||||
|
||||
private ExecutorIsh Track(ExecutorIsh executorish)
|
||||
private ExecutorBinding Track(ExecutorBinding registration)
|
||||
{
|
||||
// If the executor is unbound, create an entry for it, unless it already exists.
|
||||
// Otherwise, update the entry for it, and remove the unbound tag
|
||||
if (executorish.IsUnbound && !this._executors.ContainsKey(executorish.Id))
|
||||
if (registration.IsPlaceholder && !this._executors.ContainsKey(registration.Id))
|
||||
{
|
||||
// If this is an unbound executor, we need to track it separately
|
||||
this._unboundExecutors.Add(executorish.Id);
|
||||
this._unboundExecutors.Add(registration.Id);
|
||||
}
|
||||
else if (!executorish.IsUnbound)
|
||||
else if (!registration.IsPlaceholder)
|
||||
{
|
||||
ExecutorRegistration incoming = executorish.Registration;
|
||||
// If there is already a bound executor with this ID, we need to validate (to best efforts)
|
||||
// that the two are matching (at least based on type)
|
||||
if (this._executors.TryGetValue(executorish.Id, out ExecutorRegistration? existing))
|
||||
if (this._executors.TryGetValue(registration.Id, out ExecutorBinding? existing))
|
||||
{
|
||||
if (existing.ExecutorType != incoming.ExecutorType)
|
||||
if (existing.ExecutorType != registration.ExecutorType)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Cannot bind executor with ID '{executorish.Id}' because an executor with the same ID but a different type ({existing.ExecutorType.Name} vs {incoming.ExecutorType.Name}) is already bound.");
|
||||
$"Cannot bind executor with ID '{registration.Id}' because an executor with the same ID but a different type ({existing.ExecutorType.Name} vs {registration.ExecutorType.Name}) is already bound.");
|
||||
}
|
||||
|
||||
if (existing.RawExecutorishData is not null &&
|
||||
!ReferenceEquals(existing.RawExecutorishData, incoming.RawExecutorishData))
|
||||
if (existing.RawValue is not null &&
|
||||
!ReferenceEquals(existing.RawValue, registration.RawValue))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Cannot bind executor with ID '{executorish.Id}' because an executor with the same ID but different instance is already bound.");
|
||||
$"Cannot bind executor with ID '{registration.Id}' because an executor with the same ID but different instance is already bound.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this._executors[executorish.Id] = executorish.Registration;
|
||||
if (this._unboundExecutors.Contains(executorish.Id))
|
||||
this._executors[registration.Id] = registration;
|
||||
if (this._unboundExecutors.Contains(registration.Id))
|
||||
{
|
||||
this._unboundExecutors.Remove(executorish.Id);
|
||||
this._unboundExecutors.Remove(registration.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (executorish.ExecutorType == ExecutorIsh.Type.RequestPort)
|
||||
if (registration is RequestPortBinding portRegistration)
|
||||
{
|
||||
RequestPort port = executorish._requestPortValue!;
|
||||
this._inputPorts[port.Id] = port;
|
||||
RequestPort port = portRegistration.Port;
|
||||
this._requestPorts[port.Id] = port;
|
||||
}
|
||||
|
||||
return executorish;
|
||||
return registration;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -106,9 +105,9 @@ public class WorkflowBuilder
|
||||
/// </summary>
|
||||
/// <param name="executors"></param>
|
||||
/// <returns></returns>
|
||||
public WorkflowBuilder WithOutputFrom(params ExecutorIsh[] executors)
|
||||
public WorkflowBuilder WithOutputFrom(params ExecutorBinding[] executors)
|
||||
{
|
||||
foreach (ExecutorIsh executor in executors)
|
||||
foreach (ExecutorBinding executor in executors)
|
||||
{
|
||||
this._outputExecutors.Add(this.Track(executor).Id);
|
||||
}
|
||||
@@ -139,21 +138,21 @@ public class WorkflowBuilder
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binds the specified executor to the workflow, allowing it to participate in workflow execution.
|
||||
/// Binds the specified executor (via registration) to the workflow, allowing it to participate in workflow execution.
|
||||
/// </summary>
|
||||
/// <param name="executor">The executor instance to bind. The executor must exist in the workflow and not be already bound.</param>
|
||||
/// <param name="registration">The executor instance to bind. The executor must exist in the workflow and not be already bound.</param>
|
||||
/// <returns>The current <see cref="WorkflowBuilder"/> instance, enabling fluent configuration.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown if the specified executor is already bound or does not exist in the workflow.</exception>
|
||||
public WorkflowBuilder BindExecutor(Executor executor)
|
||||
public WorkflowBuilder BindExecutor(ExecutorBinding registration)
|
||||
{
|
||||
if (!this._unboundExecutors.Contains(executor.Id))
|
||||
if (Throw.IfNull(registration) is ExecutorPlaceholder)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Executor with ID '{executor.Id}' is already bound or does not exist in the workflow.");
|
||||
$"Cannot bind executor with ID '{registration.Id}' because it is a placeholder registration. " +
|
||||
"You must provide a concrete executor instance or registration.");
|
||||
}
|
||||
|
||||
this._executors[executor.Id] = new ExecutorIsh(executor).Registration;
|
||||
this._unboundExecutors.Remove(executor.Id);
|
||||
this.Track(registration);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -180,7 +179,7 @@ public class WorkflowBuilder
|
||||
/// <returns>The current instance of <see cref="WorkflowBuilder"/>.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown if an unconditional edge between the specified source and target
|
||||
/// executors already exists.</exception>
|
||||
public WorkflowBuilder AddEdge(ExecutorIsh source, ExecutorIsh target, bool idempotent = false)
|
||||
public WorkflowBuilder AddEdge(ExecutorBinding source, ExecutorBinding target, bool idempotent = false)
|
||||
=> this.AddEdge<object>(source, target, null, idempotent);
|
||||
|
||||
internal static Func<object?, bool>? CreateConditionFunc<T>(Func<T?, bool>? condition)
|
||||
@@ -236,7 +235,7 @@ public class WorkflowBuilder
|
||||
/// <returns>The current instance of <see cref="WorkflowBuilder"/>.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown if an unconditional edge between the specified source and target
|
||||
/// executors already exists.</exception>
|
||||
public WorkflowBuilder AddEdge<T>(ExecutorIsh source, ExecutorIsh target, Func<T?, bool>? condition = null, bool idempotent = false)
|
||||
public WorkflowBuilder AddEdge<T>(ExecutorBinding source, ExecutorBinding target, Func<T?, bool>? condition = null, bool idempotent = false)
|
||||
{
|
||||
// Add an edge from source to target with an optional condition.
|
||||
// This is a low-level builder method that does not enforce any specific executor type.
|
||||
@@ -273,7 +272,7 @@ public class WorkflowBuilder
|
||||
/// <param name="source">The source executor from which the fan-out edge originates. Cannot be null.</param>
|
||||
/// <param name="targets">One or more target executors that will receive the fan-out edge. Cannot be null or empty.</param>
|
||||
/// <returns>The current instance of <see cref="WorkflowBuilder"/>.</returns>
|
||||
public WorkflowBuilder AddFanOutEdge(ExecutorIsh source, params IEnumerable<ExecutorIsh> targets)
|
||||
public WorkflowBuilder AddFanOutEdge(ExecutorBinding source, params IEnumerable<ExecutorBinding> targets)
|
||||
=> this.AddFanOutEdge<object>(source, null, targets);
|
||||
|
||||
internal static Func<object?, int, IEnumerable<int>>? CreateEdgeAssignerFunc<T>(Func<T?, int, IEnumerable<int>>? partitioner)
|
||||
@@ -305,7 +304,7 @@ public class WorkflowBuilder
|
||||
/// If null, messages will route to all targets.</param>
|
||||
/// <param name="targets">One or more target executors that will receive the fan-out edge. Cannot be null or empty.</param>
|
||||
/// <returns>The current instance of <see cref="WorkflowBuilder"/>.</returns>
|
||||
public WorkflowBuilder AddFanOutEdge<T>(ExecutorIsh source, Func<T?, int, IEnumerable<int>>? partitioner = null, params IEnumerable<ExecutorIsh> targets)
|
||||
public WorkflowBuilder AddFanOutEdge<T>(ExecutorBinding source, Func<T?, int, IEnumerable<int>>? partitioner = null, params IEnumerable<ExecutorBinding> targets)
|
||||
{
|
||||
Throw.IfNull(source);
|
||||
Throw.IfNull(targets);
|
||||
@@ -339,7 +338,7 @@ public class WorkflowBuilder
|
||||
/// <param name="target">The target executor that receives input from the specified source executors. Cannot be null.</param>
|
||||
/// <param name="sources">One or more source executors that provide input to the target. Cannot be null or empty.</param>
|
||||
/// <returns>The current instance of <see cref="WorkflowBuilder"/>.</returns>
|
||||
public WorkflowBuilder AddFanInEdge(ExecutorIsh target, params IEnumerable<ExecutorIsh> sources)
|
||||
public WorkflowBuilder AddFanInEdge(ExecutorBinding target, params IEnumerable<ExecutorBinding> sources)
|
||||
{
|
||||
Throw.IfNull(target);
|
||||
Throw.IfNull(sources);
|
||||
@@ -398,9 +397,9 @@ public class WorkflowBuilder
|
||||
|
||||
var workflow = new Workflow(this._startExecutorId, this._name, this._description)
|
||||
{
|
||||
Registrations = this._executors,
|
||||
ExecutorBindings = this._executors,
|
||||
Edges = this._edges,
|
||||
Ports = this._inputPorts,
|
||||
Ports = this._requestPorts,
|
||||
OutputExecutors = this._outputExecutors
|
||||
};
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ public static class WorkflowBuilderExtensions
|
||||
/// <param name="source">The source executor from which messages will be forwarded.</param>
|
||||
/// <param name="executors">The target executors to which messages will be forwarded.</param>
|
||||
/// <returns>The updated <see cref="WorkflowBuilder"/> instance.</returns>
|
||||
public static WorkflowBuilder ForwardMessage<TMessage>(this WorkflowBuilder builder, ExecutorIsh source, params IEnumerable<ExecutorIsh> executors)
|
||||
public static WorkflowBuilder ForwardMessage<TMessage>(this WorkflowBuilder builder, ExecutorBinding source, params IEnumerable<ExecutorBinding> executors)
|
||||
=> builder.ForwardMessage<TMessage>(source, condition: null, executors);
|
||||
|
||||
/// <summary>
|
||||
@@ -38,7 +38,7 @@ public static class WorkflowBuilderExtensions
|
||||
/// all messages of type <typeparamref name="TMessage"/> will be forwarded.</param>
|
||||
/// <param name="executors">The target executors to which messages will be forwarded.</param>
|
||||
/// <returns>The updated <see cref="WorkflowBuilder"/> instance.</returns>
|
||||
public static WorkflowBuilder ForwardMessage<TMessage>(this WorkflowBuilder builder, ExecutorIsh source, Func<TMessage, bool>? condition = null, params IEnumerable<ExecutorIsh> executors)
|
||||
public static WorkflowBuilder ForwardMessage<TMessage>(this WorkflowBuilder builder, ExecutorBinding source, Func<TMessage, bool>? condition = null, params IEnumerable<ExecutorBinding> executors)
|
||||
{
|
||||
Throw.IfNull(executors);
|
||||
|
||||
@@ -47,7 +47,7 @@ public static class WorkflowBuilderExtensions
|
||||
#if NET
|
||||
if (executors.TryGetNonEnumeratedCount(out int count) && count == 1)
|
||||
#else
|
||||
if (executors is ICollection<ExecutorIsh> { Count: 1 })
|
||||
if (executors is ICollection<ExecutorBinding> { Count: 1 })
|
||||
#endif
|
||||
{
|
||||
return builder.AddEdge(source, executors.First(), predicate);
|
||||
@@ -68,7 +68,7 @@ public static class WorkflowBuilderExtensions
|
||||
/// <param name="source">The source executor from which messages will be forwarded.</param>
|
||||
/// <param name="executors">The target executors to which messages, except those of type <typeparamref name="TMessage"/>, will be forwarded.</param>
|
||||
/// <returns>The updated <see cref="WorkflowBuilder"/> instance with the added edges.</returns>
|
||||
public static WorkflowBuilder ForwardExcept<TMessage>(this WorkflowBuilder builder, ExecutorIsh source, params IEnumerable<ExecutorIsh> executors)
|
||||
public static WorkflowBuilder ForwardExcept<TMessage>(this WorkflowBuilder builder, ExecutorBinding source, params IEnumerable<ExecutorBinding> executors)
|
||||
{
|
||||
Throw.IfNull(executors);
|
||||
|
||||
@@ -77,7 +77,7 @@ public static class WorkflowBuilderExtensions
|
||||
#if NET
|
||||
if (executors.TryGetNonEnumeratedCount(out int count) && count == 1)
|
||||
#else
|
||||
if (executors is ICollection<ExecutorIsh> { Count: 1 })
|
||||
if (executors is ICollection<ExecutorBinding> { Count: 1 })
|
||||
#endif
|
||||
{
|
||||
return builder.AddEdge(source, executors.First(), predicate);
|
||||
@@ -102,7 +102,7 @@ public static class WorkflowBuilderExtensions
|
||||
/// <param name="executors">An ordered array of executors to be added to the chain after the source.</param>
|
||||
/// <returns>The original workflow builder instance with the specified executor chain added.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown if there is a cycle in the chain.</exception>
|
||||
public static WorkflowBuilder AddChain(this WorkflowBuilder builder, ExecutorIsh source, bool allowRepetition = false, params IEnumerable<ExecutorIsh> executors)
|
||||
public static WorkflowBuilder AddChain(this WorkflowBuilder builder, ExecutorBinding source, bool allowRepetition = false, params IEnumerable<ExecutorBinding> executors)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
Throw.IfNull(source);
|
||||
@@ -139,7 +139,7 @@ public static class WorkflowBuilderExtensions
|
||||
/// <param name="source">The source executor representing the external system or process to connect. Cannot be null.</param>
|
||||
/// <param name="portId">The unique identifier for the input port that will handle the external call. Cannot be null.</param>
|
||||
/// <returns>The original workflow builder instance with the external call added.</returns>
|
||||
public static WorkflowBuilder AddExternalCall<TRequest, TResponse>(this WorkflowBuilder builder, ExecutorIsh source, string portId)
|
||||
public static WorkflowBuilder AddExternalCall<TRequest, TResponse>(this WorkflowBuilder builder, ExecutorBinding source, string portId)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
Throw.IfNull(source);
|
||||
@@ -160,7 +160,7 @@ public static class WorkflowBuilderExtensions
|
||||
/// <param name="source">The source executor that determines the branching condition for the switch. Cannot be null.</param>
|
||||
/// <param name="configureSwitch">An action used to configure the switch builder, specifying the branches and their conditions. Cannot be null.</param>
|
||||
/// <returns>The workflow builder instance with the configured switch step added.</returns>
|
||||
public static WorkflowBuilder AddSwitch(this WorkflowBuilder builder, ExecutorIsh source, Action<SwitchBuilder> configureSwitch)
|
||||
public static WorkflowBuilder AddSwitch(this WorkflowBuilder builder, ExecutorBinding source, Action<SwitchBuilder> configureSwitch)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
Throw.IfNull(source);
|
||||
|
||||
@@ -167,7 +167,7 @@ public class JsonSerializationTests
|
||||
builder.AddEdge(forwardString, stringToInt)
|
||||
.AddEdge(stringToInt, forwardInt)
|
||||
.AddEdge(forwardInt, intToString)
|
||||
.AddEdge(intToString, StreamingAggregators.Last<int>().AsExecutor("Aggregate"));
|
||||
.AddEdge(intToString, StreamingAggregators.Last<int>().BindAsExecutor("Aggregate"));
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -38,34 +40,40 @@ public class RepresentationTests
|
||||
private static RequestPort TestRequestPort =>
|
||||
RequestPort.Create<FunctionCallContent, FunctionResultContent>("ExternalFunction");
|
||||
|
||||
private static async ValueTask RunExecutorishInfoMatchTestAsync(ExecutorIsh target)
|
||||
private static async ValueTask RunExecutorBindingInfoMatchTestAsync(ExecutorBinding binding)
|
||||
{
|
||||
ExecutorRegistration registration = target.Registration;
|
||||
ExecutorInfo info = registration.ToExecutorInfo();
|
||||
ExecutorInfo info = binding.ToExecutorInfo();
|
||||
|
||||
info.IsMatch(await registration.CreateInstanceAsync(runId: string.Empty)).Should().BeTrue();
|
||||
info.IsMatch(await binding.CreateInstanceAsync(runId: string.Empty)).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Test_Executorish_InfosAsync()
|
||||
public async Task Test_ExecutorBinding_InfosAsync()
|
||||
{
|
||||
int testsRun = 0;
|
||||
await RunExecutorishTestAsync(new TestExecutor());
|
||||
await RunExecutorishTestAsync(TestRequestPort);
|
||||
await RunExecutorishTestAsync(new TestAgent());
|
||||
await RunExecutorishTestAsync(Step1EntryPoint.WorkflowInstance.ConfigureSubWorkflow(nameof(Step1EntryPoint)));
|
||||
await RunExecutorBindingTestAsync(new TestExecutor());
|
||||
await RunExecutorBindingTestAsync(TestRequestPort);
|
||||
await RunExecutorBindingTestAsync(new TestAgent());
|
||||
await RunExecutorBindingTestAsync(Step1EntryPoint.WorkflowInstance.BindAsExecutor(nameof(Step1EntryPoint)));
|
||||
|
||||
Func<int, IWorkflowContext, CancellationToken, ValueTask> function = MessageHandlerAsync;
|
||||
await RunExecutorishTestAsync(function.AsExecutor("FunctionExecutor"));
|
||||
await RunExecutorBindingTestAsync(function.BindAsExecutor("FunctionExecutor"));
|
||||
|
||||
if (Enum.GetValues(typeof(ExecutorIsh.Type)).Length > testsRun + 1)
|
||||
Type bindingBaseType = typeof(ExecutorBinding);
|
||||
Assembly workflowAssembly = bindingBaseType.Assembly;
|
||||
int expectedTests = workflowAssembly.GetTypes()
|
||||
.Count(type => type != bindingBaseType
|
||||
&& bindingBaseType.IsAssignableFrom(type));
|
||||
expectedTests.Should().BePositive();
|
||||
|
||||
if (expectedTests > testsRun + 1)
|
||||
{
|
||||
Assert.Fail("Not all ExecutorIsh types were tested.");
|
||||
Assert.Fail("Not all ExecutorBinding types were tested.");
|
||||
}
|
||||
|
||||
async ValueTask RunExecutorishTestAsync(ExecutorIsh executorish)
|
||||
async ValueTask RunExecutorBindingTestAsync(ExecutorBinding binding)
|
||||
{
|
||||
await RunExecutorishInfoMatchTestAsync(executorish);
|
||||
await RunExecutorBindingInfoMatchTestAsync(binding);
|
||||
testsRun++;
|
||||
}
|
||||
|
||||
@@ -77,8 +85,8 @@ public class RepresentationTests
|
||||
[Fact]
|
||||
public async Task Test_SpecializedExecutor_InfosAsync()
|
||||
{
|
||||
await RunExecutorishInfoMatchTestAsync(new AIAgentHostExecutor(new TestAgent()));
|
||||
await RunExecutorishInfoMatchTestAsync(new RequestInfoExecutor(TestRequestPort));
|
||||
await RunExecutorBindingInfoMatchTestAsync(new AIAgentHostExecutor(new TestAgent()));
|
||||
await RunExecutorBindingInfoMatchTestAsync(new RequestInfoExecutor(TestRequestPort));
|
||||
}
|
||||
|
||||
private static string Source(int id) => $"Source/{id}";
|
||||
|
||||
+11
-9
@@ -11,21 +11,23 @@ internal static class Step7EntryPoint
|
||||
public static string EchoAgentId => Step6EntryPoint.EchoAgentId;
|
||||
public static string EchoPrefix => Step6EntryPoint.EchoPrefix;
|
||||
|
||||
public static async ValueTask RunAsync(TextWriter writer, IWorkflowExecutionEnvironment environment, int maxSteps = 2)
|
||||
public static async ValueTask RunAsync(TextWriter writer, IWorkflowExecutionEnvironment environment, int maxSteps = 2, int numIterations = 2)
|
||||
{
|
||||
Workflow workflow = Step6EntryPoint.CreateWorkflow(maxSteps);
|
||||
|
||||
AIAgent agent = workflow.AsAgent("group-chat-agent", "Group Chat Agent");
|
||||
|
||||
AgentThread thread = agent.GetNewThread();
|
||||
|
||||
await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(thread).ConfigureAwait(false))
|
||||
for (int i = 0; i < numIterations; i++)
|
||||
{
|
||||
string updateText = $"{update.AuthorName
|
||||
?? update.AgentId
|
||||
?? update.Role.ToString()
|
||||
?? ChatRole.Assistant.ToString()}: {update.Text}";
|
||||
writer.WriteLine(updateText);
|
||||
AgentThread thread = agent.GetNewThread();
|
||||
await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(thread).ConfigureAwait(false))
|
||||
{
|
||||
string updateText = $"{update.AuthorName
|
||||
?? update.AgentId
|
||||
?? update.Role.ToString()
|
||||
?? ChatRole.Assistant.ToString()}: {update.Text}";
|
||||
writer.WriteLine(updateText);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -29,13 +29,13 @@ internal static class Step8EntryPoint
|
||||
public static async ValueTask<List<TextProcessingResult>> RunAsync(TextWriter writer, IWorkflowExecutionEnvironment environment, List<string> textsToProcess)
|
||||
{
|
||||
Func<TextProcessingRequest, IWorkflowContext, CancellationToken, ValueTask> processTextAsyncFunc = ProcessTextAsync;
|
||||
ExecutorIsh processText = processTextAsyncFunc.AsExecutor("TextProcessor", threadsafe: true);
|
||||
ExecutorBinding processText = processTextAsyncFunc.BindAsExecutor("TextProcessor", threadsafe: true);
|
||||
|
||||
Workflow subWorkflow = new WorkflowBuilder(processText).WithOutputFrom(processText).Build();
|
||||
|
||||
ExecutorIsh textProcessor = subWorkflow.ConfigureSubWorkflow("TextProcessor");
|
||||
ExecutorBinding textProcessor = subWorkflow.BindAsExecutor("TextProcessor");
|
||||
Func<string, string, ValueTask<Executor>> createOrchestrator = (id, _) => new(new TextProcessingOrchestrator(id));
|
||||
var orchestrator = createOrchestrator.ConfigureFactory();
|
||||
var orchestrator = createOrchestrator.BindExecutor();
|
||||
|
||||
Workflow workflow = new WorkflowBuilder(orchestrator)
|
||||
.AddEdge(orchestrator, textProcessor)
|
||||
|
||||
+27
-5
@@ -62,7 +62,7 @@ internal sealed record class RequestFinished(string Id, string RequestType, Reso
|
||||
|
||||
internal static class Step9EntryPoint
|
||||
{
|
||||
public static WorkflowBuilder AddPassthroughRequestHandler<TRequest, TResponse>(this WorkflowBuilder builder, ExecutorIsh source, ExecutorIsh filter, string? id = null)
|
||||
public static WorkflowBuilder AddPassthroughRequestHandler<TRequest, TResponse>(this WorkflowBuilder builder, ExecutorBinding source, ExecutorBinding filter, string? id = null)
|
||||
{
|
||||
id ??= typeof(TRequest).Name;
|
||||
|
||||
@@ -74,10 +74,10 @@ internal static class Step9EntryPoint
|
||||
.ForwardMessage<ExternalResponse>(filter, executors: [source], condition: message => message.DataIs<TResponse>());
|
||||
}
|
||||
|
||||
public static WorkflowBuilder AddExternalRequest<TRequest, TResponse>(this WorkflowBuilder builder, ExecutorIsh source, string? id = null)
|
||||
public static WorkflowBuilder AddExternalRequest<TRequest, TResponse>(this WorkflowBuilder builder, ExecutorBinding source, string? id = null)
|
||||
=> builder.AddExternalRequest(source, out RequestPort<TRequest, TResponse> _, id);
|
||||
|
||||
public static WorkflowBuilder AddExternalRequest<TRequest, TResponse>(this WorkflowBuilder builder, ExecutorIsh source, out RequestPort<TRequest, TResponse> inputPort, string? id = null)
|
||||
public static WorkflowBuilder AddExternalRequest<TRequest, TResponse>(this WorkflowBuilder builder, ExecutorBinding source, out RequestPort<TRequest, TResponse> inputPort, string? id = null)
|
||||
{
|
||||
id = id ?? $"{source.Id}.Requests[{typeof(TRequest).Name}=>{typeof(TResponse).Name}]";
|
||||
|
||||
@@ -86,7 +86,7 @@ internal static class Step9EntryPoint
|
||||
return builder.AddExternalRequest(source, inputPort);
|
||||
}
|
||||
|
||||
public static WorkflowBuilder AddExternalRequest<TRequest, TResponse>(this WorkflowBuilder builder, ExecutorIsh source, RequestPort<TRequest, TResponse> inputPort)
|
||||
public static WorkflowBuilder AddExternalRequest<TRequest, TResponse>(this WorkflowBuilder builder, ExecutorBinding source, RequestPort<TRequest, TResponse> inputPort)
|
||||
{
|
||||
return builder.ForwardMessage<TRequest>(source, inputPort)
|
||||
.ForwardMessage<ExternalRequest>(source, inputPort)
|
||||
@@ -110,7 +110,7 @@ internal static class Step9EntryPoint
|
||||
Coordinator coordinator = new();
|
||||
ResourceCache cache = new();
|
||||
QuotaPolicyEngine policyEngine = new();
|
||||
ExecutorIsh subworkflow = CreateSubWorkflow().ConfigureSubWorkflow("ResourceWorkflow");
|
||||
ExecutorBinding subworkflow = CreateSubWorkflow().BindAsExecutor("ResourceWorkflow");
|
||||
|
||||
return new WorkflowBuilder(coordinator)
|
||||
.AddChain(coordinator, allowRepetition: true, subworkflow, coordinator)
|
||||
@@ -522,4 +522,26 @@ internal sealed class Coordinator() : Executor(nameof(Coordinator), declareCross
|
||||
return state + requests.Count;
|
||||
}
|
||||
}
|
||||
|
||||
internal async ValueTask RunWorkflowHandleEventsAsync<TInput>(Workflow workflow, TInput input) where TInput : notnull
|
||||
{
|
||||
StreamingRun run = await InProcessExecution.StreamAsync(workflow, input);
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
|
||||
{
|
||||
switch (evt)
|
||||
{
|
||||
case ExecutorInvokedEvent invoked:
|
||||
Console.WriteLine($"Executor invoked: {invoked.ExecutorId}");
|
||||
break;
|
||||
case ExecutorCompletedEvent completed:
|
||||
Console.WriteLine($"Executor completed: {completed.ExecutorId}");
|
||||
break;
|
||||
|
||||
// Other event types can be handled here as needed
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,6 +212,8 @@ public class SampleSmokeTest
|
||||
string[] lines = result.Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
Assert.Collection(lines,
|
||||
line => Assert.Contains($"{HelloAgent.DefaultId}: {HelloAgent.Greeting}", line),
|
||||
line => Assert.Contains($"{Step7EntryPoint.EchoAgentId}: {Step7EntryPoint.EchoPrefix}{HelloAgent.Greeting}", line),
|
||||
line => Assert.Contains($"{HelloAgent.DefaultId}: {HelloAgent.Greeting}", line),
|
||||
line => Assert.Contains($"{Step7EntryPoint.EchoAgentId}: {Step7EntryPoint.EchoPrefix}{HelloAgent.Greeting}", line)
|
||||
);
|
||||
|
||||
@@ -30,9 +30,9 @@ public partial class WorkflowBuilderSmokeTests
|
||||
|
||||
workflow.StartExecutorId.Should().Be("start");
|
||||
|
||||
workflow.Registrations.Should().HaveCount(1);
|
||||
workflow.Registrations.Should().ContainKey("start");
|
||||
workflow.Registrations["start"].ExecutorType.Should().Be<NoOpExecutor>();
|
||||
workflow.ExecutorBindings.Should().HaveCount(1);
|
||||
workflow.ExecutorBindings.Should().ContainKey("start");
|
||||
workflow.ExecutorBindings["start"].ExecutorType.Should().Be<NoOpExecutor>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -45,9 +45,9 @@ public partial class WorkflowBuilderSmokeTests
|
||||
|
||||
workflow.StartExecutorId.Should().Be("start");
|
||||
|
||||
workflow.Registrations.Should().HaveCount(1);
|
||||
workflow.Registrations.Should().ContainKey("start");
|
||||
workflow.Registrations["start"].ExecutorType.Should().Be<NoOpExecutor>();
|
||||
workflow.ExecutorBindings.Should().HaveCount(1);
|
||||
workflow.ExecutorBindings.Should().ContainKey("start");
|
||||
workflow.ExecutorBindings["start"].ExecutorType.Should().Be<NoOpExecutor>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -77,9 +77,9 @@ public partial class WorkflowBuilderSmokeTests
|
||||
|
||||
workflow.StartExecutorId.Should().Be("start");
|
||||
|
||||
workflow.Registrations.Should().HaveCount(1);
|
||||
workflow.Registrations.Should().ContainKey("start");
|
||||
workflow.Registrations["start"].ExecutorType.Should().Be<NoOpExecutor>();
|
||||
workflow.ExecutorBindings.Should().HaveCount(1);
|
||||
workflow.ExecutorBindings.Should().ContainKey("start");
|
||||
workflow.ExecutorBindings["start"].ExecutorType.Should().Be<NoOpExecutor>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user