Files
agent-framework/dotnet/src/Microsoft.Agents.AI.Workflows/ExecutorBinding.cs
Jacob Alber 0086d38f58 .NET: [BREAKING] Workflows API Review Naming Changes (Part 1?) (#4090)
* refactor: Normalize Run/RunStreaming with AIAgent

* refactor: Clarify Session vs. Run -level concepts

* Rename RunId to SessionId to better match Run/Session terminology in AIAgent
* [BREAKING]: Will break existing checkpointed sessions in CosmosDb due to field rename

* refactor: Rename and simplify interface around getting typed data out of ExternalRequest/Response

* Also adds hints around using value types in PortableValue

* refactor: Rename AddFanInEdge to AddFanInBarrierEdge

This will prevent a breaking change later when we introduce a programmable FanIn edge, analogous to the FanOut edge's EdgeSelector.

The goal, in the long run is to support a number of different FanIn scenarios, with naive FanIn (no barrier) by default, similar to FanOut.

* refactor: AsAgent(this Workflow, ...) => AsAIAgent(...)

* misc - part1: SwitchBuilder internal

---------

Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
2026-02-20 02:05:18 +00:00

130 lines
5.3 KiB
C#

// 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 sessionId)
=> !this.IsPlaceholder
? this.CheckId(await this.FactoryAsync(sessionId).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();
}