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-20 20:13:41 -04:00
committed by GitHub
Unverified
parent 7c1e3db846
commit 0ef4e739d5
43 changed files with 284 additions and 461 deletions
@@ -60,10 +60,7 @@ builder.AddAIAgent("knights-and-knaves", (sp, key) =>
If the user asks a general question about their surrounding, make something up which is consistent with the scenario.
""", "Narrator");
// TODO: How to avoid sync-over-async here?
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
return AgentWorkflowBuilder.BuildConcurrent([knight, knave, narrator]).AsAgentAsync(name: key).AsTask().GetAwaiter().GetResult();
#pragma warning restore VSTHRD002
return AgentWorkflowBuilder.BuildConcurrent([knight, knave, narrator]).AsAgent(name: key);
});
// Workflow consisting of multiple specialized agents
@@ -48,7 +48,7 @@ public static class Program
.Build();
// Execute the workflow
await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, "Create a slogan for a new electric SUV that is affordable and fun to drive.");
await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, input: "Create a slogan for a new electric SUV that is affordable and fun to drive.");
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
if (evt is SloganGeneratedEvent or FeedbackEvent)
@@ -35,7 +35,7 @@ public static class Program
var chatClient = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()).GetChatClient(deploymentName).AsIChatClient();
// Create the workflow and turn it into an agent
var workflow = await WorkflowHelper.GetWorkflowAsync(chatClient);
var workflow = WorkflowHelper.GetWorkflow(chatClient);
var agent = workflow.AsAgent("workflow-agent", "Workflow Agent");
var thread = agent.GetNewThread();
@@ -13,7 +13,7 @@ internal static class WorkflowHelper
/// </summary>
/// <param name="chatClient">The chat client to use for the agents</param>
/// <returns>A workflow that processes input using two language agents</returns>
internal static ValueTask<Workflow<List<ChatMessage>>> GetWorkflowAsync(IChatClient chatClient)
internal static Workflow GetWorkflow(IChatClient chatClient)
{
// Create executors
var startExecutor = new ConcurrentStartExecutor();
@@ -26,7 +26,7 @@ internal static class WorkflowHelper
.AddFanOutEdge(startExecutor, targets: [frenchAgent, englishAgent])
.AddFanInEdge(aggregationExecutor, sources: [frenchAgent, englishAgent])
.WithOutputFrom(aggregationExecutor)
.BuildAsync<List<ChatMessage>>();
.Build();
}
/// <summary>
@@ -25,7 +25,7 @@ public static class Program
private static async Task Main()
{
// Create the workflow
var workflow = await WorkflowHelper.GetWorkflowAsync();
var workflow = WorkflowHelper.GetWorkflow();
// Create checkpoint manager
var checkpointManager = CheckpointManager.Default;
@@ -67,7 +67,7 @@ public static class Program
Console.WriteLine($"Number of checkpoints created: {checkpoints.Count}");
// Rehydrate a new workflow instance from a saved checkpoint and continue execution
var newWorkflow = await WorkflowHelper.GetWorkflowAsync();
var newWorkflow = WorkflowHelper.GetWorkflow();
const int CheckpointIndex = 5;
Console.WriteLine($"\n\nHydrating a new workflow instance from the {CheckpointIndex + 1}th checkpoint.");
CheckpointInfo savedCheckpoint = checkpoints[CheckpointIndex];
@@ -13,7 +13,7 @@ internal static class WorkflowHelper
/// 2. JudgeExecutor: Evaluates the guess and provides feedback.
/// The workflow continues until the correct number is guessed.
/// </summary>
internal static ValueTask<Workflow<NumberSignal>> GetWorkflowAsync()
internal static Workflow GetWorkflow()
{
// Create the executors
GuessNumberExecutor guessNumberExecutor = new(1, 100);
@@ -24,7 +24,7 @@ internal static class WorkflowHelper
.AddEdge(guessNumberExecutor, judgeExecutor)
.AddEdge(judgeExecutor, guessNumberExecutor)
.WithOutputFrom(judgeExecutor)
.BuildAsync<NumberSignal>();
.Build();
}
}
@@ -24,7 +24,7 @@ public static class Program
private static async Task Main()
{
// Create the workflow
var workflow = await WorkflowHelper.GetWorkflowAsync();
var workflow = WorkflowHelper.GetWorkflow();
// Create checkpoint manager
var checkpointManager = CheckpointManager.Default;
@@ -13,7 +13,7 @@ internal static class WorkflowHelper
/// 2. JudgeExecutor: Evaluates the guess and provides feedback.
/// The workflow continues until the correct number is guessed.
/// </summary>
internal static ValueTask<Workflow<NumberSignal>> GetWorkflowAsync()
internal static Workflow GetWorkflow()
{
// Create the executors
GuessNumberExecutor guessNumberExecutor = new(1, 100);
@@ -24,7 +24,7 @@ internal static class WorkflowHelper
.AddEdge(guessNumberExecutor, judgeExecutor)
.AddEdge(judgeExecutor, guessNumberExecutor)
.WithOutputFrom(judgeExecutor)
.BuildAsync<NumberSignal>();
.Build();
}
}
@@ -27,7 +27,7 @@ public static class Program
private static async Task Main()
{
// Create the workflow
var workflow = await WorkflowHelper.GetWorkflowAsync();
var workflow = WorkflowHelper.GetWorkflow();
// Create checkpoint manager
var checkpointManager = CheckpointManager.Default;
@@ -10,7 +10,7 @@ internal static class WorkflowHelper
/// Get a workflow that plays a number guessing game with human-in-the-loop interaction.
/// An input port allows the external world to provide inputs to the workflow upon requests.
/// </summary>
internal static ValueTask<Workflow<SignalWithNumber>> GetWorkflowAsync()
internal static Workflow GetWorkflow()
{
// Create the executors
RequestPort numberRequest = RequestPort.Create<SignalWithNumber, int>("GuessNumber");
@@ -21,7 +21,7 @@ internal static class WorkflowHelper
.AddEdge(numberRequest, judgeExecutor)
.AddEdge(judgeExecutor, numberRequest)
.WithOutputFrom(judgeExecutor)
.BuildAsync<SignalWithNumber>();
.Build();
}
}
@@ -58,7 +58,7 @@ public static class Program
.Build();
// Execute the workflow in streaming mode
await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, "What is temperature?");
await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, input: "What is temperature?");
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
if (evt is WorkflowOutputEvent output)
@@ -99,7 +99,7 @@ public static class Program
// Step 2: Run the workflow
Console.WriteLine("\n=== RUNNING WORKFLOW ===\n");
await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, rawText);
await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, input: rawText);
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
Console.WriteLine($"Event: {evt}");
@@ -44,7 +44,7 @@ internal sealed class Program
// Run the workflow, just like any other workflow
string input = this.GetWorkflowInput();
StreamingRun run = await InProcessExecution.StreamAsync(workflow, input);
StreamingRun run = await InProcessExecution.StreamAsync(workflow, input: input);
await this.MonitorAndDisposeWorkflowRunAsync(run);
Notify("\nWORKFLOW: Done!");
@@ -24,7 +24,7 @@ public static class Program
private static async Task Main()
{
// Create the workflow
var workflow = await WorkflowHelper.GetWorkflowAsync();
var workflow = WorkflowHelper.GetWorkflow();
// Execute the workflow
await using StreamingRun handle = await InProcessExecution.StreamAsync(workflow, NumberSignal.Init);
@@ -10,7 +10,7 @@ internal static class WorkflowHelper
/// Get a workflow that plays a number guessing game with human-in-the-loop interaction.
/// An input port allows the external world to provide inputs to the workflow upon requests.
/// </summary>
internal static ValueTask<Workflow<NumberSignal>> GetWorkflowAsync()
internal static Workflow GetWorkflow()
{
// Create the executors
RequestPort numberRequestPort = RequestPort.Create<NumberSignal, int>("GuessNumber");
@@ -21,7 +21,7 @@ internal static class WorkflowHelper
.AddEdge(numberRequestPort, judgeExecutor)
.AddEdge(judgeExecutor, numberRequestPort)
.WithOutputFrom(judgeExecutor)
.BuildAsync<NumberSignal>();
.Build();
}
}
@@ -25,11 +25,11 @@ public static class Program
JudgeExecutor judgeExecutor = new("Judge", 42);
// Build the workflow by connecting executors in a loop
var workflow = await new WorkflowBuilder(guessNumberExecutor)
var workflow = new WorkflowBuilder(guessNumberExecutor)
.AddEdge(guessNumberExecutor, judgeExecutor)
.AddEdge(judgeExecutor, guessNumberExecutor)
.WithOutputFrom(judgeExecutor)
.BuildAsync<NumberSignal>();
.Build();
// Execute the workflow
await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, NumberSignal.Init);
@@ -28,7 +28,7 @@ public static class Program
var workflow = builder.Build();
// Execute the workflow in streaming mode
await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, "Hello, World!");
await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, input: "Hello, World!");
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
if (evt is ExecutorCompletedEvent executorCompleted)
@@ -57,8 +57,7 @@ AIAgent reporter = new ChatClientAgent(anthropic,
description: "Summarize the researcher's essay into a single paragraph, focusing only on the fact checker's confirmed facts.");
// Build a sequential workflow: Researcher -> Fact-Checker -> Reporter
AIAgent workflowAgent = await AgentWorkflowBuilder.BuildSequential(researcher, factChecker, reporter)
.AsAgentAsync();
AIAgent workflowAgent = AgentWorkflowBuilder.BuildSequential(researcher, factChecker, reporter).AsAgent();
// Run the workflow, streaming the output as it arrives.
string? lastAuthor = null;
@@ -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 =
@@ -40,12 +40,23 @@ public class ChatProtocolExecutorTests
}
}
[Fact]
public void ChatProtocolExecutor_DescribedProtocol_IsChatProtocol()
{
// Arrange
TestChatProtocolExecutor executor = new();
ProtocolDescriptor protocol = executor.DescribeProtocol();
// Act & Assert
protocol.Should().Match<ProtocolDescriptor>(protocol => protocol.IsChatProtocol());
}
[Fact]
public async Task ChatProtocolExecutor_Handles_ListOfChatMessagesAsync()
{
// Arrange
var executor = new TestChatProtocolExecutor();
var context = new TestWorkflowContext(executor.Id);
TestChatProtocolExecutor executor = new();
TestWorkflowContext context = new(executor.Id);
List<ChatMessage> messages =
[
@@ -68,8 +79,8 @@ public class ChatProtocolExecutorTests
public async Task ChatProtocolExecutor_Handles_ArrayOfChatMessagesAsync()
{
// Arrange
var executor = new TestChatProtocolExecutor();
var context = new TestWorkflowContext(executor.Id);
TestChatProtocolExecutor executor = new();
TestWorkflowContext context = new(executor.Id);
ChatMessage[] messages =
[
@@ -94,8 +105,8 @@ public class ChatProtocolExecutorTests
public async Task ChatProtocolExecutor_Handles_SingleChatMessageAsync()
{
// Arrange
var executor = new TestChatProtocolExecutor();
var context = new TestWorkflowContext(executor.Id);
TestChatProtocolExecutor executor = new();
TestWorkflowContext context = new(executor.Id);
var message = new ChatMessage(ChatRole.User, "Single message");
@@ -112,8 +123,8 @@ public class ChatProtocolExecutorTests
[Fact]
public async Task ChatProtocolExecutor_AccumulatesAndClearsMessagesPerTurnAsync()
{
var executor = new TestChatProtocolExecutor();
var context = new TestWorkflowContext(executor.Id);
TestChatProtocolExecutor executor = new();
TestWorkflowContext context = new(executor.Id);
// Send multiple message batches before taking a turn
await executor.ExecuteAsync(new ChatMessage(ChatRole.User, "Message 1"), new TypeId(typeof(ChatMessage)), context);
@@ -147,12 +158,12 @@ public class ChatProtocolExecutorTests
[Fact]
public async Task ChatProtocolExecutor_WithStringRole_ConvertsStringToMessageAsync()
{
var executor = new TestChatProtocolExecutor(
TestChatProtocolExecutor executor = new(
options: new ChatProtocolExecutorOptions
{
StringMessageChatRole = ChatRole.User
});
var context = new TestWorkflowContext(executor.Id);
TestWorkflowContext context = new(executor.Id);
await executor.ExecuteAsync("String message", new TypeId(typeof(string)), context);
await executor.TakeTurnAsync(new TurnToken(emitEvents: false), context);
@@ -165,8 +176,8 @@ public class ChatProtocolExecutorTests
[Fact]
public async Task ChatProtocolExecutor_EmptyCollection_HandledCorrectlyAsync()
{
var executor = new TestChatProtocolExecutor();
var context = new TestWorkflowContext(executor.Id);
TestChatProtocolExecutor executor = new();
TestWorkflowContext context = new(executor.Id);
await executor.ExecuteAsync(new List<ChatMessage>(), new TypeId(typeof(List<ChatMessage>)), context);
await executor.ExecuteAsync(Array.Empty<ChatMessage>(), new TypeId(typeof(ChatMessage[])), context);
@@ -181,8 +192,8 @@ public class ChatProtocolExecutorTests
[InlineData(typeof(ChatMessage[]))]
public async Task ChatProtocolExecutor_RoutesCollectionTypesAsync(Type collectionType)
{
var executor = new TestChatProtocolExecutor();
var context = new TestWorkflowContext(executor.Id);
TestChatProtocolExecutor executor = new();
TestWorkflowContext context = new(executor.Id);
var sourceMessages = new[] { new ChatMessage(ChatRole.User, "Test message") };
object messagesToSend = collectionType == typeof(List<ChatMessage>) ? sourceMessages.ToList() : sourceMessages;
@@ -197,8 +208,8 @@ public class ChatProtocolExecutorTests
[Fact]
public async Task ChatProtocolExecutor_MultipleTurns_EachTurnProcessesSeparatelyAsync()
{
var executor = new TestChatProtocolExecutor();
var context = new TestWorkflowContext(executor.Id);
TestChatProtocolExecutor executor = new();
TestWorkflowContext context = new(executor.Id);
await executor.ExecuteAsync(new List<ChatMessage> { new(ChatRole.User, "Turn 1") }, new TypeId(typeof(List<ChatMessage>)), context);
await executor.TakeTurnAsync(new TurnToken(emitEvents: false), context);
@@ -217,8 +228,8 @@ public class ChatProtocolExecutorTests
[Fact]
public async Task ChatProtocolExecutor_InitialWorkflowMessages_RoutedCorrectlyAsync()
{
var executor = new TestChatProtocolExecutor();
var context = new TestWorkflowContext(executor.Id);
TestChatProtocolExecutor executor = new();
TestWorkflowContext context = new(executor.Id);
List<ChatMessage> initialMessages = [new ChatMessage(ChatRole.User, "Kick off the workflow")];
@@ -155,7 +155,7 @@ public class JsonSerializationTests
private static RequestPortInfo IntToString => RequestPort.Create<int, string>(IntToStringId).ToPortInfo();
private static RequestPortInfo StringToInt => RequestPort.Create<string, int>(StringToIntId).ToPortInfo();
private static ValueTask<Workflow<string>> CreateTestWorkflowAsync()
private static Workflow CreateTestWorkflow()
{
ForwardMessageExecutor<string> forwardString = new(ForwardStringId);
ForwardMessageExecutor<int> forwardInt = new(ForwardIntId);
@@ -169,12 +169,12 @@ public class JsonSerializationTests
.AddEdge(forwardInt, intToString)
.AddEdge(intToString, StreamingAggregators.Last<int>().AsExecutor("Aggregate"));
return builder.BuildAsync<string>();
return builder.Build();
}
internal static async ValueTask<WorkflowInfo> CreateTestWorkflowInfoAsync()
internal static WorkflowInfo CreateTestWorkflowInfo()
{
Workflow<string> testWorkflow = await CreateTestWorkflowAsync().ConfigureAwait(false);
Workflow testWorkflow = CreateTestWorkflow();
return testWorkflow.ToWorkflowInfo();
}
@@ -232,7 +232,7 @@ public class JsonSerializationTests
[Fact]
public async Task Test_WorkflowInfo_JsonRoundtripAsync()
{
WorkflowInfo prototype = await CreateTestWorkflowInfoAsync();
WorkflowInfo prototype = CreateTestWorkflowInfo();
JsonMarshaller marshaller = new();
@@ -637,7 +637,7 @@ public class JsonSerializationTests
[Fact]
public async Task Test_Checkpoint_JsonRoundTripAsync()
{
WorkflowInfo testWorkflowInfo = await CreateTestWorkflowInfoAsync();
WorkflowInfo testWorkflowInfo = CreateTestWorkflowInfo();
Checkpoint prototype = new(12, testWorkflowInfo, TestRunnerStateData, TestStateData, TestEdgeState, TestParentCheckpointInfo);
Checkpoint result = RunJsonRoundtrip(prototype, TestCustomSerializedJsonOptions);
@@ -157,23 +157,17 @@ public class RepresentationTests
[Fact]
public async Task Test_Sample_WorkflowInfosAsync()
{
Workflow<string> workflowStep1 = (await Step1EntryPoint.WorkflowInstance.TryPromoteAsync<string>())!;
RunWorkflowInfoMatchTest(workflowStep1);
Workflow<string> workflowStep2 = (await Step2EntryPoint.WorkflowInstance.TryPromoteAsync<string>())!;
RunWorkflowInfoMatchTest(workflowStep2);
RunWorkflowInfoMatchTest((await Step3EntryPoint.WorkflowInstance.TryPromoteAsync<NumberSignal>())!);
RunWorkflowInfoMatchTest((await Step4EntryPoint.WorkflowInstance.TryPromoteAsync<NumberSignal>())!);
RunWorkflowInfoMatchTest(Step1EntryPoint.WorkflowInstance);
RunWorkflowInfoMatchTest(Step2EntryPoint.WorkflowInstance);
RunWorkflowInfoMatchTest(Step3EntryPoint.WorkflowInstance);
RunWorkflowInfoMatchTest(Step4EntryPoint.WorkflowInstance);
// Step 5 reuses the workflow from Step 4, so we don't need to test it separately.
RunWorkflowInfoMatchTest((await Step6EntryPoint.CreateWorkflow(2).TryPromoteAsync<List<ChatMessage>>())!);
RunWorkflowInfoMatchTest(Step6EntryPoint.CreateWorkflow(maxTurns: 2));
// Step 7 reuses the workflow from Step 6, so we don't need to test it separately.
RunWorkflowInfoMatchTest(workflowStep1, workflowStep2, expect: false);
RunWorkflowInfoMatchTest(Step1EntryPoint.WorkflowInstance, Step2EntryPoint.WorkflowInstance, expect: false);
static void RunWorkflowInfoMatchTest<TInput>(Workflow<TInput> workflow, Workflow<TInput>? comparator = null, bool expect = true)
static void RunWorkflowInfoMatchTest(Workflow workflow, Workflow? comparator = null, bool expect = true)
{
comparator ??= workflow;
@@ -27,7 +27,7 @@ internal static class Step1EntryPoint
public static async ValueTask RunAsync(TextWriter writer, IWorkflowExecutionEnvironment environment)
{
StreamingRun run = await environment.StreamAsync(WorkflowInstance, "Hello, World!").ConfigureAwait(false);
StreamingRun run = await environment.StreamAsync(WorkflowInstance, input: "Hello, World!").ConfigureAwait(false);
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
{
@@ -31,7 +31,7 @@ internal static class Step2EntryPoint
public static async ValueTask<string> RunAsync(TextWriter writer, IWorkflowExecutionEnvironment environment, string input = "This is a spam message.")
{
StreamingRun handle = await environment.StreamAsync(WorkflowInstance, input).ConfigureAwait(false);
StreamingRun handle = await environment.StreamAsync(WorkflowInstance, input: input).ConfigureAwait(false);
await foreach (WorkflowEvent evt in handle.WatchStreamAsync().ConfigureAwait(false))
{
switch (evt)
@@ -23,12 +23,6 @@ internal static class Step4EntryPoint
.Build();
}
public static ValueTask<Workflow<NumberSignal>?> GetPromotedWorklowInstanceAsync()
{
Workflow workflow = CreateWorkflowInstance(out _);
return workflow.TryPromoteAsync<NumberSignal>();
}
public static Workflow WorkflowInstance
{
get
@@ -1,6 +1,5 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
@@ -11,9 +10,7 @@ internal static class Step7EntryPoint
{
public static async ValueTask RunAsync(TextWriter writer, int maxSteps = 2)
{
Workflow<List<ChatMessage>> workflow = (await Step6EntryPoint.CreateWorkflow(maxSteps)
.TryPromoteAsync<List<ChatMessage>>()
.ConfigureAwait(false))!;
Workflow workflow = Step6EntryPoint.CreateWorkflow(maxSteps);
AIAgent agent = workflow.AsAgent("group-chat-agent", "Group Chat Agent");
@@ -538,7 +538,7 @@ public class StateManagerTests
Dictionary<ScopeKey, PortableValue> exportedState = await manager.ExportStateAsync();
Dictionary<ScopeKey, PortableValue> serializedState = JsonSerializationTests.RunJsonRoundtrip(exportedState);
Checkpoint testCheckpoint = new(0, await JsonSerializationTests.CreateTestWorkflowInfoAsync(), new([], [], []), serializedState, new());
Checkpoint testCheckpoint = new(0, JsonSerializationTests.CreateTestWorkflowInfo(), new([], [], []), serializedState, new());
manager = new();
await manager.ImportStateAsync(testCheckpoint);