diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index c26e25487e..f7a78e5535 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -83,14 +83,30 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/GettingStarted/Workflows/Foundational/03_AgentsInWorkflows/03_AgentsInWorkflows.csproj b/dotnet/samples/GettingStarted/Workflows/Agents/CustomAgentExecutors/CustomAgentExecutors.csproj
similarity index 100%
rename from dotnet/samples/GettingStarted/Workflows/Foundational/03_AgentsInWorkflows/03_AgentsInWorkflows.csproj
rename to dotnet/samples/GettingStarted/Workflows/Agents/CustomAgentExecutors/CustomAgentExecutors.csproj
diff --git a/dotnet/samples/GettingStarted/Workflows/Agents/CustomAgentExecutors/Program.cs b/dotnet/samples/GettingStarted/Workflows/Agents/CustomAgentExecutors/Program.cs
new file mode 100644
index 0000000000..fe1b612603
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Workflows/Agents/CustomAgentExecutors/Program.cs
@@ -0,0 +1,246 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading.Tasks;
+using Azure.AI.OpenAI;
+using Azure.Identity;
+using Microsoft.Agents.Workflows;
+using Microsoft.Agents.Workflows.Reflection;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.AI.Agents;
+
+namespace WorkflowCustomAgentExecutorsSample;
+
+///
+/// This sample demonstrates how to create custom executors for AI agents.
+/// This is useful when you want more control over the agent's behaviors in a workflow.
+///
+/// In this example, we create two custom executors:
+/// 1. SloganWriterExecutor: An AI agent that generates slogans based on a given task.
+/// 2. FeedbackExecutor: An AI agent that provides feedback on the generated slogans.
+/// (These two executors manage the agent instances and their conversation threads.)
+///
+/// The workflow alternates between these two executors until the slogan meets a certain
+/// quality threshold or a maximum number of attempts is reached.
+///
+///
+/// Pre-requisites:
+/// - Foundational samples should be completed first.
+/// - An Azure OpenAI chat completion deployment that supports structured outputs must be configured.
+///
+public static class Program
+{
+ private static async Task Main()
+ {
+ // Set up the Azure OpenAI client
+ var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
+ var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
+ var chatClient = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()).GetChatClient(deploymentName).AsIChatClient();
+
+ // Create the executors
+ var sloganWriter = new SloganWriterExecutor(chatClient);
+ var feedbackProvider = new FeedbackExecutor(chatClient);
+
+ // Build the workflow by adding executors and connecting them
+ WorkflowBuilder builder = new(sloganWriter);
+ builder.AddEdge(sloganWriter, feedbackProvider);
+ builder.AddEdge(feedbackProvider, sloganWriter);
+ var workflow = builder.Build();
+
+ // Execute the workflow
+ var ask = "Create a slogan for a new electric SUV that is affordable and fun to drive.";
+ StreamingRun run = await InProcessExecution.StreamAsync(workflow, ask);
+ await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
+ {
+ if (evt is SloganGeneratedEvent || evt is FeedbackEvent)
+ {
+ // Custom events to allow us to monitor the progress of the workflow.
+ Console.WriteLine($"{evt}");
+ }
+
+ if (evt is WorkflowCompletedEvent completedEvent)
+ {
+ Console.WriteLine($"{completedEvent}");
+ }
+ }
+ }
+}
+
+///
+/// A class representing the output of the slogan writer agent.
+///
+public sealed class SloganResult
+{
+ [JsonPropertyName("task")]
+ public required string Task { get; set; }
+
+ [JsonPropertyName("slogan")]
+ public required string Slogan { get; set; }
+}
+
+///
+/// A class representing the output of the feedback agent.
+///
+public sealed class FeedbackResult
+{
+ [JsonPropertyName("comments")]
+ public string Comments { get; set; } = string.Empty;
+
+ [JsonPropertyName("rating")]
+ public int Rating { get; set; }
+
+ [JsonPropertyName("actions")]
+ public string Actions { get; set; } = string.Empty;
+}
+
+///
+/// A custom event to indicate that a slogan has been generated.
+///
+internal sealed class SloganGeneratedEvent(SloganResult sloganResult) : WorkflowEvent(sloganResult)
+{
+ public override string ToString() => $"Slogan: {sloganResult.Slogan}";
+}
+
+///
+/// A custom executor that uses an AI agent to generate slogans based on a given task.
+/// Note that this executor has two message handlers:
+/// 1. HandleAsync(string message): Handles the initial task to create a slogan.
+/// 2. HandleAsync(Feedback message): Handles feedback to improve the slogan.
+///
+internal sealed class SloganWriterExecutor
+ : ReflectingExecutor,
+ IMessageHandler,
+ IMessageHandler
+{
+ private const string Instruction = """
+ You are a professional slogan writer. You will be given a task to create a slogan.
+ """;
+
+ private readonly AIAgent _agent;
+ private readonly AgentThread _thread;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The chat client to use for the AI agent.
+ public SloganWriterExecutor(IChatClient chatClient)
+ {
+ var agentOptions = new ChatClientAgentOptions(instructions: Instruction)
+ {
+ ChatOptions = new()
+ {
+ ResponseFormat = ChatResponseFormatJson.ForJsonSchema(
+ schema: AIJsonUtilities.CreateJsonSchema(typeof(SloganResult))
+ )
+ }
+ };
+
+ this._agent = new ChatClientAgent(chatClient, agentOptions);
+ this._thread = this._agent.GetNewThread();
+ }
+
+ public async ValueTask HandleAsync(string message, IWorkflowContext context)
+ {
+ var result = await this._agent.RunAsync(message, this._thread);
+
+ var sloganResult = JsonSerializer.Deserialize(result.Text) ?? throw new InvalidOperationException("Failed to deserialize slogan result.");
+
+ await context.AddEventAsync(new SloganGeneratedEvent(sloganResult));
+ return sloganResult;
+ }
+
+ public async ValueTask HandleAsync(FeedbackResult message, IWorkflowContext context)
+ {
+ var feedbackMessage = $"""
+ Here is the feedback on your previous slogan:
+ Comments: {message.Comments}
+ Rating: {message.Rating}
+ Suggested Actions: {message.Actions}
+
+ Please use this feedback to improve your slogan.
+ """;
+
+ var result = await this._agent.RunAsync(feedbackMessage, this._thread);
+ var sloganResult = JsonSerializer.Deserialize(result.Text) ?? throw new InvalidOperationException("Failed to deserialize slogan result.");
+
+ await context.AddEventAsync(new SloganGeneratedEvent(sloganResult));
+ return sloganResult;
+ }
+}
+
+///
+/// A custom event to indicate that feedback has been provided.
+///
+internal sealed class FeedbackEvent(FeedbackResult feedbackResult) : WorkflowEvent(feedbackResult)
+{
+ private readonly JsonSerializerOptions _options = new() { WriteIndented = true };
+ public override string ToString() => $"Feedback:\n{JsonSerializer.Serialize(feedbackResult, this._options)}";
+}
+
+///
+/// A custom executor that uses an AI agent to provide feedback on a slogan.
+///
+internal sealed class FeedbackExecutor : ReflectingExecutor, IMessageHandler
+{
+ private const string Instruction = """
+ You are a professional editor. You will be given a slogan and the task it is meant to accomplish.
+ """;
+
+ private readonly AIAgent _agent;
+ private readonly AgentThread _thread;
+
+ public int MinimumRating { get; init; } = 8;
+
+ public int MaxAttempts { get; init; } = 3;
+
+ private int _attempts = 0;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The chat client to use for the AI agent.
+ public FeedbackExecutor(IChatClient chatClient)
+ {
+ var agentOptions = new ChatClientAgentOptions(instructions: Instruction)
+ {
+ ChatOptions = new()
+ {
+ ResponseFormat = ChatResponseFormatJson.ForJsonSchema(
+ schema: AIJsonUtilities.CreateJsonSchema(typeof(FeedbackResult))
+ )
+ }
+ };
+
+ this._agent = new ChatClientAgent(chatClient, agentOptions);
+ this._thread = this._agent.GetNewThread();
+ }
+
+ public async ValueTask HandleAsync(SloganResult message, IWorkflowContext context)
+ {
+ var sloganMessage = $"""
+ Here is a slogan for the task '{message.Task}':
+ Slogan: {message.Slogan}
+ Please provide feedback on this slogan, including comments, a rating from 1 to 10, and suggested actions for improvement.
+ """;
+
+ var response = await this._agent.RunAsync(sloganMessage, this._thread);
+ var feedback = JsonSerializer.Deserialize(response.Text) ?? throw new InvalidOperationException("Failed to deserialize feedback.");
+
+ await context.AddEventAsync(new FeedbackEvent(feedback));
+ if (feedback.Rating >= this.MinimumRating)
+ {
+ await context.AddEventAsync(new WorkflowCompletedEvent($"The following slogan was accepted:\n\n{message.Slogan}"));
+ return;
+ }
+ if (this._attempts >= this.MaxAttempts)
+ {
+ await context.AddEventAsync(new WorkflowCompletedEvent($"The slogan was rejected after {this.MaxAttempts} attempts. Final slogan:\n\n{message.Slogan}"));
+ return;
+ }
+
+ await context.SendMessageAsync(feedback);
+ this._attempts++;
+ }
+}
diff --git a/dotnet/samples/GettingStarted/Workflows/Agents/FoundryAgent/FoundryAgent.csproj b/dotnet/samples/GettingStarted/Workflows/Agents/FoundryAgent/FoundryAgent.csproj
new file mode 100644
index 0000000000..564e214dac
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Workflows/Agents/FoundryAgent/FoundryAgent.csproj
@@ -0,0 +1,23 @@
+
+
+
+ Exe
+ net9.0
+ 12
+
+ enable
+ disable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dotnet/samples/GettingStarted/Workflows/Agents/FoundryAgent/Program.cs b/dotnet/samples/GettingStarted/Workflows/Agents/FoundryAgent/Program.cs
new file mode 100644
index 0000000000..586780f1f3
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Workflows/Agents/FoundryAgent/Program.cs
@@ -0,0 +1,82 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Threading.Tasks;
+using Azure.AI.Agents.Persistent;
+using Azure.Identity;
+using Microsoft.Agents.Workflows;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.AI.Agents;
+
+namespace WorkflowFoundryAgentSample;
+
+///
+/// This sample shows how to use Azure Foundry Agents within a workflow.
+///
+///
+/// Pre-requisites:
+/// - Foundational samples should be completed first.
+/// - An Azure Foundry project endpoint and model id.
+///
+public static class Program
+{
+ private static async Task Main()
+ {
+ // Set up the Azure OpenAI client
+ var endpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_ENDPOINT")
+ ?? throw new InvalidOperationException("AZURE_FOUNDRY_PROJECT_ENDPOINT is not set.");
+ var model = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_MODEL_ID") ?? "gpt-4o-mini";
+ var persistentAgentsClient = new PersistentAgentsClient(endpoint, new AzureCliCredential());
+
+ // Create agents
+ AIAgent frenchAgent = await GetTranslationAgentAsync("French", persistentAgentsClient, model);
+ AIAgent spanishAgent = await GetTranslationAgentAsync("Spanish", persistentAgentsClient, model);
+ AIAgent englishAgent = await GetTranslationAgentAsync("English", persistentAgentsClient, model);
+
+ // Build the workflow by adding executors and connecting them
+ WorkflowBuilder builder = new(frenchAgent);
+ builder.AddEdge(frenchAgent, spanishAgent);
+ builder.AddEdge(spanishAgent, englishAgent);
+ var workflow = builder.Build();
+
+ // Execute the workflow
+ StreamingRun run = await InProcessExecution.StreamAsync(workflow, new ChatMessage(ChatRole.User, "Hello World!"));
+ // Must send the turn token to trigger the agents.
+ // The agents are wrapped as executors. When they receive messages,
+ // they will cache the messages and only start processing when they receive a TurnToken.
+ await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
+ await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
+ {
+ if (evt is AgentRunUpdateEvent executorComplete)
+ {
+ Console.WriteLine($"{executorComplete.ExecutorId}: {executorComplete.Data}");
+ }
+ }
+
+ // Cleanup the agents created for the sample.
+ await persistentAgentsClient.Administration.DeleteAgentAsync(frenchAgent.Id);
+ await persistentAgentsClient.Administration.DeleteAgentAsync(spanishAgent.Id);
+ await persistentAgentsClient.Administration.DeleteAgentAsync(englishAgent.Id);
+ }
+
+ ///
+ /// Creates a translation agent for the specified target language.
+ ///
+ /// The target language for translation
+ /// The PersistentAgentsClient to create the agent
+ /// The model to use for the agent
+ /// A ChatClientAgent configured for the specified language
+ private static async Task GetTranslationAgentAsync(
+ string targetLanguage,
+ PersistentAgentsClient persistentAgentsClient,
+ string model)
+ {
+ string instructions = $"You are a translation assistant that translates the provided text to {targetLanguage}.";
+ var agentMetadata = await persistentAgentsClient.Administration.CreateAgentAsync(
+ model: model,
+ name: $"{targetLanguage} Translator",
+ instructions: instructions);
+
+ return await persistentAgentsClient.GetAIAgentAsync(agentMetadata.Value.Id);
+ }
+}
diff --git a/dotnet/samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/Program.cs b/dotnet/samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/Program.cs
new file mode 100644
index 0000000000..7611b14984
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/Program.cs
@@ -0,0 +1,90 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Azure.AI.OpenAI;
+using Azure.Identity;
+using Microsoft.Agents.Workflows;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.AI.Agents;
+
+namespace WorkflowAsAnAgentsSample;
+
+///
+/// This sample introduces the concepts workflows as agents, where a workflow can be
+/// treated as an . This allows you to interact with a workflow
+/// as if it were a single agent.
+///
+/// In this example, we create a workflow that uses two language agents to process
+/// input concurrently, one that responds in French and another that responds in English.
+///
+/// You will interact with the workflow in an interactive loop, sending messages and receiving
+/// streaming responses from the workflow as if it were an agent who responds in both languages.
+///
+///
+/// Pre-requisites:
+/// - Foundational samples should be completed first.
+/// - This sample uses concurrent processing.
+/// - An Azure OpenAI endpoint and deployment name.
+///
+public static class Program
+{
+ private static async Task Main()
+ {
+ // Set up the Azure OpenAI client
+ var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
+ var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
+ var chatClient = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()).GetChatClient(deploymentName).AsIChatClient();
+
+ // Create the workflow and turn it into an agent
+ var workflow = WorkflowHelper.GetWorkflow(chatClient);
+ var agent = workflow.AsAgent("workflow-agent", "Workflow Agent");
+ var thread = agent.GetNewThread();
+
+ // Start an interactive loop to interact with the workflow as if it were an agent
+ while (true)
+ {
+ Console.WriteLine();
+ Console.Write("User (or 'exit' to quit): ");
+ string? input = Console.ReadLine();
+ if (string.IsNullOrWhiteSpace(input) || input.Equals("exit", StringComparison.OrdinalIgnoreCase))
+ {
+ break;
+ }
+
+ await ProcessInputAsync(agent, thread, input);
+ }
+
+ // Helper method to process user input and display streaming responses. To display
+ // multiple interleaved responses correctly, we buffer updates by message ID and
+ // re-render all messages on each update.
+ static async Task ProcessInputAsync(AIAgent agent, AgentThread thread, string input)
+ {
+ Dictionary> buffer = [];
+ await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(input, thread).ConfigureAwait(false))
+ {
+ if (update.MessageId == null)
+ {
+ // skip updates that don't have a message ID
+ continue;
+ }
+ Console.Clear();
+
+ if (!buffer.TryGetValue(update.MessageId, out List? value))
+ {
+ value = [];
+ buffer[update.MessageId] = value;
+ }
+ value.Add(update);
+
+ foreach (var (messageId, segments) in buffer)
+ {
+ string combinedText = string.Concat(segments);
+ Console.WriteLine($"{segments[0].AuthorName}: {combinedText}");
+ Console.WriteLine();
+ }
+ }
+ }
+ }
+}
diff --git a/dotnet/samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/WorkflowAsAnAgent.csproj b/dotnet/samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/WorkflowAsAnAgent.csproj
new file mode 100644
index 0000000000..b1f90ee600
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/WorkflowAsAnAgent.csproj
@@ -0,0 +1,24 @@
+
+
+
+ Exe
+ net9.0
+ 12
+
+ enable
+ disable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dotnet/samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/WorkflowHelper.cs b/dotnet/samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/WorkflowHelper.cs
new file mode 100644
index 0000000000..9470e22c10
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/WorkflowHelper.cs
@@ -0,0 +1,95 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.Agents.Workflows;
+using Microsoft.Agents.Workflows.Reflection;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.AI.Agents;
+
+namespace WorkflowAsAnAgentsSample;
+
+internal static class WorkflowHelper
+{
+ ///
+ /// Creates a workflow that uses two language agents to process input concurrently.
+ ///
+ /// The chat client to use for the agents
+ /// A workflow that processes input using two language agents
+ internal static Workflow> GetWorkflow(IChatClient chatClient)
+ {
+ // Create executors
+ var startExecutor = new ConcurrentStartExecutor();
+ var aggregationExecutor = new ConcurrentAggregationExecutor();
+ AIAgent frenchAgent = GetLanguageAgent("French", chatClient);
+ AIAgent englishAgent = GetLanguageAgent("English", chatClient);
+
+ // Build the workflow by adding executors and connecting them
+ WorkflowBuilder builder = new(startExecutor);
+ builder.AddFanOutEdge(startExecutor, targets: [frenchAgent, englishAgent]);
+ builder.AddFanInEdge(aggregationExecutor, sources: [frenchAgent, englishAgent]);
+
+ return builder.Build>();
+ }
+
+ ///
+ /// Creates a language agent for the specified target language.
+ ///
+ /// The target language for translation
+ /// The chat client to use for the agent
+ /// A ChatClientAgent configured for the specified language
+ private static ChatClientAgent GetLanguageAgent(string targetLanguage, IChatClient chatClient)
+ {
+ string instructions = $"You're a helpful assistant who always responds in {targetLanguage}.";
+ return new ChatClientAgent(chatClient, instructions, name: $"{targetLanguage}Agent");
+ }
+
+ ///
+ /// Executor that starts the concurrent processing by sending messages to the agents.
+ ///
+ private sealed class ConcurrentStartExecutor() :
+ ReflectingExecutor("ConcurrentStartExecutor"),
+ IMessageHandler>
+ {
+ ///
+ /// Starts the concurrent processing by sending messages to the agents.
+ ///
+ /// The user message to process
+ /// Workflow context for accessing workflow services and adding events
+ public async ValueTask HandleAsync(List message, IWorkflowContext context)
+ {
+ // Broadcast the message to all connected agents. Receiving agents will queue
+ // the message but will not start processing until they receive a turn token.
+ await context.SendMessageAsync(message);
+ // Broadcast the turn token to kick off the agents.
+ await context.SendMessageAsync(new TurnToken(emitEvents: true));
+ }
+ }
+
+ ///
+ /// Executor that aggregates the results from the concurrent agents.
+ ///
+ private sealed class ConcurrentAggregationExecutor() :
+ ReflectingExecutor("ConcurrentAggregationExecutor"),
+ IMessageHandler
+ {
+ private readonly List _messages = [];
+
+ ///
+ /// Handles incoming messages from the agents and aggregates their responses.
+ ///
+ /// The message from the agent
+ /// Workflow context for accessing workflow services and adding events
+ public async ValueTask HandleAsync(ChatMessage message, IWorkflowContext context)
+ {
+ this._messages.Add(message);
+
+ if (this._messages.Count == 2)
+ {
+ var formattedMessages = string.Join(Environment.NewLine, this._messages.Select(m => $"{m.Text}"));
+ await context.AddEventAsync(new WorkflowCompletedEvent(formattedMessages));
+ }
+ }
+ }
+}
diff --git a/dotnet/samples/GettingStarted/Workflows/Foundational/01_ExecutorsAndEdges/01_ExecutorsAndEdges.csproj b/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointAndRehydrate/CheckpointAndRehydrate.csproj
similarity index 100%
rename from dotnet/samples/GettingStarted/Workflows/Foundational/01_ExecutorsAndEdges/01_ExecutorsAndEdges.csproj
rename to dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointAndRehydrate/CheckpointAndRehydrate.csproj
diff --git a/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointAndRehydrate/Program.cs b/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointAndRehydrate/Program.cs
new file mode 100644
index 0000000000..d039b889b8
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointAndRehydrate/Program.cs
@@ -0,0 +1,96 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Agents.Workflows;
+
+namespace WorkflowCheckpointAndRehydrateSample;
+
+///
+/// This sample introduces the concepts of check points and shows how to save and restore
+/// the state of a workflow using checkpoints.
+/// This sample demonstrates checkpoints, which allow you to save and restore a workflow's state.
+/// Key concepts:
+/// - Super Steps: A workflow executes in stages called "super steps". Each super step runs
+/// one or more executors and completes when all those executors finish their work.
+/// - Checkpoints: The system automatically saves the workflow's state at the end of each
+/// super step. You can use these checkpoints to resume the workflow from any saved point.
+/// - Rehydration: You can rehydrate a new workflow instance from a saved checkpoint, allowing
+/// you to continue execution from that point.
+///
+///
+/// Pre-requisites:
+/// - Foundational samples should be completed first.
+///
+public static class Program
+{
+ private static async Task Main()
+ {
+ // Create the workflow
+ var workflow = WorkflowHelper.GetWorkflow();
+
+ // Create checkpoint manager
+ var checkpointManager = new CheckpointManager();
+ var checkpoints = new List();
+
+ // Execute the workflow and save checkpoints
+ Checkpointed checkpointedRun = await InProcessExecution
+ .StreamAsync(workflow, NumberSignal.Init, checkpointManager)
+ .ConfigureAwait(false);
+ await foreach (WorkflowEvent evt in checkpointedRun.Run.WatchStreamAsync().ConfigureAwait(false))
+ {
+ if (evt is ExecutorCompletedEvent executorCompletedEvt)
+ {
+ Console.WriteLine($"* Executor {executorCompletedEvt.ExecutorId} completed.");
+ }
+
+ if (evt is SuperStepCompletedEvent superStepCompletedEvt)
+ {
+ // Checkpoints are automatically created at the end of each super step when a
+ // checkpoint manager is provided. You can store the checkpoint info for later use.
+ CheckpointInfo? checkpoint = superStepCompletedEvt.CompletionInfo!.Checkpoint;
+ if (checkpoint != null)
+ {
+ checkpoints.Add(checkpoint);
+ Console.WriteLine($"** Checkpoint created at step {checkpoints.Count}.");
+ }
+ }
+
+ if (evt is WorkflowCompletedEvent workflowCompletedEvt)
+ {
+ Console.WriteLine($"Workflow completed with result: {workflowCompletedEvt.Data}");
+ }
+ }
+
+ if (checkpoints.Count == 0)
+ {
+ throw new InvalidOperationException("No checkpoints were created during the workflow execution.");
+ }
+ Console.WriteLine($"Number of checkpoints created: {checkpoints.Count}");
+
+ // Rehydrate a new workflow instance from a saved checkpoint and continue execution
+ var newWorkflow = WorkflowHelper.GetWorkflow();
+ var checkpointIndex = 5;
+ Console.WriteLine($"\n\nHydrating a new workflow instance from the {checkpointIndex + 1}th checkpoint.");
+ CheckpointInfo savedCheckpoint = checkpoints[checkpointIndex];
+
+ Checkpointed newCheckpointedRun = await InProcessExecution
+ .StreamAsync(newWorkflow, NumberSignal.Init, checkpointManager)
+ .ConfigureAwait(false);
+ await newCheckpointedRun.RestoreCheckpointAsync(savedCheckpoint, CancellationToken.None).ConfigureAwait(false);
+ await foreach (WorkflowEvent evt in newCheckpointedRun.Run.WatchStreamAsync().ConfigureAwait(false))
+ {
+ if (evt is ExecutorCompletedEvent executorCompletedEvt)
+ {
+ Console.WriteLine($"* Executor {executorCompletedEvt.ExecutorId} completed.");
+ }
+
+ if (evt is WorkflowCompletedEvent workflowCompletedEvt)
+ {
+ Console.WriteLine($"Workflow completed with result: {workflowCompletedEvt.Data}");
+ }
+ }
+ }
+}
diff --git a/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointAndRehydrate/WorkflowHelper.cs b/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointAndRehydrate/WorkflowHelper.cs
new file mode 100644
index 0000000000..09ffc8db74
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointAndRehydrate/WorkflowHelper.cs
@@ -0,0 +1,165 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Agents.Workflows;
+using Microsoft.Agents.Workflows.Reflection;
+
+namespace WorkflowCheckpointAndRehydrateSample;
+
+internal static class WorkflowHelper
+{
+ ///
+ /// Get a workflow that plays a number guessing game with checkpointing support.
+ /// The workflow consists of two executors that are connected in a feedback loop:
+ /// 1. GuessNumberExecutor: Makes a guess based on the current known bounds.
+ /// 2. JudgeExecutor: Evaluates the guess and provides feedback.
+ /// The workflow continues until the correct number is guessed.
+ ///
+ internal static Workflow GetWorkflow()
+ {
+ // Create the executors
+ GuessNumberExecutor guessNumberExecutor = new(1, 100);
+ JudgeExecutor judgeExecutor = new(42);
+
+ // Build the workflow by connecting executors in a loop
+ var workflow = new WorkflowBuilder(guessNumberExecutor)
+ .AddEdge(guessNumberExecutor, judgeExecutor)
+ .AddEdge(judgeExecutor, guessNumberExecutor)
+ .Build();
+
+ return workflow;
+ }
+}
+
+///
+/// Signals used for communication between GuessNumberExecutor and JudgeExecutor.
+///
+internal enum NumberSignal
+{
+ Init,
+ Above,
+ Below,
+}
+
+///
+/// Executor that makes a guess based on the current bounds.
+///
+internal sealed class GuessNumberExecutor() : ReflectingExecutor("Guess"), IMessageHandler
+{
+ ///
+ /// The lower bound of the guessing range.
+ ///
+ public int LowerBound { get; private set; }
+
+ ///
+ /// The upper bound of the guessing range.
+ ///
+ public int UpperBound { get; private set; }
+
+ private const string StateKey = "GuessNumberExecutorState";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The initial lower bound of the guessing range.
+ /// The initial upper bound of the guessing range.
+ public GuessNumberExecutor(int lowerBound, int upperBound) : this()
+ {
+ this.LowerBound = lowerBound;
+ this.UpperBound = upperBound;
+ }
+
+ private int NextGuess => (this.LowerBound + this.UpperBound) / 2;
+
+ public async ValueTask HandleAsync(NumberSignal message, IWorkflowContext context)
+ {
+ switch (message)
+ {
+ case NumberSignal.Init:
+ await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
+ break;
+ case NumberSignal.Above:
+ this.UpperBound = this.NextGuess - 1;
+ await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
+ break;
+ case NumberSignal.Below:
+ this.LowerBound = this.NextGuess + 1;
+ await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
+ break;
+ }
+ }
+
+ ///
+ /// Checkpoint the current state of the executor.
+ /// This must be overridden to save any state that is needed to resume the executor.
+ ///
+ protected override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellation = default)
+ {
+ return context.QueueStateUpdateAsync(StateKey, (this.LowerBound, this.UpperBound));
+ }
+
+ ///
+ /// Restore the state of the executor from a checkpoint.
+ /// This must be overridden to restore any state that was saved during checkpointing.
+ ///
+ protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellation = default)
+ {
+ (this.LowerBound, this.UpperBound) = await context.ReadStateAsync<(int, int)>(StateKey).ConfigureAwait(false);
+ }
+}
+
+///
+/// Executor that judges the guess and provides feedback.
+///
+internal sealed class JudgeExecutor() : ReflectingExecutor("Judge"), IMessageHandler
+{
+ private readonly int _targetNumber;
+ private int _tries = 0;
+ private const string StateKey = "JudgeExecutorState";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The number to be guessed.
+ public JudgeExecutor(int targetNumber) : this()
+ {
+ this._targetNumber = targetNumber;
+ }
+
+ public async ValueTask HandleAsync(int message, IWorkflowContext context)
+ {
+ this._tries++;
+ if (message == this._targetNumber)
+ {
+ await context.AddEventAsync(new WorkflowCompletedEvent($"{this._targetNumber} found in {this._tries} tries!"))
+ .ConfigureAwait(false);
+ }
+ else if (message < this._targetNumber)
+ {
+ await context.SendMessageAsync(NumberSignal.Below).ConfigureAwait(false);
+ }
+ else
+ {
+ await context.SendMessageAsync(NumberSignal.Above).ConfigureAwait(false);
+ }
+ }
+
+ ///
+ /// Checkpoint the current state of the executor.
+ /// This must be overridden to save any state that is needed to resume the executor.
+ ///
+ protected override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellation = default)
+ {
+ return context.QueueStateUpdateAsync(StateKey, this._tries);
+ }
+
+ ///
+ /// Restore the state of the executor from a checkpoint.
+ /// This must be overridden to restore any state that was saved during checkpointing.
+ ///
+ protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellation = default)
+ {
+ this._tries = await context.ReadStateAsync(StateKey).ConfigureAwait(false);
+ }
+}
diff --git a/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointAndResume/CheckpointAndResume.csproj b/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointAndResume/CheckpointAndResume.csproj
new file mode 100644
index 0000000000..1025cb45c9
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointAndResume/CheckpointAndResume.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ net9.0
+ 12
+
+ enable
+ disable
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointAndResume/Program.cs b/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointAndResume/Program.cs
new file mode 100644
index 0000000000..ebabba2141
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointAndResume/Program.cs
@@ -0,0 +1,91 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Agents.Workflows;
+
+namespace WorkflowCheckpointAndResumeSample;
+
+///
+/// This sample introduces the concepts of check points and shows how to save and restore
+/// the state of a workflow using checkpoints.
+/// This sample demonstrates checkpoints, which allow you to save and restore a workflow's state.
+/// Key concepts:
+/// - Super Steps: A workflow executes in stages called "super steps". Each super step runs
+/// one or more executors and completes when all those executors finish their work.
+/// - Checkpoints: The system automatically saves the workflow's state at the end of each
+/// super step. You can use these checkpoints to resume the workflow from any saved point.
+/// - Resume: If needed, you can restore a checkpoint and continue execution from that state.
+///
+///
+/// Pre-requisites:
+/// - Foundational samples should be completed first.
+///
+public static class Program
+{
+ private static async Task Main()
+ {
+ // Create the workflow
+ var workflow = WorkflowHelper.GetWorkflow();
+
+ // Create checkpoint manager
+ var checkpointManager = new CheckpointManager();
+ var checkpoints = new List();
+
+ // Execute the workflow and save checkpoints
+ Checkpointed checkpointedRun = await InProcessExecution
+ .StreamAsync(workflow, NumberSignal.Init, checkpointManager)
+ .ConfigureAwait(false);
+ await foreach (WorkflowEvent evt in checkpointedRun.Run.WatchStreamAsync().ConfigureAwait(false))
+ {
+ if (evt is ExecutorCompletedEvent executorCompletedEvt)
+ {
+ Console.WriteLine($"* Executor {executorCompletedEvt.ExecutorId} completed.");
+ }
+
+ if (evt is SuperStepCompletedEvent superStepCompletedEvt)
+ {
+ // Checkpoints are automatically created at the end of each super step when a
+ // checkpoint manager is provided. You can store the checkpoint info for later use.
+ CheckpointInfo? checkpoint = superStepCompletedEvt.CompletionInfo!.Checkpoint;
+ if (checkpoint != null)
+ {
+ checkpoints.Add(checkpoint);
+ Console.WriteLine($"** Checkpoint created at step {checkpoints.Count}.");
+ }
+ }
+
+ if (evt is WorkflowCompletedEvent workflowCompletedEvt)
+ {
+ Console.WriteLine($"Workflow completed with result: {workflowCompletedEvt.Data}");
+ }
+ }
+
+ if (checkpoints.Count == 0)
+ {
+ throw new InvalidOperationException("No checkpoints were created during the workflow execution.");
+ }
+ Console.WriteLine($"Number of checkpoints created: {checkpoints.Count}");
+
+ // Restoring from a checkpoint and resuming execution
+ var checkpointIndex = 5;
+ Console.WriteLine($"\n\nRestoring from the {checkpointIndex + 1}th checkpoint.");
+ CheckpointInfo savedCheckpoint = checkpoints[checkpointIndex];
+ // Note that we are restoring the state directly to the same run instance.
+ await checkpointedRun.RestoreCheckpointAsync(savedCheckpoint, CancellationToken.None).ConfigureAwait(false);
+ await foreach (WorkflowEvent evt in checkpointedRun.Run.WatchStreamAsync().ConfigureAwait(false))
+ {
+ if (evt is ExecutorCompletedEvent executorCompletedEvt)
+ {
+ Console.WriteLine($"* Executor {executorCompletedEvt.ExecutorId} completed.");
+ }
+
+ if (evt is WorkflowCompletedEvent workflowCompletedEvt)
+ {
+ Console.WriteLine($"Workflow completed with result: {workflowCompletedEvt.Data}");
+ }
+ }
+ }
+}
diff --git a/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointAndResume/WorkflowHelper.cs b/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointAndResume/WorkflowHelper.cs
new file mode 100644
index 0000000000..aa4f5506ac
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointAndResume/WorkflowHelper.cs
@@ -0,0 +1,165 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Agents.Workflows;
+using Microsoft.Agents.Workflows.Reflection;
+
+namespace WorkflowCheckpointAndResumeSample;
+
+internal static class WorkflowHelper
+{
+ ///
+ /// Get a workflow that plays a number guessing game with checkpointing support.
+ /// The workflow consists of two executors that are connected in a feedback loop:
+ /// 1. GuessNumberExecutor: Makes a guess based on the current known bounds.
+ /// 2. JudgeExecutor: Evaluates the guess and provides feedback.
+ /// The workflow continues until the correct number is guessed.
+ ///
+ internal static Workflow GetWorkflow()
+ {
+ // Create the executors
+ GuessNumberExecutor guessNumberExecutor = new(1, 100);
+ JudgeExecutor judgeExecutor = new(42);
+
+ // Build the workflow by connecting executors in a loop
+ var workflow = new WorkflowBuilder(guessNumberExecutor)
+ .AddEdge(guessNumberExecutor, judgeExecutor)
+ .AddEdge(judgeExecutor, guessNumberExecutor)
+ .Build();
+
+ return workflow;
+ }
+}
+
+///
+/// Signals used for communication between GuessNumberExecutor and JudgeExecutor.
+///
+internal enum NumberSignal
+{
+ Init,
+ Above,
+ Below,
+}
+
+///
+/// Executor that makes a guess based on the current bounds.
+///
+internal sealed class GuessNumberExecutor() : ReflectingExecutor("Guess"), IMessageHandler
+{
+ ///
+ /// The lower bound of the guessing range.
+ ///
+ public int LowerBound { get; private set; }
+
+ ///
+ /// The upper bound of the guessing range.
+ ///
+ public int UpperBound { get; private set; }
+
+ private const string StateKey = "GuessNumberExecutorState";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The initial lower bound of the guessing range.
+ /// The initial upper bound of the guessing range.
+ public GuessNumberExecutor(int lowerBound, int upperBound) : this()
+ {
+ this.LowerBound = lowerBound;
+ this.UpperBound = upperBound;
+ }
+
+ private int NextGuess => (this.LowerBound + this.UpperBound) / 2;
+
+ public async ValueTask HandleAsync(NumberSignal message, IWorkflowContext context)
+ {
+ switch (message)
+ {
+ case NumberSignal.Init:
+ await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
+ break;
+ case NumberSignal.Above:
+ this.UpperBound = this.NextGuess - 1;
+ await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
+ break;
+ case NumberSignal.Below:
+ this.LowerBound = this.NextGuess + 1;
+ await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
+ break;
+ }
+ }
+
+ ///
+ /// Checkpoint the current state of the executor.
+ /// This must be overridden to save any state that is needed to resume the executor.
+ ///
+ protected override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellation = default)
+ {
+ return context.QueueStateUpdateAsync(StateKey, (this.LowerBound, this.UpperBound));
+ }
+
+ ///
+ /// Restore the state of the executor from a checkpoint.
+ /// This must be overridden to restore any state that was saved during checkpointing.
+ ///
+ protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellation = default)
+ {
+ (this.LowerBound, this.UpperBound) = await context.ReadStateAsync<(int, int)>(StateKey).ConfigureAwait(false);
+ }
+}
+
+///
+/// Executor that judges the guess and provides feedback.
+///
+internal sealed class JudgeExecutor() : ReflectingExecutor("Judge"), IMessageHandler
+{
+ private readonly int _targetNumber;
+ private int _tries = 0;
+ private const string StateKey = "JudgeExecutorState";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The number to be guessed.
+ public JudgeExecutor(int targetNumber) : this()
+ {
+ this._targetNumber = targetNumber;
+ }
+
+ public async ValueTask HandleAsync(int message, IWorkflowContext context)
+ {
+ this._tries++;
+ if (message == this._targetNumber)
+ {
+ await context.AddEventAsync(new WorkflowCompletedEvent($"{this._targetNumber} found in {this._tries} tries!"))
+ .ConfigureAwait(false);
+ }
+ else if (message < this._targetNumber)
+ {
+ await context.SendMessageAsync(NumberSignal.Below).ConfigureAwait(false);
+ }
+ else
+ {
+ await context.SendMessageAsync(NumberSignal.Above).ConfigureAwait(false);
+ }
+ }
+
+ ///
+ /// Checkpoint the current state of the executor.
+ /// This must be overridden to save any state that is needed to resume the executor.
+ ///
+ protected override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellation = default)
+ {
+ return context.QueueStateUpdateAsync(StateKey, this._tries);
+ }
+
+ ///
+ /// Restore the state of the executor from a checkpoint.
+ /// This must be overridden to restore any state that was saved during checkpointing.
+ ///
+ protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellation = default)
+ {
+ this._tries = await context.ReadStateAsync(StateKey).ConfigureAwait(false);
+ }
+}
diff --git a/dotnet/samples/GettingStarted/Workflows/Foundational/02_Streaming/02_Streaming.csproj b/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointWithHumanInTheLoop/CheckpointWithHumanInTheLoop.csproj
similarity index 100%
rename from dotnet/samples/GettingStarted/Workflows/Foundational/02_Streaming/02_Streaming.csproj
rename to dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointWithHumanInTheLoop/CheckpointWithHumanInTheLoop.csproj
diff --git a/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointWithHumanInTheLoop/Program.cs b/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointWithHumanInTheLoop/Program.cs
new file mode 100644
index 0000000000..167cc760d3
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointWithHumanInTheLoop/Program.cs
@@ -0,0 +1,138 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Agents.Workflows;
+
+namespace WorkflowCheckpointWithHumanInTheLoopSample;
+
+///
+/// This sample demonstrates how to create a workflow with human-in-the-loop interaction and
+/// checkpointing support. The workflow plays a number guessing game where the user provides
+/// guesses based on feedback from the workflow. The workflow state is checkpointed at the end
+/// of each super step, allowing it to be restored and resumed later.
+/// Each InputPort request and response cycle takes two super steps:
+/// 1. The InputPort sends a RequestInfoEvent to request input from the external world.
+/// 2. The external world sends a response back to the InputPort.
+/// Thus, two checkpoints are created for each human-in-the-loop interaction.
+///
+///
+/// Pre-requisites:
+/// - Foundational samples should be completed first.
+/// - This sample builds upon the HumanInTheLoopBasic sample. It's recommended to go through that
+/// sample first to understand the basics of human-in-the-loop workflows.
+/// - This sample also builds upon the CheckpointAndResume sample. It's recommended to
+/// go through that sample first to understand the basics of checkpointing and resuming workflows.
+///
+public static class Program
+{
+ private static async Task Main()
+ {
+ // Create the workflow
+ var workflow = WorkflowHelper.GetWorkflow();
+
+ // Create checkpoint manager
+ var checkpointManager = new CheckpointManager();
+ var checkpoints = new List();
+
+ // Execute the workflow and save checkpoints
+ Checkpointed checkpointedRun = await InProcessExecution
+ .StreamAsync(workflow, new SignalWithNumber(NumberSignal.Init), checkpointManager)
+ .ConfigureAwait(false);
+ await foreach (WorkflowEvent evt in checkpointedRun.Run.WatchStreamAsync().ConfigureAwait(false))
+ {
+ switch (evt)
+ {
+ case RequestInfoEvent requestInputEvt:
+ // Handle `RequestInfoEvent` from the workflow
+ ExternalResponse response = HandleExternalRequest(requestInputEvt.Request);
+ await checkpointedRun.Run.SendResponseAsync(response).ConfigureAwait(false);
+ break;
+ case ExecutorCompletedEvent executorCompletedEvt:
+ Console.WriteLine($"* Executor {executorCompletedEvt.ExecutorId} completed.");
+ break;
+ case SuperStepCompletedEvent superStepCompletedEvt:
+ // Checkpoints are automatically created at the end of each super step when a
+ // checkpoint manager is provided. You can store the checkpoint info for later use.
+ CheckpointInfo? checkpoint = superStepCompletedEvt.CompletionInfo!.Checkpoint;
+ if (checkpoint != null)
+ {
+ checkpoints.Add(checkpoint);
+ Console.WriteLine($"** Checkpoint created at step {checkpoints.Count}.");
+ }
+ break;
+ case WorkflowCompletedEvent workflowCompletedEvt:
+ Console.WriteLine($"Workflow completed with result: {workflowCompletedEvt.Data}");
+ break;
+ }
+ }
+
+ if (checkpoints.Count == 0)
+ {
+ throw new InvalidOperationException("No checkpoints were created during the workflow execution.");
+ }
+ Console.WriteLine($"Number of checkpoints created: {checkpoints.Count}");
+
+ // Restoring from a checkpoint and resuming execution
+ var checkpointIndex = 1;
+ Console.WriteLine($"\n\nRestoring from the {checkpointIndex + 1}th checkpoint.");
+ CheckpointInfo savedCheckpoint = checkpoints[checkpointIndex];
+ // Note that we are restoring the state directly to the same run instance.
+ await checkpointedRun.RestoreCheckpointAsync(savedCheckpoint, CancellationToken.None).ConfigureAwait(false);
+ await foreach (WorkflowEvent evt in checkpointedRun.Run.WatchStreamAsync().ConfigureAwait(false))
+ {
+ switch (evt)
+ {
+ case RequestInfoEvent requestInputEvt:
+ // Handle `RequestInfoEvent` from the workflow
+ ExternalResponse response = HandleExternalRequest(requestInputEvt.Request);
+ await checkpointedRun.Run.SendResponseAsync(response).ConfigureAwait(false);
+ break;
+ case ExecutorCompletedEvent executorCompletedEvt:
+ Console.WriteLine($"* Executor {executorCompletedEvt.ExecutorId} completed.");
+ break;
+ case WorkflowCompletedEvent workflowCompletedEvt:
+ Console.WriteLine($"Workflow completed with result: {workflowCompletedEvt.Data}");
+ break;
+ }
+ }
+ }
+
+ private static ExternalResponse HandleExternalRequest(ExternalRequest request)
+ {
+ if (request.Port.Request == typeof(SignalWithNumber))
+ {
+ var signal = (SignalWithNumber)request.Data;
+ switch (signal.Signal)
+ {
+ case NumberSignal.Init:
+ int initialGuess = ReadIntegerFromConsole("Please provide your initial guess: ");
+ return request.CreateResponse(initialGuess);
+ case NumberSignal.Above:
+ int lowerGuess = ReadIntegerFromConsole($"You previously guessed {signal.Number} too large. Please provide a new guess: ");
+ return request.CreateResponse(lowerGuess);
+ case NumberSignal.Below:
+ int higherGuess = ReadIntegerFromConsole($"You previously guessed {signal.Number} too small. Please provide a new guess: ");
+ return request.CreateResponse(higherGuess);
+ }
+ }
+
+ throw new NotSupportedException($"Request {request.Port.Request} is not supported");
+ }
+
+ private static int ReadIntegerFromConsole(string prompt)
+ {
+ while (true)
+ {
+ Console.Write(prompt);
+ string? input = Console.ReadLine();
+ if (int.TryParse(input, out int value))
+ {
+ return value;
+ }
+ Console.WriteLine("Invalid input. Please enter a valid integer.");
+ }
+ }
+}
diff --git a/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointWithHumanInTheLoop/WorkflowHelper.cs b/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointWithHumanInTheLoop/WorkflowHelper.cs
new file mode 100644
index 0000000000..a3e81914b7
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointWithHumanInTheLoop/WorkflowHelper.cs
@@ -0,0 +1,110 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Agents.Workflows;
+using Microsoft.Agents.Workflows.Reflection;
+
+namespace WorkflowCheckpointWithHumanInTheLoopSample;
+
+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.
+ ///
+ internal static Workflow GetWorkflow()
+ {
+ // Create the executors
+ InputPort numberInputPort = InputPort.Create("GuessNumber");
+ JudgeExecutor judgeExecutor = new(42);
+
+ // Build the workflow by connecting executors in a loop
+ var workflow = new WorkflowBuilder(numberInputPort)
+ .AddEdge(numberInputPort, judgeExecutor)
+ .AddEdge(judgeExecutor, numberInputPort)
+ .Build();
+
+ return workflow;
+ }
+}
+
+///
+/// Signals indicating if the guess was too high, too low, or an initial guess.
+///
+internal enum NumberSignal
+{
+ Init,
+ Above,
+ Below,
+}
+
+///
+/// Signals used for communication between guesses and the JudgeExecutor.
+///
+internal sealed class SignalWithNumber
+{
+ public NumberSignal Signal { get; }
+ public int? Number { get; }
+
+ public SignalWithNumber(NumberSignal signal, int? number = null)
+ {
+ this.Signal = signal;
+ this.Number = number;
+ }
+}
+
+///
+/// Executor that judges the guess and provides feedback.
+///
+internal sealed class JudgeExecutor() : ReflectingExecutor("Judge"), IMessageHandler
+{
+ private readonly int _targetNumber;
+ private int _tries = 0;
+ private const string StateKey = "JudgeExecutorState";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The number to be guessed.
+ public JudgeExecutor(int targetNumber) : this()
+ {
+ this._targetNumber = targetNumber;
+ }
+
+ public async ValueTask HandleAsync(int message, IWorkflowContext context)
+ {
+ this._tries++;
+ if (message == this._targetNumber)
+ {
+ await context.AddEventAsync(new WorkflowCompletedEvent($"{this._targetNumber} found in {this._tries} tries!"))
+ .ConfigureAwait(false);
+ }
+ else if (message < this._targetNumber)
+ {
+ await context.SendMessageAsync(new SignalWithNumber(NumberSignal.Below, message)).ConfigureAwait(false);
+ }
+ else
+ {
+ await context.SendMessageAsync(new SignalWithNumber(NumberSignal.Above, message)).ConfigureAwait(false);
+ }
+ }
+
+ ///
+ /// Checkpoint the current state of the executor.
+ /// This must be overridden to save any state that is needed to resume the executor.
+ ///
+ protected override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellation = default)
+ {
+ return context.QueueStateUpdateAsync(StateKey, this._tries);
+ }
+
+ ///
+ /// Restore the state of the executor from a checkpoint.
+ /// This must be overridden to restore any state that was saved during checkpointing.
+ ///
+ protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellation = default)
+ {
+ this._tries = await context.ReadStateAsync(StateKey).ConfigureAwait(false);
+ }
+}
diff --git a/dotnet/samples/GettingStarted/Workflows/Concurrent/Program.cs b/dotnet/samples/GettingStarted/Workflows/Concurrent/Program.cs
index 2c3e1594c6..b6e6cfcc00 100644
--- a/dotnet/samples/GettingStarted/Workflows/Concurrent/Program.cs
+++ b/dotnet/samples/GettingStarted/Workflows/Concurrent/Program.cs
@@ -85,7 +85,7 @@ internal sealed class ConcurrentStartExecutor() :
///
/// The user message to process
/// Workflow context for accessing workflow services and adding events
- ///
+ /// A task representing the asynchronous operation
public async ValueTask HandleAsync(string message, IWorkflowContext context)
{
// Broadcast the message to all connected agents. Receiving agents will queue
@@ -110,7 +110,7 @@ internal sealed class ConcurrentAggregationExecutor() :
///
/// The message from the agent
/// Workflow context for accessing workflow services and adding events
- ///
+ /// A task representing the asynchronous operation
public async ValueTask HandleAsync(ChatMessage message, IWorkflowContext context)
{
this._messages.Add(message);
diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/Program.cs b/dotnet/samples/GettingStarted/Workflows/Declarative/Program.cs
index 78e582dfc9..ffacf3123a 100644
--- a/dotnet/samples/GettingStarted/Workflows/Declarative/Program.cs
+++ b/dotnet/samples/GettingStarted/Workflows/Declarative/Program.cs
@@ -92,13 +92,13 @@ internal sealed class Program
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
{
- if (evt is ExecutorInvokeEvent executorInvoked)
+ if (evt is ExecutorInvokedEvent executorInvoked)
{
Debug.WriteLine($"EXECUTOR ENTER #{executorInvoked.ExecutorId}");
}
- else if (evt is ExecutorCompleteEvent executorComplete)
+ else if (evt is ExecutorCompletedEvent executorCompleted)
{
- Debug.WriteLine($"EXECUTOR EXIT #{executorComplete.ExecutorId}");
+ Debug.WriteLine($"EXECUTOR EXIT #{executorCompleted.ExecutorId}");
}
if (evt is DeclarativeActionInvokeEvent actionInvoked)
{
diff --git a/dotnet/samples/GettingStarted/Workflows/HumanInTheLoop/HumanInTheLoopBasic/HumanInTheLoopBasic.csproj b/dotnet/samples/GettingStarted/Workflows/HumanInTheLoop/HumanInTheLoopBasic/HumanInTheLoopBasic.csproj
new file mode 100644
index 0000000000..bbee070910
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Workflows/HumanInTheLoop/HumanInTheLoopBasic/HumanInTheLoopBasic.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ net9.0
+ 12
+
+ enable
+ disable
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dotnet/samples/GettingStarted/Workflows/HumanInTheLoop/HumanInTheLoopBasic/Program.cs b/dotnet/samples/GettingStarted/Workflows/HumanInTheLoop/HumanInTheLoopBasic/Program.cs
new file mode 100644
index 0000000000..07ffe8c06d
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Workflows/HumanInTheLoop/HumanInTheLoopBasic/Program.cs
@@ -0,0 +1,86 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Threading.Tasks;
+using Microsoft.Agents.Workflows;
+
+namespace WorkflowHumanInTheLoopBasicSample;
+
+///
+/// This sample introduces the concept of InputPort and ExternalRequest to enable
+/// human-in-the-loop interaction scenarios.
+/// An input port can be used as if it were an executor in the workflow graph. Upon receiving
+/// a message, the input port generates an RequestInfoEvent that gets emitted to the external world.
+/// The external world can then respond to the request by sending an ExternalResponse back to
+/// the workflow.
+/// The sample implements a simple number guessing game where the external user tries to guess
+/// a pre-defined target number. The workflow consists of a single JudgeExecutor that judges
+/// the user's guesses and provides feedback.
+///
+///
+/// Pre-requisites:
+/// - Foundational samples should be completed first.
+///
+public static class Program
+{
+ private static async Task Main()
+ {
+ // Create the workflow
+ var workflow = WorkflowHelper.GetWorkflow();
+
+ // Execute the workflow
+ StreamingRun handle = await InProcessExecution.StreamAsync(workflow, NumberSignal.Init).ConfigureAwait(false);
+ await foreach (WorkflowEvent evt in handle.WatchStreamAsync().ConfigureAwait(false))
+ {
+ switch (evt)
+ {
+ case RequestInfoEvent requestInputEvt:
+ // Handle `RequestInfoEvent` from the workflow
+ ExternalResponse response = HandleExternalRequest(requestInputEvt.Request);
+ await handle.SendResponseAsync(response).ConfigureAwait(false);
+ break;
+
+ case WorkflowCompletedEvent workflowCompleteEvt:
+ // The workflow has completed successfully
+ Console.WriteLine($"Workflow completed with result: {workflowCompleteEvt.Data}");
+ return;
+ }
+ }
+ }
+
+ private static ExternalResponse HandleExternalRequest(ExternalRequest request)
+ {
+ if (request.Port.Request == typeof(NumberSignal))
+ {
+ var signal = (NumberSignal)request.Data;
+ switch (signal)
+ {
+ case NumberSignal.Init:
+ int initialGuess = ReadIntegerFromConsole("Please provide your initial guess: ");
+ return request.CreateResponse(initialGuess);
+ case NumberSignal.Above:
+ int lowerGuess = ReadIntegerFromConsole("You previously guessed too large. Please provide a new guess: ");
+ return request.CreateResponse(lowerGuess);
+ case NumberSignal.Below:
+ int higherGuess = ReadIntegerFromConsole("You previously guessed too small. Please provide a new guess: ");
+ return request.CreateResponse(higherGuess);
+ }
+ }
+
+ throw new NotSupportedException($"Request {request.Port.Request} is not supported");
+ }
+
+ private static int ReadIntegerFromConsole(string prompt)
+ {
+ while (true)
+ {
+ Console.Write(prompt);
+ string? input = Console.ReadLine();
+ if (int.TryParse(input, out int value))
+ {
+ return value;
+ }
+ Console.WriteLine("Invalid input. Please enter a valid integer.");
+ }
+ }
+}
diff --git a/dotnet/samples/GettingStarted/Workflows/HumanInTheLoop/HumanInTheLoopBasic/WorkflowHelper.cs b/dotnet/samples/GettingStarted/Workflows/HumanInTheLoop/HumanInTheLoopBasic/WorkflowHelper.cs
new file mode 100644
index 0000000000..7e11a4b39d
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Workflows/HumanInTheLoop/HumanInTheLoopBasic/WorkflowHelper.cs
@@ -0,0 +1,75 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Threading.Tasks;
+using Microsoft.Agents.Workflows;
+using Microsoft.Agents.Workflows.Reflection;
+
+namespace WorkflowHumanInTheLoopBasicSample;
+
+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.
+ ///
+ internal static Workflow GetWorkflow()
+ {
+ // Create the executors
+ InputPort numberInputPort = InputPort.Create("GuessNumber");
+ JudgeExecutor judgeExecutor = new(42);
+
+ // Build the workflow by connecting executors in a loop
+ var workflow = new WorkflowBuilder(numberInputPort)
+ .AddEdge(numberInputPort, judgeExecutor)
+ .AddEdge(judgeExecutor, numberInputPort)
+ .Build();
+
+ return workflow;
+ }
+}
+
+///
+/// Signals used for communication between guesses and the JudgeExecutor.
+///
+internal enum NumberSignal
+{
+ Init,
+ Above,
+ Below,
+}
+
+///
+/// Executor that judges the guess and provides feedback.
+///
+internal sealed class JudgeExecutor() : ReflectingExecutor("Judge"), IMessageHandler
+{
+ private readonly int _targetNumber;
+ private int _tries = 0;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The number to be guessed.
+ public JudgeExecutor(int targetNumber) : this()
+ {
+ this._targetNumber = targetNumber;
+ }
+
+ public async ValueTask HandleAsync(int message, IWorkflowContext context)
+ {
+ this._tries++;
+ if (message == this._targetNumber)
+ {
+ await context.AddEventAsync(new WorkflowCompletedEvent($"{this._targetNumber} found in {this._tries} tries!"))
+ .ConfigureAwait(false);
+ }
+ else if (message < this._targetNumber)
+ {
+ await context.SendMessageAsync(NumberSignal.Below).ConfigureAwait(false);
+ }
+ else
+ {
+ await context.SendMessageAsync(NumberSignal.Above).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/dotnet/samples/GettingStarted/Workflows/Loop/Loop.csproj b/dotnet/samples/GettingStarted/Workflows/Loop/Loop.csproj
new file mode 100644
index 0000000000..75b48d7b65
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Workflows/Loop/Loop.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ net9.0
+ 12
+
+ enable
+ disable
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dotnet/samples/GettingStarted/Workflows/Loop/Program.cs b/dotnet/samples/GettingStarted/Workflows/Loop/Program.cs
new file mode 100644
index 0000000000..97a21b8268
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Workflows/Loop/Program.cs
@@ -0,0 +1,139 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Threading.Tasks;
+using Microsoft.Agents.Workflows;
+using Microsoft.Agents.Workflows.Reflection;
+
+namespace WorkflowLoopSample;
+
+///
+/// This sample demonstrates a simple number guessing game using a workflow with looping behavior.
+///
+/// The workflow consists of two executors that are connected in a feedback loop:
+/// 1. GuessNumberExecutor: Makes a guess based on the current known bounds.
+/// 2. JudgeExecutor: Evaluates the guess and provides feedback.
+/// The workflow continues until the correct number is guessed.
+///
+///
+/// Pre-requisites:
+/// - Foundational samples should be completed first.
+///
+public static class Program
+{
+ private static async Task Main()
+ {
+ // Create the executors
+ GuessNumberExecutor guessNumberExecutor = new(1, 100);
+ JudgeExecutor judgeExecutor = new(42);
+
+ // Build the workflow by connecting executors in a loop
+ var workflow = new WorkflowBuilder(guessNumberExecutor)
+ .AddEdge(guessNumberExecutor, judgeExecutor)
+ .AddEdge(judgeExecutor, guessNumberExecutor)
+ .Build();
+
+ // Execute the workflow
+ StreamingRun run = await InProcessExecution.StreamAsync(workflow, NumberSignal.Init).ConfigureAwait(false);
+ await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
+ {
+ if (evt is WorkflowCompletedEvent workflowCompleteEvt)
+ {
+ Console.WriteLine($"Result: {workflowCompleteEvt}");
+ }
+ }
+ }
+}
+
+///
+/// Signals used for communication between GuessNumberExecutor and JudgeExecutor.
+///
+internal enum NumberSignal
+{
+ Init,
+ Above,
+ Below,
+}
+
+///
+/// Executor that makes a guess based on the current bounds.
+///
+internal sealed class GuessNumberExecutor : ReflectingExecutor, IMessageHandler
+{
+ ///
+ /// The lower bound of the guessing range.
+ ///
+ public int LowerBound { get; private set; }
+
+ ///
+ /// The upper bound of the guessing range.
+ ///
+ public int UpperBound { get; private set; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The initial lower bound of the guessing range.
+ /// The initial upper bound of the guessing range.
+ public GuessNumberExecutor(int lowerBound, int upperBound)
+ {
+ this.LowerBound = lowerBound;
+ this.UpperBound = upperBound;
+ }
+
+ private int NextGuess => (this.LowerBound + this.UpperBound) / 2;
+
+ public async ValueTask HandleAsync(NumberSignal message, IWorkflowContext context)
+ {
+ switch (message)
+ {
+ case NumberSignal.Init:
+ await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
+ break;
+ case NumberSignal.Above:
+ this.UpperBound = this.NextGuess - 1;
+ await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
+ break;
+ case NumberSignal.Below:
+ this.LowerBound = this.NextGuess + 1;
+ await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
+ break;
+ }
+ }
+}
+
+///
+/// Executor that judges the guess and provides feedback.
+///
+internal sealed class JudgeExecutor : ReflectingExecutor, IMessageHandler
+{
+ private readonly int _targetNumber;
+ private int _tries = 0;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The number to be guessed.
+ public JudgeExecutor(int targetNumber)
+ {
+ this._targetNumber = targetNumber;
+ }
+
+ public async ValueTask HandleAsync(int message, IWorkflowContext context)
+ {
+ this._tries++;
+ if (message == this._targetNumber)
+ {
+ await context.AddEventAsync(new WorkflowCompletedEvent($"{this._targetNumber} found in {this._tries} tries!"))
+ .ConfigureAwait(false);
+ }
+ else if (message < this._targetNumber)
+ {
+ await context.SendMessageAsync(NumberSignal.Below).ConfigureAwait(false);
+ }
+ else
+ {
+ await context.SendMessageAsync(NumberSignal.Above).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/dotnet/samples/GettingStarted/Workflows/README.md b/dotnet/samples/GettingStarted/Workflows/README.md
index 13dda87e01..55ffa1a2df 100644
--- a/dotnet/samples/GettingStarted/Workflows/README.md
+++ b/dotnet/samples/GettingStarted/Workflows/README.md
@@ -6,24 +6,40 @@ The getting started with workflow samples demonstrate the fundamental concepts a
### Foundational Concepts - Start Here
-Please begin with the [Foundational](./Foundational) samples in order. These three samples introduce the core concepts of executors, edges, agents in workflows, streaming, and workflow construction.
+Please begin with the [Foundational](./_Foundational) samples in order. These three samples introduce the core concepts of executors, edges, agents in workflows, streaming, and workflow construction.
+
+> The folder name starts with an underscore (`_Foundational`) to ensure it appears first in the explorer view.
| Sample | Concepts |
|--------|----------|
-| [Executors and Edges](./Foundational/01_ExecutorsAndEdges) | Minimal workflow with basic executors and edges |
-| [Streaming](./Foundational/02_Streaming) | Extends workflows with event streaming |
-| [Agents](./Foundational/03_AgentsInWorkflows) | Use agents in workflows |
+| [Executors and Edges](./_Foundational/01_ExecutorsAndEdges) | Minimal workflow with basic executors and edges |
+| [Streaming](./_Foundational/02_Streaming) | Extends workflows with event streaming |
+| [Agents](./_Foundational/03_AgentsInWorkflows) | Use agents in workflows |
Once completed, please proceed to other samples listed below.
> Note that you don't need to follow a strict order after the foundational samples. However, some samples build upon concepts from previous ones, so it's beneficial to be aware of the dependencies.
+### Agents
+
+| Sample | Concepts |
+|--------|----------|
+| [Foundry Agents in Workflows](./Agents/FoundryAgent) | Demonstrates using Azure Foundry Agents within a workflow |
+| [Custom Agent Executors](./Agents/CustomAgentExecutors) | Shows how to create a custom agent executor for more complex scenarios |
+| [Workflow as an Agent](./Agents/WorkflowAsAgent) | Illustrates how to encapsulate a workflow as an agent |
+
### Concurrent Execution
| Sample | Concepts |
|--------|----------|
| [Fan-Out and Fan-In](./Concurrent) | Introduces parallel processing with fan-out and fan-in patterns |
+### Loop
+
+| Sample | Concepts |
+|--------|----------|
+| [Looping](./Loop) | Shows how to create a loop within a workflow |
+
### Workflow Shared States
| Sample | Concepts |
@@ -40,10 +56,22 @@ Once completed, please proceed to other samples listed below.
> These 3 samples build upon each other. It's recommended to explore them in sequence to fully grasp the concepts.
-
### Declarative Workflows
| Sample | Concepts |
|--------|----------|
| [DeclarativeWorkflow](./DeclarativeWorkflow) | Demonstrates execution of declartive workflows. |
+### Checkpointing
+
+| Sample | Concepts |
+|--------|----------|
+| [Checkpoint and Resume](./Checkpoint/CheckpointAndResume) | Introduces checkpoints for saving and restoring workflow state for time travel purposes |
+| [Checkpoint and Rehydrate](./Checkpoint/CheckpointAndRehydrate) | Demonstrates hydrating a new workflow instance from a saved checkpoint |
+| [Checkpoint with Human-in-the-Loop](./Checkpoint/CheckpointWithHumanInTheLoop) | Combines checkpointing with human-in-the-loop interactions |
+
+### Human-in-the-Loop
+
+| Sample | Concepts |
+|--------|----------|
+| [Basic Human-in-the-Loop](./HumanInTheLoop/HumanIntheLoopBasic) | Introduces human-in-the-loop interaction using input ports and external requests |
diff --git a/dotnet/samples/GettingStarted/Workflows/_Foundational/01_ExecutorsAndEdges/01_ExecutorsAndEdges.csproj b/dotnet/samples/GettingStarted/Workflows/_Foundational/01_ExecutorsAndEdges/01_ExecutorsAndEdges.csproj
new file mode 100644
index 0000000000..bbee070910
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Workflows/_Foundational/01_ExecutorsAndEdges/01_ExecutorsAndEdges.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ net9.0
+ 12
+
+ enable
+ disable
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dotnet/samples/GettingStarted/Workflows/Foundational/01_ExecutorsAndEdges/Program.cs b/dotnet/samples/GettingStarted/Workflows/_Foundational/01_ExecutorsAndEdges/Program.cs
similarity index 98%
rename from dotnet/samples/GettingStarted/Workflows/Foundational/01_ExecutorsAndEdges/Program.cs
rename to dotnet/samples/GettingStarted/Workflows/_Foundational/01_ExecutorsAndEdges/Program.cs
index 00682a60ef..85fd569605 100644
--- a/dotnet/samples/GettingStarted/Workflows/Foundational/01_ExecutorsAndEdges/Program.cs
+++ b/dotnet/samples/GettingStarted/Workflows/_Foundational/01_ExecutorsAndEdges/Program.cs
@@ -35,7 +35,7 @@ public static class Program
Run run = await InProcessExecution.RunAsync(workflow, "Hello, World!");
foreach (WorkflowEvent evt in run.NewEvents)
{
- if (evt is ExecutorCompleteEvent executorComplete)
+ if (evt is ExecutorCompletedEvent executorComplete)
{
Console.WriteLine($"{executorComplete.ExecutorId}: {executorComplete.Data}");
}
diff --git a/dotnet/samples/GettingStarted/Workflows/_Foundational/02_Streaming/02_Streaming.csproj b/dotnet/samples/GettingStarted/Workflows/_Foundational/02_Streaming/02_Streaming.csproj
new file mode 100644
index 0000000000..bbee070910
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Workflows/_Foundational/02_Streaming/02_Streaming.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ net9.0
+ 12
+
+ enable
+ disable
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dotnet/samples/GettingStarted/Workflows/Foundational/02_Streaming/Program.cs b/dotnet/samples/GettingStarted/Workflows/_Foundational/02_Streaming/Program.cs
similarity index 95%
rename from dotnet/samples/GettingStarted/Workflows/Foundational/02_Streaming/Program.cs
rename to dotnet/samples/GettingStarted/Workflows/_Foundational/02_Streaming/Program.cs
index 01bd694a5d..ce0db4a7fd 100644
--- a/dotnet/samples/GettingStarted/Workflows/Foundational/02_Streaming/Program.cs
+++ b/dotnet/samples/GettingStarted/Workflows/_Foundational/02_Streaming/Program.cs
@@ -34,9 +34,9 @@ public static class Program
StreamingRun run = await InProcessExecution.StreamAsync(workflow, "Hello, World!");
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
{
- if (evt is ExecutorCompleteEvent executorComplete)
+ if (evt is ExecutorCompletedEvent executorCompleted)
{
- Console.WriteLine($"{executorComplete.ExecutorId}: {executorComplete.Data}");
+ Console.WriteLine($"{executorCompleted.ExecutorId}: {executorCompleted.Data}");
}
}
}
diff --git a/dotnet/samples/GettingStarted/Workflows/_Foundational/03_AgentsInWorkflows/03_AgentsInWorkflows.csproj b/dotnet/samples/GettingStarted/Workflows/_Foundational/03_AgentsInWorkflows/03_AgentsInWorkflows.csproj
new file mode 100644
index 0000000000..b1f90ee600
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Workflows/_Foundational/03_AgentsInWorkflows/03_AgentsInWorkflows.csproj
@@ -0,0 +1,24 @@
+
+
+
+ Exe
+ net9.0
+ 12
+
+ enable
+ disable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dotnet/samples/GettingStarted/Workflows/Foundational/03_AgentsInWorkflows/Program.cs b/dotnet/samples/GettingStarted/Workflows/_Foundational/03_AgentsInWorkflows/Program.cs
similarity index 100%
rename from dotnet/samples/GettingStarted/Workflows/Foundational/03_AgentsInWorkflows/Program.cs
rename to dotnet/samples/GettingStarted/Workflows/_Foundational/03_AgentsInWorkflows/Program.cs
diff --git a/dotnet/src/Microsoft.Agents.Workflows/Executor.cs b/dotnet/src/Microsoft.Agents.Workflows/Executor.cs
index c3e704ed69..8b813c08c6 100644
--- a/dotnet/src/Microsoft.Agents.Workflows/Executor.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows/Executor.cs
@@ -67,7 +67,7 @@ public abstract class Executor : IIdentified
/// An exception is generated while handling the message.
public async ValueTask