// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text.Json;
using System.Threading;
using Microsoft.Agents.AI.Workflows.Checkpointing;
using Microsoft.Agents.AI.Workflows.Observability;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI.Workflows;
///
/// Provides a builder for constructing and configuring a workflow by defining executors and the connections between
/// them.
///
/// 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 if they were intially specified as
/// .
public class WorkflowBuilder
{
private readonly record struct EdgeConnection(string SourceId, string TargetId)
{
public override string ToString() => $"{this.SourceId} -> {this.TargetId}";
}
private int _edgeCount;
private readonly Dictionary _executors = [];
private readonly Dictionary> _edges = [];
private readonly HashSet _unboundExecutors = [];
private readonly HashSet _conditionlessConnections = [];
private readonly Dictionary _requestPorts = [];
private readonly HashSet _outputExecutors = [];
private readonly string _startExecutorId;
private string? _name;
private string? _description;
private static readonly string s_namespace = typeof(WorkflowBuilder).Namespace!;
private static readonly ActivitySource s_activitySource = new(s_namespace);
///
/// Initializes a new instance of the WorkflowBuilder class with the specified starting executor.
///
/// The executor that defines the starting point of the workflow. Cannot be null.
public WorkflowBuilder(ExecutorBinding start)
{
this._startExecutorId = this.Track(start).Id;
}
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 (registration.IsPlaceholder && !this._executors.ContainsKey(registration.Id))
{
// If this is an unbound executor, we need to track it separately
this._unboundExecutors.Add(registration.Id);
}
else if (!registration.IsPlaceholder)
{
// 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(registration.Id, out ExecutorBinding? existing))
{
if (existing.ExecutorType != registration.ExecutorType)
{
throw new InvalidOperationException(
$"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.RawValue is not null &&
!ReferenceEquals(existing.RawValue, registration.RawValue))
{
throw new InvalidOperationException(
$"Cannot bind executor with ID '{registration.Id}' because an executor with the same ID but different instance is already bound.");
}
}
else
{
this._executors[registration.Id] = registration;
if (this._unboundExecutors.Contains(registration.Id))
{
this._unboundExecutors.Remove(registration.Id);
}
}
}
if (registration is RequestPortBinding portRegistration)
{
RequestPort port = portRegistration.Port;
this._requestPorts[port.Id] = port;
}
return registration;
}
///
/// Register executors as an output source. Executors can use to yield output values.
/// By default, message handlers with a non-void return type will also be yielded, unless
/// is set to .
///
///
///
public WorkflowBuilder WithOutputFrom(params ExecutorBinding[] executors)
{
foreach (ExecutorBinding executor in executors)
{
this._outputExecutors.Add(this.Track(executor).Id);
}
return this;
}
///
/// Sets the human-readable name for the workflow.
///
/// The name of the workflow.
/// The current instance, enabling fluent configuration.
public WorkflowBuilder WithName(string name)
{
this._name = name;
return this;
}
///
/// Sets the description for the workflow.
///
/// The description of what the workflow does.
/// The current instance, enabling fluent configuration.
public WorkflowBuilder WithDescription(string description)
{
this._description = description;
return this;
}
///
/// Binds the specified executor (via registration) to the workflow, allowing it to participate in workflow execution.
///
/// The executor instance to bind. The executor must exist in the workflow and not be already bound.
/// The current instance, enabling fluent configuration.
/// Thrown if the specified executor is already bound or does not exist in the workflow.
public WorkflowBuilder BindExecutor(ExecutorBinding registration)
{
if (Throw.IfNull(registration) is ExecutorPlaceholder)
{
throw new InvalidOperationException(
$"Cannot bind executor with ID '{registration.Id}' because it is a placeholder registration. " +
"You must provide a concrete executor instance or registration.");
}
this.Track(registration);
return this;
}
private HashSet EnsureEdgesFor(string sourceId)
{
// Ensure that there is a set of edges for the given source ID.
// If it does not exist, create a new one.
if (!this._edges.TryGetValue(sourceId, out HashSet? edges))
{
this._edges[sourceId] = edges = [];
}
return edges;
}
///
/// Adds a directed edge from the specified source executor to the target executor, optionally guarded by a
/// condition.
///
/// The executor that acts as the source node of the edge. Cannot be null.
/// The executor that acts as the target node of the edge. Cannot be null.
/// If set to , adding the same edge multiple times will be a NoOp,
/// rather than an error.
/// The current instance of .
/// Thrown if an unconditional edge between the specified source and target
/// executors already exists.
public WorkflowBuilder AddEdge(ExecutorBinding source, ExecutorBinding target, bool idempotent = false)
=> this.AddEdge