diff --git a/dotnet/src/Microsoft.Agents.Workflows/AgentWorkflowBuilder.cs b/dotnet/src/Microsoft.Agents.Workflows/AgentWorkflowBuilder.cs
index 85b9e1cef0..2983861c49 100644
--- a/dotnet/src/Microsoft.Agents.Workflows/AgentWorkflowBuilder.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows/AgentWorkflowBuilder.cs
@@ -145,7 +145,7 @@ public static partial class AgentWorkflowBuilder
///
/// Executor that runs the agent and forwards all messages, input and output, to the next executor.
///
- private sealed class AgentRunStreamingExecutor(AIAgent agent, bool includeInputInOutput) : Executor(GetDescriptiveIdFromAgent(agent))
+ private sealed class AgentRunStreamingExecutor(AIAgent agent, bool includeInputInOutput) : Executor(GetDescriptiveIdFromAgent(agent)), IResettableExecutor
{
private readonly List _pendingMessages = [];
@@ -185,20 +185,28 @@ public static partial class AgentWorkflowBuilder
await context.SendMessageAsync(messages).ConfigureAwait(false);
await context.SendMessageAsync(token).ConfigureAwait(false);
});
+
+ public ValueTask ResetAsync()
+ {
+ this._pendingMessages.Clear();
+ return default;
+ }
}
///
/// Provides an executor that batches received chat messages that it then publishes as the final result
/// when receiving a .
///
- private sealed class OutputMessagesExecutor() : ChatProtocolExecutor("OutputMessages")
+ private sealed class OutputMessagesExecutor() : ChatProtocolExecutor("OutputMessages"), IResettableExecutor
{
protected override ValueTask TakeTurnAsync(List messages, IWorkflowContext context, bool? emitEvents, CancellationToken cancellation = default)
=> context.YieldOutputAsync(messages);
+
+ ValueTask IResettableExecutor.ResetAsync() => this.ResetAsync();
}
/// Executor that forwards all messages.
- private sealed class ChatForwardingExecutor(string id) : Executor(id)
+ private sealed class ChatForwardingExecutor(string id) : Executor(id), IResettableExecutor
{
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
routeBuilder
@@ -206,23 +214,27 @@ public static partial class AgentWorkflowBuilder
.AddHandler((message, context) => context.SendMessageAsync(message))
.AddHandler>((messages, context) => context.SendMessageAsync(messages))
.AddHandler((turnToken, context) => context.SendMessageAsync(turnToken));
+
+ public ValueTask ResetAsync() => default;
}
///
/// Provides an executor that batches received chat messages that it then releases when
/// receiving a .
///
- private sealed class BatchChatMessagesToListExecutor(string id) : ChatProtocolExecutor(id)
+ private sealed class BatchChatMessagesToListExecutor(string id) : ChatProtocolExecutor(id), IResettableExecutor
{
protected override ValueTask TakeTurnAsync(List messages, IWorkflowContext context, bool? emitEvents, CancellationToken cancellation = default)
=> context.SendMessageAsync(messages);
+
+ ValueTask IResettableExecutor.ResetAsync() => this.ResetAsync();
}
///
/// Provides an executor that accepts the output messages from each of the concurrent agents
/// and produces a result list containing the last message from each.
///
- private sealed class ConcurrentEndExecutor : Executor
+ private sealed class ConcurrentEndExecutor : Executor, IResettableExecutor
{
private readonly int _expectedInputs;
private readonly Func>, List> _aggregator;
@@ -238,6 +250,12 @@ public static partial class AgentWorkflowBuilder
this._remaining = expectedInputs;
}
+ private void Reset()
+ {
+ this._allResults = new List>(this._expectedInputs);
+ this._remaining = this._expectedInputs;
+ }
+
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
routeBuilder.AddHandler>(async (messages, context) =>
{
@@ -259,6 +277,12 @@ public static partial class AgentWorkflowBuilder
await context.YieldOutputAsync(this._aggregator(results)).ConfigureAwait(false);
}
});
+
+ public ValueTask ResetAsync()
+ {
+ this.Reset();
+ return default;
+ }
}
///
@@ -428,7 +452,7 @@ public static partial class AgentWorkflowBuilder
}
/// Executor used at the start of a handoffs workflow to accumulate messages and emit them as HandoffState upon receiving a turn token.
- private sealed class StartHandoffsExecutor() : Executor("HandoffStart")
+ private sealed class StartHandoffsExecutor() : Executor("HandoffStart"), IResettableExecutor
{
private readonly List _pendingMessages = [];
@@ -445,20 +469,28 @@ public static partial class AgentWorkflowBuilder
this._pendingMessages.Clear();
await context.SendMessageAsync(new HandoffState(token, null, messages)).ConfigureAwait(false);
});
+
+ public ValueTask ResetAsync()
+ {
+ this._pendingMessages.Clear();
+ return default;
+ }
}
/// Executor used at the end of a handoff workflow to raise a final completed event.
- private sealed class EndHandoffsExecutor() : Executor("HandoffEnd")
+ private sealed class EndHandoffsExecutor() : Executor("HandoffEnd"), IResettableExecutor
{
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
routeBuilder.AddHandler((handoff, context) =>
context.YieldOutputAsync(handoff.Messages));
+
+ public ValueTask ResetAsync() => default;
}
/// Executor used to represent an agent in a handoffs workflow, responding to events.
private sealed class HandoffAgentExecutor(
AIAgent agent,
- string? handoffInstructions) : Executor(GetDescriptiveIdFromAgent(agent))
+ string? handoffInstructions) : Executor(GetDescriptiveIdFromAgent(agent)), IResettableExecutor
{
private static readonly JsonElement s_handoffSchema = AIFunctionFactory.Create(
([Description("The reason for the handoff")] string? reasonForHandoff) => { }).JsonSchema;
@@ -548,6 +580,8 @@ public static partial class AgentWorkflowBuilder
}
}
});
+
+ public ValueTask ResetAsync() => default;
}
private record class HandoffState(
@@ -746,7 +780,7 @@ public static partial class AgentWorkflowBuilder
return builder.WithOutputFrom(host).Build();
}
- private sealed class GroupChatHost(AIAgent[] agents, Dictionary agentMap, Func, GroupChatManager> managerFactory) : Executor("GroupChatHost")
+ private sealed class GroupChatHost(AIAgent[] agents, Dictionary agentMap, Func, GroupChatManager> managerFactory) : Executor("GroupChatHost"), IResettableExecutor
{
private readonly AIAgent[] _agents = agents;
private readonly Dictionary _agentMap = agentMap;
@@ -786,6 +820,14 @@ public static partial class AgentWorkflowBuilder
this._manager = null;
await context.YieldOutputAsync(messages).ConfigureAwait(false);
});
+
+ public ValueTask ResetAsync()
+ {
+ this._pendingMessages.Clear();
+ this._manager = null;
+
+ return default;
+ }
}
}
diff --git a/dotnet/src/Microsoft.Agents.Workflows/ChatProtocolExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows/ChatProtocolExecutor.cs
index 3c86865d36..4582fd73d3 100644
--- a/dotnet/src/Microsoft.Agents.Workflows/ChatProtocolExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows/ChatProtocolExecutor.cs
@@ -18,6 +18,14 @@ internal abstract class ChatProtocolExecutor(string id, ChatProtocolExecutorOpti
private List _pendingMessages = [];
private readonly ChatRole? _stringMessageChatRole = options?.StringMessageChatRole;
+ // Note that we explicitly do not implement IResettableExecutor here, as we want to allow derived classes to
+ // implement it if they want to be resettable, but do not want to opt them into it.
+ protected ValueTask ResetAsync()
+ {
+ this._pendingMessages = [];
+ return default;
+ }
+
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder)
{
if (this._stringMessageChatRole.HasValue)
diff --git a/dotnet/src/Microsoft.Agents.Workflows/Checkpointing/PortableMessageEnvelope.cs b/dotnet/src/Microsoft.Agents.Workflows/Checkpointing/PortableMessageEnvelope.cs
index c2e73de73d..3df7a5479a 100644
--- a/dotnet/src/Microsoft.Agents.Workflows/Checkpointing/PortableMessageEnvelope.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows/Checkpointing/PortableMessageEnvelope.cs
@@ -9,13 +9,15 @@ internal sealed class PortableMessageEnvelope
{
public TypeId MessageType { get; }
public PortableValue Message { get; }
+ public ExecutorIdentity Source { get; }
public string? TargetId { get; }
[JsonConstructor]
- internal PortableMessageEnvelope(TypeId messageType, PortableValue message, string? targetId)
+ internal PortableMessageEnvelope(TypeId messageType, ExecutorIdentity source, PortableValue message, string? targetId)
{
this.MessageType = messageType;
this.Message = message;
+ this.Source = source;
this.TargetId = targetId;
}
@@ -28,6 +30,6 @@ internal sealed class PortableMessageEnvelope
public MessageEnvelope ToMessageEnvelope()
{
- return new MessageEnvelope(this.Message, this.MessageType, this.TargetId);
+ return new MessageEnvelope(this.Message, this.Source, this.MessageType, this.TargetId);
}
}
diff --git a/dotnet/src/Microsoft.Agents.Workflows/Checkpointing/PortableValueConverter.cs b/dotnet/src/Microsoft.Agents.Workflows/Checkpointing/PortableValueConverter.cs
index 512a459216..12144bb4ba 100644
--- a/dotnet/src/Microsoft.Agents.Workflows/Checkpointing/PortableValueConverter.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows/Checkpointing/PortableValueConverter.cs
@@ -13,8 +13,8 @@ namespace Microsoft.Agents.Workflows.Checkpointing;
/// at the time of initial deserialization, e.g. user-defined state types.
///
/// This operates in conjuction with and to abstract
-/// away the speicfics of a given serialization format in favor of and
-/// .
+/// away the speicfics of a given serialization format in favor of and
+/// and related methods.
///
///
internal sealed class PortableValueConverter(JsonMarshaller marshaller) : JsonConverter
diff --git a/dotnet/src/Microsoft.Agents.Workflows/Execution/DeliveryMapping.cs b/dotnet/src/Microsoft.Agents.Workflows/Execution/DeliveryMapping.cs
new file mode 100644
index 0000000000..ecfaf0d79c
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.Workflows/Execution/DeliveryMapping.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Agents.Workflows.Execution;
+
+internal sealed class DeliveryMapping
+{
+ private readonly IEnumerable _envelopes;
+ private readonly IEnumerable _targets;
+
+ public DeliveryMapping(IEnumerable envelopes, IEnumerable targets)
+ {
+ this._envelopes = Throw.IfNull(envelopes);
+ this._targets = Throw.IfNull(targets);
+ }
+
+ public DeliveryMapping(MessageEnvelope envelope, Executor target) : this([envelope], [target]) { }
+ public DeliveryMapping(MessageEnvelope envelope, IEnumerable targets) : this([envelope], targets) { }
+ public DeliveryMapping(IEnumerable envelopes, Executor target) : this(envelopes, [target]) { }
+
+ public IEnumerable Deliveries => from target in this._targets
+ from envelope in this._envelopes
+ select new MessageDelivery(envelope, target);
+
+ public void MapInto(StepContext nextStep)
+ {
+ foreach (Executor target in this._targets)
+ {
+ nextStep.MessagesFor(target.Id).AddRange(this._envelopes.Select(envelope => envelope));
+ }
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.Workflows/Execution/DirectEdgeRunner.cs b/dotnet/src/Microsoft.Agents.Workflows/Execution/DirectEdgeRunner.cs
index e39ebdeedf..e28fa9bdde 100644
--- a/dotnet/src/Microsoft.Agents.Workflows/Execution/DirectEdgeRunner.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows/Execution/DirectEdgeRunner.cs
@@ -1,6 +1,5 @@
// Copyright (c) Microsoft. All rights reserved.
-using System.Collections.Generic;
using System.Threading.Tasks;
namespace Microsoft.Agents.Workflows.Execution;
@@ -13,26 +12,25 @@ internal sealed class DirectEdgeRunner(IRunnerContext runContext, DirectEdgeData
private async ValueTask FindRouterAsync(IStepTracer? tracer) => await this.RunContext.EnsureExecutorAsync(this.EdgeData.SinkId, tracer)
.ConfigureAwait(false);
- public async ValueTask> ChaseAsync(MessageEnvelope envelope, IStepTracer? tracer)
+ protected internal override async ValueTask ChaseEdgeAsync(MessageEnvelope envelope, IStepTracer? stepTracer)
{
if (envelope.TargetId is not null && this.EdgeData.SinkId != envelope.TargetId)
{
- return [];
+ return null;
}
object message = envelope.Message;
if (this.EdgeData.Condition is not null && !this.EdgeData.Condition(message))
{
- return [];
+ return null;
}
- Executor target = await this.FindRouterAsync(tracer).ConfigureAwait(false);
+ Executor target = await this.FindRouterAsync(stepTracer).ConfigureAwait(false);
if (target.CanHandle(envelope.MessageType))
{
- tracer?.TraceActivated(target.Id);
- return [await target.ExecuteAsync(message, envelope.MessageType, this.WorkflowContext).ConfigureAwait(false)];
+ return new DeliveryMapping(envelope, target);
}
- return [];
+ return null;
}
}
diff --git a/dotnet/src/Microsoft.Agents.Workflows/Execution/EdgeMap.cs b/dotnet/src/Microsoft.Agents.Workflows/Execution/EdgeMap.cs
index cc2c7309a0..6c0e111282 100644
--- a/dotnet/src/Microsoft.Agents.Workflows/Execution/EdgeMap.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows/Execution/EdgeMap.cs
@@ -10,12 +10,23 @@ namespace Microsoft.Agents.Workflows.Execution;
internal sealed class EdgeMap
{
- private readonly Dictionary _edgeRunners = [];
- private readonly Dictionary _fanInState = [];
+ private readonly Dictionary _edgeRunners = [];
+ private readonly Dictionary _statefulRunners = new();
private readonly Dictionary _portEdgeRunners;
+
private readonly InputEdgeRunner _inputRunner;
private readonly IStepTracer? _stepTracer;
+ public EdgeMap(IRunnerContext runContext,
+ Workflow workflow,
+ IStepTracer? stepTracer)
+ : this(runContext,
+ workflow.Edges,
+ workflow.Ports.Values,
+ workflow.StartExecutorId,
+ stepTracer)
+ { }
+
public EdgeMap(IRunnerContext runContext,
Dictionary> workflowEdges,
IEnumerable workflowPorts,
@@ -24,7 +35,7 @@ internal sealed class EdgeMap
{
foreach (Edge edge in workflowEdges.Values.SelectMany(e => e))
{
- object edgeRunner = edge.Kind switch
+ EdgeRunner edgeRunner = edge.Kind switch
{
EdgeKind.Direct => new DirectEdgeRunner(runContext, edge.DirectEdgeData!),
EdgeKind.FanOut => new FanOutEdgeRunner(runContext, edge.FanOutEdgeData!),
@@ -32,12 +43,12 @@ internal sealed class EdgeMap
_ => throw new NotSupportedException($"Unsupported edge type: {edge.Kind}")
};
- if (edgeRunner is FanInEdgeRunner fanInRunner)
- {
- this._fanInState[edge.Data.Id] = fanInRunner.CreateState();
- }
-
this._edgeRunners[edge.Data.Id] = edgeRunner;
+
+ if (edgeRunner is IStatefulEdgeRunner statefulRunner)
+ {
+ this._statefulRunners[edge.Data.Id] = statefulRunner;
+ }
}
this._portEdgeRunners = workflowPorts.ToDictionary(
@@ -49,103 +60,52 @@ internal sealed class EdgeMap
this._stepTracer = stepTracer;
}
- public async ValueTask> InvokeEdgeAsync(Edge edge, string sourceId, MessageEnvelope message)
+ public ValueTask PrepareDeliveryForEdgeAsync(Edge edge, MessageEnvelope message)
{
EdgeId id = edge.Data.Id;
- if (!this._edgeRunners.TryGetValue(id, out object? edgeRunner))
+ if (!this._edgeRunners.TryGetValue(id, out EdgeRunner? edgeRunner))
{
throw new InvalidOperationException($"Edge {edge} not found in the edge map.");
}
- IEnumerable