11// Copyright (c) Microsoft. All rights reserved.
22
33using System . Collections . Generic ;
4+ using System . Linq ;
5+ using System . Threading ;
46using System . Threading . Tasks ;
57using Microsoft . Extensions . AI ;
68
79namespace Microsoft . Agents . AI . Workflows ;
810
11+ /// <summary>
12+ /// Provides configuration options for <see cref="ChatForwardingExecutor"/>.
13+ /// </summary>
14+ public class ChatForwardingExecutorOptions
15+ {
16+ /// <summary>
17+ /// Gets or sets the chat role to use when converting string messages to <see cref="ChatMessage"/> instances.
18+ /// If set, the executor will accept string messages and convert them to chat messages with this role.
19+ /// </summary>
20+ public ChatRole ? StringMessageChatRole { get ; set ; }
21+ }
22+
923/// <summary>
1024/// A ChatProtocol executor that forwards all messages it receives. Useful for splitting inputs into parallel
1125/// processing paths.
@@ -14,15 +28,43 @@ namespace Microsoft.Agents.AI.Workflows;
1428/// multiple chat-related types, enabling flexible message forwarding scenarios. Thread safety and reusability are
1529/// ensured by its design.</remarks>
1630/// <param name="id">The unique identifier for the executor instance. Used to distinguish this executor within the system.</param>
17- public sealed class ChatForwardingExecutor ( string id ) : Executor ( id , declareCrossRunShareable : true ) , IResettableExecutor
31+ /// <param name="options">Optional configuration settings for the executor. If null, default options are used.</param>
32+ public sealed class ChatForwardingExecutor ( string id , ChatForwardingExecutorOptions ? options = null ) : Executor ( id , declareCrossRunShareable : true ) , IResettableExecutor
1833{
34+ private readonly ChatRole ? _stringMessageChatRole = options ? . StringMessageChatRole ;
35+
1936 /// <inheritdoc/>
20- protected sealed override RouteBuilder ConfigureRoutes ( RouteBuilder routeBuilder ) =>
21- routeBuilder
22- . AddHandler < string > ( ( message , context , cancellationToken ) => context . SendMessageAsync ( new ChatMessage ( ChatRole . User , message ) , cancellationToken : cancellationToken ) )
23- . AddHandler < ChatMessage > ( ( message , context , cancellationToken ) => context . SendMessageAsync ( message , cancellationToken : cancellationToken ) )
24- . AddHandler < List < ChatMessage > > ( ( messages , context , cancellationToken ) => context . SendMessageAsync ( messages , cancellationToken : cancellationToken ) )
25- . AddHandler < TurnToken > ( ( turnToken , context , cancellationToken ) => context . SendMessageAsync ( turnToken , cancellationToken : cancellationToken ) ) ;
37+ protected override RouteBuilder ConfigureRoutes ( RouteBuilder routeBuilder )
38+ {
39+ if ( this . _stringMessageChatRole . HasValue )
40+ {
41+ routeBuilder = routeBuilder . AddHandler < string > (
42+ ( message , context ) => context . SendMessageAsync ( new ChatMessage ( ChatRole . User , message ) ) ) ;
43+ }
44+
45+ return routeBuilder . AddHandler < ChatMessage > ( ForwardMessageAsync )
46+ . AddHandler < IEnumerable < ChatMessage > > ( ForwardMessagesAsync )
47+ . AddHandler < ChatMessage [ ] > ( ForwardMessagesAsync )
48+ . AddHandler < List < ChatMessage > > ( ForwardMessagesAsync )
49+ . AddHandler < TurnToken > ( ForwardTurnTokenAsync ) ;
50+ }
51+
52+ private static ValueTask ForwardMessageAsync ( ChatMessage message , IWorkflowContext context , CancellationToken cancellationToken )
53+ => context . SendMessageAsync ( message , cancellationToken ) ;
54+
55+ // Note that this can be used to split a turn into multiple parallel turns taken, which will cause streaming ChatMessages
56+ // to overlap.
57+ private static ValueTask ForwardTurnTokenAsync ( TurnToken message , IWorkflowContext context , CancellationToken cancellationToken )
58+ => context . SendMessageAsync ( message , cancellationToken ) ;
59+
60+ // TODO: This is not ideal, but until we have a way of guaranteeing correct routing of interfaces across serialization
61+ // boundaries, we need to do type unification. It behaves better when used as a handler in ChatProtocolExecutor because
62+ // it is a strictly contravariant use, whereas this forces invariance on the type because it is directly forwarded.
63+ private static ValueTask ForwardMessagesAsync ( IEnumerable < ChatMessage > messages , IWorkflowContext context , CancellationToken cancellationToken )
64+ => context . SendMessageAsync ( messages is List < ChatMessage > messageList ? messageList : messages . ToList ( ) , cancellationToken ) ;
65+
66+ private static ValueTask ForwardMessagesAsync ( ChatMessage [ ] messages , IWorkflowContext context , CancellationToken cancellationToken )
67+ => context . SendMessageAsync ( messages , cancellationToken ) ;
2668
2769 /// <inheritdoc/>
2870 public ValueTask ResetAsync ( ) => default ;
0 commit comments