.NET: Make ChatProtocolExecutor public (#1781)

* Initial plan

* Make ChatProtocolExecutor and ChatProtocolExecutorOptions public with XML documentation

Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com>

* Address PR review feedback - remove remarks, fix parameter descriptions

Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com>

* ci: Empty Commit to kick over CI

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com>
Co-authored-by: Jacob Alber <jaalber@microsoft.com>
This commit is contained in:
Copilot
2025-10-29 19:02:27 +00:00
committed by GitHub
co-authored by lokitoth copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Jacob Alber
parent 8a3cab38ee
commit a2ee840eef
@@ -8,23 +8,40 @@ using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Workflows; namespace Microsoft.Agents.AI.Workflows;
internal class ChatProtocolExecutorOptions /// <summary>
/// Provides configuration options for <see cref="ChatProtocolExecutor"/>.
/// </summary>
public class ChatProtocolExecutorOptions
{ {
/// <summary>
/// Gets or sets the chat role to use when converting string messages to <see cref="ChatMessage"/> instances.
/// If set, the executor will accept string messages and convert them to chat messages with this role.
/// </summary>
public ChatRole? StringMessageChatRole { get; set; } public ChatRole? StringMessageChatRole { get; set; }
} }
// TODO: Make this a public type (in a later PR; todo: make an issue) /// <summary>
internal abstract class ChatProtocolExecutor : StatefulExecutor<List<ChatMessage>> /// Provides a base class for executors that implement the Agent Workflow Chat Protocol.
/// This executor maintains a list of chat messages and processes them when a turn is taken.
/// </summary>
public abstract class ChatProtocolExecutor : StatefulExecutor<List<ChatMessage>>
{ {
private readonly static Func<List<ChatMessage>> s_initFunction = () => []; private readonly static Func<List<ChatMessage>> s_initFunction = () => [];
private readonly ChatRole? _stringMessageChatRole; private readonly ChatRole? _stringMessageChatRole;
internal ChatProtocolExecutor(string id, ChatProtocolExecutorOptions? options = null, bool declareCrossRunShareable = false) /// <summary>
/// Initializes a new instance of the <see cref="ChatProtocolExecutor"/> class.
/// </summary>
/// <param name="id">The unique identifier for this executor instance. Cannot be null or empty.</param>
/// <param name="options">Optional configuration settings for the executor. If null, default options are used.</param>
/// <param name="declareCrossRunShareable">Declare that this executor may be used simultaneously by multiple runs safely.</param>
protected ChatProtocolExecutor(string id, ChatProtocolExecutorOptions? options = null, bool declareCrossRunShareable = false)
: base(id, () => [], declareCrossRunShareable: declareCrossRunShareable) : base(id, () => [], declareCrossRunShareable: declareCrossRunShareable)
{ {
this._stringMessageChatRole = options?.StringMessageChatRole; this._stringMessageChatRole = options?.StringMessageChatRole;
} }
/// <inheritdoc/>
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder)
{ {
if (this._stringMessageChatRole.HasValue) if (this._stringMessageChatRole.HasValue)
@@ -40,6 +57,13 @@ internal abstract class ChatProtocolExecutor : StatefulExecutor<List<ChatMessage
.AddHandler<TurnToken>(this.TakeTurnAsync); .AddHandler<TurnToken>(this.TakeTurnAsync);
} }
/// <summary>
/// Adds a single chat message to the accumulated messages for the current turn.
/// </summary>
/// <param name="message">The chat message to add.</param>
/// <param name="context">The workflow context in which the executor executes.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
/// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
protected ValueTask AddMessageAsync(ChatMessage message, IWorkflowContext context, CancellationToken cancellationToken = default) protected ValueTask AddMessageAsync(ChatMessage message, IWorkflowContext context, CancellationToken cancellationToken = default)
{ {
return this.InvokeWithStateAsync(ForwardMessageAsync, context, cancellationToken: cancellationToken); return this.InvokeWithStateAsync(ForwardMessageAsync, context, cancellationToken: cancellationToken);
@@ -52,6 +76,13 @@ internal abstract class ChatProtocolExecutor : StatefulExecutor<List<ChatMessage
} }
} }
/// <summary>
/// Adds multiple chat messages to the accumulated messages for the current turn.
/// </summary>
/// <param name="messages">The collection of chat messages to add.</param>
/// <param name="context">The workflow context in which the executor executes.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
/// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
protected ValueTask AddMessagesAsync(IEnumerable<ChatMessage> messages, IWorkflowContext context, CancellationToken cancellationToken = default) protected ValueTask AddMessagesAsync(IEnumerable<ChatMessage> messages, IWorkflowContext context, CancellationToken cancellationToken = default)
{ {
return this.InvokeWithStateAsync(ForwardMessageAsync, context, cancellationToken: cancellationToken); return this.InvokeWithStateAsync(ForwardMessageAsync, context, cancellationToken: cancellationToken);
@@ -64,6 +95,13 @@ internal abstract class ChatProtocolExecutor : StatefulExecutor<List<ChatMessage
} }
} }
/// <summary>
/// Handles a turn token by processing all accumulated chat messages and then resetting the message state.
/// </summary>
/// <param name="token">The turn token that triggers message processing.</param>
/// <param name="context">The workflow context in which the executor executes.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
/// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
public ValueTask TakeTurnAsync(TurnToken token, IWorkflowContext context, CancellationToken cancellationToken = default) public ValueTask TakeTurnAsync(TurnToken token, IWorkflowContext context, CancellationToken cancellationToken = default)
{ {
return this.InvokeWithStateAsync(InvokeTakeTurnAsync, context, cancellationToken: cancellationToken); return this.InvokeWithStateAsync(InvokeTakeTurnAsync, context, cancellationToken: cancellationToken);
@@ -81,5 +119,13 @@ internal abstract class ChatProtocolExecutor : StatefulExecutor<List<ChatMessage
} }
} }
/// <summary>
/// When overridden in a derived class, processes the accumulated chat messages for a single turn.
/// </summary>
/// <param name="messages">The list of chat messages accumulated since the last turn.</param>
/// <param name="context">The workflow context in which the executor executes.</param>
/// <param name="emitEvents">Indicates whether events should be emitted during processing. If null, the default behavior is used.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
/// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
protected abstract ValueTask TakeTurnAsync(List<ChatMessage> messages, IWorkflowContext context, bool? emitEvents, CancellationToken cancellationToken = default); protected abstract ValueTask TakeTurnAsync(List<ChatMessage> messages, IWorkflowContext context, bool? emitEvents, CancellationToken cancellationToken = default);
} }