refactor: [BREAKING] Remove generic Workflow<T> (#1551)

Remove input type checking in favour of explicit `.DescribeProtocolAsync()` flow. Also removes `.AsAgentAsync()` as the validation happens at workflow run time. This makes it easier to use Workflows with DI without resorting to async-over-sync.
This commit is contained in:
Jacob Alber
2025-10-21 00:13:41 +00:00
committed by GitHub
parent 7c1e3db846
commit 0ef4e739d5
43 changed files with 284 additions and 461 deletions
@@ -27,12 +27,7 @@ public static class HostedWorkflowBuilderExtensions
public static IHostedAgentBuilder AddAsAIAgent(this IHostedWorkflowBuilder builder, string? name)
{
var agentName = name ?? builder.Name;
return builder.HostApplicationBuilder.AddAIAgent(agentName, (sp, key) =>
{
var workflow = sp.GetRequiredKeyedService<Workflow>(key);
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
return workflow.AsAgentAsync(name: key).AsTask().GetAwaiter().GetResult();
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
});
return builder.HostApplicationBuilder.AddAIAgent(agentName, (sp, key) => sp.GetRequiredKeyedService<Workflow>(key)
.AsAgent(name: key));
}
}
@@ -16,7 +16,7 @@ namespace Microsoft.Agents.AI.Workflows;
public static partial class AgentWorkflowBuilder
{
/// <summary>
/// Builds a <see cref="Workflow{T}"/> composed of a pipeline of agents where the output of one agent is the input to the next.
/// Builds a <see cref="Workflow"/> composed of a pipeline of agents where the output of one agent is the input to the next.
/// </summary>
/// <param name="agents">The sequence of agents to compose into a sequential workflow.</param>
/// <returns>The built workflow composed of the supplied <paramref name="agents"/>, in the order in which they were yielded from the source.</returns>
@@ -24,7 +24,7 @@ public static partial class AgentWorkflowBuilder
=> BuildSequentialCore(workflowName: null, agents);
/// <summary>
/// Builds a <see cref="Workflow{T}"/> composed of a pipeline of agents where the output of one agent is the input to the next.
/// Builds a <see cref="Workflow"/> composed of a pipeline of agents where the output of one agent is the input to the next.
/// </summary>
/// <param name="workflowName">The name of workflow.</param>
/// <param name="agents">The sequence of agents to compose into a sequential workflow.</param>
@@ -76,7 +76,7 @@ public static partial class AgentWorkflowBuilder
}
/// <summary>
/// Builds a <see cref="Workflow{T}"/> composed of agents that operate concurrently on the same input,
/// Builds a <see cref="Workflow"/> composed of agents that operate concurrently on the same input,
/// aggregating their outputs into a single collection.
/// </summary>
/// <param name="agents">The set of agents to compose into a concurrent workflow.</param>
@@ -92,7 +92,7 @@ public static partial class AgentWorkflowBuilder
=> BuildConcurrentCore(workflowName: null, agents, aggregator);
/// <summary>
/// Builds a <see cref="Workflow{T}"/> composed of agents that operate concurrently on the same input,
/// Builds a <see cref="Workflow"/> composed of agents that operate concurrently on the same input,
/// aggregating their outputs into a single collection.
/// </summary>
/// <param name="workflowName">The name of the workflow.</param>
@@ -0,0 +1,61 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Workflows;
/// <summary>
/// Provides extension methods for determining and enforcing whether a protocol descriptor represents the Agent Workflow
/// Chat Protocol.
///
/// This is defined as supporting a <see cref="List{ChatMessage}"/> and <see cref="TurnToken"/> as input. Optional support
/// for additional <see cref="ChatMessage"/> payloads (e.g. string, when a default role is defined), or other collections of
/// messages are optional to support.
/// </summary>
public static class ChatProtocolExtensions
{
/// <summary>
/// Determines whether the specified protocol descriptor represents the Agent Workflow Chat Protocol.
/// </summary>
/// <param name="descriptor">The protocol descriptor to evaluate.</param>
/// <returns><see langword="true"/> if the protocol descriptor represents a supported chat protocol; otherwise, <see
/// langword="false"/>.</returns>
public static bool IsChatProtocol(this ProtocolDescriptor descriptor)
{
bool foundListChatMessageInput = false;
bool foundTurnTokenInput = false;
// We require that the workflow be a ChatProtocol; right now that is defined as accepting at
// least List<ChatMessage> as input (pending polymorphism/interface-input support), as well as
// TurnToken. Since output is mediated by events, which we forward, we don't need to validate
// output type.
foreach (Type inputType in descriptor.Accepts)
{
if (inputType == typeof(List<ChatMessage>))
{
foundListChatMessageInput = true;
}
else if (inputType == typeof(TurnToken))
{
foundTurnTokenInput = true;
}
}
return foundListChatMessageInput && foundTurnTokenInput;
}
/// <summary>
/// Throws an exception if the specified protocol descriptor does not represent a valid chat protocol.
/// </summary>
/// <param name="descriptor">The protocol descriptor to validate as a chat protocol. Cannot be null.</param>
public static void ThrowIfNotChatProtocol(this ProtocolDescriptor descriptor)
{
if (!descriptor.IsChatProtocol())
{
throw new InvalidOperationException("Workflow does not support ChatProtocol: At least List<ChatMessage>" +
" and TurnToken must be supported as input.");
}
}
}
@@ -33,7 +33,7 @@ internal static class RepresentationExtensions
return new(new TypeId(port.Request), new TypeId(port.Response), port.Id);
}
private static WorkflowInfo ToWorkflowInfo(this Workflow workflow, TypeId? inputType, TypeId? outputType, string? outputExecutorId)
public static WorkflowInfo ToWorkflowInfo(this Workflow workflow)
{
Throw.IfNull(workflow);
@@ -48,12 +48,6 @@ internal static class RepresentationExtensions
HashSet<RequestPortInfo> inputPorts = new(workflow.Ports.Values.Select(ToPortInfo));
return new WorkflowInfo(executors, edges, inputPorts, inputType, workflow.StartExecutorId, workflow.OutputExecutors);
return new WorkflowInfo(executors, edges, inputPorts, workflow.StartExecutorId, workflow.OutputExecutors);
}
public static WorkflowInfo ToWorkflowInfo(this Workflow workflow)
=> workflow.ToWorkflowInfo(inputType: null, outputType: null, outputExecutorId: null);
public static WorkflowInfo ToWorkflowInfo<TInput>(this Workflow<TInput> workflow)
=> workflow.ToWorkflowInfo(inputType: new(workflow.InputType), outputType: null, outputExecutorId: null);
}
@@ -14,7 +14,6 @@ internal sealed class WorkflowInfo
Dictionary<string, ExecutorInfo> executors,
Dictionary<string, List<EdgeInfo>> edges,
HashSet<RequestPortInfo> requestPorts,
TypeId? inputType,
string startExecutorId,
HashSet<string>? outputExecutorIds)
{
@@ -22,7 +21,6 @@ internal sealed class WorkflowInfo
this.Edges = Throw.IfNull(edges);
this.RequestPorts = Throw.IfNull(requestPorts);
this.InputType = inputType;
this.StartExecutorId = Throw.IfNullOrEmpty(startExecutorId);
this.OutputExecutorIds = outputExecutorIds ?? [];
}
@@ -91,7 +89,4 @@ internal sealed class WorkflowInfo
return true;
}
public bool IsMatch<TInput>(Workflow<TInput> workflow) =>
this.IsMatch(workflow as Workflow) && this.InputType?.IsMatch<TInput>() == true;
}
@@ -200,6 +200,18 @@ public abstract class Executor : IIdentified
/// </summary>
public ISet<Type> OutputTypes { get; } = new HashSet<Type>([typeof(object)]);
/// <summary>
/// Describes the protocol for communication with this <see cref="Executor"/>.
/// </summary>
/// <returns></returns>
public ProtocolDescriptor DescribeProtocol()
{
// TODO: Once burden of annotating yield/output messages becomes easier for the non-Auto case,
// we should (1) start checking for validity on output/send side, and (2) add the Yield/Send
// types to the ProtocolDescriptor.
return new(this.InputTypes);
}
/// <summary>
/// Checks if the executor can handle a specific message type.
/// </summary>
@@ -21,9 +21,8 @@ public static class ExecutorIshConfigurationExtensions
/// 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, if this is used as a start node of a typed <see cref="Workflow{TInput}"/> via <see cref="Workflow.TryPromoteAsync{TInput}"/>,
/// it will be instantiated as part of the workflow's construction, to validate that its input type matches the
/// demanded <c>TInput</c>.
/// 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>
@@ -38,9 +37,8 @@ public static class ExecutorIshConfigurationExtensions
/// </summary>
/// <remarks>
/// Although this will generally result in a delay-instantiated <see cref="Executor"/> once messages are available
/// for it, if this is used as a start node of a typed <see cref="Workflow{TInput}"/> via <see cref="Workflow.TryPromoteAsync{TInput}"/>,
/// it will be instantiated as part of the workflow's construction, to validate that its input type matches the
/// demanded <c>TInput</c>.
/// 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>
@@ -56,9 +54,8 @@ public static class ExecutorIshConfigurationExtensions
/// </summary>
/// <remarks>
/// Although this will generally result in a delay-instantiated <see cref="Executor"/> once messages are available
/// for it, if this is used as a start node of a typed <see cref="Workflow{TInput}"/> via <see cref="Workflow.TryPromoteAsync{TInput}"/>,
/// it will be instantiated as part of the workflow's construction, to validate that its input type matches the
/// demanded <c>TInput</c>.
/// 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>
@@ -139,7 +139,7 @@ public sealed class HandoffsWorkflowBuilder
}
/// <summary>
/// Builds a <see cref="Workflow{T}"/> composed of agents that operate via handoffs, with the next
/// Builds a <see cref="Workflow"/> composed of agents that operate via handoffs, with the next
/// agent to process messages selected by the current agent.
/// </summary>
/// <returns>The workflow built based on the handoffs in the builder.</returns>
@@ -11,6 +11,16 @@ namespace Microsoft.Agents.AI.Workflows;
/// </summary>
public interface IWorkflowExecutionEnvironment
{
/// <summary>
/// Initiates a streaming run of the specified workflow without sending any initial input.
/// </summary>
/// <param name="workflow">The workflow to execute. Cannot be null.</param>
/// <param name="runId">An optional identifier for the run. If null, a new run identifier will be generated.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the streaming operation.</param>
/// <returns>A ValueTask that represents the asynchronous operation. The result contains a StreamingRun object for accessing
/// the streamed workflow output.</returns>
ValueTask<StreamingRun> StreamAsync(Workflow workflow, string? runId = null, CancellationToken cancellationToken = default);
/// <summary>
/// Initiates an asynchronous streaming execution using the specified input.
/// </summary>
@@ -21,25 +31,24 @@ public interface IWorkflowExecutionEnvironment
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
/// <param name="input">The input message to be processed as part of the streaming run.</param>
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ValueTask{StreamingRun}"/> that represents the asynchronous operation. The result contains a <see
/// cref="StreamingRun"/> for managing and interacting with the streaming run.</returns>
ValueTask<StreamingRun> StreamAsync<TInput>(Workflow workflow, TInput input, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull;
/// <summary>
/// Initiates an asynchronous streaming execution using the specified input.
/// Initiates an asynchronous streaming execution without sending any initial input, with checkpointing.
/// </summary>
/// <remarks>The returned <see cref="StreamingRun"/> provides methods to observe and control
/// the ongoing streaming execution. The operation will continue until the streaming execution is finished or
/// cancelled.</remarks>
/// <typeparam name="TInput">A type of input accepted by the workflow. Must be non-nullable.</typeparam>
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
/// <param name="input">The input message to be processed as part of the streaming run.</param>
/// <param name="checkpointManager">The <see cref="CheckpointManager"/> to use with this run.</param>
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ValueTask{StreamingRun}"/> that represents the asynchronous operation. The result contains a <see
/// cref="StreamingRun"/> for managing and interacting with the streaming run.</returns>
ValueTask<StreamingRun> StreamAsync<TInput>(Workflow<TInput> workflow, TInput input, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull;
ValueTask<Checkpointed<StreamingRun>> StreamAsync(Workflow workflow, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default);
/// <summary>
/// Initiates an asynchronous streaming execution using the specified input, with checkpointing.
@@ -52,27 +61,11 @@ public interface IWorkflowExecutionEnvironment
/// <param name="input">The input message to be processed as part of the streaming run.</param>
/// <param name="checkpointManager">The <see cref="CheckpointManager"/> to use with this run.</param>
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ValueTask{StreamingRun}"/> that represents the asynchronous operation. The result contains a <see
/// cref="StreamingRun"/> for managing and interacting with the streaming run.</returns>
ValueTask<Checkpointed<StreamingRun>> StreamAsync<TInput>(Workflow workflow, TInput input, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull;
/// <summary>
/// Initiates an asynchronous streaming execution using the specified input, with checkpointing.
/// </summary>
/// <remarks>The returned <see cref="StreamingRun"/> provides methods to observe and control
/// the ongoing streaming execution. The operation will continue until the streaming execution is finished or
/// cancelled.</remarks>
/// <typeparam name="TInput">The type of input accepted by the workflow. Must be non-nullable.</typeparam>
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
/// <param name="input">The input message to be processed as part of the streaming run.</param>
/// <param name="checkpointManager">The <see cref="CheckpointManager"/> to use with this run.</param>
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ValueTask{StreamingRun}"/> that represents the asynchronous operation. The result contains a <see
/// cref="StreamingRun"/> for managing and interacting with the streaming run.</returns>
ValueTask<Checkpointed<StreamingRun>> StreamAsync<TInput>(Workflow<TInput> workflow, TInput input, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull;
/// <summary>
/// Resumes an asynchronous streaming execution for the specified input from a checkpoint.
/// </summary>
@@ -82,24 +75,10 @@ public interface IWorkflowExecutionEnvironment
/// <param name="fromCheckpoint">The <see cref="CheckpointInfo"/> corresponding to the checkpoint from which to resume.</param>
/// <param name="checkpointManager">The <see cref="CheckpointManager"/> to use with this run.</param>
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="StreamingRun"/> that provides access to the results of the streaming run.</returns>
ValueTask<Checkpointed<StreamingRun>> ResumeStreamAsync(Workflow workflow, CheckpointInfo fromCheckpoint, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default);
/// <summary>
/// Resumes an asynchronous streaming execution for the specified input from a checkpoint.
/// </summary>
/// <remarks>If the operation is cancelled via the <paramref name="cancellationToken"/> token, the streaming execution will
/// be terminated.</remarks>
/// <typeparam name="TInput">The type of input accepted by the workflow. Must be non-nullable.</typeparam>
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
/// <param name="fromCheckpoint">The <see cref="CheckpointInfo"/> corresponding to the checkpoint from which to resume.</param>
/// <param name="checkpointManager">The <see cref="CheckpointManager"/> to use with this run.</param>
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="StreamingRun"/> that provides access to the results of the streaming run.</returns>
ValueTask<Checkpointed<StreamingRun>> ResumeStreamAsync<TInput>(Workflow<TInput> workflow, CheckpointInfo fromCheckpoint, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull;
/// <summary>
/// Initiates a non-streaming execution of the workflow with the specified input.
/// </summary>
@@ -109,25 +88,11 @@ public interface IWorkflowExecutionEnvironment
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
/// <param name="input">The input message to be processed as part of the run.</param>
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ValueTask{Run}"/> that represents the asynchronous operation. The result contains a <see
/// cref="Run"/> for managing and interacting with the streaming run.</returns>
ValueTask<Run> RunAsync<TInput>(Workflow workflow, TInput input, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull;
/// <summary>
/// Initiates a non-streaming execution of the workflow with the specified input.
/// </summary>
/// <remarks>The workflow will run until its first halt, and the returned <see cref="Run"/> will capture
/// all outgoing events. Use the <c>Run</c> instance to resume execution with responses to outgoing events.</remarks>
/// <typeparam name="TInput">The type of input accepted by the workflow. Must be non-nullable.</typeparam>
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
/// <param name="input">The input message to be processed as part of the run.</param>
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ValueTask{Run}"/> that represents the asynchronous operation. The result contains a <see
/// cref="Run"/> for managing and interacting with the streaming run.</returns>
ValueTask<Run> RunAsync<TInput>(Workflow<TInput> workflow, TInput input, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull;
/// <summary>
/// Initiates a non-streaming execution of the workflow with the specified input, with checkpointing.
/// </summary>
@@ -138,26 +103,11 @@ public interface IWorkflowExecutionEnvironment
/// <param name="input">The input message to be processed as part of the run.</param>
/// <param name="checkpointManager">The <see cref="CheckpointManager"/> to use with this run.</param>
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ValueTask{Run}"/> that represents the asynchronous operation. The result contains a <see
/// cref="Run"/> for managing and interacting with the streaming run.</returns>
ValueTask<Checkpointed<Run>> RunAsync<TInput>(Workflow workflow, TInput input, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull;
/// <summary>
/// Initiates a non-streaming execution of the workflow with the specified input, with checkpointing.
/// </summary>
/// <remarks>The workflow will run until its first halt, and the returned <see cref="Run"/> will capture
/// all outgoing events. Use the <c>Run</c> instance to resume execution with responses to outgoing events.</remarks>
/// <typeparam name="TInput">The type of input accepted by the workflow. Must be non-nullable.</typeparam>
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
/// <param name="input">The input message to be processed as part of the run.</param>
/// <param name="checkpointManager">The <see cref="CheckpointManager"/> to use with this run.</param>
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ValueTask{Run}"/> that represents the asynchronous operation. The result contains a <see
/// cref="Run"/> for managing and interacting with the streaming run.</returns>
ValueTask<Checkpointed<Run>> RunAsync<TInput>(Workflow<TInput> workflow, TInput input, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull;
/// <summary>
/// Resumes a non-streaming execution of the workflow from a checkpoint.
/// </summary>
@@ -167,22 +117,8 @@ public interface IWorkflowExecutionEnvironment
/// <param name="fromCheckpoint">The <see cref="CheckpointInfo"/> corresponding to the checkpoint from which to resume.</param>
/// <param name="checkpointManager">The <see cref="CheckpointManager"/> to use with this run.</param>
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ValueTask{Run}"/> that represents the asynchronous operation. The result contains a <see
/// cref="Run"/> for managing and interacting with the streaming run.</returns>
ValueTask<Checkpointed<Run>> ResumeAsync(Workflow workflow, CheckpointInfo fromCheckpoint, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default);
/// <summary>
/// Resumes a non-streaming execution of the workflow from a checkpoint.
/// </summary>
/// <remarks>The workflow will run until its first halt, and the returned <see cref="Run"/> will capture
/// all outgoing events. Use the <c>Run</c> instance to resume execution with responses to outgoing events.</remarks>
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
/// <param name="fromCheckpoint">The <see cref="CheckpointInfo"/> corresponding to the checkpoint from which to resume.</param>
/// <param name="checkpointManager">The <see cref="CheckpointManager"/> to use with this run.</param>
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ValueTask{Run}"/> that represents the asynchronous operation. The result contains a <see
/// cref="Run"/> for managing and interacting with the streaming run.</returns>
ValueTask<Checkpointed<Run>> ResumeAsync<TInput>(Workflow<TInput> workflow, CheckpointInfo fromCheckpoint, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull;
}
@@ -36,6 +36,18 @@ public sealed class InProcessExecutionEnvironment : IWorkflowExecutionEnvironmen
return runner.ResumeStreamAsync(this.ExecutionMode, fromCheckpoint, cancellationToken);
}
/// <inheritdoc/>
public async ValueTask<StreamingRun> StreamAsync(
Workflow workflow,
string? runId = null,
CancellationToken cancellationToken = default)
{
AsyncRunHandle runHandle = await this.BeginRunAsync(workflow, checkpointManager: null, runId: runId, [], cancellationToken)
.ConfigureAwait(false);
return new(runHandle);
}
/// <inheritdoc/>
public async ValueTask<StreamingRun> StreamAsync<TInput>(
Workflow workflow,
@@ -50,16 +62,17 @@ public sealed class InProcessExecutionEnvironment : IWorkflowExecutionEnvironmen
}
/// <inheritdoc/>
public async ValueTask<StreamingRun> StreamAsync<TInput>(
Workflow<TInput> workflow,
TInput input,
public async ValueTask<Checkpointed<StreamingRun>> StreamAsync(
Workflow workflow,
CheckpointManager checkpointManager,
string? runId = null,
CancellationToken cancellationToken = default) where TInput : notnull
CancellationToken cancellationToken = default)
{
AsyncRunHandle runHandle = await this.BeginRunAsync(workflow, checkpointManager: null, runId: runId, [typeof(TInput)], cancellationToken)
AsyncRunHandle runHandle = await this.BeginRunAsync(workflow, checkpointManager, runId: runId, [], cancellationToken)
.ConfigureAwait(false);
return await runHandle.EnqueueAndStreamAsync(input, cancellationToken).ConfigureAwait(false);
return await runHandle.WithCheckpointingAsync<StreamingRun>(() => new(new StreamingRun(runHandle)))
.ConfigureAwait(false);
}
/// <inheritdoc/>
@@ -77,21 +90,6 @@ public sealed class InProcessExecutionEnvironment : IWorkflowExecutionEnvironmen
.ConfigureAwait(false);
}
/// <inheritdoc/>
public async ValueTask<Checkpointed<StreamingRun>> StreamAsync<TInput>(
Workflow<TInput> workflow,
TInput input,
CheckpointManager checkpointManager,
string? runId = null,
CancellationToken cancellationToken = default) where TInput : notnull
{
AsyncRunHandle runHandle = await this.BeginRunAsync(workflow, checkpointManager, runId: runId, [typeof(TInput)], cancellationToken)
.ConfigureAwait(false);
return await runHandle.WithCheckpointingAsync(() => runHandle.EnqueueAndStreamAsync(input, cancellationToken))
.ConfigureAwait(false);
}
/// <inheritdoc/>
public async ValueTask<Checkpointed<StreamingRun>> ResumeStreamAsync(
Workflow workflow,
@@ -107,19 +105,24 @@ public sealed class InProcessExecutionEnvironment : IWorkflowExecutionEnvironmen
.ConfigureAwait(false);
}
/// <inheritdoc/>
public async ValueTask<Checkpointed<StreamingRun>> ResumeStreamAsync<TInput>(
Workflow<TInput> workflow,
CheckpointInfo fromCheckpoint,
CheckpointManager checkpointManager,
private async ValueTask<AsyncRunHandle> BeginRunHandlingChatProtocolAsync<TInput>(Workflow workflow,
TInput input,
CheckpointManager? checkpointManager,
string? runId = null,
CancellationToken cancellationToken = default) where TInput : notnull
CancellationToken cancellationToken = default)
{
AsyncRunHandle runHandle = await this.ResumeRunAsync(workflow, checkpointManager, runId: runId, fromCheckpoint, [typeof(TInput)], cancellationToken)
ProtocolDescriptor descriptor = await workflow.DescribeProtocolAsync(cancellationToken).ConfigureAwait(false);
AsyncRunHandle runHandle = await this.BeginRunAsync(workflow, checkpointManager, runId, descriptor.Accepts, cancellationToken)
.ConfigureAwait(false);
return await runHandle.WithCheckpointingAsync<StreamingRun>(() => new(new StreamingRun(runHandle)))
.ConfigureAwait(false);
await runHandle.EnqueueMessageAsync(input, cancellationToken).ConfigureAwait(false);
if (descriptor.IsChatProtocol() && input is not TurnToken)
{
await runHandle.EnqueueMessageAsync(new TurnToken(emitEvents: true), cancellationToken).ConfigureAwait(false);
}
return runHandle;
}
/// <inheritdoc/>
@@ -129,21 +132,13 @@ public sealed class InProcessExecutionEnvironment : IWorkflowExecutionEnvironmen
string? runId = null,
CancellationToken cancellationToken = default) where TInput : notnull
{
var runHandle = await this.GetRunHandleWithTurnTokenAsync(workflow: workflow, input: input, checkpointManager: null, runId: runId, cancellationToken).ConfigureAwait(false);
Run run = new(runHandle);
await run.RunToNextHaltAsync(cancellationToken).ConfigureAwait(false);
return run;
}
/// <inheritdoc/>
public async ValueTask<Run> RunAsync<TInput>(
Workflow<TInput> workflow,
TInput input,
string? runId = null,
CancellationToken cancellationToken = default) where TInput : notnull
{
var runHandle = await this.GetRunHandleWithTurnTokenAsync(workflow: workflow, input: input, checkpointManager: null, runId: runId, cancellationToken).ConfigureAwait(false);
AsyncRunHandle runHandle = await this.BeginRunHandlingChatProtocolAsync(
workflow,
input,
checkpointManager: null,
runId,
cancellationToken)
.ConfigureAwait(false);
Run run = new(runHandle);
await run.RunToNextHaltAsync(cancellationToken).ConfigureAwait(false);
@@ -158,23 +153,13 @@ public sealed class InProcessExecutionEnvironment : IWorkflowExecutionEnvironmen
string? runId = null,
CancellationToken cancellationToken = default) where TInput : notnull
{
var runHandle = await this.GetRunHandleWithTurnTokenAsync(workflow: workflow, input: input, checkpointManager: checkpointManager, runId: runId, cancellationToken).ConfigureAwait(false);
Run run = new(runHandle);
await run.RunToNextHaltAsync(cancellationToken).ConfigureAwait(false);
return await runHandle.WithCheckpointingAsync(() => new ValueTask<Run>(run))
.ConfigureAwait(false);
}
/// <inheritdoc/>
public async ValueTask<Checkpointed<Run>> RunAsync<TInput>(
Workflow<TInput> workflow,
TInput input,
CheckpointManager checkpointManager,
string? runId = null,
CancellationToken cancellationToken = default) where TInput : notnull
{
var runHandle = await this.GetRunHandleWithTurnTokenAsync(workflow: workflow, input: input, checkpointManager: checkpointManager, runId: runId, cancellationToken).ConfigureAwait(false);
AsyncRunHandle runHandle = await this.BeginRunHandlingChatProtocolAsync(
workflow,
input,
checkpointManager,
runId,
cancellationToken)
.ConfigureAwait(false);
Run run = new(runHandle);
await run.RunToNextHaltAsync(cancellationToken).ConfigureAwait(false);
@@ -196,63 +181,4 @@ public sealed class InProcessExecutionEnvironment : IWorkflowExecutionEnvironmen
return await runHandle.WithCheckpointingAsync<Run>(() => new(new Run(runHandle)))
.ConfigureAwait(false);
}
/// <inheritdoc/>
public async ValueTask<Checkpointed<Run>> ResumeAsync<TInput>(
Workflow<TInput> workflow,
CheckpointInfo fromCheckpoint,
CheckpointManager checkpointManager,
string? runId = null,
CancellationToken cancellationToken = default) where TInput : notnull
{
AsyncRunHandle runHandle = await this.ResumeRunAsync(workflow, checkpointManager, runId: runId, fromCheckpoint, [typeof(TInput)], cancellationToken)
.ConfigureAwait(false);
return await runHandle.WithCheckpointingAsync<Run>(() => new(new Run(runHandle)))
.ConfigureAwait(false);
}
// Helper to construct a RunHandle with the provided input enqueued. If the starting executor supports it, a TurnToken will be enqueued also.
private async ValueTask<AsyncRunHandle> GetRunHandleWithTurnTokenAsync<TInput>(
Workflow workflow,
TInput input,
CheckpointManager? checkpointManager,
string? runId,
CancellationToken cancellationToken)
{
var knownTypes = new List<Type>() { typeof(TInput) };
var needsTurnToken = await StartingExecutorHandlesTurnTokenAsync<TInput>(workflow).ConfigureAwait(false);
if (needsTurnToken)
{
knownTypes.Add(typeof(TurnToken));
}
AsyncRunHandle runHandle = await this.BeginRunAsync(workflow, checkpointManager: checkpointManager, runId: runId, knownTypes, cancellationToken)
.ConfigureAwait(false);
await runHandle.EnqueueMessageAsync(input, cancellationToken).ConfigureAwait(false);
if (needsTurnToken)
{
await runHandle.EnqueueMessageAsync(new TurnToken(emitEvents: true), cancellationToken).ConfigureAwait(false);
}
return runHandle;
}
/// <summary>
/// Helper method to detect if the starting executor of a given workflow accepts the provided input type as well as a TurnToken.
/// </summary>
private static async ValueTask<bool> StartingExecutorHandlesTurnTokenAsync<TInput>(Workflow workflow)
{
if (workflow.Registrations.TryGetValue(workflow.StartExecutorId, out var registration))
{
// Create instance to check type
Executor startExecutor = await registration.CreateInstanceAsync(string.Empty)
.ConfigureAwait(false);
return startExecutor.CanHandle(typeof(TInput)) && startExecutor.CanHandle(typeof(TurnToken));
}
return false;
}
}
@@ -41,51 +41,35 @@ public static class InProcessExecution
/// </summary>
internal static InProcessExecutionEnvironment Subworkflow { get; } = new(ExecutionMode.Subworkflow);
/// <inheritdoc cref="IWorkflowExecutionEnvironment.StreamAsync(Workflow, string?, CancellationToken)"/>
public static ValueTask<StreamingRun> StreamAsync(Workflow workflow, string? runId = null, CancellationToken cancellationToken = default)
=> Default.StreamAsync(workflow, runId, cancellationToken);
/// <inheritdoc cref="IWorkflowExecutionEnvironment.StreamAsync{TInput}(Workflow, TInput, string?, CancellationToken)"/>
public static ValueTask<StreamingRun> StreamAsync<TInput>(Workflow workflow, TInput input, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull
=> Default.StreamAsync(workflow, input, runId, cancellationToken);
/// <inheritdoc cref="IWorkflowExecutionEnvironment.StreamAsync{TInput}(Workflow{TInput}, TInput, string?, CancellationToken)"/>
public static ValueTask<StreamingRun> StreamAsync<TInput>(Workflow<TInput> workflow, TInput input, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull
=> Default.StreamAsync(workflow, input, runId, cancellationToken);
/// <inheritdoc cref="IWorkflowExecutionEnvironment.StreamAsync(Workflow, CheckpointManager, string?, CancellationToken)"/>
public static ValueTask<Checkpointed<StreamingRun>> StreamAsync(Workflow workflow, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default)
=> Default.StreamAsync(workflow, checkpointManager, runId, cancellationToken);
/// <inheritdoc cref="IWorkflowExecutionEnvironment.StreamAsync{TInput}(Workflow, TInput, CheckpointManager, string?, CancellationToken)"/>
public static ValueTask<Checkpointed<StreamingRun>> StreamAsync<TInput>(Workflow workflow, TInput input, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull
=> Default.StreamAsync(workflow, input, checkpointManager, runId, cancellationToken);
/// <inheritdoc cref="IWorkflowExecutionEnvironment.StreamAsync{TInput}(Workflow{TInput}, TInput, CheckpointManager, string?, CancellationToken)"/>
public static ValueTask<Checkpointed<StreamingRun>> StreamAsync<TInput>(Workflow<TInput> workflow, TInput input, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull
=> Default.StreamAsync(workflow, input, checkpointManager, runId, cancellationToken);
/// <inheritdoc cref="IWorkflowExecutionEnvironment.ResumeStreamAsync(Workflow, CheckpointInfo, CheckpointManager, string?, CancellationToken)"/>
public static ValueTask<Checkpointed<StreamingRun>> ResumeStreamAsync(Workflow workflow, CheckpointInfo fromCheckpoint, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default)
=> Default.ResumeStreamAsync(workflow, fromCheckpoint, checkpointManager, runId, cancellationToken);
/// <inheritdoc cref="IWorkflowExecutionEnvironment.ResumeStreamAsync{TInput}(Workflow{TInput}, CheckpointInfo, CheckpointManager, string?, CancellationToken)"/>
public static ValueTask<Checkpointed<StreamingRun>> ResumeStreamAsync<TInput>(Workflow<TInput> workflow, CheckpointInfo fromCheckpoint, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull
=> Default.ResumeStreamAsync(workflow, fromCheckpoint, checkpointManager, runId, cancellationToken);
/// <inheritdoc cref="IWorkflowExecutionEnvironment.RunAsync{TInput}(Workflow, TInput, string?, CancellationToken)"/>
public static ValueTask<Run> RunAsync<TInput>(Workflow workflow, TInput input, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull
=> Default.RunAsync(workflow, input, runId, cancellationToken);
/// <inheritdoc cref="IWorkflowExecutionEnvironment.RunAsync{TInput}(Workflow{TInput}, TInput, string?, CancellationToken)"/>
public static ValueTask<Run> RunAsync<TInput>(Workflow<TInput> workflow, TInput input, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull
=> Default.RunAsync(workflow, input, runId, cancellationToken);
/// <inheritdoc cref="IWorkflowExecutionEnvironment.RunAsync{TInput}(Workflow, TInput, CheckpointManager, string?, CancellationToken)"/>
public static ValueTask<Checkpointed<Run>> RunAsync<TInput>(Workflow workflow, TInput input, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull
=> Default.RunAsync(workflow, input, checkpointManager, runId, cancellationToken);
/// <inheritdoc cref="IWorkflowExecutionEnvironment.RunAsync{TInput}(Workflow{TInput}, TInput, CheckpointManager, string?, CancellationToken)"/>
public static ValueTask<Checkpointed<Run>> RunAsync<TInput>(Workflow<TInput> workflow, TInput input, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull
=> Default.RunAsync(workflow, input, checkpointManager, runId, cancellationToken);
/// <inheritdoc cref="IWorkflowExecutionEnvironment.ResumeAsync(Workflow, CheckpointInfo, CheckpointManager, string?, CancellationToken)"/>
public static ValueTask<Checkpointed<Run>> ResumeAsync(Workflow workflow, CheckpointInfo fromCheckpoint, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default)
=> Default.ResumeAsync(workflow, fromCheckpoint, checkpointManager, runId, cancellationToken);
/// <inheritdoc cref="IWorkflowExecutionEnvironment.ResumeAsync{TInput}(Workflow{TInput}, CheckpointInfo, CheckpointManager, string?, CancellationToken)"/>
public static ValueTask<Checkpointed<Run>> ResumeAsync<TInput>(Workflow<TInput> workflow, CheckpointInfo fromCheckpoint, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull
=> Default.ResumeAsync(workflow, fromCheckpoint, checkpointManager, runId, cancellationToken);
}
@@ -0,0 +1,23 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
namespace Microsoft.Agents.AI.Workflows;
/// <summary>
/// Describes the protocol for communication with a <see cref="Workflow"/> or <see cref="Executor"/>.
/// </summary>
public class ProtocolDescriptor
{
/// <summary>
/// Get the collection of types accepted by the <see cref="Workflow"/> or <see cref="Executor"/>.
/// </summary>
public IEnumerable<Type> Accepts { get; }
internal ProtocolDescriptor(IEnumerable<Type> acceptedTypes)
{
this.Accepts = acceptedTypes.ToArray();
}
}
@@ -85,42 +85,6 @@ public class Workflow
this.Description = description;
}
/// <summary>
/// Attempts to promote the current workflow to a type pre-checked instance that can handle input of type <typeparamref name="TInput"/>.
/// </summary>
/// <typeparam name="TInput">The desired input type.</typeparam>
/// <returns>A type-parametrized workflow definitely able to process input of type <typeparamref name="TInput"/> or
/// <see langword="null" /> if the workflow does not accept that type of input.</returns>
/// <exception cref="InvalidOperationException"></exception>
internal async ValueTask<Workflow<TInput>?> TryPromoteAsync<TInput>()
{
// Grab the start node, and make sure it has the right type?
if (!this.Registrations.TryGetValue(this.StartExecutorId, out ExecutorRegistration? startRegistration))
{
// TODO: This should never be able to be hit
throw new InvalidOperationException($"Start executor with ID '{this.StartExecutorId}' is not bound.");
}
// TODO: Can we cache this somehow to avoid having to instantiate a new one when running?
// Does that break some user expectations?
Executor startExecutor = await startRegistration.CreateInstanceAsync(string.Empty).ConfigureAwait(false);
if (!startExecutor.InputTypes.Any(t => t.IsAssignableFrom(typeof(TInput))))
{
// We have no handlers for the input type T, which means the built workflow will not be able to
// process messages of the desired type
return null;
}
return new Workflow<TInput>(this.StartExecutorId)
{
Registrations = this.Registrations,
Edges = this.Edges,
Ports = this.Ports,
OutputExecutors = this.OutputExecutors
};
}
private bool _needsReset;
private bool IsResettable => this.Registrations.Values.All(registration => !registration.IsUnresettableSharedInstance);
@@ -215,27 +179,18 @@ public class Workflow
await this.TryResetExecutorRegistrationsAsync().ConfigureAwait(false);
}
}
/// <summary>
/// Represents a workflow that operates on data of type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type of input to the workflow.</typeparam>
public class Workflow<T> : Workflow
{
/// <summary>
/// Initializes a new instance of the <see cref="Workflow{T}"/> class with the specified starting executor identifier
/// Retrieves a <see cref="ProtocolDescriptor"/> defining how to interact with this workflow.
/// </summary>
/// <param name="startExecutorId">The unique identifier of the starting executor for the workflow. Cannot be <c>null</c>.</param>
/// <param name="name">Optional human-readable name for the workflow.</param>
/// <param name="description">Optional description of what the workflow does.</param>
public Workflow(string startExecutorId, string? name = null, string? description = null)
: base(startExecutorId, name, description)
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ValueTask{ProtocolDescriptor}"/> that represents that asynchronous operation. The result contains
/// 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];
Executor startExecutor = await startExecutorRegistration.CreateInstanceAsync(string.Empty)
.ConfigureAwait(false);
return startExecutor.DescribeProtocol();
}
/// <summary>
/// Gets the type of input expected by the starting executor of the workflow.
/// </summary>
public Type InputType => typeof(T);
}
@@ -6,7 +6,6 @@ using System.Diagnostics;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Checkpointing;
using Microsoft.Agents.AI.Workflows.Observability;
using Microsoft.Shared.Diagnostics;
@@ -441,34 +440,4 @@ public class WorkflowBuilder
return workflow;
}
/// <summary>
/// Attempts to build a workflow instance configured to process messages of the specified input type.
/// </summary>
/// <typeparam name="TInput">The desired input type for the workflow.</typeparam>
/// <exception cref="InvalidOperationException">Thrown if the built workflow cannot process messages of the specified input type,</exception>
public async ValueTask<Workflow<TInput>> BuildAsync<TInput>() where TInput : notnull
{
using Activity? activity = s_activitySource.StartActivity(ActivityNames.WorkflowBuild);
Workflow<TInput>? maybeWorkflow = await this.BuildInternal(activity)
.TryPromoteAsync<TInput>()
.ConfigureAwait(false);
if (maybeWorkflow is null)
{
var exception = new InvalidOperationException(
$"The built workflow cannot process input of type '{typeof(TInput).FullName}'.");
activity?.AddEvent(new ActivityEvent(EventNames.BuildError, tags: new() {
{ Tags.BuildErrorMessage, exception.Message },
{ Tags.BuildErrorType, exception.GetType().FullName }
}));
activity?.CaptureException(exception);
throw exception;
}
activity?.AddEvent(new ActivityEvent(EventNames.BuildCompleted));
return maybeWorkflow;
}
}
@@ -18,12 +18,14 @@ internal sealed class WorkflowHostAgent : AIAgent
private readonly string? _id;
private readonly CheckpointManager? _checkpointManager;
private readonly IWorkflowExecutionEnvironment _executionEnvironment;
private readonly Task<ProtocolDescriptor> _describeTask;
private readonly ConcurrentDictionary<string, string> _assignedRunIds = [];
public WorkflowHostAgent(Workflow<List<ChatMessage>> workflow, string? id = null, string? name = null, string? description = null, CheckpointManager? checkpointManager = null, IWorkflowExecutionEnvironment? executionEnvironment = null)
public WorkflowHostAgent(Workflow workflow, string? id = null, string? name = null, string? description = null, CheckpointManager? checkpointManager = null, IWorkflowExecutionEnvironment? executionEnvironment = null)
{
this._workflow = Throw.IfNull(workflow);
this._executionEnvironment = executionEnvironment ?? (workflow.AllowConcurrent
? InProcessExecution.Concurrent
: InProcessExecution.OffThread);
@@ -32,6 +34,9 @@ internal sealed class WorkflowHostAgent : AIAgent
this._id = id;
this.Name = name;
this.Description = description;
// Kick off the typecheck right away by starting the DescribeProtocol task.
this._describeTask = this._workflow.DescribeProtocolAsync().AsTask();
}
public override string Id => this._id ?? base.Id;
@@ -50,6 +55,12 @@ internal sealed class WorkflowHostAgent : AIAgent
return result;
}
private async ValueTask ValidateWorkflowAsync()
{
ProtocolDescriptor protocol = await this._describeTask.ConfigureAwait(false);
protocol.ThrowIfNotChatProtocol();
}
public override AgentThread GetNewThread() => new WorkflowThread(this._workflow, this.GenerateNewId(), this._executionEnvironment, this._checkpointManager);
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
@@ -75,6 +86,8 @@ internal sealed class WorkflowHostAgent : AIAgent
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
await this.ValidateWorkflowAsync().ConfigureAwait(false);
WorkflowThread workflowThread = await this.UpdateThreadAsync(messages, thread, cancellationToken).ConfigureAwait(false);
MessageMerger merger = new();
@@ -95,6 +108,8 @@ internal sealed class WorkflowHostAgent : AIAgent
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await this.ValidateWorkflowAsync().ConfigureAwait(false);
WorkflowThread workflowThread = await this.UpdateThreadAsync(messages, thread, cancellationToken).ConfigureAwait(false);
await foreach (AgentRunResponseUpdate update in workflowThread.InvokeStageAsync(cancellationToken)
.ConfigureAwait(false)
@@ -1,8 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Workflows;
@@ -25,29 +23,6 @@ public static class WorkflowHostingExtensions
/// <see cref="InProcessExecution.Lockstep"/> for the in-process environments.</param>
/// <returns></returns>
public static AIAgent AsAgent(
this Workflow<List<ChatMessage>> workflow,
string? id = null,
string? name = null,
string? description = null,
CheckpointManager? checkpointManager = null,
IWorkflowExecutionEnvironment? executionEnvironment = null)
{
return new WorkflowHostAgent(workflow, id, name, description, checkpointManager, executionEnvironment);
}
/// <summary>
/// Convert a workflow with the appropriate primary input type to an <see cref="AIAgent"/>.
/// </summary>
/// <param name="workflow">The workflow to be hosted by the resulting <see cref="AIAgent"/></param>
/// <param name="id">A unique id for the hosting <see cref="AIAgent"/>.</param>
/// <param name="name">A name for the hosting <see cref="AIAgent"/>.</param>
/// /// <param name="description">A description for the hosting <see cref="AIAgent"/>.</param>
/// <param name="checkpointManager">A <see cref="CheckpointManager"/> to enable persistence of run state.</param>
/// <param name="executionEnvironment">Specify the execution environment to use when running the workflows. See
/// <see cref="InProcessExecution.OffThread"/>, <see cref="InProcessExecution.Concurrent"/> and
/// <see cref="InProcessExecution.Lockstep"/> for the in-process environments.</param>
/// <returns></returns>
public static async ValueTask<AIAgent> AsAgentAsync(
this Workflow workflow,
string? id = null,
string? name = null,
@@ -55,15 +30,7 @@ public static class WorkflowHostingExtensions
CheckpointManager? checkpointManager = null,
IWorkflowExecutionEnvironment? executionEnvironment = null)
{
Workflow<List<ChatMessage>>? maybeTyped = await workflow.TryPromoteAsync<List<ChatMessage>>()
.ConfigureAwait(false);
if (maybeTyped is null)
{
throw new InvalidOperationException("Cannot host a workflow that does not accept List<ChatMessage> as an input");
}
return maybeTyped.AsAgent(id, name, description, checkpointManager, executionEnvironment);
return new WorkflowHostAgent(workflow, id, name, description, checkpointManager, executionEnvironment);
}
internal static FunctionCallContent ToFunctionCall(this ExternalRequest request)
@@ -102,6 +102,8 @@ internal sealed class WorkflowThread : AgentThread
private async ValueTask<Checkpointed<StreamingRun>> CreateOrResumeRunAsync(List<ChatMessage> messages, CancellationToken cancellationToken = default)
{
// The workflow is validated to be a ChatProtocol workflow by the WorkflowHostAgent before creating the thread,
// and does not need to be checked again here.
if (this.LastCheckpoint is not null)
{
Checkpointed<StreamingRun> checkpointed =