From b70030daec641b6ce23d736de13cffcdfd6a7e6a Mon Sep 17 00:00:00 2001 From: Chris <66376200+crickman@users.noreply.github.com> Date: Tue, 28 Oct 2025 05:29:01 -0700 Subject: [PATCH] .NET Workflows - Expose SendMessage override to all platforms (#1741) * For real * Fine-tune * One more miss --- .../ObjectModel/InvokeAzureAgentExecutor.cs | 4 +- .../ObjectModel/QuestionExecutor.cs | 2 +- .../IWorkflowContext.cs | 68 +----------------- .../IWorkflowContextExtensions.cs | 71 +++++++++++++++++++ 4 files changed, 75 insertions(+), 70 deletions(-) create mode 100644 dotnet/src/Microsoft.Agents.AI.Workflows/IWorkflowContextExtensions.cs diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs index f9b9775ad1..8c4c9eee61 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs @@ -73,7 +73,7 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA { isComplete = false; UserInputRequest approvalRequest = new(agentName, inputRequests.OfType().ToArray()); - await context.SendMessageAsync(approvalRequest, targetId: null, cancellationToken).ConfigureAwait(false); + await context.SendMessageAsync(approvalRequest, cancellationToken).ConfigureAwait(false); } // Identify function calls that have no associated result. @@ -82,7 +82,7 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA { isComplete = false; AgentFunctionToolRequest toolRequest = new(agentName, functionCalls); - await context.SendMessageAsync(toolRequest, targetId: null, cancellationToken).ConfigureAwait(false); + await context.SendMessageAsync(toolRequest, cancellationToken).ConfigureAwait(false); } } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/QuestionExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/QuestionExecutor.cs index f4035d7258..145567f2ce 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/QuestionExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/QuestionExecutor.cs @@ -76,7 +76,7 @@ internal sealed class QuestionExecutor(Question model, WorkflowAgentProvider age { int count = await this._promptCount.ReadAsync(context).ConfigureAwait(false); AnswerRequest inputRequest = new(this.FormatPrompt(this.Model.Prompt)); - await context.SendMessageAsync(inputRequest, targetId: null, cancellationToken).ConfigureAwait(false); + await context.SendMessageAsync(inputRequest, cancellationToken).ConfigureAwait(false); await this._promptCount.WriteAsync(context, count + 1).ConfigureAwait(false); } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/IWorkflowContext.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/IWorkflowContext.cs index 57dcdc2b64..b8b35fffd6 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/IWorkflowContext.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/IWorkflowContext.cs @@ -7,60 +7,6 @@ using System.Threading.Tasks; namespace Microsoft.Agents.AI.Workflows; -/// -/// Provides extension methods for working with instances. -/// -public static class WorkflowContextExtensions -{ - /// - /// Invokes an asynchronous operation that reads, updates, and persists workflow state associated with the specified - /// key. - /// - /// The type of the state object to read, update, and persist. - /// The workflow context used to access and update state. - /// A delegate that receives the current state, workflow context, and cancellation token, and returns the updated - /// state asynchronously. - /// The key identifying the state to read and update. Cannot be null or empty. - /// An optional scope name that further qualifies the state key. If null, the default scope is used. - /// A cancellation token that can be used to cancel the asynchronous operation. - /// A ValueTask that represents the asynchronous operation. - public static async ValueTask InvokeWithStateAsync(this IWorkflowContext context, - Func> invocation, - string key, - string? scopeName = null, - CancellationToken cancellationToken = default) - { - TState? state = await context.ReadStateAsync(key, scopeName, cancellationToken).ConfigureAwait(false); - state = await invocation(state, context, cancellationToken).ConfigureAwait(false); - await context.QueueStateUpdateAsync(key, state, scopeName, cancellationToken).ConfigureAwait(false); - } - - /// - /// Invokes an asynchronous operation that reads, updates, and persists workflow state associated with the specified - /// key. - /// - /// The type of the state object to read, update, and persist. - /// The workflow context used to access and update state. - /// A delegate that receives the current state, workflow context, and cancellation token, and returns the updated - /// state asynchronously. - /// The key identifying the state to read and update. Cannot be null or empty. - /// A factory to initialize state to if it is not set at the provided key. - /// An optional scope name that further qualifies the state key. If null, the default scope is used. - /// A cancellation token that can be used to cancel the asynchronous operation. - /// A ValueTask that represents the asynchronous operation. - public static async ValueTask InvokeWithStateAsync(this IWorkflowContext context, - Func> invocation, - string key, - Func initialStateFactory, - string? scopeName = null, - CancellationToken cancellationToken = default) - { - TState? state = await context.ReadOrInitStateAsync(key, initialStateFactory, scopeName, cancellationToken).ConfigureAwait(false); - state = await invocation(state, context, cancellationToken).ConfigureAwait(false); - await context.QueueStateUpdateAsync(key, state ?? initialStateFactory(), scopeName, cancellationToken).ConfigureAwait(false); - } -} - /// /// Provides services for an during the execution of a workflow. /// @@ -86,19 +32,7 @@ public interface IWorkflowContext /// The to monitor for cancellation requests. /// The default is . /// A representing the asynchronous operation. - ValueTask SendMessageAsync(object message, string? targetId = null, CancellationToken cancellationToken = default); - -#if NET // What's the right way to do this so we do not make life a misery for netstandard2.0 targets? - // What's the value if they have to still write `cancellationToken: cancellationToken` to skip the targetId parameter? - // TODO: Remove this? (Maybe not: NET will eventually be the only target framework, right?) - /// - /// Queues a message to be sent to connected executors. The message will be sent during the next SuperStep. - /// - /// The message to be sent. - /// The to monitor for cancellation requests. - /// A representing the asynchronous operation. - ValueTask SendMessageAsync(object message, CancellationToken cancellationToken) => this.SendMessageAsync(message, null, cancellationToken); -#endif + ValueTask SendMessageAsync(object message, string? targetId, CancellationToken cancellationToken = default); /// /// Adds an output value to the workflow's output queue. These outputs will be bubbled out of the workflow using the diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/IWorkflowContextExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/IWorkflowContextExtensions.cs new file mode 100644 index 0000000000..950078cd3d --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/IWorkflowContextExtensions.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Agents.AI.Workflows; + +/// +/// Provides extension methods for working with instances. +/// +public static class IWorkflowContextExtensions +{ + /// + /// Invokes an asynchronous operation that reads, updates, and persists workflow state associated with the specified + /// key. + /// + /// The type of the state object to read, update, and persist. + /// The workflow context used to access and update state. + /// A delegate that receives the current state, workflow context, and cancellation token, and returns the updated + /// state asynchronously. + /// The key identifying the state to read and update. Cannot be null or empty. + /// An optional scope name that further qualifies the state key. If null, the default scope is used. + /// A cancellation token that can be used to cancel the asynchronous operation. + /// A ValueTask that represents the asynchronous operation. + public static async ValueTask InvokeWithStateAsync(this IWorkflowContext context, + Func> invocation, + string key, + string? scopeName = null, + CancellationToken cancellationToken = default) + { + TState? state = await context.ReadStateAsync(key, scopeName, cancellationToken).ConfigureAwait(false); + state = await invocation(state, context, cancellationToken).ConfigureAwait(false); + await context.QueueStateUpdateAsync(key, state, scopeName, cancellationToken).ConfigureAwait(false); + } + + /// + /// Invokes an asynchronous operation that reads, updates, and persists workflow state associated with the specified + /// key. + /// + /// The type of the state object to read, update, and persist. + /// The workflow context used to access and update state. + /// A delegate that receives the current state, workflow context, and cancellation token, and returns the updated + /// state asynchronously. + /// The key identifying the state to read and update. Cannot be null or empty. + /// A factory to initialize state to if it is not set at the provided key. + /// An optional scope name that further qualifies the state key. If null, the default scope is used. + /// A cancellation token that can be used to cancel the asynchronous operation. + /// A ValueTask that represents the asynchronous operation. + public static async ValueTask InvokeWithStateAsync(this IWorkflowContext context, + Func> invocation, + string key, + Func initialStateFactory, + string? scopeName = null, + CancellationToken cancellationToken = default) + { + TState? state = await context.ReadOrInitStateAsync(key, initialStateFactory, scopeName, cancellationToken).ConfigureAwait(false); + state = await invocation(state, context, cancellationToken).ConfigureAwait(false); + await context.QueueStateUpdateAsync(key, state ?? initialStateFactory(), scopeName, cancellationToken).ConfigureAwait(false); + } + + /// + /// Queues a message to be sent to connected executors. The message will be sent during the next SuperStep. + /// + /// The workflow context used to access and update state. + /// The message to be sent. + /// The to monitor for cancellation requests. + /// A representing the asynchronous operation. + public static ValueTask SendMessageAsync(this IWorkflowContext context, object message, CancellationToken cancellationToken = default) => + context.SendMessageAsync(message, null, cancellationToken); +}