diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index 3e2529a0b7..9ba4eb3ae2 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -83,10 +83,10 @@
-
+
-
+
diff --git a/dotnet/samples/A2AClientServer/A2AServer/Models/InvoiceQuery.cs b/dotnet/samples/A2AClientServer/A2AServer/Models/InvoiceQuery.cs
index 56ba14b49f..2b2d142c46 100644
--- a/dotnet/samples/A2AClientServer/A2AServer/Models/InvoiceQuery.cs
+++ b/dotnet/samples/A2AClientServer/A2AServer/Models/InvoiceQuery.cs
@@ -118,7 +118,7 @@ public class InvoiceQuery
public static DateTime GetRandomDateWithinLastTwoMonths()
{
// Get the current date and time
- DateTime endDate = DateTime.Now;
+ DateTime endDate = DateTime.UtcNow;
// Calculate the start date, which is two months before the current date
DateTime startDate = endDate.AddMonths(-2);
diff --git a/dotnet/samples/AgentWebChat/AgentWebChat.Web/Components/Pages/Home.razor b/dotnet/samples/AgentWebChat/AgentWebChat.Web/Components/Pages/Home.razor
index 9db3d9efb6..cc3a2576b3 100644
--- a/dotnet/samples/AgentWebChat/AgentWebChat.Web/Components/Pages/Home.razor
+++ b/dotnet/samples/AgentWebChat/AgentWebChat.Web/Components/Pages/Home.razor
@@ -909,7 +909,7 @@
private sealed class Conversation
{
- public string SessionId { get; set; } = Guid.NewGuid().ToString();
+ public string SessionId { get; set; } = Guid.NewGuid().ToString("N");
public string AgentName { get; set; } = "";
public List Messages { get; set; } = new();
}
diff --git a/dotnet/samples/GettingStarted/AgentOpenTelemetry/Program.cs b/dotnet/samples/GettingStarted/AgentOpenTelemetry/Program.cs
index 1ee48514f8..10c6a058ff 100644
--- a/dotnet/samples/GettingStarted/AgentOpenTelemetry/Program.cs
+++ b/dotnet/samples/GettingStarted/AgentOpenTelemetry/Program.cs
@@ -123,7 +123,7 @@ appLogger.LogInformation("Agent created successfully with ID: {AgentId}", agent.
// Create a parent span for the entire agent session
using var sessionActivity = activitySource.StartActivity("Agent Session");
-var sessionId = Guid.NewGuid().ToString();
+var sessionId = Guid.NewGuid().ToString("N");
sessionActivity?
.SetTag("agent.name", "OpenTelemetryDemoAgent")
.SetTag("session.id", sessionId)
diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Program.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Program.cs
index fe3e3ba0b2..ed5dfda403 100644
--- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Program.cs
+++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Program.cs
@@ -31,6 +31,8 @@ namespace SampleApp
// Custom agent that parrot's the user input back in upper case.
internal sealed class UpperCaseParrotAgent : AIAgent
{
+ public override string? Name => "UpperCaseParrotAgent";
+
public override AgentThread GetNewThread()
=> new CustomAgentThread();
@@ -51,7 +53,7 @@ namespace SampleApp
return new AgentRunResponse
{
AgentId = this.Id,
- ResponseId = Guid.NewGuid().ToString(),
+ ResponseId = Guid.NewGuid().ToString("N"),
Messages = responseMessages
};
}
@@ -75,8 +77,8 @@ namespace SampleApp
AuthorName = this.DisplayName,
Role = ChatRole.Assistant,
Contents = message.Contents,
- ResponseId = Guid.NewGuid().ToString(),
- MessageId = Guid.NewGuid().ToString()
+ ResponseId = Guid.NewGuid().ToString("N"),
+ MessageId = Guid.NewGuid().ToString("N")
};
}
}
@@ -86,7 +88,7 @@ namespace SampleApp
// Clone the message and update its author to be the agent.
var messageClone = x.Clone();
messageClone.Role = ChatRole.Assistant;
- messageClone.MessageId = Guid.NewGuid().ToString();
+ messageClone.MessageId = Guid.NewGuid().ToString("N");
messageClone.AuthorName = agentName;
// Clone and convert any text content to upper case.
@@ -109,8 +111,7 @@ namespace SampleApp
///
internal sealed class CustomAgentThread : InMemoryAgentThread
{
- internal CustomAgentThread()
- : base() { }
+ internal CustomAgentThread() { }
internal CustomAgentThread(JsonElement serializedThreadState, JsonSerializerOptions? jsonSerializerOptions = null)
: base(serializedThreadState, jsonSerializerOptions) { }
diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step07_3rdPartyThreadStorage/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step07_3rdPartyThreadStorage/Program.cs
index fca3fb99b3..6a57e8e7e7 100644
--- a/dotnet/samples/GettingStarted/Agents/Agent_Step07_3rdPartyThreadStorage/Program.cs
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step07_3rdPartyThreadStorage/Program.cs
@@ -98,7 +98,7 @@ namespace SampleApp
public override async Task AddMessagesAsync(IEnumerable messages, CancellationToken cancellationToken)
{
- this.ThreadDbKey ??= Guid.NewGuid().ToString();
+ this.ThreadDbKey ??= Guid.NewGuid().ToString("N");
var collection = this._vectorStore.GetCollection("ChatHistory");
await collection.EnsureCollectionExistsAsync(cancellationToken);
diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step08_Observability/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step08_Observability/Program.cs
index e0a32594ef..a5a29c6039 100644
--- a/dotnet/samples/GettingStarted/Agents/Agent_Step08_Observability/Program.cs
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step08_Observability/Program.cs
@@ -21,7 +21,7 @@ AppContext.SetSwitch("Microsoft.Extensions.AI.Agents.EnableTelemetry", true);
// Create TracerProvider with console exporter
// This will output the telemetry data to the console.
-string sourceName = Guid.NewGuid().ToString();
+string sourceName = Guid.NewGuid().ToString("N");
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddSource(sourceName)
.AddConsoleExporter()
diff --git a/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/01_EdgeCondition/Program.cs b/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/01_EdgeCondition/Program.cs
index 7b09b9d4d9..1488e93d60 100644
--- a/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/01_EdgeCondition/Program.cs
+++ b/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/01_EdgeCondition/Program.cs
@@ -166,7 +166,7 @@ internal sealed class SpamDetectionExecutor : ReflectingExecutor(
// Read file content from embedded resource
string fileContent = Resources.Read(message);
// Store file content in a shared state for access by other executors
- string fileID = Guid.NewGuid().ToString();
+ string fileID = Guid.NewGuid().ToString("N");
await context.QueueStateUpdateAsync(fileID, fileContent, scopeName: FileContentStateConstants.FileContentStateScope);
return fileID;
diff --git a/dotnet/samples/GettingStarted/Workflows/_Foundational/04_AgentWorkflowPatterns/Program.cs b/dotnet/samples/GettingStarted/Workflows/_Foundational/04_AgentWorkflowPatterns/Program.cs
index c8604244b5..f936a8f495 100644
--- a/dotnet/samples/GettingStarted/Workflows/_Foundational/04_AgentWorkflowPatterns/Program.cs
+++ b/dotnet/samples/GettingStarted/Workflows/_Foundational/04_AgentWorkflowPatterns/Program.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Text.Json;
using System.Threading.Tasks;
using Azure.AI.OpenAI;
using Azure.Identity;
@@ -30,7 +31,7 @@ public static class Program
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
var client = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()).GetChatClient(deploymentName).AsIChatClient();
- Console.Write("Choose workflow type ('sequential', 'concurrent', 'handoffs'): ");
+ Console.Write("Choose workflow type ('sequential', 'concurrent', 'handoffs', 'groupchat'): ");
switch (Console.ReadLine())
{
case "sequential":
@@ -58,10 +59,9 @@ public static class Program
"You determine which agent to use based on the user's homework question. ALWAYS handoff to another agent.",
"triage_agent",
"Routes messages to the appropriate specialist agent");
- var workflow = AgentWorkflowBuilder.StartHandoffWith(triageAgent)
- .WithHandoff(triageAgent, [mathTutor, historyTutor])
- .WithHandoff(mathTutor, triageAgent)
- .WithHandoff(historyTutor, triageAgent)
+ var workflow = AgentWorkflowBuilder.CreateHandoffBuilderWith(triageAgent)
+ .WithHandoffs(triageAgent, [mathTutor, historyTutor])
+ .WithHandoffs([mathTutor, historyTutor], triageAgent)
.Build();
List messages = [];
@@ -72,22 +72,45 @@ public static class Program
messages.AddRange(await RunWorkflowAsync(workflow, messages));
}
+ case "groupchat":
+ await RunWorkflowAsync(
+ AgentWorkflowBuilder.CreateGroupChatBuilderWith(agents => new AgentWorkflowBuilder.RoundRobinGroupChatManager(agents) { MaximumIterationCount = 5 })
+ .AddParticipants(from lang in (string[])["French", "Spanish", "English"] select GetTranslationAgent(lang, client))
+ .Build(),
+ [new(ChatRole.User, "Hello, world!")]);
+ break;
+
default:
throw new InvalidOperationException("Invalid workflow type.");
}
static async Task> RunWorkflowAsync(Workflow> workflow, List messages)
{
+ string? lastExecutorId = null;
+
StreamingRun run = await InProcessExecution.StreamAsync(workflow, messages);
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
{
if (evt is AgentRunUpdateEvent e)
{
- Console.WriteLine($"{e.ExecutorId}: {e.Data}");
+ if (e.ExecutorId != lastExecutorId)
+ {
+ lastExecutorId = e.ExecutorId;
+ Console.WriteLine();
+ Console.WriteLine(e.ExecutorId);
+ }
+
+ Console.Write(e.Update.Text);
+ if (e.Update.Contents.OfType().FirstOrDefault() is FunctionCallContent call)
+ {
+ Console.WriteLine();
+ Console.WriteLine($" [Calling function '{call.Name}' with arguments: {JsonSerializer.Serialize(call.Arguments)}]");
+ }
}
else if (evt is WorkflowCompletedEvent completed)
{
+ Console.WriteLine();
return (List)completed.Data!;
}
}
diff --git a/dotnet/src/Microsoft.Agents.Orchestration/OrchestratingAgentThread.cs b/dotnet/src/Microsoft.Agents.Orchestration/OrchestratingAgentThread.cs
index 05db5d566d..08c33e4fea 100644
--- a/dotnet/src/Microsoft.Agents.Orchestration/OrchestratingAgentThread.cs
+++ b/dotnet/src/Microsoft.Agents.Orchestration/OrchestratingAgentThread.cs
@@ -10,8 +10,7 @@ namespace Microsoft.Agents.Orchestration;
///
internal sealed class OrchestratingAgentThread : InMemoryAgentThread
{
- internal OrchestratingAgentThread()
- : base() { }
+ internal OrchestratingAgentThread() { }
internal OrchestratingAgentThread(JsonElement serializedThreadState, JsonSerializerOptions? jsonSerializerOptions = null)
: base(serializedThreadState, jsonSerializerOptions) { }
diff --git a/dotnet/src/Microsoft.Agents.Workflows/AIAgentsAbstractionsExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows/AIAgentsAbstractionsExtensions.cs
index cab241d4a4..4152a3c75b 100644
--- a/dotnet/src/Microsoft.Agents.Workflows/AIAgentsAbstractionsExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows/AIAgentsAbstractionsExtensions.cs
@@ -1,6 +1,5 @@
// Copyright (c) Microsoft. All rights reserved.
-using System.Diagnostics;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
@@ -18,19 +17,4 @@ internal static class AIAgentsAbstractionsExtensions
MessageId = update.MessageId,
RawRepresentation = update.RawRepresentation ?? update,
};
-
- public static ChatMessage UpdateWith(this ChatMessage baseMessage, AgentRunResponseUpdate update)
- {
- Debug.Assert(update.MessageId is null || baseMessage.MessageId == update.MessageId);
-
- return new()
- {
- AuthorName = update.AuthorName ?? baseMessage.AuthorName,
- Contents = [.. baseMessage.Contents, .. update.Contents],
- Role = update.Role ?? baseMessage.Role,
- CreatedAt = update.CreatedAt ?? baseMessage.CreatedAt,
- MessageId = baseMessage.MessageId,
- RawRepresentation = update.RawRepresentation ?? baseMessage.RawRepresentation ?? update,
- };
- }
}
diff --git a/dotnet/src/Microsoft.Agents.Workflows/AgentWorkflowBuilder.cs b/dotnet/src/Microsoft.Agents.Workflows/AgentWorkflowBuilder.cs
index 3e49919ced..b4ee59244f 100644
--- a/dotnet/src/Microsoft.Agents.Workflows/AgentWorkflowBuilder.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows/AgentWorkflowBuilder.cs
@@ -2,25 +2,24 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using System.ComponentModel;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.Json;
+using System.Text.RegularExpressions;
+using System.Threading;
using System.Threading.Tasks;
-using Microsoft.Agents.Workflows.Specialized;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Shared.Diagnostics;
-#if NET
-using System.Security.Cryptography;
-#endif
namespace Microsoft.Agents.Workflows;
///
-/// Provides utility methods for constructing common patterns of agent workflows.
+/// Provides utility methods for constructing common patterns of workflows composed of agents.
///
-public static class AgentWorkflowBuilder
+public static partial class AgentWorkflowBuilder
{
///
/// Builds a composed of a pipeline of agents where the output of one agent is the input to the next.
@@ -37,7 +36,7 @@ public static class AgentWorkflowBuilder
ExecutorIsh? previous = null;
foreach (var agent in agents)
{
- AIAgentHostExecutor agentExecutor = new(agent);
+ AgentRunStreamingExecutor agentExecutor = new(agent, includeInputInOutput: true);
if (builder is null)
{
@@ -60,31 +59,11 @@ public static class AgentWorkflowBuilder
// Add an ending executor that batches up all messages from the last agent
// so that it's published as a single list result.
Debug.Assert(builder is not null);
- builder.AddEdge(previous, new SequentialEndExecutor());
+ builder.AddEdge(previous, new ConvertMessageListToCompletedEventExecutor());
return builder.Build>();
}
- ///
- /// Provides an executor that batches received chat messages that it then publishes as the final result
- /// when receiving a .
- ///
- private sealed class SequentialEndExecutor : Executor
- {
- private readonly List _pendingMessages = [];
-
- protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
- routeBuilder
- .AddHandler((message, context) => this._pendingMessages.Add(message))
- .AddHandler>((messages, _) => this._pendingMessages.AddRange(messages))
- .AddHandler(async (token, context) =>
- {
- var messages = new List(this._pendingMessages);
- this._pendingMessages.Clear();
- await context.AddEventAsync(new WorkflowCompletedEvent(messages)).ConfigureAwait(false);
- });
- }
-
///
/// Builds a composed of agents that operate concurrently on the same input,
/// aggregating their outputs into a single collection.
@@ -110,8 +89,8 @@ public static class AgentWorkflowBuilder
// so that the final accumulator receives a single list of messages from each agent. Otherwise, the
// accumulator would not be able to determine what came from what agent, as there's currently no
// provenance tracking exposed in the workflow context passed to a handler.
- ExecutorIsh[] agentExecutors = (from agent in agents select (ExecutorIsh)agent).ToArray();
- ExecutorIsh[] accumulators = [.. from agent in agentExecutors select (ExecutorIsh)new ChatMessageBatchingExecutor()];
+ ExecutorIsh[] agentExecutors = (from agent in agents select (ExecutorIsh)new AgentRunStreamingExecutor(agent, includeInputInOutput: false)).ToArray();
+ ExecutorIsh[] accumulators = [.. from agent in agentExecutors select (ExecutorIsh)new BatchChatMessagesToListExecutor()];
builder.AddFanOutEdge(start, targets: agentExecutors);
for (int i = 0; i < agentExecutors.Length; i++)
{
@@ -137,13 +116,96 @@ public static class AgentWorkflowBuilder
/// The must be capable of understanding those provided. If the agent
/// ignores the tools or is otherwise unable to advertize them to the underlying provider, handoffs will not occur.
///
- public static HandoffsWorkflowBuilder StartHandoffWith(AIAgent initialAgent)
+ public static HandoffsWorkflowBuilder CreateHandoffBuilderWith(AIAgent initialAgent)
{
Throw.IfNull(initialAgent);
return new(initialAgent);
}
- /// Executor that forwards all relevant messages.
+ /// Creates a new with .
+ ///
+ /// Function that will create the for the workflow instance. The manager will be
+ /// provided with the set of agents that will participate in the group chat.
+ ///
+ /// The builder for creating a workflow based on handoffs.
+ ///
+ /// Handoffs between agents are achieved by the current agent invoking an provided to an agent
+ /// via 's ..
+ /// The must be capable of understanding those provided. If the agent
+ /// ignores the tools or is otherwise unable to advertize them to the underlying provider, handoffs will not occur.
+ ///
+ public static GroupChatWorkflowBuilder CreateGroupChatBuilderWith(Func, GroupChatManager> managerFactory)
+ {
+ Throw.IfNull(managerFactory);
+ return new GroupChatWorkflowBuilder(managerFactory);
+ }
+
+ ///
+ /// 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 readonly List _pendingMessages = [];
+
+ protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
+ routeBuilder
+ .AddHandler((message, context) => this._pendingMessages.Add(new(ChatRole.User, message)))
+ .AddHandler((message, context) => this._pendingMessages.Add(message))
+ .AddHandler>((messages, _) => this._pendingMessages.AddRange(messages))
+ .AddHandler((messages, _) => this._pendingMessages.AddRange(messages)) // TODO: Remove once https://github.com/microsoft/agent-framework/issues/782 is addressed
+ .AddHandler>((messages, _) => this._pendingMessages.AddRange(messages)) // TODO: Remove once https://github.com/microsoft/agent-framework/issues/782 is addressed
+ .AddHandler(async (token, context) =>
+ {
+ List messages = [.. this._pendingMessages];
+ this._pendingMessages.Clear();
+
+ List? roleChanged = ChangeAssistantToUserForOtherParticipants(agent.DisplayName, messages);
+
+ List updates = [];
+ await foreach (var update in agent.RunStreamingAsync(messages).ConfigureAwait(false))
+ {
+ updates.Add(update);
+ if (token.EmitEvents is true)
+ {
+ await context.AddEventAsync(new AgentRunUpdateEvent(this.Id, update)).ConfigureAwait(false);
+ }
+ }
+
+ ResetUserToAssistantForChangedRoles(roleChanged);
+
+ if (!includeInputInOutput)
+ {
+ messages.Clear();
+ }
+
+ messages.AddRange(updates.ToAgentRunResponse().Messages);
+
+ await context.SendMessageAsync(messages).ConfigureAwait(false);
+ await context.SendMessageAsync(token).ConfigureAwait(false);
+ });
+ }
+
+ ///
+ /// Provides an executor that batches received chat messages that it then publishes as the final result
+ /// when receiving a .
+ ///
+ private sealed class ConvertMessageListToCompletedEventExecutor : Executor
+ {
+ private readonly List _pendingMessages = [];
+
+ protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
+ routeBuilder
+ .AddHandler((message, context) => this._pendingMessages.Add(message))
+ .AddHandler>((messages, _) => this._pendingMessages.AddRange(messages))
+ .AddHandler(async (token, context) =>
+ {
+ var messages = new List(this._pendingMessages);
+ this._pendingMessages.Clear();
+ await context.AddEventAsync(new WorkflowCompletedEvent(messages)).ConfigureAwait(false);
+ });
+ }
+
+ /// Executor that forwards all messages.
private sealed class ForwardingExecutor : Executor
{
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
@@ -154,7 +216,7 @@ public static class AgentWorkflowBuilder
/// Provides an executor that batches received chat messages that it then releases when
/// receiving a .
///
- private sealed class ChatMessageBatchingExecutor : Executor
+ private sealed class BatchChatMessagesToListExecutor : Executor
{
private readonly List _pendingMessages = [];
@@ -195,26 +257,36 @@ public static class AgentWorkflowBuilder
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
routeBuilder.AddHandler>(async (messages, context) =>
{
- this._allResults.Add(messages);
- if (--this._remaining == 0)
+ // TODO: https://github.com/microsoft/agent-framework/issues/784
+ // This locking should not be necessary.
+ bool done;
+ lock (this._allResults)
+ {
+ this._allResults.Add(messages);
+ done = --this._remaining == 0;
+ }
+
+ if (done)
{
this._remaining = this._expectedInputs;
+
var results = this._allResults;
this._allResults = new List>(this._expectedInputs);
+
await context.AddEventAsync(new WorkflowCompletedEvent(this._aggregator(results))).ConfigureAwait(false);
}
});
}
///
- /// Defines the orchestration handoff relationships for all agents in the system.
+ /// Provides a builder for specifying the handoff relationships between agents and building the resulting workflow.
///
public sealed class HandoffsWorkflowBuilder
{
private const string FunctionPrefix = "handoff_to_";
private readonly AIAgent _initialAgent;
private readonly Dictionary> _targets = [];
- private readonly Dictionary _allAgents = [];
+ private readonly HashSet _allAgents = new(AIAgentIDEqualityComparer.Instance);
///
/// Initializes a new instance of the class with no handoff relationships.
@@ -223,11 +295,11 @@ public static class AgentWorkflowBuilder
internal HandoffsWorkflowBuilder(AIAgent initialAgent)
{
this._initialAgent = initialAgent;
- this._allAgents.Add(initialAgent.Id, initialAgent);
+ this._allAgents.Add(initialAgent);
}
///
- /// Gets or sets additional instructions to provide to an agent about how to perform handoffs.
+ /// Gets or sets additional instructions to provide to an agent that has handoffs about how and when to perform them.
///
///
/// By default, simple instructions are included. This may be set to to avoid including
@@ -235,9 +307,10 @@ public static class AgentWorkflowBuilder
///
public string? HandoffInstructions { get; set; } =
$"""
- You are part of a multi-agent system. Each agent encompasses instructions and tools and can hand off a conversation to another agent
- when appropriate. Handoffs are achieved by calling a handoff function, generally named `{FunctionPrefix}`. Handoffs
- between agents are handled seamlessly in the background; do not mention or draw attention to these handoffs in your conversation with the user.
+ You are one agent in a multi-agent system. You can hand off the conversation to another agent if appropriate. Handoffs are achieved
+ by calling a handoff function, named in the form `{FunctionPrefix}`; the description of the function provides details on the
+ target agent of that handoff. Handoffs between agents are handled seamlessly in the background; never mention or narrate these handoffs
+ in your conversation with the user.
""";
///
@@ -246,8 +319,8 @@ public static class AgentWorkflowBuilder
/// The source agent.
/// The target agents to add as handoff targets for the source agent.
/// The updated instance.
- /// The handoff reason for each target is derived from its description or name.
- public HandoffsWorkflowBuilder WithHandoff(AIAgent from, IEnumerable to)
+ /// The handoff reason for each target in is derived from that agent's description or name.
+ public HandoffsWorkflowBuilder WithHandoffs(AIAgent from, IEnumerable to)
{
Throw.IfNull(from);
Throw.IfNull(to);
@@ -265,32 +338,51 @@ public static class AgentWorkflowBuilder
return this;
}
+ ///
+ /// Adds handoff relationships from one or more sources agent to a target agent.
+ ///
+ /// The source agents.
+ /// The target agent to add as a handoff target for each source agent.
+ ///
+ /// The reason the should hand off to the .
+ /// If , the reason is derived from 's description or name.
+ ///
+ /// The updated instance.
+ public HandoffsWorkflowBuilder WithHandoffs(IEnumerable from, AIAgent to, string? handoffReason = null)
+ {
+ Throw.IfNull(from);
+ Throw.IfNull(to);
+
+ foreach (var source in from)
+ {
+ if (source is null)
+ {
+ Throw.ArgumentNullException(nameof(from), "One or more source agents are null.");
+ }
+
+ this.WithHandoff(source, to, handoffReason);
+ }
+
+ return this;
+ }
+
///
/// Adds a handoff relationship from a source agent to a target agent with a custom handoff reason.
///
/// The source agent.
/// The target agent.
- /// The reason the should hand off to the .
+ ///
+ /// The reason the should hand off to the .
+ /// If , the reason is derived from 's description or name.
+ ///
/// The updated instance.
public HandoffsWorkflowBuilder WithHandoff(AIAgent from, AIAgent to, string? handoffReason = null)
{
Throw.IfNull(from);
Throw.IfNull(to);
-#if NET
- this._allAgents.TryAdd(from.Id, from);
- this._allAgents.TryAdd(to.Id, to);
-#else
- if (!this._allAgents.ContainsKey(from.Id))
- {
- this._allAgents.Add(from.Id, from);
- }
-
- if (!this._allAgents.ContainsKey(to.Id))
- {
- this._allAgents.Add(to.Id, to);
- }
-#endif
+ this._allAgents.Add(from);
+ this._allAgents.Add(to);
if (!this._targets.TryGetValue(from, out var handoffs))
{
@@ -324,12 +416,12 @@ public static class AgentWorkflowBuilder
/// The workflow built based on the handoffs in the builder.
public Workflow> Build()
{
- StartHandoffs start = new();
- EndExecutor end = new();
+ StartHandoffsExecutor start = new();
+ EndHandoffsExecutor end = new();
WorkflowBuilder builder = new(start);
// Create an AgentExecutor for each again.
- Dictionary executors = this._allAgents.ToDictionary(a => a.Key, a => new AgentExecutor(a.Value, this.HandoffInstructions));
+ Dictionary executors = this._allAgents.ToDictionary(a => a.Id, a => new HandoffAgentExecutor(a, this.HandoffInstructions));
// Connect the start executor to the initial agent.
builder.AddEdge(start, executors[this._initialAgent.Id]);
@@ -337,8 +429,8 @@ public static class AgentWorkflowBuilder
// Initialize each executor with its handoff targets to the other executors.
foreach (var agent in this._allAgents)
{
- executors[agent.Key].Initialize(builder, end, executors,
- this._targets.TryGetValue(agent.Value, out HashSet? targets) ? targets : []);
+ executors[agent.Id].Initialize(builder, end, executors,
+ this._targets.TryGetValue(agent, out HashSet? targets) ? targets : []);
}
// Build the workflow.
@@ -353,7 +445,7 @@ public static 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 StartHandoffs : Executor
+ private sealed class StartHandoffsExecutor : Executor
{
private readonly List _pendingMessages = [];
@@ -373,7 +465,7 @@ public static class AgentWorkflowBuilder
}
/// Executor used at the end of a handoff workflow to raise a final completed event.
- private sealed class EndExecutor : Executor
+ private sealed class EndHandoffsExecutor : Executor
{
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
routeBuilder.AddHandler((handoff, context) =>
@@ -381,45 +473,47 @@ public static class AgentWorkflowBuilder
}
/// Executor used to represent an agent in a handoffs workflow, responding to events.
- private sealed class AgentExecutor(
+ private sealed class HandoffAgentExecutor(
AIAgent agent,
- string? instructions) : Executor($"{agent.DisplayName}/{CreateId()}")
+ string? handoffInstructions) : Executor(GetDescriptiveIdFromAgent(agent))
{
private static readonly JsonElement s_handoffSchema = AIFunctionFactory.Create(
([Description("The reason for the handoff")] string? reasonForHandoff) => { }).JsonSchema;
- private static readonly AIFunctionDeclaration s_endFunction = AIFunctionFactory.CreateDeclaration(
- name: $"end_{CreateId()}",
- description: "Invoke this function when all work is completed and no further interactions are required.",
- jsonSchema: AIFunctionFactory.Create(() => { }).JsonSchema);
private readonly AIAgent _agent = agent;
private readonly HashSet _handoffFunctionNames = [];
- private readonly ChatClientAgentRunOptions _agentOptions = new()
- {
- ChatOptions = new()
- {
- Instructions = instructions,
- Tools = [s_endFunction],
- }
- };
+ private ChatClientAgentRunOptions? _agentOptions;
public void Initialize(
WorkflowBuilder builder,
Executor end,
- Dictionary executors,
- IEnumerable handoffs) =>
+ Dictionary executors,
+ HashSet handoffs) =>
builder.AddSwitch(this, sb =>
{
- foreach (HandoffTarget handoff in handoffs)
+ if (handoffs.Count != 0)
{
- var handoffFunc = AIFunctionFactory.CreateDeclaration($"{FunctionPrefix}{CreateId()}", handoff.Reason, s_handoffSchema);
+ Debug.Assert(this._agentOptions is null);
+ this._agentOptions = new()
+ {
+ ChatOptions = new()
+ {
+ AllowMultipleToolCalls = false,
+ Instructions = handoffInstructions,
+ Tools = [],
+ },
+ };
- this._handoffFunctionNames.Add(handoffFunc.Name);
+ foreach (HandoffTarget handoff in handoffs)
+ {
+ var handoffFunc = AIFunctionFactory.CreateDeclaration($"{FunctionPrefix}{GetDescriptiveIdFromAgent(handoff.Target)}", handoff.Reason, s_handoffSchema);
- this._agentOptions.ChatOptions!.Tools!.Add(handoffFunc);
- this._agentOptions.ChatOptions.AllowMultipleToolCalls = false;
+ this._handoffFunctionNames.Add(handoffFunc.Name);
- sb.AddCase(state => state?.InvokedHandoff == handoffFunc.Name, executors[handoff.Target.Id]);
+ this._agentOptions.ChatOptions.Tools.Add(handoffFunc);
+
+ sb.AddCase(state => state?.InvokedHandoff == handoffFunc.Name, executors[handoff.Target.Id]);
+ }
}
sb.WithDefault(end);
@@ -432,43 +526,34 @@ public static class AgentWorkflowBuilder
List updates = [];
List allMessages = handoffState.Messages;
- while (requestedHandoff is null)
+ List? roleChanges = ChangeAssistantToUserForOtherParticipants(this._agent.DisplayName, allMessages);
+
+ await foreach (var update in this._agent.RunStreamingAsync(allMessages, options: this._agentOptions).ConfigureAwait(false))
{
- updates.Clear();
- await foreach (var update in this._agent.RunStreamingAsync(allMessages, options: this._agentOptions).ConfigureAwait(false))
+ await AddUpdateAsync(update).ConfigureAwait(false);
+
+ foreach (var c in update.Contents)
{
- await AddUpdateAsync(update).ConfigureAwait(false);
- for (int i = 0; i < update.Contents.Count; i++)
+ if (c is FunctionCallContent fcc && this._handoffFunctionNames.Contains(fcc.Name))
{
- var c = update.Contents[i];
- if (c is FunctionCallContent fcc)
+ requestedHandoff = fcc.Name;
+ await AddUpdateAsync(new AgentRunResponseUpdate
{
- if (this._handoffFunctionNames.Contains(fcc.Name))
- {
- requestedHandoff = fcc.Name;
- await AddUpdateAsync(new AgentRunResponseUpdate
- {
- AgentId = this._agent.Id,
- AuthorName = this._agent.DisplayName,
- Contents = [new FunctionResultContent(fcc.CallId, "Transferred.")],
- CreatedAt = DateTimeOffset.UtcNow,
- MessageId = Guid.NewGuid().ToString("N"),
- Role = ChatRole.Tool,
- }).ConfigureAwait(false);
- }
- else if (fcc.Name == s_endFunction.Name)
- {
- requestedHandoff = s_endFunction.Name;
- update.Contents.RemoveAt(i);
- i--;
- }
- }
+ AgentId = this._agent.Id,
+ AuthorName = this._agent.DisplayName,
+ Contents = [new FunctionResultContent(fcc.CallId, "Transferred.")],
+ CreatedAt = DateTimeOffset.UtcNow,
+ MessageId = Guid.NewGuid().ToString("N"),
+ Role = ChatRole.Tool,
+ }).ConfigureAwait(false);
}
}
-
- allMessages.AddRange(updates.ToAgentRunResponse().Messages);
}
+ allMessages.AddRange(updates.ToAgentRunResponse().Messages);
+
+ ResetUserToAssistantForChangedRoles(roleChanges);
+
await context.SendMessageAsync(new HandoffState(handoffState.TurnToken, requestedHandoff, allMessages)).ConfigureAwait(false);
async Task AddUpdateAsync(AgentRunResponseUpdate update)
@@ -486,12 +571,297 @@ public static class AgentWorkflowBuilder
TurnToken TurnToken,
string? InvokedHandoff,
List Messages);
+ }
- private static string CreateId() =>
+ ///
+ /// A manager that manages the flow of a group chat.
+ ///
+ public abstract class GroupChatManager
+ {
+ private int _maximumIterationCount = 40;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ protected GroupChatManager() { }
+
+ ///
+ /// Gets the number of iterations in the group chat so far.
+ ///
+ public int IterationCount { get; internal set; }
+
+ ///
+ /// Gets or sets the maximum number of iterations allowed.
+ ///
+ ///
+ /// Each iteration involves a single interaction with a participating agent.
+ /// The default is 40.
+ ///
+ public int MaximumIterationCount
+ {
+ get => this._maximumIterationCount;
+ set => this._maximumIterationCount = Throw.IfLessThan(value, 1);
+ }
+
+ ///
+ /// Selects the next agent to participate in the group chat based on the provided chat history and team.
+ ///
+ /// The chat history to consider.
+ /// A cancellation token that can be used to cancel the operation.
+ /// The next to speak. This agent must be part of the chat.
+ protected internal abstract ValueTask SelectNextAgentAsync(
+ IReadOnlyList history,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Filters the chat history before it's passed to the next agent.
+ ///
+ /// The chat history to filter.
+ /// A cancellation token that can be used to cancel the operation.
+ /// The filtered chat history.
+ protected internal virtual ValueTask> UpdateHistoryAsync(
+ IReadOnlyList history,
+ CancellationToken cancellationToken = default) =>
+ new(history);
+
+ ///
+ /// Determines whether the group chat should be terminated based on the provided chat history and iteration count.
+ ///
+ /// The chat history to consider.
+ /// A cancellation token that can be used to cancel the operation.
+ /// A indicating whether the chat should be terminated.
+ protected internal virtual ValueTask ShouldTerminateAsync(
+ IReadOnlyList history,
+ CancellationToken cancellationToken = default) =>
+ new(this.MaximumIterationCount is int max && this.IterationCount >= max);
+
+ ///
+ /// Resets the state of the manager for a new group chat session.
+ ///
+ protected internal virtual void Reset()
+ {
+ this.IterationCount = 0;
+ }
+ }
+
+ ///
+ /// Provides a that selects agents in a round-robin fashion.
+ ///
+ public class RoundRobinGroupChatManager : GroupChatManager
+ {
+ private readonly IReadOnlyList _agents;
+ private readonly Func, CancellationToken, ValueTask>? _shouldTerminateFunc;
+ private int _nextIndex;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The agents to be managed as part of this workflow.
+ ///
+ /// An optional function that determines whether the group chat should terminate based on the chat history
+ /// before factoring in the default behavior, which is to terminate based only on the iteration count.
+ ///
+ public RoundRobinGroupChatManager(
+ IReadOnlyList agents,
+ Func, CancellationToken, ValueTask>? shouldTerminateFunc = null)
+ {
+ Throw.IfNullOrEmpty(agents);
+ foreach (var agent in agents)
+ {
+ Throw.IfNull(agent, nameof(agents));
+ }
+
+ this._agents = agents;
+ this._shouldTerminateFunc = shouldTerminateFunc;
+ }
+
+ ///
+ protected internal override ValueTask SelectNextAgentAsync(
+ IReadOnlyList history, CancellationToken cancellationToken = default)
+ {
+ AIAgent nextAgent = this._agents[this._nextIndex];
+
+ this._nextIndex = (this._nextIndex + 1) % this._agents.Count;
+
+ return new ValueTask(nextAgent);
+ }
+
+ ///
+ protected internal override async ValueTask ShouldTerminateAsync(
+ IReadOnlyList history, CancellationToken cancellationToken = default)
+ {
+ if (this._shouldTerminateFunc is { } func && await func(this, history, cancellationToken).ConfigureAwait(false))
+ {
+ return true;
+ }
+
+ return await base.ShouldTerminateAsync(history, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ protected internal override void Reset()
+ {
+ base.Reset();
+ this._nextIndex = 0;
+ }
+ }
+
+ ///
+ /// Provides a builder for specifying group chat relationships between agents and building the resulting workflow.
+ ///
+ public sealed class GroupChatWorkflowBuilder
+ {
+ private readonly Func, GroupChatManager> _managerFactory;
+ private readonly HashSet _participants = new(AIAgentIDEqualityComparer.Instance);
+
+ internal GroupChatWorkflowBuilder(Func, GroupChatManager> managerFactory) =>
+ this._managerFactory = managerFactory;
+
+ ///
+ /// Adds the specified as participants to the group chat workflow.
+ ///
+ /// The agents to add as participants.
+ /// This instance of the .
+ public GroupChatWorkflowBuilder AddParticipants(params IEnumerable agents)
+ {
+ Throw.IfNull(agents);
+
+ foreach (var agent in agents)
+ {
+ if (agent is null)
+ {
+ Throw.ArgumentNullException(nameof(agents), "One or more target agents are null.");
+ }
+
+ this._participants.Add(agent);
+ }
+
+ return this;
+ }
+
+ ///
+ /// Builds a composed of agents that operate via group chat, with the next
+ /// agent to process messages selected by the group chat manager.
+ ///
+ /// The workflow built based on the group chat in the builder.
+ public Workflow> Build()
+ {
+ AIAgent[] agents = this._participants.ToArray();
+ Dictionary agentMap = agents.ToDictionary(a => a, a => (ExecutorIsh)new AgentRunStreamingExecutor(a, includeInputInOutput: true));
+
+ GroupChatHost host = new(agents, agentMap, this._managerFactory);
+
+ WorkflowBuilder builder = new(host);
+
+ foreach (var participant in agentMap.Values)
+ {
+ builder
+ .AddEdge(host, participant)
+ .AddEdge(participant, host);
+ }
+
+ return builder.Build>();
+ }
+
+ private sealed class GroupChatHost(AIAgent[] agents, Dictionary agentMap, Func, GroupChatManager> managerFactory) : Executor
+ {
+ private readonly AIAgent[] _agents = agents;
+ private readonly Dictionary _agentMap = agentMap;
+ private readonly Func, GroupChatManager> _managerFactory = managerFactory;
+ private readonly List _pendingMessages = [];
+
+ private GroupChatManager? _manager;
+
+ protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) => routeBuilder
+ .AddHandler((message, context) => this._pendingMessages.Add(new(ChatRole.User, message)))
+ .AddHandler((message, context) => this._pendingMessages.Add(message))
+ .AddHandler>((messages, _) => this._pendingMessages.AddRange(messages))
+ .AddHandler((messages, _) => this._pendingMessages.AddRange(messages)) // TODO: Remove once https://github.com/microsoft/agent-framework/issues/782 is addressed
+ .AddHandler>((messages, _) => this._pendingMessages.AddRange(messages)) // TODO: Remove once https://github.com/microsoft/agent-framework/issues/782 is addressed
+ .AddHandler(async (token, context) =>
+ {
+ List messages = [.. this._pendingMessages];
+ this._pendingMessages.Clear();
+
+ this._manager ??= this._managerFactory(this._agents);
+
+ if (!await this._manager.ShouldTerminateAsync(messages).ConfigureAwait(false))
+ {
+ var filtered = await this._manager.UpdateHistoryAsync(messages).ConfigureAwait(false);
+ messages = filtered is null || ReferenceEquals(filtered, messages) ? messages : [.. filtered];
+
+ if (await this._manager.SelectNextAgentAsync(messages).ConfigureAwait(false) is AIAgent nextAgent &&
+ this._agentMap.TryGetValue(nextAgent, out var executor))
+ {
+ this._manager.IterationCount++;
+ await context.SendMessageAsync(messages, executor.Id).ConfigureAwait(false);
+ await context.SendMessageAsync(token, executor.Id).ConfigureAwait(false);
+ return;
+ }
+ }
+
+ this._manager = null;
+ await context.AddEventAsync(new WorkflowCompletedEvent(messages)).ConfigureAwait(false);
+ });
+ }
+ }
+
+ ///
+ /// Iterates through looking for messages and swapping
+ /// any that have a different from to .
+ ///
+ private static List? ChangeAssistantToUserForOtherParticipants(string targetAgentName, List messages)
+ {
+ List? roleChanged = null;
+ foreach (var m in messages)
+ {
+ if (m.Role == ChatRole.Assistant &&
+ m.AuthorName != targetAgentName &&
+ m.Contents.All(c => c is TextContent or DataContent or UriContent or UsageContent))
+ {
+ m.Role = ChatRole.User;
+ (roleChanged ??= []).Add(m);
+ }
+ }
+
+ return roleChanged;
+ }
+
+ ///
+ /// Undoes changes made by
+ /// when passed the list of changes made by that method.
+ ///
+ private static void ResetUserToAssistantForChangedRoles(List? roleChanged)
+ {
+ if (roleChanged is not null)
+ {
+ foreach (var m in roleChanged)
+ {
+ m.Role = ChatRole.Assistant;
+ }
+ }
+ }
+
+ /// Derives from an agent a unique but also hopefully descriptive name that can be used as an executor's name or in a function name.
+ private static string GetDescriptiveIdFromAgent(AIAgent agent)
+ {
+ string id = string.IsNullOrEmpty(agent.Name) ? agent.Id : $"{agent.Name}_{agent.Id}";
+ return InvalidNameCharsRegex().Replace(id, "_");
+ }
+
+ /// Regex that flags any character other than ASCII digits or letters or the underscore.
#if NET
- RandomNumberGenerator.GetString("abcdefghijklmnopqrstuvwxyz0123456789", 24);
+ [GeneratedRegex("[^0-9A-Za-z_]+")]
+ private static partial Regex InvalidNameCharsRegex();
#else
- Guid.NewGuid().ToString("N");
+ private static Regex InvalidNameCharsRegex() => s_invalidNameCharsRegex;
+ private static readonly Regex s_invalidNameCharsRegex = new("[^0-9A-Za-z_]+", RegexOptions.Compiled);
#endif
+
+ private sealed class AIAgentIDEqualityComparer : IEqualityComparer
+ {
+ public static AIAgentIDEqualityComparer Instance { get; } = new();
+ public bool Equals(AIAgent? x, AIAgent? y) => x?.Id == y?.Id;
+ public int GetHashCode([DisallowNull] AIAgent obj) => obj?.GetHashCode() ?? 0;
}
}
diff --git a/dotnet/src/Microsoft.Agents.Workflows/MessageMerger.cs b/dotnet/src/Microsoft.Agents.Workflows/MessageMerger.cs
index 1a7e0c7b63..1b9cfefbce 100644
--- a/dotnet/src/Microsoft.Agents.Workflows/MessageMerger.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows/MessageMerger.cs
@@ -57,9 +57,7 @@ internal sealed class MessageMerger
public List ComputeFlattened()
{
- List result = this.UpdatesByMessageId.Keys.Select(AggregateUpdatesToMessage)
- .ToList();
-
+ List result = this.UpdatesByMessageId.Keys.SelectMany(AggregateUpdatesToMessage).ToList();
if (this.DanglingUpdates.Count > 0)
{
result.AddRange(this.ComputeDangling().Messages);
@@ -67,7 +65,7 @@ internal sealed class MessageMerger
return result;
- ChatMessage AggregateUpdatesToMessage(string messageId)
+ IList AggregateUpdatesToMessage(string messageId)
{
List updates = this.UpdatesByMessageId[messageId];
if (updates.Count == 0)
@@ -75,13 +73,19 @@ internal sealed class MessageMerger
throw new InvalidOperationException($"No updates found for message ID '{messageId}' in response '{this.ResponseId}'.");
}
- return updates.Aggregate(null,
- (ChatMessage? previous, AgentRunResponseUpdate current) =>
+ return updates.Select(oldUpdate =>
+ oldUpdate.RawRepresentation as ChatResponseUpdate ??
+ new()
{
- return previous is null
- ? current.ToChatMessage()
- : previous.UpdateWith(current);
- })!;
+ AdditionalProperties = oldUpdate.AdditionalProperties,
+ AuthorName = oldUpdate.AuthorName,
+ Contents = oldUpdate.Contents,
+ CreatedAt = oldUpdate.CreatedAt,
+ MessageId = oldUpdate.MessageId,
+ RawRepresentation = oldUpdate.RawRepresentation,
+ Role = oldUpdate.Role,
+ ResponseId = oldUpdate.ResponseId,
+ }).ToChatResponse().Messages;
}
}
}
@@ -170,13 +174,28 @@ internal sealed class MessageMerger
}
messages.AddRange(this._danglingState.ComputeFlattened());
+
+ // Remove any empty text contents or messages that are now empty.
+ foreach (var m in messages)
+ {
+ for (int i = m.Contents.Count - 1; i >= 0; i--)
+ {
+ if (m.Contents[i] is TextContent textContent &&
+ string.IsNullOrWhiteSpace(textContent.Text))
+ {
+ m.Contents.RemoveAt(i);
+ }
+ }
+ }
+ messages.RemoveAll(m => m.Contents.Count == 0);
+
return new AgentRunResponse(messages)
{
ResponseId = primaryResponseId,
AgentId = primaryAgentId
?? primaryAgentName
?? (agentIds.Count == 1 ? agentIds.First() : null),
- CreatedAt = DateTimeOffset.Now,
+ CreatedAt = DateTimeOffset.UtcNow,
Usage = usage,
AdditionalProperties = additionalProperties
};
diff --git a/dotnet/src/Microsoft.Agents.Workflows/Run.cs b/dotnet/src/Microsoft.Agents.Workflows/Run.cs
index 8435a7658b..a3e8f66666 100644
--- a/dotnet/src/Microsoft.Agents.Workflows/Run.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows/Run.cs
@@ -118,7 +118,7 @@ public class Run
/// A that can be used to cancel the workflow execution.
/// An array of objects to send to the workflow.
/// true if the workflow had any output events, false otherwise.
- public async ValueTask ResumeAsync(CancellationToken cancellation = default, params ExternalResponse[] responses)
+ public async ValueTask ResumeAsync(CancellationToken cancellation = default, params IEnumerable responses)
{
foreach (ExternalResponse response in responses)
{
@@ -135,9 +135,9 @@ public class Run
/// An array of messages to send to the workflow. Messages will only be sent if they are valid
/// input types to the starting executor or a .
/// true if the workflow had any output events, false otherwise.
- public async ValueTask ResumeAsync(CancellationToken cancellation = default, params T[] messages)
+ public async ValueTask ResumeAsync(CancellationToken cancellation = default, params IEnumerable messages)
{
- if (messages is ExternalResponse[] responses)
+ if (messages is IEnumerable responses)
{
return await this.ResumeAsync(cancellation, responses).ConfigureAwait(false);
}
diff --git a/dotnet/src/Microsoft.Agents.Workflows/Specialized/AIAgentHostExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows/Specialized/AIAgentHostExecutor.cs
index cdcb9293b3..6065b3d5d1 100644
--- a/dotnet/src/Microsoft.Agents.Workflows/Specialized/AIAgentHostExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows/Specialized/AIAgentHostExecutor.cs
@@ -115,7 +115,7 @@ internal sealed class AIAgentHostExecutor : Executor
async ValueTask PublishCurrentMessageAsync()
{
- if (currentStreamingMessage is not null)
+ if (currentStreamingMessage is not null && updates.Count > 0)
{
currentStreamingMessage.Contents = updates;
updates = [];
diff --git a/dotnet/src/Microsoft.Agents.Workflows/SwitchBuilder.cs b/dotnet/src/Microsoft.Agents.Workflows/SwitchBuilder.cs
index 2411f62294..495ee3474a 100644
--- a/dotnet/src/Microsoft.Agents.Workflows/SwitchBuilder.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows/SwitchBuilder.cs
@@ -30,7 +30,7 @@ public sealed class SwitchBuilder
/// One or more executors to associate with the predicate. Each executor will be invoked if the predicate matches.
/// Cannot be null.
/// The current instance, allowing for method chaining.
- public SwitchBuilder AddCase(Func predicate, params ExecutorIsh[] executors)
+ public SwitchBuilder AddCase(Func predicate, params IEnumerable executors)
{
Throw.IfNull(predicate);
Throw.IfNull(executors);
@@ -60,7 +60,7 @@ public sealed class SwitchBuilder
///
///
///
- public SwitchBuilder WithDefault(params ExecutorIsh[] executors)
+ public SwitchBuilder WithDefault(params IEnumerable executors)
{
Throw.IfNull(executors);
diff --git a/dotnet/src/Microsoft.Agents.Workflows/WorkflowBuilder.cs b/dotnet/src/Microsoft.Agents.Workflows/WorkflowBuilder.cs
index 2607898acb..4f53e09af4 100644
--- a/dotnet/src/Microsoft.Agents.Workflows/WorkflowBuilder.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows/WorkflowBuilder.cs
@@ -211,7 +211,7 @@ public class WorkflowBuilder
/// The source executor from which the fan-out edge originates. Cannot be null.
/// One or more target executors that will receive the fan-out edge. Cannot be null or empty.
/// The current instance of .
- public WorkflowBuilder AddFanOutEdge(ExecutorIsh source, params ExecutorIsh[] targets)
+ public WorkflowBuilder AddFanOutEdge(ExecutorIsh source, params IEnumerable targets)
=> this.AddFanOutEdge