diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index c48afe4ebf..27f1c0821e 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -13,6 +13,7 @@ + diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index e33215905e..a38787036d 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -115,7 +115,6 @@ - @@ -129,9 +128,6 @@ - - - diff --git a/dotnet/samples/GettingStarted/AgentSample.cs b/dotnet/samples/GettingStarted/AgentSample.cs index d518607691..0be5aa1551 100644 --- a/dotnet/samples/GettingStarted/AgentSample.cs +++ b/dotnet/samples/GettingStarted/AgentSample.cs @@ -159,11 +159,8 @@ public class AgentSample(ITestOutputHelper output) : BaseSample(output) private async Task AzureAIAgentsPersistentAgentCleanUpAsync(ChatClientAgent agent, AgentThread? thread, CancellationToken cancellationToken) { - var persistentAgentsClient = agent.ChatClient.GetService(); - if (persistentAgentsClient is null) - { + var persistentAgentsClient = agent.ChatClient.GetService() ?? throw new InvalidOperationException("The provided chat client is not a Persistent Agents Chat Client"); - } await persistentAgentsClient.Administration.DeleteAgentAsync(agent.Id, cancellationToken); diff --git a/dotnet/samples/GettingStarted/Orchestration/ConcurrentOrchestration_Intro.cs b/dotnet/samples/GettingStarted/Orchestration/ConcurrentOrchestration_Intro.cs index 1c8acfc101..1ab57d9e6b 100644 --- a/dotnet/samples/GettingStarted/Orchestration/ConcurrentOrchestration_Intro.cs +++ b/dotnet/samples/GettingStarted/Orchestration/ConcurrentOrchestration_Intro.cs @@ -1,9 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using Microsoft.Agents.Orchestration; -using Microsoft.Agents.Orchestration.Concurrent; using Microsoft.Extensions.AI.Agents; -using Microsoft.Extensions.AI.Agents.Runtime.InProcess; namespace Orchestration; @@ -42,20 +40,14 @@ public class ConcurrentOrchestration_Intro(ITestOutputHelper output) : Orchestra StreamingResponseCallback = streamedResponse ? monitor.StreamingResultCallback : null, }; - // Start the runtime - await using InProcessRuntime runtime = new(); - await runtime.StartAsync(); - // Run the orchestration string input = "What is temperature?"; Console.WriteLine($"\n# INPUT: {input}\n"); - OrchestrationResult result = await orchestration.InvokeAsync(input, runtime); + OrchestrationResult result = await orchestration.InvokeAsync(input); - string[] output = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); + string[] output = await result; Console.WriteLine($"\n# RESULT:\n{string.Join("\n\n", output.Select(text => $"{text}"))}"); - await runtime.RunUntilIdleAsync(); - this.DisplayHistory(monitor.History); } } diff --git a/dotnet/samples/GettingStarted/Orchestration/ConcurrentOrchestration_With_StructuredOutput.cs b/dotnet/samples/GettingStarted/Orchestration/ConcurrentOrchestration_With_StructuredOutput.cs index f62c9e2a9e..1b6ce726e2 100644 --- a/dotnet/samples/GettingStarted/Orchestration/ConcurrentOrchestration_With_StructuredOutput.cs +++ b/dotnet/samples/GettingStarted/Orchestration/ConcurrentOrchestration_With_StructuredOutput.cs @@ -2,10 +2,7 @@ using System.Text.Json; using Microsoft.Agents.Orchestration; -using Microsoft.Agents.Orchestration.Concurrent; -using Microsoft.Agents.Orchestration.Transforms; using Microsoft.Extensions.AI.Agents; -using Microsoft.Extensions.AI.Agents.Runtime.InProcess; using Microsoft.Shared.Samples; namespace Orchestration; @@ -43,20 +40,14 @@ public class ConcurrentOrchestration_With_StructuredOutput(ITestOutputHelper out ResultTransform = outputTransform.TransformAsync, }; - // Start the runtime - await using InProcessRuntime runtime = new(); - await runtime.StartAsync(); - // Run the orchestration const string resourceId = "Hamlet_full_play_summary.txt"; string input = Resources.Read(resourceId); Console.WriteLine($"\n# INPUT: @{resourceId}\n"); - OrchestrationResult result = await orchestration.InvokeAsync(input, runtime); + OrchestrationResult result = await orchestration.InvokeAsync(input); - Analysis output = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds * 2)); + Analysis output = await result; Console.WriteLine($"\n# RESULT:\n{JsonSerializer.Serialize(output, s_options)}"); - - await runtime.RunUntilIdleAsync(); } #pragma warning disable CA1812 // Avoid uninstantiated internal classes diff --git a/dotnet/samples/GettingStarted/Orchestration/GroupChatOrchestration_Intro.cs b/dotnet/samples/GettingStarted/Orchestration/GroupChatOrchestration_Intro.cs index 920ebec115..9a47dc7293 100644 --- a/dotnet/samples/GettingStarted/Orchestration/GroupChatOrchestration_Intro.cs +++ b/dotnet/samples/GettingStarted/Orchestration/GroupChatOrchestration_Intro.cs @@ -1,9 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using Microsoft.Agents.Orchestration; -using Microsoft.Agents.Orchestration.GroupChat; using Microsoft.Extensions.AI.Agents; -using Microsoft.Extensions.AI.Agents.Runtime.InProcess; namespace Orchestration; @@ -68,17 +66,10 @@ public class GroupChatOrchestration_Intro(ITestOutputHelper output) : Orchestrat StreamingResponseCallback = streamedResponse ? monitor.StreamingResultCallback : null, }; - // Start the runtime - await using InProcessRuntime runtime = new(); - await runtime.StartAsync(); - string input = "Create a slogon for a new eletric SUV that is affordable and fun to drive."; Console.WriteLine($"\n# INPUT: {input}\n"); - OrchestrationResult result = await orchestration.InvokeAsync(input, runtime); - string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds * 3)); - Console.WriteLine($"\n# RESULT: {text}"); - - await runtime.RunUntilIdleAsync(); + OrchestrationResult result = await orchestration.InvokeAsync(input); + Console.WriteLine($"\n# RESULT: {await result}"); this.DisplayHistory(monitor.History); } diff --git a/dotnet/samples/GettingStarted/Orchestration/GroupChatOrchestration_With_AIManager.cs b/dotnet/samples/GettingStarted/Orchestration/GroupChatOrchestration_With_AIManager.cs index b26b37b721..42f4844a92 100644 --- a/dotnet/samples/GettingStarted/Orchestration/GroupChatOrchestration_With_AIManager.cs +++ b/dotnet/samples/GettingStarted/Orchestration/GroupChatOrchestration_With_AIManager.cs @@ -1,10 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. using Microsoft.Agents.Orchestration; -using Microsoft.Agents.Orchestration.GroupChat; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; -using Microsoft.Extensions.AI.Agents.Runtime.InProcess; namespace Orchestration; @@ -132,17 +130,10 @@ public class GroupChatOrchestration_With_AIManager(ITestOutputHelper output) : O ResponseCallback = monitor.ResponseCallback, }; - // Start the runtime - await using InProcessRuntime runtime = new(); - await runtime.StartAsync(); - // Run the orchestration Console.WriteLine($"\n# INPUT: {topic}\n"); - OrchestrationResult result = await orchestration.InvokeAsync(topic, runtime); - string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds * 3)); - Console.WriteLine($"\n# RESULT: {text}"); - - await runtime.RunUntilIdleAsync(); + OrchestrationResult result = await orchestration.InvokeAsync(topic); + Console.WriteLine($"\n# RESULT: {await result}"); this.DisplayHistory(monitor.History); } diff --git a/dotnet/samples/GettingStarted/Orchestration/GroupChatOrchestration_With_HumanInTheLoop.cs b/dotnet/samples/GettingStarted/Orchestration/GroupChatOrchestration_With_HumanInTheLoop.cs index 39e6a7c4da..9e6e6286f5 100644 --- a/dotnet/samples/GettingStarted/Orchestration/GroupChatOrchestration_With_HumanInTheLoop.cs +++ b/dotnet/samples/GettingStarted/Orchestration/GroupChatOrchestration_With_HumanInTheLoop.cs @@ -1,10 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. using Microsoft.Agents.Orchestration; -using Microsoft.Agents.Orchestration.GroupChat; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; -using Microsoft.Extensions.AI.Agents.Runtime.InProcess; namespace Orchestration; @@ -68,18 +66,11 @@ public class GroupChatOrchestration_With_HumanInTheLoop(ITestOutputHelper output ResponseCallback = monitor.ResponseCallback, }; - // Start the runtime - await using InProcessRuntime runtime = new(); - await runtime.StartAsync(); - // Run the orchestration string input = "Create a slogon for a new eletric SUV that is affordable and fun to drive."; Console.WriteLine($"\n# INPUT: {input}\n"); - OrchestrationResult result = await orchestration.InvokeAsync(input, runtime); - string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds * 3)); - Console.WriteLine($"\n# RESULT: {text}"); - - await runtime.RunUntilIdleAsync(); + OrchestrationResult result = await orchestration.InvokeAsync(input); + Console.WriteLine($"\n# RESULT: {await result}"); this.DisplayHistory(monitor.History); } @@ -97,23 +88,12 @@ public class GroupChatOrchestration_With_HumanInTheLoop(ITestOutputHelper output { string? lastAgent = history.LastOrDefault()?.AuthorName; - GroupChatManagerResult result; + GroupChatManagerResult result = + lastAgent is null ? new(false) { Reason = "No agents have spoken yet." } : + lastAgent is "Reviewer" ? new(true) { Reason = "User input is needed after the reviewer's message." } : + new(false) { Reason = "User input is not needed until the reviewer's message." }; - if (lastAgent is null) - { - result = new GroupChatManagerResult(false) { Reason = "No agents have spoken yet." }; - } - - if (lastAgent == "Reviewer") - { - result = new GroupChatManagerResult(true) { Reason = "User input is needed after the reviewer's message." }; - } - else - { - result = new GroupChatManagerResult(false) { Reason = "User input is not needed until the reviewer's message." }; - } - - return new ValueTask>(result); + return new(result); } } } diff --git a/dotnet/samples/GettingStarted/Orchestration/HandoffOrchestration_Intro.cs b/dotnet/samples/GettingStarted/Orchestration/HandoffOrchestration_Intro.cs index 641a6b5b72..cee35fa613 100644 --- a/dotnet/samples/GettingStarted/Orchestration/HandoffOrchestration_Intro.cs +++ b/dotnet/samples/GettingStarted/Orchestration/HandoffOrchestration_Intro.cs @@ -1,10 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. using Microsoft.Agents.Orchestration; -using Microsoft.Agents.Orchestration.Handoff; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; -using Microsoft.Extensions.AI.Agents.Runtime.InProcess; namespace Orchestration; @@ -65,11 +63,7 @@ public class HandoffOrchestration_Intro(ITestOutputHelper output) : Orchestratio .Add(triageAgent, statusAgent, returnAgent, refundAgent) .Add(statusAgent, triageAgent, "Transfer to this agent if the issue is not status related") .Add(returnAgent, triageAgent, "Transfer to this agent if the issue is not return related") - .Add(refundAgent, triageAgent, "Transfer to this agent if the issue is not refund related"), - triageAgent, - statusAgent, - returnAgent, - refundAgent) + .Add(refundAgent, triageAgent, "Transfer to this agent if the issue is not refund related")) { InteractiveCallback = () => { @@ -84,18 +78,11 @@ public class HandoffOrchestration_Intro(ITestOutputHelper output) : Orchestratio StreamingResponseCallback = streamedResponse ? monitor.StreamingResultCallback : null, }; - // Start the runtime - await using InProcessRuntime runtime = new(); - await runtime.StartAsync(); - // Run the orchestration Console.WriteLine($"\n# INPUT:\n{task}\n"); - OrchestrationResult result = await orchestration.InvokeAsync(task, runtime); + OrchestrationResult result = await orchestration.InvokeAsync(task); - string text = await result.GetValueAsync(TimeSpan.FromSeconds(300)); - Console.WriteLine($"\n# RESULT: {text}"); - - await runtime.RunUntilIdleAsync(); + Console.WriteLine($"\n# RESULT: {await result}"); this.DisplayHistory(monitor.History); } diff --git a/dotnet/samples/GettingStarted/Orchestration/HandoffOrchestration_With_StructuredInput.cs b/dotnet/samples/GettingStarted/Orchestration/HandoffOrchestration_With_StructuredInput.cs index 454a0fcb53..4778570fcb 100644 --- a/dotnet/samples/GettingStarted/Orchestration/HandoffOrchestration_With_StructuredInput.cs +++ b/dotnet/samples/GettingStarted/Orchestration/HandoffOrchestration_With_StructuredInput.cs @@ -2,10 +2,8 @@ using System.Text.Json.Serialization; using Microsoft.Agents.Orchestration; -using Microsoft.Agents.Orchestration.Handoff; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; -using Microsoft.Extensions.AI.Agents.Runtime.InProcess; namespace Orchestration; @@ -49,10 +47,7 @@ public class HandoffOrchestration_With_StructuredInput(ITestOutputHelper output) HandoffOrchestration orchestration = new(OrchestrationHandoffs .StartWith(triageAgent) - .Add(triageAgent, dotnetAgent, pythonAgent), - triageAgent, - pythonAgent, - dotnetAgent) + .Add(triageAgent, dotnetAgent, pythonAgent)) { LoggerFactory = this.LoggerFactory, ResponseCallback = monitor.ResponseCallback, @@ -83,18 +78,11 @@ public class HandoffOrchestration_With_StructuredInput(ITestOutputHelper output) Labels = [] }; - // Start the runtime - await using InProcessRuntime runtime = new(); - await runtime.StartAsync(); - // Run the orchestration Console.WriteLine($"\n# INPUT:\n{input.Id}: {input.Title}\n"); - OrchestrationResult result = await orchestration.InvokeAsync(input, runtime); - string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); - Console.WriteLine($"\n# RESULT: {text}"); + OrchestrationResult result = await orchestration.InvokeAsync(input); + Console.WriteLine($"\n# RESULT: {await result}"); Console.WriteLine($"\n# LABELS: {string.Join(",", githubPlugin.Labels["12345"])}\n"); - - await runtime.RunUntilIdleAsync(); } private sealed class GithubIssue diff --git a/dotnet/samples/GettingStarted/Orchestration/SequentialOrchestration_Intro.cs b/dotnet/samples/GettingStarted/Orchestration/SequentialOrchestration_Intro.cs index 44e0771af4..579aecf96f 100644 --- a/dotnet/samples/GettingStarted/Orchestration/SequentialOrchestration_Intro.cs +++ b/dotnet/samples/GettingStarted/Orchestration/SequentialOrchestration_Intro.cs @@ -1,9 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using Microsoft.Agents.Orchestration; -using Microsoft.Agents.Orchestration.Sequential; using Microsoft.Extensions.AI.Agents; -using Microsoft.Extensions.AI.Agents.Runtime.InProcess; namespace Orchestration; @@ -64,18 +62,11 @@ public class SequentialOrchestration_Intro(ITestOutputHelper output) : Orchestra StreamingResponseCallback = streamedResponse ? monitor.StreamingResultCallback : null, }; - // Start the runtime - await using InProcessRuntime runtime = new(); - await runtime.StartAsync(); - // Run the orchestration string input = "An eco-friendly stainless steel water bottle that keeps drinks cold for 24 hours"; Console.WriteLine($"\n# INPUT: {input}\n"); - OrchestrationResult result = await orchestration.InvokeAsync(input, runtime); - string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); - Console.WriteLine($"\n# RESULT: {text}"); - - await runtime.RunUntilIdleAsync(); + OrchestrationResult result = await orchestration.InvokeAsync(input); + Console.WriteLine($"\n# RESULT: {await result}"); this.DisplayHistory(monitor.History); } diff --git a/dotnet/samples/GettingStarted/Orchestration/SequentialOrchestration_With_Cancellation.cs b/dotnet/samples/GettingStarted/Orchestration/SequentialOrchestration_With_Cancellation.cs index a4394dac46..6b93c8590b 100644 --- a/dotnet/samples/GettingStarted/Orchestration/SequentialOrchestration_With_Cancellation.cs +++ b/dotnet/samples/GettingStarted/Orchestration/SequentialOrchestration_With_Cancellation.cs @@ -1,9 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using Microsoft.Agents.Orchestration; -using Microsoft.Agents.Orchestration.Sequential; using Microsoft.Extensions.AI.Agents; -using Microsoft.Extensions.AI.Agents.Runtime.InProcess; namespace Orchestration; @@ -26,29 +24,22 @@ public class SequentialOrchestration_With_Cancellation(ITestOutputHelper output) // Define the orchestration SequentialOrchestration orchestration = new(agent) { LoggerFactory = this.LoggerFactory }; - // Start the runtime - await using InProcessRuntime runtime = new(); - await runtime.StartAsync(); - // Run the orchestration string input = "42"; Console.WriteLine($"\n# INPUT: {input}\n"); - OrchestrationResult result = await orchestration.InvokeAsync(input, runtime); + OrchestrationResult result = await orchestration.InvokeAsync(input); result.Cancel(); await Task.Delay(TimeSpan.FromSeconds(3)); try { - string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); - Console.WriteLine($"\n# RESULT: {text}"); + Console.WriteLine($"\n# RESULT: {await result}"); } - catch (AggregateException exception) + catch (TimeoutException exception) { - Console.WriteLine($"\n# CANCELLED: {exception.InnerException?.Message}"); + Console.WriteLine($"\n# CANCELED: {exception.Message}"); } - - await runtime.RunUntilIdleAsync(); } } diff --git a/dotnet/src/LegacySupport/IsExternalInit/IsExternalInit.cs b/dotnet/src/LegacySupport/IsExternalInit/IsExternalInit.cs index b340f84592..e86a0cd0e7 100644 --- a/dotnet/src/LegacySupport/IsExternalInit/IsExternalInit.cs +++ b/dotnet/src/LegacySupport/IsExternalInit/IsExternalInit.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. /* This enables support for C# 9/10 records on older frameworks */ diff --git a/dotnet/src/Microsoft.Agents.Orchestration/AgentActor.cs b/dotnet/src/Microsoft.Agents.Orchestration/AgentActor.cs index 925221ed78..b3bdbccdd3 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/AgentActor.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/AgentActor.cs @@ -25,12 +25,7 @@ public abstract class AgentActor : OrchestrationActor /// An . /// The logger to use for the actor protected AgentActor(ActorId id, IAgentRuntime runtime, OrchestrationContext context, Agent agent, ILogger? logger = null) - : base( - id, - runtime, - context, - agent.Description, - logger) + : base(id, runtime, context, agent.Description, logger) { this.Agent = agent; this.Thread = this.Agent.GetNewThread(); @@ -55,102 +50,75 @@ public abstract class AgentActor : OrchestrationActor } /// - /// Invokes the agent for a regular (not streamed) response. + /// Invokes the agent for a non-streamed responses. /// /// The messages to send. /// The options for running the agent. /// A cancellation token for the operation. /// A task that represents the asynchronous operation. /// - /// Override this method to customize the invocation of the agent. + /// This method is not intended to be called directly; instead, use . + /// This method exists to be overridden in derived classes in order to customize the invocation of the agent by . /// - protected virtual Task InvokeAsync( - IReadOnlyCollection messages, - AgentRunOptions options, - CancellationToken cancellationToken = default) => - this.Agent.RunAsync( - [.. messages], - this.Thread, - options, - cancellationToken); + protected virtual Task InvokeCoreAsync( + IReadOnlyCollection messages, AgentRunOptions? options, CancellationToken cancellationToken) => + this.Agent.RunAsync([.. messages], this.Thread, options, cancellationToken); /// - /// Invokes the agent for a streamed response. + /// Invokes the agent for a streamed responses. /// /// The messages to send. /// The options for running the agent. /// A cancellation token for the operation. /// A task that represents the asynchronous operation. /// - /// Override this method to customize the invocation of the agent. + /// This method is not intended to be called directly; instead, use . + /// This method exists to be overridden in derived classes in order to customize the invocation of the agent by . /// - protected virtual IAsyncEnumerable InvokeStreamingAsync(IReadOnlyCollection messages, AgentRunOptions options, CancellationToken cancellationToken) => - this.Agent.RunStreamingAsync( - messages, - this.Thread, - options, - cancellationToken); + protected virtual IAsyncEnumerable InvokeStreamingCoreAsync( + IReadOnlyCollection messages, AgentRunOptions? options, CancellationToken cancellationToken) => + this.Agent.RunStreamingAsync(messages, this.Thread, options, cancellationToken); /// - /// Invokes the agent with a single chat message. - /// This method sets the message role to and delegates to the overload accepting multiple messages. - /// - /// The chat message content to send. - /// A cancellation token that can be used to cancel the operation. - /// A task that returns the response . - protected ValueTask InvokeAsync(ChatMessage input, CancellationToken cancellationToken) => - this.InvokeAsync([input], cancellationToken); - - /// - /// Invokes the agent with input messages and respond with both streamed and regular messages. + /// Runs the agent with input messages and respond with both streamed and regular messages. /// /// The list of chat messages to send. /// A cancellation token that can be used to cancel the operation. /// A task that returns the response . - protected async ValueTask InvokeAsync(IEnumerable input, CancellationToken cancellationToken) + protected async ValueTask RunAsync(IEnumerable input, CancellationToken cancellationToken) { - this.Context.Cancellation.ThrowIfCancellationRequested(); + using CancellationTokenSource combined = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, this.Context.CancellationToken); + cancellationToken = combined.Token; - AgentRunOptions options = new(); - - if (this.Context.StreamingResponseCallback == null) + // Utilize streaming iff a streaming callback is provided; otherwise, use the non-streaming API. + AgentRunResponse response; + if (this.Context.StreamingResponseCallback is { } streamingCallback) { - // No need to utilize streaming if no callback is provided - AgentRunResponse response = await this.InvokeAsync([.. input], options, cancellationToken).ConfigureAwait(false); + // For streaming, enumerate all the updates, invoking the callback for each, and storing them all. + // Then convert them all into a single response instance. + List updates = []; - if (this.Context.ResponseCallback is not null) + await foreach (AgentRunResponseUpdate update in this.InvokeStreamingCoreAsync([.. input], options: null, cancellationToken).WithCancellation(this.Context.CancellationToken).ConfigureAwait(false)) { - await this.Context.ResponseCallback.Invoke(response.Messages).ConfigureAwait(false); + updates.Add(update); + await streamingCallback(update).ConfigureAwait(false); } - return response.Messages.Last(); + response = updates.ToAgentRunResponse(); } - - IAsyncEnumerable streamedResponses = this.InvokeStreamingAsync([.. input], options, cancellationToken); - AgentRunResponseUpdate? lastStreamedResponse = null; - List updates = []; - await foreach (AgentRunResponseUpdate streamedResponse in streamedResponses.ConfigureAwait(false)) + else { - this.Context.Cancellation.ThrowIfCancellationRequested(); - - await HandleStreamedMessage(lastStreamedResponse, isFinal: false).ConfigureAwait(false); - - lastStreamedResponse = streamedResponse; + // For non-streaming, just invoke the non-streaming method and get back the response. + response = await this.InvokeCoreAsync([.. input], options: null, cancellationToken).ConfigureAwait(false); } - return updates.ToAgentRunResponse().Messages.Last(); - - async ValueTask HandleStreamedMessage(AgentRunResponseUpdate? streamedResponse, bool isFinal) + // Regardless of whether we invoked streaming callbacks for individual updates, invoke the non-streaming callback with the final response instance. + // This can be used as an indication of completeness if someone otherwise only cares about the streaming updates. + if (this.Context.ResponseCallback is { } responseCallback) { - if (this.Context.StreamingResponseCallback != null && streamedResponse != null) - { - await this.Context.StreamingResponseCallback.Invoke(streamedResponse, isFinal).ConfigureAwait(false); - } - - if (streamedResponse != null) - { - updates.Add(streamedResponse); - } + await responseCallback.Invoke(response.Messages).ConfigureAwait(false); } + + return response.Messages.LastOrDefault() ?? new(); } } diff --git a/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.RequestActor.cs b/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.RequestActor.cs index 70cbb60d90..7a03982f97 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.RequestActor.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.RequestActor.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Microsoft.Agents.Orchestration.Transforms; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; @@ -18,7 +17,7 @@ public abstract partial class AgentOrchestration /// private sealed class RequestActor : OrchestrationActor { - private readonly OrchestrationInputTransform _transform; + private readonly Func>> _transform; private readonly Func, ValueTask> _action; private readonly TaskCompletionSource _completionSource; @@ -36,7 +35,7 @@ public abstract partial class AgentOrchestration ActorId id, IAgentRuntime runtime, OrchestrationContext context, - OrchestrationInputTransform transform, + Func>> transform, TaskCompletionSource completionSource, Func, ValueTask> action, ILogger? logger = null) diff --git a/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.ResultActor.cs b/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.ResultActor.cs index c92d35f6bd..a593c47a58 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.ResultActor.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.ResultActor.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Microsoft.Agents.Orchestration.Transforms; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; @@ -19,8 +18,8 @@ public abstract partial class AgentOrchestration private sealed class ResultActor : OrchestrationActor { private readonly TaskCompletionSource _completionSource; - private readonly OrchestrationResultTransform _transformResult; - private readonly OrchestrationOutputTransform _transform; + private readonly Func> _transformResult; + private readonly Func, CancellationToken, ValueTask> _transform; /// /// Initializes a new instance of the class. @@ -36,8 +35,8 @@ public abstract partial class AgentOrchestration ActorId id, IAgentRuntime runtime, OrchestrationContext context, - OrchestrationResultTransform transformResult, - OrchestrationOutputTransform transformOutput, + Func> transformResult, + Func, CancellationToken, ValueTask> transformOutput, TaskCompletionSource completionSource, ILogger>? logger = null) : base(id, runtime, context, $"{id.Type}_Actor", logger) diff --git a/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.cs b/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.cs index 8f32787357..8e3dd2424f 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.cs @@ -2,33 +2,20 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.Agents.Orchestration.Extensions; -using Microsoft.Agents.Orchestration.Transforms; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; using Microsoft.Extensions.AI.Agents.Runtime; +using Microsoft.Extensions.AI.Agents.Runtime.InProcess; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Shared.Diagnostics; +#pragma warning disable CA2000 // Dispose objects before losing scope + namespace Microsoft.Agents.Orchestration; -/// -/// Called for every response is produced by any agent. -/// -/// The agent response -public delegate ValueTask OrchestrationResponseCallback(IEnumerable response); - -/// -/// Called to expose the streamed response produced by any agent. -/// -/// The agent response -/// Indicates if streamed content is final chunk of the message. -public delegate ValueTask OrchestrationStreamingCallback(AgentRunResponseUpdate response, bool isFinal); - /// /// Called when human interaction is requested. /// @@ -47,9 +34,17 @@ public abstract partial class AgentOrchestration /// Specifies the member agents or orchestrations participating in this orchestration. protected AgentOrchestration(params Agent[] members) { + _ = Throw.IfNull(members); + // Capture orchestration root name without generic parameters for use in // agent type and topic formatting as well as logging. - this.OrchestrationLabel = this.GetType().Name.Split('`').First(); + string name = this.GetType().Name; + int pos = name.IndexOf('`'); + if (pos > 0) + { + name = name.Substring(0, pos); + } + this.OrchestrationLabel = name; this.Members = members; } @@ -72,22 +67,22 @@ public abstract partial class AgentOrchestration /// /// Transforms the orchestration input into a source input suitable for processing. /// - public OrchestrationInputTransform InputTransform { get; init; } = DefaultTransforms.FromInput; + public Func>> InputTransform { get; init; } = DefaultTransforms.FromInput; /// /// Transforms the processed result into the final output form. /// - public OrchestrationOutputTransform ResultTransform { get; init; } = DefaultTransforms.ToOutput; + public Func, CancellationToken, ValueTask> ResultTransform { get; init; } = DefaultTransforms.ToOutput; /// /// Optional callback that is invoked for every agent response. /// - public OrchestrationResponseCallback? ResponseCallback { get; init; } + public Func, ValueTask>? ResponseCallback { get; init; } /// - /// Optional callback that is invoked for every agent response. + /// Optional callback that is invoked for every agent update. /// - public OrchestrationStreamingCallback? StreamingResponseCallback { get; init; } + public Func? StreamingResponseCallback { get; init; } /// /// Gets the list of member targets involved in the orchestration. @@ -108,14 +103,17 @@ public abstract partial class AgentOrchestration /// A cancellation token that can be used to cancel the operation. public async ValueTask> InvokeAsync( TInput input, - IAgentRuntime runtime, + IAgentRuntime? runtime = null, CancellationToken cancellationToken = default) { Throw.IfNull(input, nameof(input)); - TopicId topic = new($"{this.OrchestrationLabel}_{Guid.NewGuid().ToString().Replace("-", string.Empty)}"); + cancellationToken.ThrowIfCancellationRequested(); + + TopicId topic = new($"{this.OrchestrationLabel}_{Guid.NewGuid():N}"); CancellationTokenSource orchestrationCancelSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + cancellationToken = orchestrationCancelSource.Token; OrchestrationContext context = new(this.OrchestrationLabel, @@ -129,9 +127,10 @@ public abstract partial class AgentOrchestration TaskCompletionSource completion = new(); - ActorType orchestrationType = await this.RegisterAsync(runtime, context, completion, handoff: null).ConfigureAwait(false); + InProcessRuntime? temporaryRuntime = null; + runtime ??= temporaryRuntime = InProcessRuntime.StartNew(); - cancellationToken.ThrowIfCancellationRequested(); + ActorType orchestrationType = await this.RegisterAsync(runtime, context, completion, handoff: null).ConfigureAwait(false); logger.LogOrchestrationInvoke(this.OrchestrationLabel, topic); @@ -139,7 +138,7 @@ public abstract partial class AgentOrchestration logger.LogOrchestrationYield(this.OrchestrationLabel, topic); - return new OrchestrationResult(context, completion, orchestrationCancelSource, logger); + return new OrchestrationResult(context, completion, orchestrationCancelSource, logger, temporaryRuntime); } /// @@ -191,24 +190,22 @@ public abstract partial class AgentOrchestration ActorType orchestrationEntry = await runtime.RegisterOrchestrationAgentAsync( this.FormatAgentType(context.Topic, "Boot"), - (agentId, runtime) => - { - RequestActor actor = - new(agentId, - runtime, - context, - this.InputTransform, - completion, - StartAsync, - context.LoggerFactory.CreateLogger()); - return new ValueTask(actor); - }).ConfigureAwait(false); + (agentId, runtime) => + { + RequestActor actor = + new(agentId, + runtime, + context, + this.InputTransform, + completion, + input => this.StartAsync(runtime, context.Topic, input, entryAgent), + context.LoggerFactory.CreateLogger()); + return new ValueTask(actor); + }).ConfigureAwait(false); logger.LogOrchestrationRegistrationDone(context.Orchestration, context.Topic); return orchestrationEntry; - - ValueTask StartAsync(IEnumerable input) => this.StartAsync(runtime, context.Topic, input, entryAgent); } /// @@ -219,12 +216,12 @@ public abstract partial class AgentOrchestration IAgentRuntime runtime, OrchestrationContext context, TaskCompletionSource completion, - OrchestrationOutputTransform outputTransform) + Func, CancellationToken, ValueTask> outputTransform) { /// /// Register the final result type. /// - public async ValueTask RegisterResultTypeAsync(OrchestrationResultTransform resultTransform) + public async ValueTask RegisterResultTypeAsync(Func> resultTransform) { // Register actor for final result ActorType registeredType = diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Logging/AgentOrchestrationLogMessages.cs b/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestrationLogMessages.cs similarity index 100% rename from dotnet/src/Microsoft.Agents.Orchestration/Logging/AgentOrchestrationLogMessages.cs rename to dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestrationLogMessages.cs diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Extensions/RuntimeExtensions.cs b/dotnet/src/Microsoft.Agents.Orchestration/AgentRuntimeExtensions.cs similarity index 75% rename from dotnet/src/Microsoft.Agents.Orchestration/Extensions/RuntimeExtensions.cs rename to dotnet/src/Microsoft.Agents.Orchestration/AgentRuntimeExtensions.cs index 94400a6746..9ae4fcf822 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Extensions/RuntimeExtensions.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/AgentRuntimeExtensions.cs @@ -3,22 +3,19 @@ using System; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.AI.Agents.Runtime; -namespace Microsoft.Agents.Orchestration.Extensions; +namespace Microsoft.Extensions.AI.Agents.Runtime; /// /// Extension methods for . /// -public static class RuntimeExtensions +internal static class AgentRuntimeExtensions { /// /// Sends a message to the specified agent. /// - public static async ValueTask PublishMessageAsync(this IAgentRuntime runtime, object message, ActorType agentType, CancellationToken cancellationToken = default) - { - await runtime.PublishMessageAsync(message, new TopicId(agentType.Name), sender: null, messageId: null, cancellationToken).ConfigureAwait(false); - } + public static ValueTask PublishMessageAsync(this IAgentRuntime runtime, object message, ActorType agentType, CancellationToken cancellationToken = default) => + runtime.PublishMessageAsync(message, new TopicId(agentType.Name), sender: null, messageId: null, cancellationToken); /// /// Registers an agent factory for the specified agent type and associates it with the runtime. @@ -42,10 +39,8 @@ public static class RuntimeExtensions /// /// The runtime for managing the subscription. /// The agent type to subscribe. - public static async Task SubscribeAsync(this IAgentRuntime runtime, ActorType agentType) - { - await runtime.AddSubscriptionAsync(new TypeSubscription(agentType.Name, agentType)).ConfigureAwait(false); - } + public static Task SubscribeAsync(this IAgentRuntime runtime, ActorType agentType) => + runtime.AddSubscriptionAsync(new TypeSubscription(agentType.Name, agentType)).AsTask(); /// /// Subscribes the specified agent type to the provided topics. diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentActor.cs b/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentActor.cs index 5a1a10e7b1..077c3670c9 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentActor.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentActor.cs @@ -7,7 +7,7 @@ using Microsoft.Extensions.AI.Agents; using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; -namespace Microsoft.Agents.Orchestration.Concurrent; +namespace Microsoft.Agents.Orchestration; /// /// An used with the . @@ -37,10 +37,10 @@ internal sealed class ConcurrentActor : AgentActor { this.Logger.LogConcurrentAgentInvoke(this.Id); - ChatMessage response = await this.InvokeAsync(item.Messages, cancellationToken).ConfigureAwait(false); + ChatMessage response = await this.RunAsync(item.Messages, cancellationToken).ConfigureAwait(false); this.Logger.LogConcurrentAgentResult(this.Id, response.Text); - await this.PublishMessageAsync(response.AsResultMessage(), this._handoffActor, cancellationToken).ConfigureAwait(false); + await this.PublishMessageAsync(new ConcurrentMessages.Result(response), this._handoffActor, cancellationToken).ConfigureAwait(false); } } diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentMessages.cs b/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentMessages.cs index 947bfbf7be..c095f8f9c1 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentMessages.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentMessages.cs @@ -3,52 +3,20 @@ using System.Collections.Generic; using Microsoft.Extensions.AI; -namespace Microsoft.Agents.Orchestration.Concurrent; +namespace Microsoft.Agents.Orchestration; /// /// Common messages used by the . /// internal static class ConcurrentMessages { - /// - /// An empty message instance as a default. - /// - public static readonly ChatMessage Empty = new(); - /// /// The input task for a . /// - public sealed class Request - { - /// - /// The request input. - /// - public IList Messages { get; init; } = []; - } + public sealed record Request(IList Messages); /// /// A result from a . /// - public sealed class Result - { - /// - /// The result message. - /// - public ChatMessage Message { get; init; } = Empty; - } - - /// - /// Extension method to convert a to a . - /// - public static Result AsResultMessage(this string text, ChatRole? role = null) => new() { Message = new ChatMessage(role ?? ChatRole.Assistant, text) }; - - /// - /// Extension method to convert a to a . - /// - public static Result AsResultMessage(this ChatMessage message) => new() { Message = message }; - - /// - /// Extension method to convert a collection of to a . - /// - public static Request AsInputMessage(this IEnumerable messages) => new() { Messages = [.. messages] }; + public sealed record Result(ChatMessage Message); } diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentOrchestration.String.cs b/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentOrchestration.String.cs index 4f82f6309f..8e7e57bb31 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentOrchestration.String.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentOrchestration.String.cs @@ -5,7 +5,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.AI.Agents; -namespace Microsoft.Agents.Orchestration.Concurrent; +namespace Microsoft.Agents.Orchestration; /// /// An orchestration that broadcasts the input message to each agent. diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentOrchestration.cs b/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentOrchestration.cs index 6e9e19d0a1..9a2c0d86f6 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentOrchestration.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentOrchestration.cs @@ -3,13 +3,12 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Microsoft.Agents.Orchestration.Extensions; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; -namespace Microsoft.Agents.Orchestration.Concurrent; +namespace Microsoft.Agents.Orchestration; /// /// An orchestration that broadcasts the input message to each agent. @@ -32,7 +31,7 @@ public class ConcurrentOrchestration /// protected override ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable input, ActorType? entryAgent) { - return runtime.PublishMessageAsync(input.AsInputMessage(), topic); + return runtime.PublishMessageAsync(new ConcurrentMessages.Request([.. input]), topic); } /// diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Logging/ConcurrentOrchestrationLogMessages.cs b/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentOrchestrationLogMessages.cs similarity index 96% rename from dotnet/src/Microsoft.Agents.Orchestration/Logging/ConcurrentOrchestrationLogMessages.cs rename to dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentOrchestrationLogMessages.cs index eb76da967b..d74eea7965 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Logging/ConcurrentOrchestrationLogMessages.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentOrchestrationLogMessages.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.Diagnostics.CodeAnalysis; -using Microsoft.Agents.Orchestration.Concurrent; using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentResultActor.cs b/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentResultActor.cs index db5bbcb573..c8741501a4 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentResultActor.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentResultActor.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; -namespace Microsoft.Agents.Orchestration.Concurrent; +namespace Microsoft.Agents.Orchestration; /// /// Actor for capturing each message. diff --git a/dotnet/src/Microsoft.Agents.Orchestration/DefaultTransforms.cs b/dotnet/src/Microsoft.Agents.Orchestration/DefaultTransforms.cs new file mode 100644 index 0000000000..9b24dcb31b --- /dev/null +++ b/dotnet/src/Microsoft.Agents.Orchestration/DefaultTransforms.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.Orchestration; + +internal static class DefaultTransforms +{ + public static ValueTask> FromInput(TInput input, CancellationToken cancellationToken = default) => + new(input switch + { + IEnumerable messages => messages, + ChatMessage message => [message], + string text => [new ChatMessage(ChatRole.User, text)], + _ => [new ChatMessage(ChatRole.User, JsonSerializer.Serialize(input))] + }); + + public static ValueTask ToOutput(IList result, CancellationToken cancellationToken = default) + { + bool isSingleResult = result.Count == 1; + + TOutput output = + GetDefaultOutput() ?? + GetObjectOutput() ?? + throw new InvalidOperationException($"Unable to transform output to {typeof(TOutput)}."); + + return new ValueTask(output); + + TOutput? GetObjectOutput() + { + if (isSingleResult) + { + try + { + return JsonSerializer.Deserialize(result[0].Text); + } + catch (JsonException) + { + } + } + + return default; + } + + TOutput? GetDefaultOutput() + { + if (typeof(TOutput).IsInstanceOfType(result)) + { + return (TOutput)(object)result; + } + + if (isSingleResult) + { + if (typeof(ChatMessage).IsAssignableFrom(typeof(TOutput))) + { + return (TOutput)(object)result[0]; + } + + if (typeof(string) == typeof(TOutput)) + { + return (TOutput)(object)(result[0].Text ?? string.Empty); + } + } + + return default; + } + } +} diff --git a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatAgentActor.cs b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatAgentActor.cs index c71d8dc398..458fcea9eb 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatAgentActor.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatAgentActor.cs @@ -8,7 +8,7 @@ using Microsoft.Extensions.AI.Agents; using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; -namespace Microsoft.Agents.Orchestration.GroupChat; +namespace Microsoft.Agents.Orchestration; /// /// An used with the . @@ -30,32 +30,20 @@ internal sealed class GroupChatAgentActor : AgentActor { this._cache = []; - this.RegisterMessageHandler(this.HandleAsync); - this.RegisterMessageHandler(this.HandleAsync); + this.RegisterMessageHandler((item, ctx) => this._cache.AddRange(item.Messages)); + this.RegisterMessageHandler((item, ctx) => this.ResetThread()); this.RegisterMessageHandler(this.HandleAsync); } - private ValueTask HandleAsync(GroupChatMessages.Group item, MessageContext messageContext, CancellationToken cancellationToken) - { - this._cache.AddRange(item.Messages); - return default; - } - - private ValueTask HandleAsync(GroupChatMessages.Reset item, MessageContext messageContext, CancellationToken cancellationToken) - { - this.ResetThread(); - return default; - } - private async ValueTask HandleAsync(GroupChatMessages.Speak item, MessageContext messageContext, CancellationToken cancellationToken) { this.Logger.LogChatAgentInvoke(this.Id); - ChatMessage response = await this.InvokeAsync(this._cache, cancellationToken).ConfigureAwait(false); + ChatMessage response = await this.RunAsync(this._cache, cancellationToken).ConfigureAwait(false); this.Logger.LogChatAgentResult(this.Id, response.Text); this._cache.Clear(); - await this.PublishMessageAsync(response.AsGroupMessage(), this.Context.Topic, cancellationToken: cancellationToken).ConfigureAwait(false); + await this.PublishMessageAsync(new GroupChatMessages.Group([response]), this.Context.Topic, cancellationToken: cancellationToken).ConfigureAwait(false); } } diff --git a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatManager.cs b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatManager.cs index de6999d928..8063475e8c 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatManager.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatManager.cs @@ -5,7 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; -namespace Microsoft.Agents.Orchestration.GroupChat; +namespace Microsoft.Agents.Orchestration; /// /// Represents the result of a group chat manager operation, including a value and a reason. diff --git a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatManagerActor.cs b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatManagerActor.cs index 69eb38972b..15a995f51d 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatManagerActor.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatManagerActor.cs @@ -7,7 +7,7 @@ using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; -namespace Microsoft.Agents.Orchestration.GroupChat; +namespace Microsoft.Agents.Orchestration; /// /// An used to manage a . @@ -52,7 +52,7 @@ internal sealed class GroupChatManagerActor : OrchestrationActor this._chat.AddRange(item.Messages); - await this.PublishMessageAsync(item.Messages.AsGroupMessage(), this.Context.Topic, cancellationToken: cancellationToken).ConfigureAwait(false); + await this.PublishMessageAsync(new GroupChatMessages.Group(item.Messages), this.Context.Topic, cancellationToken: cancellationToken).ConfigureAwait(false); await this.ManageAsync(messageContext, cancellationToken).ConfigureAwait(false); } @@ -78,7 +78,7 @@ internal sealed class GroupChatManagerActor : OrchestrationActor ChatMessage input = await this._manager.InteractiveCallback.Invoke().ConfigureAwait(false); this.Logger.LogChatManagerUserInput(this.Id, input.Text); this._chat.Add(input); - await this.PublishMessageAsync(input.AsGroupMessage(), this.Context.Topic, cancellationToken: cancellationToken).ConfigureAwait(false); + await this.PublishMessageAsync(new GroupChatMessages.Group([input]), this.Context.Topic, cancellationToken: cancellationToken).ConfigureAwait(false); } } @@ -88,7 +88,7 @@ internal sealed class GroupChatManagerActor : OrchestrationActor { GroupChatManagerResult filterResult = await this._manager.FilterResults(this._chat, cancellationToken).ConfigureAwait(false); this.Logger.LogChatManagerResult(this.Id, filterResult.Value, filterResult.Reason); - await this.PublishMessageAsync(filterResult.Value.AsResultMessage(), this._orchestrationType, cancellationToken).ConfigureAwait(false); + await this.PublishMessageAsync(new GroupChatMessages.Result(new(ChatRole.Assistant, filterResult.Value)), this._orchestrationType, cancellationToken).ConfigureAwait(false); return; } diff --git a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatMessages.cs b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatMessages.cs index f4bc53e9ac..a70236e70b 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatMessages.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatMessages.cs @@ -3,28 +3,17 @@ using System.Collections.Generic; using Microsoft.Extensions.AI; -namespace Microsoft.Agents.Orchestration.GroupChat; +namespace Microsoft.Agents.Orchestration; /// /// Common messages used for agent chat patterns. /// -public static class GroupChatMessages +internal static class GroupChatMessages { - /// - /// An empty message instance as a default. - /// - internal static readonly ChatMessage Empty = new(); - /// /// Broadcast a message to all . /// - public sealed class Group - { - /// - /// The chat message being broadcast. - /// - public IEnumerable Messages { get; init; } = []; - } + public sealed record Group(IEnumerable Messages); /// /// Reset/clear the conversation history for all . @@ -34,13 +23,7 @@ public static class GroupChatMessages /// /// The final result. /// - public sealed class Result - { - /// - /// The chat response message. - /// - public ChatMessage Message { get; init; } = Empty; - } + public sealed record Result(ChatMessage Message); /// /// Signal a to respond. @@ -50,36 +33,11 @@ public static class GroupChatMessages /// /// The input task. /// - public sealed class InputTask + public sealed record InputTask(IEnumerable Messages) { /// - /// A task that does not require any action. + /// Gets an input task that does not require any action. /// - public static readonly InputTask None = new(); - - /// - /// The input that defines the task goal. - /// - public IEnumerable Messages { get; init; } = []; + public static InputTask None { get; } = new([]); } - - /// - /// Extension method to convert a to a . - /// - public static Group AsGroupMessage(this ChatMessage message) => new() { Messages = [message] }; - - /// - /// Extension method to convert a to a . - /// - public static Group AsGroupMessage(this IEnumerable messages) => new() { Messages = messages }; - - /// - /// Extension method to convert a to a . - /// - public static InputTask AsInputTaskMessage(this IEnumerable messages) => new() { Messages = messages }; - - /// - /// Extension method to convert a to a . - /// - public static Result AsResultMessage(this string text) => new() { Message = new(ChatRole.Assistant, text) }; } diff --git a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatOrchestration.String.cs b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatOrchestration.String.cs index f1a17142ee..2392d7251c 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatOrchestration.String.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatOrchestration.String.cs @@ -2,7 +2,7 @@ using Microsoft.Extensions.AI.Agents; -namespace Microsoft.Agents.Orchestration.GroupChat; +namespace Microsoft.Agents.Orchestration; /// /// An orchestration that broadcasts the input message to each agent. diff --git a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatOrchestration.cs b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatOrchestration.cs index e921eb7362..f93fee7bbb 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatOrchestration.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatOrchestration.cs @@ -3,14 +3,13 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Microsoft.Agents.Orchestration.Extensions; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; using Microsoft.Shared.Diagnostics; -namespace Microsoft.Agents.Orchestration.GroupChat; +namespace Microsoft.Agents.Orchestration; /// /// An orchestration that coordinates a group-chat. @@ -42,7 +41,7 @@ public class GroupChatOrchestration : { throw new ArgumentException("Entry agent is not defined.", nameof(entryAgent)); } - return runtime.PublishMessageAsync(input.AsInputTaskMessage(), entryAgent.Value); + return runtime.PublishMessageAsync(new GroupChatMessages.InputTask(input), entryAgent.Value); } /// diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Logging/GroupChatOrchestrationLogMessages.cs b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatOrchestrationLogMessages.cs similarity index 98% rename from dotnet/src/Microsoft.Agents.Orchestration/Logging/GroupChatOrchestrationLogMessages.cs rename to dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatOrchestrationLogMessages.cs index 6f06c39dee..83446d778f 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Logging/GroupChatOrchestrationLogMessages.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatOrchestrationLogMessages.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.Diagnostics.CodeAnalysis; -using Microsoft.Agents.Orchestration.GroupChat; using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; diff --git a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatTeam.cs b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatTeam.cs index 9409c5561a..fcf844a18b 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatTeam.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatTeam.cs @@ -4,29 +4,22 @@ using System; using System.Collections.Generic; using System.Linq; -namespace Microsoft.Agents.Orchestration.GroupChat; +namespace Microsoft.Agents.Orchestration; /// /// Describes a team of agents participating in a group chat. /// -public class GroupChatTeam : Dictionary; - -/// -/// Extensions for . -/// -public static class ChatGroupExtensions +public sealed class GroupChatTeam : Dictionary { /// /// Format the names of the agents in the team as a comma delimimted list. /// - /// The agent team /// A comma delimimted list of agent name. - public static string FormatNames(this GroupChatTeam team) => string.Join(",", team.Select(t => t.Key)); + public string FormatNames() => string.Join(",", this.Select(t => t.Key)); /// /// Format the names and descriptions of the agents in the team as a markdown list. /// - /// The agent team /// A markdown list of agent names and descriptions. - public static string FormatList(this GroupChatTeam team) => string.Join(Environment.NewLine, team.Select(t => $"- {t.Key}: {t.Value.Description}")); + public string FormatList() => string.Join(Environment.NewLine, this.Select(t => $"- {t.Key}: {t.Value.Description}")); } diff --git a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/RoundRobinGroupChatManager.cs b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/RoundRobinGroupChatManager.cs index cb65c6c7e9..96170bf341 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/RoundRobinGroupChatManager.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/RoundRobinGroupChatManager.cs @@ -6,7 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; -namespace Microsoft.Agents.Orchestration.GroupChat; +namespace Microsoft.Agents.Orchestration; /// /// A that selects agents in a round-robin fashion. diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffActor.cs b/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffActor.cs index 8d699e9bda..da7fca1f42 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffActor.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffActor.cs @@ -9,7 +9,7 @@ using Microsoft.Extensions.AI.Agents; using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; -namespace Microsoft.Agents.Orchestration.Handoff; +namespace Microsoft.Agents.Orchestration; /// /// An actor used with the . @@ -54,48 +54,35 @@ internal sealed class HandoffActor : AgentActor ToolMode = ChatToolMode.Auto }; - this.RegisterMessageHandler(this.HandleAsync); + this.RegisterMessageHandler(this.Handle); this.RegisterMessageHandler(this.HandleAsync); - this.RegisterMessageHandler(this.HandleAsync); + this.RegisterMessageHandler(this.Handle); } /// - protected override Task InvokeAsync( - IReadOnlyCollection messages, - AgentRunOptions options, - CancellationToken cancellationToken = default) => - this._chatAgent.RunAsync( - [.. messages], - this.Thread, - options, - this._options, - cancellationToken); + protected override Task InvokeCoreAsync( + IReadOnlyCollection messages, AgentRunOptions? options, CancellationToken cancellationToken) => + this._chatAgent.RunAsync([.. messages], this.Thread, options, this._options, cancellationToken); /// - protected override IAsyncEnumerable InvokeStreamingAsync(IReadOnlyCollection messages, AgentRunOptions options, CancellationToken cancellationToken) => - this._chatAgent.RunStreamingAsync( - messages, - this.Thread, - options, - this._options, - cancellationToken); + protected override IAsyncEnumerable InvokeStreamingCoreAsync( + IReadOnlyCollection messages, AgentRunOptions? options, CancellationToken cancellationToken) => + this._chatAgent.RunStreamingAsync(messages, this.Thread, options, this._options, cancellationToken); /// /// Gets or sets the callback to be invoked for interactive input. /// public OrchestrationInteractiveCallback? InteractiveCallback { get; init; } - private ValueTask HandleAsync(HandoffMessages.InputTask item, MessageContext messageContext, CancellationToken cancellationToken) + private void Handle(HandoffMessages.InputTask item, MessageContext messageContext) { this._taskSummary = null; this._cache.AddRange(item.Messages); - return default; } - private ValueTask HandleAsync(HandoffMessages.Response item, MessageContext messageContext, CancellationToken cancellationToken) + private void Handle(HandoffMessages.Response item, MessageContext messageContext) { this._cache.Add(item.Message); - return default; } private async ValueTask HandleAsync(HandoffMessages.Request item, MessageContext messageContext, CancellationToken cancellationToken) @@ -109,7 +96,7 @@ internal sealed class HandoffActor : AgentActor ChatMessage response; try { - response = await this.InvokeAsync(this._cache, cancellationToken).ConfigureAwait(false); + response = await this.RunAsync(this._cache, cancellationToken).ConfigureAwait(false); } catch (Exception exception) { @@ -126,7 +113,7 @@ internal sealed class HandoffActor : AgentActor // Since we don't want to publish that message, so we only publish if the response is an ASSISTANT message. if (response.Role == ChatRole.Assistant) { - await this.PublishMessageAsync(new HandoffMessages.Response { Message = response }, this.Context.Topic, messageId: null, cancellationToken).ConfigureAwait(false); + await this.PublishMessageAsync(new HandoffMessages.Response(response), this.Context.Topic, messageId: null, cancellationToken).ConfigureAwait(false); } if (this._handoffAgent != null) @@ -141,7 +128,7 @@ internal sealed class HandoffActor : AgentActor if (this.InteractiveCallback != null && this._taskSummary == null) { ChatMessage input = await this.InteractiveCallback().ConfigureAwait(false); - await this.PublishMessageAsync(new HandoffMessages.Response { Message = input }, this.Context.Topic, messageId: null, cancellationToken).ConfigureAwait(false); + await this.PublishMessageAsync(new HandoffMessages.Response(input), this.Context.Topic, messageId: null, cancellationToken).ConfigureAwait(false); this._cache.Add(input); continue; } @@ -187,7 +174,7 @@ internal sealed class HandoffActor : AgentActor { this.Logger.LogHandoffSummary(this.Id, summary); this._taskSummary = summary; - await this.PublishMessageAsync(new HandoffMessages.Result { Message = new ChatMessage(ChatRole.Assistant, summary) }, this._resultHandoff, cancellationToken).ConfigureAwait(false); + await this.PublishMessageAsync(new HandoffMessages.Result(new(ChatRole.Assistant, summary)), this._resultHandoff, cancellationToken).ConfigureAwait(false); if (FunctionInvokingChatClient.CurrentContext is not null) { diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffMessages.cs b/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffMessages.cs index a4764871c1..a4b68b3bb0 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffMessages.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffMessages.cs @@ -3,39 +3,22 @@ using System.Collections.Generic; using Microsoft.Extensions.AI; -namespace Microsoft.Agents.Orchestration.Handoff; +namespace Microsoft.Agents.Orchestration; /// /// A message that describes the input task and captures results for a . /// internal static class HandoffMessages { - /// - /// An empty message instance as a default. - /// - internal static readonly ChatMessage Empty = new(); - /// /// The input message. /// - public sealed class InputTask - { - /// - /// The orchestration input messages. - /// - public IList Messages { get; init; } = []; - } + public sealed record InputTask(IList Messages); /// /// The final result. /// - public sealed class Result - { - /// - /// The orchestration result message. - /// - public ChatMessage Message { get; init; } = Empty; - } + public sealed record Result(ChatMessage Message); /// /// Signals the handoff to another agent. @@ -45,21 +28,5 @@ internal static class HandoffMessages /// /// Broadcast an agent response to all actors in the orchestration. /// - public sealed class Response - { - /// - /// The chat response message. - /// - public ChatMessage Message { get; init; } = Empty; - } - - /// - /// Extension method to convert a to a . - /// - public static InputTask AsInputTaskMessage(this IEnumerable messages) => new() { Messages = [.. messages] }; - - /// - /// Extension method to convert a to a . - /// - public static Result AsResultMessage(this ChatMessage message) => new() { Message = message }; + public sealed record Response(ChatMessage Message); } diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffOrchestration.String.cs b/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffOrchestration.String.cs index bd698778f9..203b9cdb7c 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffOrchestration.String.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffOrchestration.String.cs @@ -2,7 +2,7 @@ using Microsoft.Extensions.AI.Agents; -namespace Microsoft.Agents.Orchestration.Handoff; +namespace Microsoft.Agents.Orchestration; /// /// An orchestration that passes the input message to the first agent, and diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffOrchestration.cs b/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffOrchestration.cs index b90063aa81..bbeff667e1 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffOrchestration.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffOrchestration.cs @@ -4,13 +4,12 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Microsoft.Agents.Orchestration.Extensions; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; -namespace Microsoft.Agents.Orchestration.Handoff; +namespace Microsoft.Agents.Orchestration; /// /// An orchestration that provides the input message to the first agent @@ -24,17 +23,21 @@ public class HandoffOrchestration : AgentOrchestration class. /// /// Defines the handoff connections for each agent. - /// The agents participating in the orchestration. - public HandoffOrchestration(OrchestrationHandoffs handoffs, params Agent[] agents) - : base(agents) + /// Additional agents participating in the orchestration that weren't passed to . + public HandoffOrchestration(OrchestrationHandoffs handoffs, params Agent[] agents) : base( + agents is { Length: 0 } ? handoffs.Agents.ToArray() : + handoffs.Agents is { Count: 0 } ? agents : + handoffs.Agents.Concat(agents).Distinct().ToArray()) { // Create list of distinct agent names - HashSet agentNames = new(agents.Select(a => a.Name ?? a.Id), StringComparer.Ordinal) + HashSet agentNames = new(base.Members.Select(a => a.Name ?? a.Id), StringComparer.Ordinal) { handoffs.FirstAgentName }; + // Extract names from handoffs that don't align with a member agent. string[] badNames = [.. handoffs.Keys.Concat(handoffs.Values.SelectMany(h => h.Keys)).Where(name => !agentNames.Contains(name))]; + // Fail fast if invalid names are present. if (badNames.Length > 0) { @@ -56,7 +59,7 @@ public class HandoffOrchestration : AgentOrchestration /// Defines the handoff relationships for a given agent. @@ -38,7 +38,9 @@ public sealed class OrchestrationHandoffs : Dictionary /// The first agent to be invoked (prior to any handoff). public OrchestrationHandoffs(Agent firstAgent) : this(firstAgent.Name ?? firstAgent.Id) - { } + { + this.Agents.Add(firstAgent); + } /// /// Initializes a new instance of the class with no handoff relationships. @@ -62,26 +64,19 @@ public sealed class OrchestrationHandoffs : Dictionary /// The source agent. /// The updated instance. public static OrchestrationHandoffs StartWith(Agent source) => new(source); -} -/// -/// Extension methods for building and modifying relationships. -/// -public static class OrchestrationHandoffsExtensions -{ /// /// Adds handoff relationships from a source agent to one or more target agents. /// Each target agent's name or ID is mapped to its description. /// - /// The orchestration handoffs collection to update. /// The source agent. /// The target agents to add as handoff targets for the source agent. /// The updated instance. - public static OrchestrationHandoffs Add(this OrchestrationHandoffs handoffs, Agent source, params Agent[] targets) + public OrchestrationHandoffs Add(Agent source, params Agent[] targets) { string key = source.Name ?? source.Id; - AgentHandoffs agentHandoffs = handoffs.GetAgentHandoffs(key); + AgentHandoffs agentHandoffs = this.GetAgentHandoffs(key); foreach (Agent target in targets) { @@ -90,60 +85,61 @@ public static class OrchestrationHandoffsExtensions throw new InvalidOperationException($"The provided target agent with Id '{target.Id}' has no description or name, and no handoff description has been provided. At least one of these are required to register a handoff so that the appropriate target agent can be chosen."); } + this.Agents.Add(target); agentHandoffs[target.Name ?? target.Id] = target.Description ?? target.Name!; } - return handoffs; + this.Agents.Add(source); + + return this; } /// /// Adds a handoff relationship from a source agent to a target agent with a custom description. /// - /// The orchestration handoffs collection to update. /// The source agent. /// The target agent. /// The handoff description. /// The updated instance. - public static OrchestrationHandoffs Add(this OrchestrationHandoffs handoffs, Agent source, Agent target, string description) - => handoffs.Add(source.Name ?? source.Id, target.Name ?? target.Id, description); + public OrchestrationHandoffs Add(Agent source, Agent target, string description) + => this.Add(source.Name ?? source.Id, target.Name ?? target.Id, description); /// /// Adds a handoff relationship from a source agent to a target agent name/ID with a custom description. /// - /// The orchestration handoffs collection to update. /// The source agent. /// The target agent's name or ID. /// The handoff description. /// The updated instance. - public static OrchestrationHandoffs Add(this OrchestrationHandoffs handoffs, Agent source, string targetName, string description) - => handoffs.Add(source.Name ?? source.Id, targetName, description); + public OrchestrationHandoffs Add(Agent source, string targetName, string description) + => this.Add(source.Name ?? source.Id, targetName, description); /// /// Adds a handoff relationship from a source agent name/ID to a target agent name/ID with a custom description. /// - /// The orchestration handoffs collection to update. /// The source agent's name or ID. /// The target agent's name or ID. /// The handoff description. /// The updated instance. - public static OrchestrationHandoffs Add(this OrchestrationHandoffs handoffs, string sourceName, string targetName, string description) + public OrchestrationHandoffs Add(string sourceName, string targetName, string description) { - AgentHandoffs agentHandoffs = handoffs.GetAgentHandoffs(sourceName); + AgentHandoffs agentHandoffs = this.GetAgentHandoffs(sourceName); agentHandoffs[targetName] = description; - return handoffs; + return this; } - private static AgentHandoffs GetAgentHandoffs(this OrchestrationHandoffs handoffs, string key) + private AgentHandoffs GetAgentHandoffs(string key) { - if (!handoffs.TryGetValue(key, out AgentHandoffs? agentHandoffs)) + if (!this.TryGetValue(key, out AgentHandoffs? agentHandoffs)) { - agentHandoffs = []; - handoffs[key] = agentHandoffs; + this[key] = agentHandoffs = []; } return agentHandoffs; } + + internal HashSet Agents { get; } = []; } /// diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Logging/OrchestrationResultLogMessages.cs b/dotnet/src/Microsoft.Agents.Orchestration/Logging/OrchestrationResultLogMessages.cs deleted file mode 100644 index 98760093b7..0000000000 --- a/dotnet/src/Microsoft.Agents.Orchestration/Logging/OrchestrationResultLogMessages.cs +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Diagnostics.CodeAnalysis; -using Microsoft.Extensions.AI.Agents.Runtime; -using Microsoft.Extensions.Logging; - -namespace Microsoft.Agents.Orchestration; - -/// -/// Extensions for logging . -/// -/// -/// This extension uses the to -/// generate logging code at compile time to achieve optimized code. -/// -[ExcludeFromCodeCoverage] -internal static partial class OrchestrationResultLogMessages -{ - /// - /// Logs awaiting the orchestration. - /// - [LoggerMessage( - Level = LogLevel.Trace, - Message = "AWAIT {Orchestration}: {Topic}")] - public static partial void LogOrchestrationResultAwait( - this ILogger logger, - string orchestration, - TopicId topic); - - /// - /// Logs timeout while awaiting the orchestration. - /// - [LoggerMessage( - Level = LogLevel.Error, - Message = "TIMEOUT {Orchestration}: {Topic}")] - public static partial void LogOrchestrationResultTimeout( - this ILogger logger, - string orchestration, - TopicId topic); - - /// - /// Logs cancelled the orchestration. - /// - [LoggerMessage( - Level = LogLevel.Error, - Message = "CANCELLED {Orchestration}: {Topic}")] - public static partial void LogOrchestrationResultCancelled( - this ILogger logger, - string orchestration, - TopicId topic); - - /// - /// Logs the awaited the orchestration has completed. - /// - [LoggerMessage( - Level = LogLevel.Trace, - Message = "COMPLETE {Orchestration}: {Topic}")] - public static partial void LogOrchestrationResultComplete( - this ILogger logger, - string orchestration, - TopicId topic); -} diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Marker.cs b/dotnet/src/Microsoft.Agents.Orchestration/Marker.cs deleted file mode 100644 index 061b173c01..0000000000 --- a/dotnet/src/Microsoft.Agents.Orchestration/Marker.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -#if !NET5_0_OR_GREATER -using System.ComponentModel; - -namespace System.Runtime.CompilerServices; - -[EditorBrowsable(EditorBrowsableState.Never)] -internal static class IsExternalInit { } -#endif diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Microsoft.Agents.Orchestration.csproj b/dotnet/src/Microsoft.Agents.Orchestration/Microsoft.Agents.Orchestration.csproj index 3be18dcea3..78878572cc 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Microsoft.Agents.Orchestration.csproj +++ b/dotnet/src/Microsoft.Agents.Orchestration/Microsoft.Agents.Orchestration.csproj @@ -10,6 +10,7 @@ true + true @@ -23,7 +24,6 @@ - diff --git a/dotnet/src/Microsoft.Agents.Orchestration/OrchestrationActor.cs b/dotnet/src/Microsoft.Agents.Orchestration/OrchestrationActor.cs index 165a83f766..651cd10016 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/OrchestrationActor.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/OrchestrationActor.cs @@ -33,11 +33,9 @@ public abstract class OrchestrationActor : RuntimeActor /// The recipient agent's type. /// A token used to cancel the operation if needed. /// The agent identifier, if it exists. - protected async ValueTask PublishMessageAsync( + protected ValueTask PublishMessageAsync( object message, ActorType agentType, - CancellationToken cancellationToken = default) - { - await base.PublishMessageAsync(message, new TopicId(agentType.Name), messageId: null, cancellationToken).ConfigureAwait(false); - } + CancellationToken cancellationToken = default) => + base.PublishMessageAsync(message, new TopicId(agentType.Name), messageId: null, cancellationToken); } diff --git a/dotnet/src/Microsoft.Agents.Orchestration/OrchestrationContext.cs b/dotnet/src/Microsoft.Agents.Orchestration/OrchestrationContext.cs index 3f6a439fd8..84e4e318a5 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/OrchestrationContext.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/OrchestrationContext.cs @@ -1,6 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI.Agents; using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; @@ -14,17 +19,17 @@ public sealed class OrchestrationContext internal OrchestrationContext( string orchestration, TopicId topic, - OrchestrationResponseCallback? responseCallback, - OrchestrationStreamingCallback? streamingCallback, + Func, ValueTask>? responseCallback, + Func? streamingCallback, ILoggerFactory loggerFactory, - CancellationToken cancellation) + CancellationToken cancellationToken) { this.Orchestration = orchestration; this.Topic = topic; this.ResponseCallback = responseCallback; this.StreamingResponseCallback = streamingCallback; this.LoggerFactory = loggerFactory; - this.Cancellation = cancellation; + this.CancellationToken = cancellationToken; } /// @@ -43,7 +48,7 @@ public sealed class OrchestrationContext /// /// Gets the cancellation token that can be used to observe cancellation requests for the orchestration. /// - public CancellationToken Cancellation { get; } + public CancellationToken CancellationToken { get; } /// /// Gets the associated logger factory for creating loggers within the orchestration context. @@ -53,10 +58,10 @@ public sealed class OrchestrationContext /// /// Optional callback that is invoked for every agent response. /// - public OrchestrationResponseCallback? ResponseCallback { get; } + public Func, ValueTask>? ResponseCallback { get; } /// /// Optional callback that is invoked for every agent response. /// - public OrchestrationStreamingCallback? StreamingResponseCallback { get; } + public Func? StreamingResponseCallback { get; } } diff --git a/dotnet/src/Microsoft.Agents.Orchestration/OrchestrationResult.cs b/dotnet/src/Microsoft.Agents.Orchestration/OrchestrationResult.cs index c5de6b2e8d..c898a0c9c2 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/OrchestrationResult.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/OrchestrationResult.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.ComponentModel; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI.Agents.Runtime; @@ -13,31 +15,39 @@ namespace Microsoft.Agents.Orchestration; /// This class encapsulates the asynchronous completion of an orchestration process. /// /// The type of the value produced by the orchestration. -public sealed class OrchestrationResult : IDisposable +public sealed partial class OrchestrationResult : IAsyncDisposable { private readonly OrchestrationContext _context; private readonly CancellationTokenSource _cancelSource; private readonly TaskCompletionSource _completion; private readonly ILogger _logger; + private readonly IAsyncDisposable? _additionalDisposable; private bool _isDisposed; - internal OrchestrationResult(OrchestrationContext context, TaskCompletionSource completion, CancellationTokenSource orchestrationCancelSource, ILogger logger) + internal OrchestrationResult(OrchestrationContext context, TaskCompletionSource completion, CancellationTokenSource orchestrationCancelSource, ILogger logger, IAsyncDisposable? additionalDisposable = null) { this._cancelSource = orchestrationCancelSource; this._context = context; this._completion = completion; this._logger = logger; + this._additionalDisposable = additionalDisposable; } /// /// Releases all resources used by the instance. /// - public void Dispose() + public async ValueTask DisposeAsync() { if (!this._isDisposed) { - this._cancelSource.Dispose(); this._isDisposed = true; + + this._cancelSource.Dispose(); + + if (this._additionalDisposable is { } ad) + { + await ad.DisposeAsync().ConfigureAwait(false); + } } } @@ -52,42 +62,9 @@ public sealed class OrchestrationResult : IDisposable public TopicId Topic => this._context.Topic; /// - /// Asynchronously retrieves the orchestration result value. - /// If a timeout is specified, the method will throw a - /// if the orchestration does not complete within the allotted time. + /// Gets a task that represents the completion of the orchestration result. /// - /// An optional representing the maximum wait duration. - /// A cancellation token that can be used to cancel the operation. - /// A representing the result of the orchestration. - /// Thrown if this instance has been disposed. - /// Thrown if the orchestration does not complete within the specified timeout period. - public async ValueTask GetValueAsync(TimeSpan? timeout = null, CancellationToken cancellationToken = default) - { -#if NET - ObjectDisposedException.ThrowIf(this._isDisposed, this); -#else - if (this._isDisposed) - { - throw new ObjectDisposedException(this.GetType().Name); - } -#endif - - this._logger.LogOrchestrationResultAwait(this.Orchestration, this.Topic); - - if (timeout.HasValue) - { - Task[] tasks = [this._completion.Task]; - if (!Task.WaitAll(tasks, timeout.Value)) - { - this._logger.LogOrchestrationResultTimeout(this.Orchestration, this.Topic); - throw new TimeoutException($"Orchestration did not complete within the allowed duration ({timeout})."); - } - } - - this._logger.LogOrchestrationResultComplete(this.Orchestration, this.Topic); - - return await this._completion.Task.ConfigureAwait(false); - } + public Task Task => this._completion.Task; /// /// Cancel the orchestration associated with this result. @@ -108,8 +85,21 @@ public sealed class OrchestrationResult : IDisposable } #endif - this._logger.LogOrchestrationResultCancelled(this.Orchestration, this.Topic); + this.LogOrchestrationResultCanceled(this.Orchestration, this.Topic); this._cancelSource.Cancel(); - this._completion.SetCanceled(); } + + /// Enable directly awaiting an by using 's awaiter. + [EditorBrowsable(EditorBrowsableState.Never)] + public TaskAwaiter GetAwaiter() => this.Task.GetAwaiter(); + + /// + /// Logs canceled the orchestration. + /// + [LoggerMessage( + Level = LogLevel.Error, + Message = "CANCELED {Orchestration}: {Topic}")] + private partial void LogOrchestrationResultCanceled( + string orchestration, + TopicId topic); } diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Sequential/SequentialActor.cs b/dotnet/src/Microsoft.Agents.Orchestration/Sequential/SequentialActor.cs index 6be2d40fdc..9d5be2c91b 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Sequential/SequentialActor.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Sequential/SequentialActor.cs @@ -8,7 +8,7 @@ using Microsoft.Extensions.AI.Agents; using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; -namespace Microsoft.Agents.Orchestration.Sequential; +namespace Microsoft.Agents.Orchestration; /// /// An actor used with the . @@ -48,10 +48,10 @@ internal sealed class SequentialActor : AgentActor this.Logger.LogSequentialAgentInvoke(this.Id); - ChatMessage response = await this.InvokeAsync(input, cancellationToken).ConfigureAwait(false); + ChatMessage response = await this.RunAsync(input, cancellationToken).ConfigureAwait(false); this.Logger.LogSequentialAgentResult(this.Id, response.Text); - await this.PublishMessageAsync(response.AsResponseMessage(), this._nextAgent, cancellationToken: cancellationToken).ConfigureAwait(false); + await this.PublishMessageAsync(new SequentialMessages.Response(response), this._nextAgent, cancellationToken: cancellationToken).ConfigureAwait(false); } } diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Sequential/SequentialMessages.cs b/dotnet/src/Microsoft.Agents.Orchestration/Sequential/SequentialMessages.cs index ec9fae3a0c..632e511222 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Sequential/SequentialMessages.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Sequential/SequentialMessages.cs @@ -3,58 +3,20 @@ using System.Collections.Generic; using Microsoft.Extensions.AI; -namespace Microsoft.Agents.Orchestration.Sequential; +namespace Microsoft.Agents.Orchestration; /// /// A message that describes the input task and captures results for a . /// internal static class SequentialMessages { - /// - /// An empty message instance as a default. - /// - public static readonly ChatMessage Empty = new(); - /// /// Represents a request containing a sequence of chat messages to be processed by the sequential orchestration. /// - public sealed class Request - { - /// - /// The request input. - /// - public IList Messages { get; init; } = []; - } + public sealed record Request(IList Messages); /// /// Represents a response containing the result message from the sequential orchestration. /// - public sealed class Response - { - /// - /// The response message. - /// - public ChatMessage Message { get; init; } = Empty; - } - - /// - /// Extension method to convert a to a . - /// - /// The chat message to include in the request. - /// A containing the provided messages. - public static Request AsRequestMessage(this ChatMessage message) => new() { Messages = [message] }; - - /// - /// Extension method to convert a collection of to a . - /// - /// The collection of chat messages to include in the request. - /// A containing the provided messages. - public static Request AsRequestMessage(this IEnumerable messages) => new() { Messages = [.. messages] }; - - /// - /// Extension method to convert a to a . - /// - /// The chat message to include in the response. - /// A containing the provided message. - public static Response AsResponseMessage(this ChatMessage message) => new() { Message = message }; + public sealed record Response(ChatMessage Message); } diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Sequential/SequentialOrchestration.String.cs b/dotnet/src/Microsoft.Agents.Orchestration/Sequential/SequentialOrchestration.String.cs index a44a2c372f..53b1077d4e 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Sequential/SequentialOrchestration.String.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Sequential/SequentialOrchestration.String.cs @@ -2,7 +2,7 @@ using Microsoft.Extensions.AI.Agents; -namespace Microsoft.Agents.Orchestration.Sequential; +namespace Microsoft.Agents.Orchestration; /// /// An orchestration that passes the input message to the first agent, and diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Sequential/SequentialOrchestration.cs b/dotnet/src/Microsoft.Agents.Orchestration/Sequential/SequentialOrchestration.cs index c8dba0116e..26ec06f814 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Sequential/SequentialOrchestration.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Sequential/SequentialOrchestration.cs @@ -3,13 +3,12 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Microsoft.Agents.Orchestration.Extensions; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; -namespace Microsoft.Agents.Orchestration.Sequential; +namespace Microsoft.Agents.Orchestration; /// /// An orchestration that provides the input message to the first agent @@ -33,7 +32,7 @@ public class SequentialOrchestration : AgentOrchestration diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Logging/SequentialOrchestrationLogMessages.cs b/dotnet/src/Microsoft.Agents.Orchestration/Sequential/SequentialOrchestrationLogMessages.cs similarity index 95% rename from dotnet/src/Microsoft.Agents.Orchestration/Logging/SequentialOrchestrationLogMessages.cs rename to dotnet/src/Microsoft.Agents.Orchestration/Sequential/SequentialOrchestrationLogMessages.cs index 93d28eef8a..6d647a7519 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Logging/SequentialOrchestrationLogMessages.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Sequential/SequentialOrchestrationLogMessages.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.Diagnostics.CodeAnalysis; -using Microsoft.Agents.Orchestration.Sequential; using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Transforms/DefaultTransforms.cs b/dotnet/src/Microsoft.Agents.Orchestration/Transforms/DefaultTransforms.cs deleted file mode 100644 index b9e082f4bf..0000000000 --- a/dotnet/src/Microsoft.Agents.Orchestration/Transforms/DefaultTransforms.cs +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.AI; - -namespace Microsoft.Agents.Orchestration.Transforms; - -internal static class DefaultTransforms -{ - public static ValueTask> FromInput(TInput input, CancellationToken cancellationToken = default) - { - return new ValueTask>(TransformInput()); - - IEnumerable TransformInput() => - input switch - { - IEnumerable messages => messages, - ChatMessage message => [message], - string text => [new ChatMessage(ChatRole.User, text)], - _ => [new ChatMessage(ChatRole.User, JsonSerializer.Serialize(input))] - }; - } - - public static ValueTask ToOutput(IList result, CancellationToken cancellationToken = default) - { - bool isSingleResult = result.Count == 1; - - TOutput output = - GetDefaultOutput() ?? - GetObjectOutput() ?? - throw new InvalidOperationException($"Unable to transform output to {typeof(TOutput)}."); - - return new ValueTask(output); - - TOutput? GetObjectOutput() - { - if (!isSingleResult) - { - return default; - } - - try - { - return JsonSerializer.Deserialize(result[0].Text); - } - catch (JsonException) - { - return default; - } - } - - TOutput? GetDefaultOutput() - { - object? output = null; - if (typeof(TOutput).IsAssignableFrom(result.GetType())) - { - output = (object)result; - } - else if (isSingleResult && typeof(ChatMessage).IsAssignableFrom(typeof(TOutput))) - { - output = (object)result[0]; - } - else if (isSingleResult && typeof(string) == typeof(TOutput)) - { - output = result[0].Text ?? string.Empty; - } - - return (TOutput?)output; - } - } -} diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Transforms/OrchestrationTransforms.cs b/dotnet/src/Microsoft.Agents.Orchestration/Transforms/OrchestrationTransforms.cs deleted file mode 100644 index 6a1dfe9f45..0000000000 --- a/dotnet/src/Microsoft.Agents.Orchestration/Transforms/OrchestrationTransforms.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.AI; - -namespace Microsoft.Agents.Orchestration.Transforms; - -/// -/// Delegate for transforming an input of type into a collection of . -/// This is typically used to convert user or system input into a format suitable for chat orchestration. -/// -/// The input object to transform. -/// A cancellation token that can be used to cancel the operation. -/// A containing an enumerable of representing the transformed input. -public delegate ValueTask> OrchestrationInputTransform(TInput input, CancellationToken cancellationToken = default); - -/// -/// Delegate for transforming a into an output of type . -/// This is typically used to convert a chat response into a desired output format. -/// -/// The result messages to transform. -/// A cancellation token that can be used to cancel the operation. -/// A containing the transformed output of type . -public delegate ValueTask OrchestrationOutputTransform(IList result, CancellationToken cancellationToken = default); - -/// -/// Delegate for transforming the internal result message for an orchestration into a . -/// -/// The result message type -/// The result messages -/// The orchestration result as a . -public delegate IList OrchestrationResultTransform(TResult result); diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Transforms/StructuredOutputTransform.cs b/dotnet/src/Microsoft.Agents.Orchestration/Transforms/StructuredOutputTransform.cs index cda2788285..52c7fb08a0 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Transforms/StructuredOutputTransform.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Transforms/StructuredOutputTransform.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.AI; using Microsoft.Shared.Diagnostics; -namespace Microsoft.Agents.Orchestration.Transforms; +namespace Microsoft.Agents.Orchestration; /// /// Populates the target result type into a structured output. diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorMetadata.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorMetadata.cs index 317907deaa..04ba72acb7 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorMetadata.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorMetadata.cs @@ -22,11 +22,6 @@ public readonly struct ActorMetadata : IEquatable throw new ArgumentException("Invalid actor key.", nameof(key)); } - if (description is null) - { - throw new ArgumentNullException(nameof(description)); - } - this.Type = type; this.Key = key; this.Description = description; diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/AgentRuntimeExtensions.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/AgentRuntimeExtensions.cs new file mode 100644 index 0000000000..a517da7881 --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/AgentRuntimeExtensions.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI.Agents.Runtime; + +/// +/// Provides extension methods for the agent runtime. +/// +public static class AgentRuntimeExtensions +{ + /// + /// Retrieves an actor by its type. + /// + /// The agent runtime. + /// The type of the actor. + /// An optional key to specify variations of the actor. Defaults to "default". + /// If true, the actor is fetched lazily. + /// A token to cancel the operation if needed. + /// A task representing the asynchronous operation, returning the actor's ID. + public static ValueTask GetActorAsync(this IAgentRuntime agentRuntime, ActorType actorType, string? key = null, bool lazy = true, CancellationToken cancellationToken = default) + { + Throw.IfNull(agentRuntime); + + return agentRuntime.GetActorAsync(actorType.Name, key, lazy, cancellationToken); + } + + /// + /// Retrieves an actor by its string representation. + /// + /// The agent runtime. + /// The string representation of the actor. + /// An optional key to specify variations of the actor. Defaults to "default". + /// If true, the actor is fetched lazily. + /// A token to cancel the operation if needed. + /// A task representing the asynchronous operation, returning the actor's ID. + public static ValueTask GetActorAsync(this IAgentRuntime agentRuntime, string actor, string? key = null, bool lazy = true, CancellationToken cancellationToken = default) + { + Throw.IfNull(agentRuntime); + + return agentRuntime.GetActorAsync(new ActorId(actor, key ?? "default"), lazy, cancellationToken); + } + + /// + /// Registers an actor factory with the runtime, associating it with a specific actor type. + /// + /// The type of actor created by the factory. + /// The agent runtime. + /// The actor type to associate with the factory. + /// A function that asynchronously creates the actor instance. + /// A token to cancel the operation if needed. + /// A task representing the asynchronous operation, returning the registered actor type. + public static ValueTask RegisterActorFactoryAsync( + this IAgentRuntime agentRuntime, + ActorType type, + Func> factoryFunc, + CancellationToken cancellationToken = default) + where TActor : IRuntimeActor + { + Throw.IfNull(agentRuntime); + Throw.IfNull(factoryFunc); + + return agentRuntime.RegisterActorFactoryAsync( + type, + async ValueTask (actorId, runtime) => await factoryFunc(actorId, runtime).ConfigureAwait(false), + cancellationToken); + } +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IAgentRuntime.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IAgentRuntime.cs index 4dcafd8b40..feb9261c37 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IAgentRuntime.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IAgentRuntime.cs @@ -47,26 +47,6 @@ public interface IAgentRuntime : ISaveState /// A task representing the asynchronous operation, returning the actor's ID. ValueTask GetActorAsync(ActorId actorId, bool lazy = true, CancellationToken cancellationToken = default); - /// - /// Retrieves an actor by its type. - /// - /// The type of the actor. - /// An optional key to specify variations of the actor. Defaults to "default". - /// If true, the actor is fetched lazily. - /// A token to cancel the operation if needed. - /// A task representing the asynchronous operation, returning the actor's ID. - ValueTask GetActorAsync(ActorType actorType, string key = "default", bool lazy = true, CancellationToken cancellationToken = default); - - /// - /// Retrieves an actor by its string representation. - /// - /// The string representation of the actor. - /// An optional key to specify variations of the actor. Defaults to "default". - /// If true, the actor is fetched lazily. - /// A token to cancel the operation if needed. - /// A task representing the asynchronous operation, returning the actor's ID. - ValueTask GetActorAsync(string actor, string key = "default", bool lazy = true, CancellationToken cancellationToken = default); - /// /// Saves the state of an actor. /// The result must be JSON serializable. diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IRuntimeActor.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IRuntimeActor.cs index d94871f644..6bba64fa4b 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IRuntimeActor.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IRuntimeActor.cs @@ -32,6 +32,6 @@ public interface IRuntimeActor : ISaveState /// A task representing the asynchronous operation, returning a response to the message. /// The response can be null if no reply is necessary. /// - /// Thrown if the message was cancelled. + /// Thrown if the message was canceled. ValueTask OnMessageAsync(object message, MessageContext messageContext, CancellationToken cancellationToken = default); } diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/InProcessRuntime.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/InProcessRuntime.cs new file mode 100644 index 0000000000..ffa59c48e5 --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/InProcessRuntime.cs @@ -0,0 +1,425 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +#pragma warning disable CA1711 // Identifiers should not have incorrect suffix + +namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess; + +/// Provides an in-process/in-memory implementation of the agent runtime. +public sealed partial class InProcessRuntime : IAgentRuntime, IAsyncDisposable +{ + private static readonly UnboundedChannelOptions s_singleReaderOptions = new(); + + private readonly Dictionary>> _actorFactories = []; + private readonly Dictionary _subscriptions = []; + private readonly Channel _messages = Channel.CreateUnbounded(s_singleReaderOptions); + private readonly CancellationTokenSource _shutdownTokenSource = new(); + + private Task? _messageDeliveryTask; + private int _remainingWork = 1; // initial count of 1 represents overall operation, decremented when shutting down. + private int _signaledCompletion = 0; + + // Internal for testing purposes. + internal readonly Dictionary _actorInstances = []; + + /// Initializes a new instance of the in-memory runtime. + public InProcessRuntime() { } + + /// Gets the number of pending work items. + /// Internal for testing purposes. + internal int MessageCountForTesting => this._remainingWork - (1 - this._signaledCompletion); + + /// Creates and starts a new instance. + /// The started runtime. + public static InProcessRuntime StartNew() + { + InProcessRuntime runtime = new(); + runtime.Start(); + return runtime; + } + + /// Starts the runtime. + /// Thrown if the runtime is already started. + public void Start() + { + ThrowIfInvalid(this._signaledCompletion != 0 || this._messageDeliveryTask is not null, "Runtime was already started or shutdown."); + + CancellationToken ct = this._shutdownTokenSource.Token; + this._messageDeliveryTask = Task.Run(() => this.RunAsync(ct)); + } + + /// + public async ValueTask DisposeAsync() + { + if (Interlocked.Exchange(ref this._signaledCompletion, 1) == 0 && this._messageDeliveryTask is not null) + { + this.DecrementRemainingWork(); + this._shutdownTokenSource.Cancel(); + this._shutdownTokenSource.Dispose(); + await this._messageDeliveryTask.ConfigureAwait(false); + } + } + + /// + public ValueTask PublishMessageAsync(object message, TopicId topic, ActorId? sender = null, string? messageId = null, CancellationToken cancellationToken = default) + { + MessageToProcess m = new(this, message, messageId, sender, topic, cancellationToken); + + this.IncrementRemainingWork(); + this._messages.Writer.TryWrite(m); + + return new(m.ResultTcs.Task); + } + + /// + public ValueTask SendMessageAsync(object message, ActorId recipient, ActorId? sender = null, string? messageId = null, CancellationToken cancellationToken = default) + { + MessageToProcess m = new(this, message, messageId, sender, recipient, cancellationToken); + + this.IncrementRemainingWork(); + this._messages.Writer.TryWrite(m); + + return new(m.ResultTcs.Task); + } + + /// + public async ValueTask GetActorAsync(ActorId actorId, bool lazy = true, CancellationToken cancellationToken = default) + { + if (!lazy) + { + await this.EnsureActorAsync(actorId, cancellationToken).ConfigureAwait(false); + } + + return actorId; + } + + /// + public async ValueTask GetActorMetadataAsync(ActorId actorId, CancellationToken cancellationToken = default) + { + IRuntimeActor actor = await this.EnsureActorAsync(actorId, cancellationToken).ConfigureAwait(false); + return actor.Metadata; + } + + /// + public async ValueTask TryGetUnderlyingActorInstanceAsync(ActorId actorId, CancellationToken cancellationToken = default) where TActor : IRuntimeActor + { + IRuntimeActor actor = await this.EnsureActorAsync(actorId, cancellationToken).ConfigureAwait(false); + + if (actor is not TActor concreteActor) + { + throw new InvalidOperationException($"Actor with name {actorId.Type} is not of type {typeof(TActor).Name}."); + } + + return concreteActor; + } + + /// + public async ValueTask LoadActorStateAsync(ActorId actorId, JsonElement state, CancellationToken cancellationToken = default) + { + IRuntimeActor actor = await this.EnsureActorAsync(actorId, cancellationToken).ConfigureAwait(false); + await actor.LoadStateAsync(state, cancellationToken).ConfigureAwait(false); + } + + /// + public async ValueTask SaveActorStateAsync(ActorId actorId, CancellationToken cancellationToken = default) + { + IRuntimeActor actor = await this.EnsureActorAsync(actorId, cancellationToken).ConfigureAwait(false); + return await actor.SaveStateAsync(cancellationToken).ConfigureAwait(false); + } + + /// + public ValueTask AddSubscriptionAsync(ISubscriptionDefinition subscription, CancellationToken cancellationToken = default) + { + ThrowIfInvalid(this._subscriptions.ContainsKey(subscription.Id), "Subscription with the specified ID already exists."); + + this._subscriptions.Add(subscription.Id, subscription); + + return default; + } + + /// + public ValueTask RemoveSubscriptionAsync(string subscriptionId, CancellationToken cancellationToken = default) + { + ThrowIfInvalid(!this._subscriptions.ContainsKey(subscriptionId), "Subscription with the specified ID does not exist."); + + this._subscriptions.Remove(subscriptionId); + + return default; + } + + /// + public async ValueTask LoadStateAsync(JsonElement state, CancellationToken cancellationToken = default) + { + foreach (JsonProperty actorIdStr in state.EnumerateObject()) + { + ActorId actorId = ActorId.Parse(actorIdStr.Name); + + if (this._actorFactories.ContainsKey(actorId.Type)) + { + IRuntimeActor actor = await this.EnsureActorAsync(actorId, cancellationToken).ConfigureAwait(false); + await actor.LoadStateAsync(actorIdStr.Value, cancellationToken).ConfigureAwait(false); + } + } + } + + /// + public async ValueTask SaveStateAsync(CancellationToken cancellationToken = default) + { + Dictionary state = []; + foreach (KeyValuePair actor in this._actorInstances) + { + state[actor.Key.ToString()] = await actor.Value.SaveStateAsync(cancellationToken).ConfigureAwait(false); + } + + return JsonSerializer.SerializeToElement(state, InProcessRuntimeContext.Default.DictionaryStringJsonElement); + } + + /// + public async ValueTask RegisterActorFactoryAsync(ActorType type, Func> factoryFunc, CancellationToken cancellationToken = default) + { + ThrowIfInvalid(this._actorFactories.ContainsKey(type), "Actor type already registered."); + + this._actorFactories.Add(type, factoryFunc); + + return type; + } + + /// + public async ValueTask TryGetActorProxyAsync(ActorId actorId, CancellationToken cancellationToken = default) => + new(this, actorId); + + private async Task RunAsync(CancellationToken cancellationToken) + { + try + { + Dictionary pendingTasks = []; + + long currentId = 0; + await foreach (MessageToProcess message in this._messages.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) + { + this.DecrementRemainingWork(); + + ValueTask processTask = message.InvokeAsync(cancellationToken); + if (!processTask.IsCompleted) + { + currentId++; + Task t = WaitAndRemoveAsync(currentId, processTask); + lock (pendingTasks) + { + if (!t.IsCompleted) + { + pendingTasks.Add(currentId, t); + } + } + + async Task WaitAndRemoveAsync(long taskId, ValueTask processTask) + { + try + { + await processTask.ConfigureAwait(false); + } + finally + { + lock (pendingTasks) + { + pendingTasks.Remove(taskId); + } + } + } + } + } + + await Task.WhenAll(pendingTasks.Values).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // Ignore cancellation exceptions, as they are expected when the runtime is shutting down. + } + finally + { + foreach (var actor in this._actorInstances) + { + if (actor.Value is IAsyncDisposable closeableActor) + { + await closeableActor.DisposeAsync().ConfigureAwait(false); + } + } + } + } + + private static readonly Func> s_publishServicer = + async (MessageToProcess message, CancellationToken cancellationToken) => + { + Debug.Assert(message.Topic.HasValue); + + List? exceptions = null; + TopicId topic = message.Topic!.Value; + foreach (KeyValuePair subscription in message.Runtime._subscriptions) + { + if (!subscription.Value.Matches(topic)) + { + continue; + } + + try + { + using CancellationTokenSource combinedSource = CancellationTokenSource.CreateLinkedTokenSource(message.Cancellation, cancellationToken); + combinedSource.Token.ThrowIfCancellationRequested(); + + ActorId actorId = subscription.Value.MapToActor(topic); + ActorId? sender = message.Sender; + if (sender is null || sender != actorId) + { + IRuntimeActor actor = await message.Runtime.EnsureActorAsync(actorId, combinedSource.Token).ConfigureAwait(false); + await actor.OnMessageAsync(message.Message, new() + { + MessageId = message.MessageId, + Sender = sender, + Topic = topic, + }, combinedSource.Token).ConfigureAwait(false); + } + } + catch (Exception ex) + { + (exceptions ??= []).Add(ex); + } + } + + if (exceptions is not null) + { + throw new AggregateException("One or more exceptions occurred while processing the message.", exceptions); + } + + // This method is effectively void, with the result never being used. But it's typed the same as SendMessageServicerAsync + // in order to be able to share the same consuming code. + return null; + }; + + private static readonly Func> s_sendServicer = + async (MessageToProcess message, CancellationToken cancellationToken) => + { + Debug.Assert(message.Receiver.HasValue); + + using CancellationTokenSource combinedSource = CancellationTokenSource.CreateLinkedTokenSource(message.Cancellation, cancellationToken); + + IRuntimeActor actor = await message.Runtime.EnsureActorAsync(message.Receiver!.Value, combinedSource.Token).ConfigureAwait(false); + return await actor.OnMessageAsync(message.Message, new() + { + MessageId = message.MessageId, + Sender = message.Sender, + }, combinedSource.Token).ConfigureAwait(false); + }; + + private async ValueTask EnsureActorAsync(ActorId actorId, CancellationToken cancellationToken) + { + if (!this._actorInstances.TryGetValue(actorId, out IRuntimeActor? actor)) + { + this._actorFactories.TryGetValue(actorId.Type, out Func>? factoryFunc); + ThrowIfInvalid(factoryFunc is null, "Actor with the specified name not found."); + + actor = await factoryFunc(actorId, this).ConfigureAwait(false); + this._actorInstances.Add(actorId, actor); + } + + return actor; + } + + private void IncrementRemainingWork() + { + int current; + do + { + current = this._remainingWork; + ThrowIfInvalid(current <= 0, "Runtime has already shut down."); + } + while (Interlocked.CompareExchange(ref this._remainingWork, current + 1, current) != current); + } + + private void DecrementRemainingWork() + { + int current; + do + { + current = this._remainingWork; + ThrowIfInvalid(current <= 0, "Runtime has already shut down."); + } + while (Interlocked.CompareExchange(ref this._remainingWork, current - 1, current) != current); + + if (current == 1) + { + this._messages.Writer.TryComplete(); + } + } + + private static void ThrowIfInvalid([DoesNotReturnIf(true)] bool isInvalid, string message) + { + if (isInvalid) + { + throw new InvalidOperationException(message); + } + } + + [JsonSerializable(typeof(Dictionary))] + private sealed partial class InProcessRuntimeContext : JsonSerializerContext; + + private sealed class MessageToProcess + { + public MessageToProcess(InProcessRuntime runtime, object message, string? messageId, ActorId? sender, ActorId receiver, CancellationToken cancellationToken) : + this(runtime, message, messageId, sender, s_sendServicer, cancellationToken) + { + this.Receiver = Throw.IfNull(receiver); + } + + public MessageToProcess(InProcessRuntime runtime, object message, string? messageId, ActorId? sender, TopicId topic, CancellationToken cancellationToken) : + this(runtime, message, messageId, sender, s_publishServicer, cancellationToken) + { + this.Topic = Throw.IfNull(topic); + } + + private MessageToProcess(InProcessRuntime runtime, object message, string? messageId, ActorId? sender, Func> servicer, CancellationToken cancellationToken) + { + this.Runtime = runtime; + this.Message = message; + this.MessageId = messageId ?? Guid.NewGuid().ToString(); + this.Sender = sender; + this.Servicer = servicer; + this.Cancellation = cancellationToken; + } + + public InProcessRuntime Runtime { get; } + public object Message { get; } + public string MessageId { get; } + public ActorId? Sender { get; } + public TopicId? Topic { get; } + public ActorId? Receiver { get; } + public CancellationToken Cancellation { get; } + public TaskCompletionSource ResultTcs { get; } = new(); + private Func> Servicer { get; } + + public async ValueTask InvokeAsync(CancellationToken cancellationToken) + { + try + { + this.ResultTcs.SetResult(await this.Servicer(this, cancellationToken).ConfigureAwait(false)); + } + catch (OperationCanceledException exception) + { + this.ResultTcs.TrySetCanceled(exception.CancellationToken); + } + catch (Exception exception) + { + this.ResultTcs.SetException(exception); + } + } + } +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.csproj b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.csproj index 781c7bf2db..6941c59277 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.csproj +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.csproj @@ -8,6 +8,7 @@ + true true true @@ -15,7 +16,7 @@ - + @@ -25,6 +26,7 @@ + diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/RuntimeActor.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/RuntimeActor.cs index 81da06e9f0..30a1312c51 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/RuntimeActor.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/RuntimeActor.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI.Agents.Runtime; @@ -63,6 +64,24 @@ public abstract class RuntimeActor : IRuntimeActor this.Metadata = new ActorMetadata(this.Id.Type, this.Id.Key, description); } + /// Registers a handler for . + /// The type of the input message for the handler. + /// The handler function that processes the message. + /// Thrown when a handler for the specified type is already registered. + /// + /// The base implementation of will use these registered handlers to process incoming messages. + /// + protected void RegisterMessageHandler(Action messageHandler) + { + _ = Throw.IfNull(messageHandler); + + this.RegisterMessageHandler(async (input, ctx, cancellationToken) => + { + messageHandler(input, ctx); + return null; + }); + } + /// Registers a handler for . /// The type of the input message for the handler. /// The handler function that processes the message. @@ -72,23 +91,28 @@ public abstract class RuntimeActor : IRuntimeActor /// protected void RegisterMessageHandler(Func messageHandler) { - if (messageHandler is null) - { - throw new ArgumentNullException(nameof(messageHandler)); - } + _ = Throw.IfNull(messageHandler); - if (this._handlerInvokers.ContainsKey(typeof(TInput))) + this.RegisterMessageHandler(async (input, ctx, cancellationToken) => { - throw new InvalidOperationException($"A handler for type {typeof(TInput)} is already registered."); - } + await messageHandler(input, ctx, cancellationToken).ConfigureAwait(false); + return null; + }); + } - this._handlerInvokers.Add( - typeof(TInput), - async (message, messageContext, cancellationToken) => - { - await messageHandler((TInput)message!, messageContext, cancellationToken).ConfigureAwait(false); - return null; // No return value for void handlers - }); + /// Registers a handler for . + /// The type of the input message for the handler. + /// The type of the output message for the handler. + /// The handler function that processes the message. + /// Thrown when a handler for the specified type is already registered. + /// + /// The base implementation of will use these registered handlers to process incoming messages. + /// + protected void RegisterMessageHandler(Func messageHandler) + { + _ = Throw.IfNull(messageHandler); + + this.RegisterMessageHandler(async (input, ctx, cancellationToken) => messageHandler(input, ctx)); } /// Registers a handler for that produces a . @@ -101,10 +125,7 @@ public abstract class RuntimeActor : IRuntimeActor /// protected void RegisterMessageHandler(Func> messageHandler) { - if (messageHandler is null) - { - throw new ArgumentNullException(nameof(messageHandler)); - } + _ = Throw.IfNull(messageHandler); if (this._handlerInvokers.ContainsKey(typeof(TInput))) { @@ -113,11 +134,7 @@ public abstract class RuntimeActor : IRuntimeActor this._handlerInvokers.Add( typeof(TInput), - async (message, messageContext, cancellationToken) => - { - TOutput? result = await messageHandler((TInput)message!, messageContext, cancellationToken).ConfigureAwait(false); - return (object?)result; - }); + async (message, messageContext, cancellationToken) => await messageHandler((TInput)message!, messageContext, cancellationToken).ConfigureAwait(false)); } /// diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/InProcessRuntime.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/InProcessRuntime.cs deleted file mode 100644 index 87aa7e817a..0000000000 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/InProcessRuntime.cs +++ /dev/null @@ -1,437 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess; - -/// -/// Provides an in-process/in-memory implementation of the agent runtime. -/// -public sealed partial class InProcessRuntime : IAgentRuntime, IAsyncDisposable -{ - private readonly Dictionary>> _actorFactories = []; - private readonly Dictionary _subscriptions = []; - private readonly ConcurrentQueue _messageDeliveryQueue = new(); - - private CancellationTokenSource? _shutdownSource; - private CancellationTokenSource? _finishSource; - private Task _messageDeliveryTask = Task.CompletedTask; - private Func _shouldContinue = () => true; - - // Exposed for testing purposes. - internal int _messageQueueCount; - internal readonly Dictionary _actorInstances = []; - - /// - /// Gets or sets a value indicating whether actors should receive messages they send themselves. - /// - public bool DeliverToSelf { get; set; } - - /// - public async ValueTask DisposeAsync() - { - await this.RunUntilIdleAsync().ConfigureAwait(false); - this._shutdownSource?.Dispose(); - this._finishSource?.Dispose(); - } - - /// - /// Starts the runtime service. - /// - /// Token to monitor for shutdown requests. - /// A task representing the asynchronous operation. - /// Thrown if the runtime is already started. - public Task StartAsync(CancellationToken cancellationToken = default) - { - if (this._shutdownSource != null) - { - throw new InvalidOperationException("Runtime is already running."); - } - - this._shutdownSource = new CancellationTokenSource(); - this._messageDeliveryTask = Task.Run(() => this.RunAsync(this._shutdownSource.Token), cancellationToken); - - return Task.CompletedTask; - } - - /// - /// Stops the runtime service. - /// - /// Token to propagate when stopping the runtime. - /// A task representing the asynchronous operation. - /// Thrown if the runtime is in the process of stopping. - public Task StopAsync(CancellationToken cancellationToken = default) - { - if (this._shutdownSource != null) - { - if (this._finishSource != null) - { - throw new InvalidOperationException("Runtime is already stopping."); - } - - this._finishSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - - this._shutdownSource.Cancel(); - } - - return Task.CompletedTask; - } - - /// - /// This will run until the message queue is empty and then stop the runtime. - /// - public async Task RunUntilIdleAsync(CancellationToken cancellationToken = default) - { - Func oldShouldContinue = this._shouldContinue; - this._shouldContinue = () => !this._messageDeliveryQueue.IsEmpty; - - // TODO: Do we want detach semantics? - await this._messageDeliveryTask.ConfigureAwait(false); - - this._shouldContinue = oldShouldContinue; - } - - /// - public ValueTask PublishMessageAsync(object message, TopicId topic, ActorId? sender = null, string? messageId = null, CancellationToken cancellationToken = default) - { - return this.ExecuteTracedAsync(async () => - { - MessageDelivery delivery = - new MessageEnvelope(message, messageId, cancellationToken) - .WithSender(sender) - .ForPublish(topic, this.PublishMessageServicerAsync); - - this._messageDeliveryQueue.Enqueue(delivery); - Interlocked.Increment(ref this._messageQueueCount); - - await delivery.ResultTask.ConfigureAwait(false); - }); - } - - /// - public async ValueTask SendMessageAsync(object message, ActorId recipient, ActorId? sender = null, string? messageId = null, CancellationToken cancellationToken = default) - { - return await this.ExecuteTracedAsync(async () => - { - MessageDelivery delivery = - new MessageEnvelope(message, messageId, cancellationToken) - .WithSender(sender) - .ForSend(recipient, this.SendMessageServicerAsync); - - this._messageDeliveryQueue.Enqueue(delivery); - Interlocked.Increment(ref this._messageQueueCount); - - return await delivery.ResultTask.ConfigureAwait(false); - }).ConfigureAwait(false); - } - - /// - public async ValueTask GetActorAsync(ActorId actorId, bool lazy = true, CancellationToken cancellationToken = default) - { - if (!lazy) - { - await this.EnsureActorAsync(actorId, cancellationToken).ConfigureAwait(false); - } - - return actorId; - } - - /// - public ValueTask GetActorAsync(ActorType actorType, string? key = null, bool lazy = true, CancellationToken cancellationToken = default) - => this.GetActorAsync(actorType.Name, key, lazy, cancellationToken); - - /// - public ValueTask GetActorAsync(string actor, string? key = null, bool lazy = true, CancellationToken cancellationToken = default) - => this.GetActorAsync(new ActorId(actor, key ?? "default"), lazy, cancellationToken); - - /// - public async ValueTask GetActorMetadataAsync(ActorId actorId, CancellationToken cancellationToken = default) - { - IRuntimeActor actor = await this.EnsureActorAsync(actorId, cancellationToken).ConfigureAwait(false); - return actor.Metadata; - } - - /// - public async ValueTask TryGetUnderlyingActorInstanceAsync(ActorId actorId, CancellationToken cancellationToken = default) where TActor : IRuntimeActor - { - IRuntimeActor actor = await this.EnsureActorAsync(actorId, cancellationToken).ConfigureAwait(false); - - if (actor is not TActor concreteActor) - { - throw new InvalidOperationException($"Actor with name {actorId.Type} is not of type {typeof(TActor).Name}."); - } - - return concreteActor; - } - - /// - public async ValueTask LoadActorStateAsync(ActorId actorId, JsonElement state, CancellationToken cancellationToken = default) - { - IRuntimeActor actor = await this.EnsureActorAsync(actorId, cancellationToken).ConfigureAwait(false); - await actor.LoadStateAsync(state, cancellationToken).ConfigureAwait(false); - } - - /// - public async ValueTask SaveActorStateAsync(ActorId actorId, CancellationToken cancellationToken = default) - { - IRuntimeActor actor = await this.EnsureActorAsync(actorId, cancellationToken).ConfigureAwait(false); - return await actor.SaveStateAsync(cancellationToken).ConfigureAwait(false); - } - - /// - public ValueTask AddSubscriptionAsync(ISubscriptionDefinition subscription, CancellationToken cancellationToken = default) - { - if (this._subscriptions.ContainsKey(subscription.Id)) - { - throw new InvalidOperationException($"Subscription with id {subscription.Id} already exists."); - } - - this._subscriptions.Add(subscription.Id, subscription); - - return default; - } - - /// - public ValueTask RemoveSubscriptionAsync(string subscriptionId, CancellationToken cancellationToken = default) - { - if (!this._subscriptions.ContainsKey(subscriptionId)) - { - throw new InvalidOperationException($"Subscription with id {subscriptionId} does not exist."); - } - - this._subscriptions.Remove(subscriptionId); - - return default; - } - - /// - public async ValueTask LoadStateAsync(JsonElement state, CancellationToken cancellationToken = default) - { - foreach (JsonProperty actorIdStr in state.EnumerateObject()) - { - ActorId actorId = ActorId.Parse(actorIdStr.Name); - - if (this._actorFactories.ContainsKey(actorId.Type)) - { - IRuntimeActor actor = await this.EnsureActorAsync(actorId, cancellationToken).ConfigureAwait(false); - await actor.LoadStateAsync(actorIdStr.Value, cancellationToken).ConfigureAwait(false); - } - } - } - - /// - public async ValueTask SaveStateAsync(CancellationToken cancellationToken = default) - { - Dictionary state = []; - foreach (ActorId actorId in this._actorInstances.Keys) - { - JsonElement actorState = await this._actorInstances[actorId].SaveStateAsync(cancellationToken).ConfigureAwait(false); - state[actorId.ToString()] = actorState; - } - return JsonSerializer.SerializeToElement(state, InProcessRuntimeContext.Default.DictionaryStringJsonElement); - } - - /// - /// Registers an actor factory with the runtime, associating it with a specific actor type. - /// - /// The type of actor created by the factory. - /// The actor type to associate with the factory. - /// A function that asynchronously creates the actor instance. - /// A token to cancel the operation if needed. - /// A task representing the asynchronous operation, returning the registered actor type. - public ValueTask RegisterActorFactoryAsync(ActorType type, Func> factoryFunc, CancellationToken cancellationToken = default) where TActor : IRuntimeActor - // Declare the lambda return type explicitly, as otherwise the compiler will infer 'ValueTask' - // and recurse into the same call, causing a stack overflow. - => this.RegisterActorFactoryAsync(type, async ValueTask (actorId, runtime) => await factoryFunc(actorId, runtime).ConfigureAwait(false), cancellationToken); - - /// - public async ValueTask RegisterActorFactoryAsync(ActorType type, Func> factoryFunc, CancellationToken cancellationToken = default) - { - if (this._actorFactories.ContainsKey(type)) - { - throw new InvalidOperationException($"Actor with type {type} already exists."); - } - - this._actorFactories.Add(type, factoryFunc); - - return type; - } - - /// - public async ValueTask TryGetActorProxyAsync(ActorId actorId, CancellationToken cancellationToken = default) - { - IdProxyActor proxy = new(this, actorId); - - return proxy; - } - - private async ValueTask ProcessNextMessageAsync(CancellationToken cancellation = default) - { - if (this._messageDeliveryQueue.TryDequeue(out MessageDelivery? delivery)) - { - Interlocked.Decrement(ref this._messageQueueCount); - Debug.WriteLine($"Processing message {delivery.Message.MessageId}..."); - await delivery.InvokeAsync(cancellation).ConfigureAwait(false); - } - } - - private async Task RunAsync(CancellationToken cancellation) - { - ConcurrentDictionary pendingTasks = []; - while (!cancellation.IsCancellationRequested && this._shouldContinue()) - { - // Get a unique task id. - Guid taskId = Guid.NewGuid(); - - // There is potentially a race condition here, but even if we leak a Task, we will - // still catch it on the Finish() pass. - ValueTask processTask = this.ProcessNextMessageAsync(cancellation); - await Task.Yield(); - - if (!processTask.IsCompleted) - { - pendingTasks.TryAdd(taskId, processTask.AsTask().ContinueWith(t => pendingTasks.TryRemove(taskId, out _), TaskScheduler.Current)); - } - } - - // The pending task dictionary may contain null values when a race condition is experienced during - // the prior "ContinueWith" call. This could be solved with a ConcurrentDictionary, but locking - // is entirely undesirable in this context. - await Task.WhenAll(pendingTasks.Values.Where(task => task is not null)).ConfigureAwait(false); - await this.FinishAsync(this._finishSource?.Token ?? CancellationToken.None).ConfigureAwait(false); - } - - private async ValueTask PublishMessageServicerAsync(MessageEnvelope envelope, CancellationToken deliveryToken) - { - if (!envelope.Topic.HasValue) - { - throw new InvalidOperationException("Message must have a topic to be published."); - } - - List? exceptions = null; - TopicId topic = envelope.Topic.Value; - foreach (ISubscriptionDefinition subscription in this._subscriptions.Values.Where(subscription => subscription.Matches(topic))) - { - try - { - deliveryToken.ThrowIfCancellationRequested(); - - ActorId? sender = envelope.Sender; - - using CancellationTokenSource combinedSource = CancellationTokenSource.CreateLinkedTokenSource(envelope.Cancellation, deliveryToken); - - ActorId actorId = subscription.MapToActor(topic); - if (!this.DeliverToSelf && sender.HasValue && sender == actorId) - { - continue; - } - - MessageContext messageContext = new() - { - MessageId = envelope.MessageId, - Sender = sender, - Topic = topic, - IsRpc = false - }; - - IRuntimeActor actor = await this.EnsureActorAsync(actorId, combinedSource.Token).ConfigureAwait(false); - - await actor.OnMessageAsync(envelope.Message, messageContext, combinedSource.Token).ConfigureAwait(false); - } - catch (Exception ex) - { - (exceptions ??= []).Add(ex); - } - } - - if (exceptions is not null) - { - throw new AggregateException("One or more exceptions occurred while processing the message.", exceptions); - } - } - - private async ValueTask SendMessageServicerAsync(MessageEnvelope envelope, CancellationToken deliveryToken) - { - if (!envelope.Receiver.HasValue) - { - throw new InvalidOperationException("Message must have a receiver to be sent."); - } - - using CancellationTokenSource combinedSource = CancellationTokenSource.CreateLinkedTokenSource(envelope.Cancellation, deliveryToken); - MessageContext messageContext = new() - { - MessageId = envelope.MessageId, - Sender = envelope.Sender, - IsRpc = false - }; - - ActorId receiver = envelope.Receiver.Value; - IRuntimeActor actor = await this.EnsureActorAsync(receiver, combinedSource.Token).ConfigureAwait(false); - - return await actor.OnMessageAsync(envelope.Message, messageContext, combinedSource.Token).ConfigureAwait(false); - } - - private async ValueTask EnsureActorAsync(ActorId actorId, CancellationToken cancellationToken) - { - if (!this._actorInstances.TryGetValue(actorId, out IRuntimeActor? actor)) - { - if (!this._actorFactories.TryGetValue(actorId.Type, out Func>? factoryFunc)) - { - throw new InvalidOperationException($"Actor with name {actorId.Type} not found."); - } - - actor = await factoryFunc(actorId, this).ConfigureAwait(false); - this._actorInstances.Add(actorId, actor); - } - - return actor; - } - - private async Task FinishAsync(CancellationToken token) - { - foreach (IRuntimeActor actor in this._actorInstances.Values) - { - if (!token.IsCancellationRequested && actor is IAsyncDisposable closeableActor) - { - await closeableActor.DisposeAsync().ConfigureAwait(false); - } - } - - if (this._shutdownSource is { } shutdownSource) - { - this._shutdownSource = null; - shutdownSource.Dispose(); - } - - if (this._finishSource is { } finishSource) - { - this._finishSource = null; - finishSource.Dispose(); - } - } - -#pragma warning disable CA1822 // Mark members as static - private ValueTask ExecuteTracedAsync(Func> func) - { - // TODO: Bind tracing - return func(); - } - - private ValueTask ExecuteTracedAsync(Func func) - { - // TODO: Bind tracing - return func(); - } -#pragma warning restore CA1822 // Mark members as static - - [JsonSerializable(typeof(Dictionary))] - private sealed partial class InProcessRuntimeContext : JsonSerializerContext; -} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/MessageDelivery.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/MessageDelivery.cs deleted file mode 100644 index 52669e9a01..0000000000 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/MessageDelivery.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess; - -internal sealed class MessageDelivery(MessageEnvelope message, Func servicer, Task resultTask) -{ - public MessageEnvelope Message { get; } = message; - public Func Servicer { get; } = servicer; - public Task ResultTask { get; } = resultTask; - - public ValueTask InvokeAsync(CancellationToken cancellation) => this.Servicer(this.Message, cancellation); -} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/MessageEnvelope.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/MessageEnvelope.cs deleted file mode 100644 index 8186252d96..0000000000 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/MessageEnvelope.cs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess; - -internal sealed class MessageEnvelope(object message, string? messageId = null, CancellationToken cancellation = default) -{ - public object Message { get; } = message; - public string MessageId { get; } = messageId ?? Guid.NewGuid().ToString(); - public TopicId? Topic { get; private set; } - public ActorId? Sender { get; private set; } - public ActorId? Receiver { get; private set; } - public CancellationToken Cancellation { get; } = cancellation; - - public MessageEnvelope WithSender(ActorId? sender) - { - this.Sender = sender; - return this; - } - - public MessageDelivery ForSend(ActorId receiver, Func> servicer) - { - this.Receiver = receiver; - - TaskCompletionSource tcs = new(); - return new MessageDelivery(this, async (MessageEnvelope envelope, CancellationToken cancellation) => - { - try - { - object? result = await servicer(envelope, cancellation).ConfigureAwait(false); - tcs.SetResult(result); - } - catch (OperationCanceledException exception) - { - tcs.TrySetCanceled(exception.CancellationToken); - } - catch (Exception exception) - { - tcs.SetException(exception); - } - }, tcs.Task); - } - - public MessageDelivery ForPublish(TopicId topic, Func servicer) - { - this.Topic = topic; - - TaskCompletionSource tcs = new(); - return new MessageDelivery(this, async (envelope, cancellation) => - { - try - { - await servicer(envelope, cancellation).ConfigureAwait(false); - tcs.SetResult(null); - } - catch (Exception ex) - { - tcs.SetException(ex); - } - }, tcs.Task); - } -} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/Microsoft.Extensions.AI.Agents.Runtime.InProcess.csproj b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/Microsoft.Extensions.AI.Agents.Runtime.InProcess.csproj deleted file mode 100644 index 2169aa805f..0000000000 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/Microsoft.Extensions.AI.Agents.Runtime.InProcess.csproj +++ /dev/null @@ -1,27 +0,0 @@ - - - - $(ProjectsTargetFrameworks) - $(ProjectsDebugTargetFrameworks) - alpha - - - - true - - - - - - - - - - - - - - - - - diff --git a/dotnet/src/Shared/Samples/OrchestrationSample.cs b/dotnet/src/Shared/Samples/OrchestrationSample.cs index 1d5570ef77..3a4a908cc4 100644 --- a/dotnet/src/Shared/Samples/OrchestrationSample.cs +++ b/dotnet/src/Shared/Samples/OrchestrationSample.cs @@ -16,11 +16,6 @@ namespace Microsoft.Shared.SampleUtilities; /// public abstract class OrchestrationSample : BaseSample { - /// - /// This constant defines the timeout duration for result retrieval, measured in seconds. - /// - protected const int ResultTimeoutInSeconds = 30; - /// /// Creates a new instance using the specified instructions, description, name, and functions. /// @@ -136,28 +131,23 @@ public abstract class OrchestrationSample : BaseSample /// A representing the asynchronous operation. public ValueTask ResponseCallback(IEnumerable response) { + WriteStreamedResponse(this.StreamedResponses); + this.StreamedResponses.Clear(); + this.History.AddRange(response); WriteResponse(response); - return new ValueTask(); + return default; } /// /// Callback to handle a streamed agent run response update, adding it to the list and writing output if final. /// /// The to process. - /// Indicates whether this is the final update in the stream. /// A representing the asynchronous operation. - public ValueTask StreamingResultCallback(AgentRunResponseUpdate streamedResponse, bool isFinal) + public ValueTask StreamingResultCallback(AgentRunResponseUpdate streamedResponse) { this.StreamedResponses.Add(streamedResponse); - - if (isFinal) - { - WriteStreamedResponse(this.StreamedResponses); - this.StreamedResponses.Clear(); - } - - return new ValueTask(); + return default; } } diff --git a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/ChatGroupExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/ChatGroupExtensionsTests.cs index 401875b185..457109fe1c 100644 --- a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/ChatGroupExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/ChatGroupExtensionsTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using Microsoft.Agents.Orchestration.GroupChat; namespace Microsoft.Agents.Orchestration.UnitTest; diff --git a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/ConcurrentOrchestrationTests.cs b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/ConcurrentOrchestrationTests.cs index 8516a466e9..9e06e7b81d 100644 --- a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/ConcurrentOrchestrationTests.cs +++ b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/ConcurrentOrchestrationTests.cs @@ -1,10 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Threading.Tasks; -using Microsoft.Agents.Orchestration.Concurrent; using Microsoft.Extensions.AI.Agents; -using Microsoft.Extensions.AI.Agents.Runtime.InProcess; namespace Microsoft.Agents.Orchestration.UnitTest; @@ -17,11 +14,10 @@ public class ConcurrentOrchestrationTests public async Task ConcurrentOrchestrationWithSingleAgentAsync() { // Arrange - await using InProcessRuntime runtime = new(); MockAgent mockAgent1 = MockAgent.CreateWithResponse(1, "xyz"); // Act: Create and execute the orchestration - string[] response = await ExecuteOrchestrationAsync(runtime, mockAgent1); + string[] response = await ExecuteOrchestrationAsync(mockAgent1); // Assert Assert.Equal(1, mockAgent1.InvokeCount); @@ -32,14 +28,12 @@ public class ConcurrentOrchestrationTests public async Task ConcurrentOrchestrationWithMultipleAgentsAsync() { // Arrange - await using InProcessRuntime runtime = new(); - MockAgent mockAgent1 = MockAgent.CreateWithResponse(1, "abc"); MockAgent mockAgent2 = MockAgent.CreateWithResponse(2, "xyz"); MockAgent mockAgent3 = MockAgent.CreateWithResponse(3, "lmn"); // Act: Create and execute the orchestration - string[] response = await ExecuteOrchestrationAsync(runtime, mockAgent1, mockAgent2, mockAgent3); + string[] response = await ExecuteOrchestrationAsync(mockAgent1, mockAgent2, mockAgent3); // Assert Assert.Equal(1, mockAgent1.InvokeCount); @@ -50,24 +44,18 @@ public class ConcurrentOrchestrationTests Assert.Contains("abc", response); } - private static async Task ExecuteOrchestrationAsync(InProcessRuntime runtime, params Agent[] mockAgents) + private static async Task ExecuteOrchestrationAsync(params Agent[] mockAgents) { // Act - await runtime.StartAsync(); - ConcurrentOrchestration orchestration = new(mockAgents); const string InitialInput = "123"; - OrchestrationResult result = await orchestration.InvokeAsync(InitialInput, runtime); + OrchestrationResult result = await orchestration.InvokeAsync(InitialInput); // Assert Assert.NotNull(result); // Act - string[] response = await result.GetValueAsync(TimeSpan.FromSeconds(20)); - - await runtime.RunUntilIdleAsync(); - - return response; + return await result.Task; } } diff --git a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/DefaultTransformsTests.cs b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/DefaultTransformsTests.cs index 0e142f3965..7a760b023d 100644 --- a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/DefaultTransformsTests.cs +++ b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/DefaultTransformsTests.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Threading.Tasks; -using Microsoft.Agents.Orchestration.Transforms; using Microsoft.Extensions.AI; namespace Microsoft.Agents.Orchestration.UnitTest; diff --git a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/GroupChatOrchestrationTests.cs b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/GroupChatOrchestrationTests.cs index f5bbef1711..22cf7db876 100644 --- a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/GroupChatOrchestrationTests.cs +++ b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/GroupChatOrchestrationTests.cs @@ -1,10 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Threading.Tasks; -using Microsoft.Agents.Orchestration.GroupChat; using Microsoft.Extensions.AI.Agents; -using Microsoft.Extensions.AI.Agents.Runtime.InProcess; namespace Microsoft.Agents.Orchestration.UnitTest; @@ -17,11 +14,10 @@ public class GroupChatOrchestrationTests public async Task GroupChatOrchestrationWithSingleAgentAsync() { // Arrange - await using InProcessRuntime runtime = new(); MockAgent mockAgent1 = MockAgent.CreateWithResponse(2, "xyz"); // Act: Create and execute the orchestration - string response = await ExecuteOrchestrationAsync(runtime, mockAgent1); + string response = await ExecuteOrchestrationAsync(mockAgent1); // Assert Assert.Equal(1, mockAgent1.InvokeCount); @@ -32,14 +28,12 @@ public class GroupChatOrchestrationTests public async Task GroupChatOrchestrationWithMultipleAgentsAsync() { // Arrange - await using InProcessRuntime runtime = new(); - MockAgent mockAgent1 = MockAgent.CreateWithResponse(1, "abc"); MockAgent mockAgent2 = MockAgent.CreateWithResponse(2, "xyz"); MockAgent mockAgent3 = MockAgent.CreateWithResponse(3, "lmn"); // Act: Create and execute the orchestration - string response = await ExecuteOrchestrationAsync(runtime, mockAgent1, mockAgent2, mockAgent3); + string response = await ExecuteOrchestrationAsync(mockAgent1, mockAgent2, mockAgent3); // Assert Assert.Equal(1, mockAgent1.InvokeCount); @@ -48,24 +42,18 @@ public class GroupChatOrchestrationTests Assert.Equal("lmn", response); } - private static async Task ExecuteOrchestrationAsync(InProcessRuntime runtime, params Agent[] mockAgents) + private static async Task ExecuteOrchestrationAsync(params Agent[] mockAgents) { // Act - await runtime.StartAsync(); - GroupChatOrchestration orchestration = new(new RoundRobinGroupChatManager() { MaximumInvocationCount = mockAgents.Length }, mockAgents); const string InitialInput = "123"; - OrchestrationResult result = await orchestration.InvokeAsync(InitialInput, runtime); + OrchestrationResult result = await orchestration.InvokeAsync(InitialInput); // Assert Assert.NotNull(result); // Act - string response = await result.GetValueAsync(TimeSpan.FromSeconds(20)); - - await runtime.RunUntilIdleAsync(); - - return response; + return await result.Task; } } diff --git a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/HandoffOrchestrationTests.cs b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/HandoffOrchestrationTests.cs index 3549e86b66..31ec9931da 100644 --- a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/HandoffOrchestrationTests.cs +++ b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/HandoffOrchestrationTests.cs @@ -7,10 +7,8 @@ using System.Collections.Generic; using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using Microsoft.Agents.Orchestration.Handoff; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; -using Microsoft.Extensions.AI.Agents.Runtime.InProcess; using OpenAI; namespace Microsoft.Agents.Orchestration.UnitTest; @@ -51,7 +49,7 @@ public sealed class HandoffOrchestrationTests : IDisposable Responses.Message("Final response")); // Act: Create and execute the orchestration - string response = await ExecuteOrchestrationAsync(OrchestrationHandoffs.StartWith(mockAgent1), mockAgent1); + string response = await ExecuteOrchestrationAsync(OrchestrationHandoffs.StartWith(mockAgent1)); // Assert Assert.Equal("Final response", response); @@ -81,35 +79,26 @@ public sealed class HandoffOrchestrationTests : IDisposable string response = await ExecuteOrchestrationAsync( OrchestrationHandoffs .StartWith(mockAgent1) - .Add(mockAgent1, mockAgent2, mockAgent3), - mockAgent1, - mockAgent2, - mockAgent3); + .Add(mockAgent1, mockAgent2, mockAgent3)); // Assert Assert.Equal("Final response", response); } - private static async Task ExecuteOrchestrationAsync(OrchestrationHandoffs handoffs, params Agent[] mockAgents) + private static async Task ExecuteOrchestrationAsync(OrchestrationHandoffs handoffs) { // Arrange - await using InProcessRuntime runtime = new(); - await runtime.StartAsync(); - - HandoffOrchestration orchestration = new(handoffs, mockAgents); + HandoffOrchestration orchestration = new(handoffs); // Act const string InitialInput = "123"; - OrchestrationResult result = await orchestration.InvokeAsync(InitialInput, runtime); + OrchestrationResult result = await orchestration.InvokeAsync(InitialInput); // Assert Assert.NotNull(result); // Act - string response = await result.GetValueAsync(TimeSpan.FromSeconds(10)); - await runtime.RunUntilIdleAsync(); - - return response; + return await result.Task; } private ChatClientAgent CreateMockAgent(string name, string description, params string[] responses) diff --git a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/HandoffsTests.cs b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/HandoffsTests.cs index 52357167ed..ce2fe86ca3 100644 --- a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/HandoffsTests.cs +++ b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/HandoffsTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using Microsoft.Agents.Orchestration.Handoff; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; using Moq; diff --git a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/OrchestrationResultTests.cs b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/OrchestrationResultTests.cs index ae9187a372..94dda8f6e5 100644 --- a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/OrchestrationResultTests.cs +++ b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/OrchestrationResultTests.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI.Agents.Runtime; @@ -11,7 +10,7 @@ namespace Microsoft.Agents.Orchestration.UnitTest; public class OrchestrationResultTests { [Fact] - public void ConstructorInitializesPropertiesCorrectly() + public async Task ConstructorInitializesPropertiesCorrectlyAsync() { // Arrange OrchestrationContext context = new("TestOrchestration", new TopicId("testTopic"), null, null, NullLoggerFactory.Instance, CancellationToken.None); @@ -19,7 +18,7 @@ public class OrchestrationResultTests // Act using CancellationTokenSource cancelSource = new(); - using OrchestrationResult result = new(context, tcs, cancelSource, NullLogger.Instance); + await using OrchestrationResult result = new(context, tcs, cancelSource, NullLogger.Instance); // Assert Assert.Equal("TestOrchestration", result.Orchestration); @@ -33,51 +32,17 @@ public class OrchestrationResultTests OrchestrationContext context = new("TestOrchestration", new TopicId("testTopic"), null, null, NullLoggerFactory.Instance, CancellationToken.None); TaskCompletionSource tcs = new(); using CancellationTokenSource cancelSource = new(); - using OrchestrationResult result = new(context, tcs, cancelSource, NullLogger.Instance); + await using OrchestrationResult result = new(context, tcs, cancelSource, NullLogger.Instance); string expectedValue = "Result value"; // Act tcs.SetResult(expectedValue); - string actualValue = await result.GetValueAsync(); + string actualValue = await result.Task; // Assert Assert.Equal(expectedValue, actualValue); } - [Fact] - public async Task GetValueAsyncWithTimeoutReturnsCompletedValueWhenTaskCompletesWithinTimeoutAsync() - { - // Arrange - OrchestrationContext context = new("TestOrchestration", new TopicId("testTopic"), null, null, NullLoggerFactory.Instance, CancellationToken.None); - TaskCompletionSource tcs = new(); - using CancellationTokenSource cancelSource = new(); - using OrchestrationResult result = new(context, tcs, cancelSource, NullLogger.Instance); - string expectedValue = "Result value"; - TimeSpan timeout = TimeSpan.FromSeconds(1); - - // Act - tcs.SetResult(expectedValue); - string actualValue = await result.GetValueAsync(timeout); - - // Assert - Assert.Equal(expectedValue, actualValue); - } - - [Fact] - public async Task GetValueAsyncWithTimeoutThrowsTimeoutExceptionWhenTaskDoesNotCompleteWithinTimeoutAsync() - { - // Arrange - OrchestrationContext context = new("TestOrchestration", new TopicId("testTopic"), null, null, NullLoggerFactory.Instance, CancellationToken.None); - TaskCompletionSource tcs = new(); - using CancellationTokenSource cancelSource = new(); - using OrchestrationResult result = new(context, tcs, cancelSource, NullLogger.Instance); - TimeSpan timeout = TimeSpan.FromMilliseconds(50); - - // Act & Assert - TimeoutException exception = await Assert.ThrowsAsync(() => result.GetValueAsync(timeout).AsTask()); - Assert.Contains("Orchestration did not complete within the allowed duration", exception.Message); - } - [Fact] public async Task GetValueAsyncReturnsCompletedValueWhenCompletionIsDelayedAsync() { @@ -85,7 +50,7 @@ public class OrchestrationResultTests OrchestrationContext context = new("TestOrchestration", new TopicId("testTopic"), null, null, NullLoggerFactory.Instance, CancellationToken.None); TaskCompletionSource tcs = new(); using CancellationTokenSource cancelSource = new(); - using OrchestrationResult result = new(context, tcs, cancelSource, NullLogger.Instance); + await using OrchestrationResult result = new(context, tcs, cancelSource, NullLogger.Instance); int expectedValue = 42; // Act @@ -96,7 +61,7 @@ public class OrchestrationResultTests tcs.SetResult(expectedValue); }); - int actualValue = await result.GetValueAsync(); + int actualValue = await result.Task; // Assert Assert.Equal(expectedValue, actualValue); diff --git a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/SequentialOrchestrationTests.cs b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/SequentialOrchestrationTests.cs index 9d0a332e0f..c59bb8dae9 100644 --- a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/SequentialOrchestrationTests.cs +++ b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/SequentialOrchestrationTests.cs @@ -1,10 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Threading.Tasks; -using Microsoft.Agents.Orchestration.Sequential; using Microsoft.Extensions.AI.Agents; -using Microsoft.Extensions.AI.Agents.Runtime.InProcess; namespace Microsoft.Agents.Orchestration.UnitTest; @@ -17,11 +14,10 @@ public class SequentialOrchestrationTests public async Task SequentialOrchestrationWithSingleAgentAsync() { // Arrange - await using InProcessRuntime runtime = new(); MockAgent mockAgent1 = MockAgent.CreateWithResponse(2, "xyz"); // Act: Create and execute the orchestration - string response = await ExecuteOrchestrationAsync(runtime, mockAgent1); + string response = await ExecuteOrchestrationAsync(mockAgent1); // Assert Assert.Equal(1, mockAgent1.InvokeCount); @@ -32,14 +28,12 @@ public class SequentialOrchestrationTests public async Task SequentialOrchestrationWithMultipleAgentsAsync() { // Arrange - await using InProcessRuntime runtime = new(); - MockAgent mockAgent1 = MockAgent.CreateWithResponse(1, "abc"); MockAgent mockAgent2 = MockAgent.CreateWithResponse(2, "xyz"); MockAgent mockAgent3 = MockAgent.CreateWithResponse(3, "lmn"); // Act: Create and execute the orchestration - string response = await ExecuteOrchestrationAsync(runtime, mockAgent1, mockAgent2, mockAgent3); + string response = await ExecuteOrchestrationAsync(mockAgent1, mockAgent2, mockAgent3); // Assert Assert.Equal(1, mockAgent1.InvokeCount); @@ -48,24 +42,18 @@ public class SequentialOrchestrationTests Assert.Equal("lmn", response); } - private static async Task ExecuteOrchestrationAsync(InProcessRuntime runtime, params Agent[] mockAgents) + private static async Task ExecuteOrchestrationAsync(params Agent[] mockAgents) { // Act - await runtime.StartAsync(); - SequentialOrchestration orchestration = new(mockAgents); const string InitialInput = "123"; - OrchestrationResult result = await orchestration.InvokeAsync(InitialInput, runtime); + OrchestrationResult result = await orchestration.InvokeAsync(InitialInput); // Assert Assert.NotNull(result); // Act - string response = await result.GetValueAsync(TimeSpan.FromSeconds(20)); - - await runtime.RunUntilIdleAsync(); - - return response; + return await result.Task; } } diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/InProcessRuntimeTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/InProcessRuntimeTests.cs similarity index 88% rename from dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/InProcessRuntimeTests.cs rename to dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/InProcessRuntimeTests.cs index 3a49bc43cf..7f06f98208 100644 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/InProcessRuntimeTests.cs +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/InProcessRuntimeTests.cs @@ -18,25 +18,20 @@ public class InProcessRuntimeTests() await using InProcessRuntime runtime = new(); // Assert - Assert.False(runtime.DeliverToSelf); - Assert.Equal(0, runtime._messageQueueCount); + Assert.Equal(0, runtime.MessageCountForTesting); - // Act - await runtime.StopAsync(); // Already stopped - await runtime.RunUntilIdleAsync(); // Never throws - - await runtime.StartAsync(); + runtime.Start(); // Assert // Invalid to start runtime that is already started - await Assert.ThrowsAsync(() => runtime.StartAsync()); - Assert.Equal(0, runtime._messageQueueCount); + Assert.Throws(runtime.Start); + Assert.Equal(0, runtime.MessageCountForTesting); // Act - await runtime.StopAsync(); + await runtime.DisposeAsync(); // Assert - Assert.Equal(0, runtime._messageQueueCount); + Assert.Equal(0, runtime.MessageCountForTesting); } [Fact] @@ -194,26 +189,21 @@ public class InProcessRuntimeTests() Assert.Empty(agent.ReceivedMessages); // Act: Send message - await runtime.StartAsync(); + runtime.Start(); await runtime.SendMessageAsync("TestMessage", agent.Id); - await runtime.RunUntilIdleAsync(); + await runtime.DisposeAsync(); // Assert - Assert.Equal(0, runtime._messageQueueCount); + Assert.Equal(0, runtime.MessageCountForTesting); Assert.Single(agent.ReceivedMessages); } - // Agent will not deliver to self will success when runtime.DeliverToSelf is false (default) - [Theory] - [InlineData(false, 0)] - [InlineData(true, 1)] - public async Task RuntimeAgentPublishToSelfTestAsync(bool selfPublish, int receiveCount) + // Agent will not deliver to self + [Fact] + public async Task RuntimeAgentPublishToSelfTestAsync() { // Arrange - await using InProcessRuntime runtime = new() - { - DeliverToSelf = selfPublish - }; + await using InProcessRuntime runtime = new(); MockAgent? agent = null; await runtime.RegisterActorFactoryAsync(new("MyAgent"), async (id, runtime) => @@ -238,12 +228,12 @@ public class InProcessRuntimeTests() await runtime.AddSubscriptionAsync(new TestSubscription(TopicType, agentId.Type)); // Act - await runtime.StartAsync(); + runtime.Start(); await runtime.PublishMessageAsync("SelfMessage", new TopicId(TopicType), sender: agentId); - await runtime.RunUntilIdleAsync(); + await runtime.DisposeAsync(); // Assert - Assert.Equal(receiveCount, agent.ReceivedMessages.Count); + Assert.Empty(agent.ReceivedMessages); } [Fact] @@ -263,9 +253,9 @@ public class InProcessRuntimeTests() const string TopicType = "TestTopic"; await runtime.AddSubscriptionAsync(new TestSubscription(TopicType, agentId.Type)); - await runtime.StartAsync(); + runtime.Start(); await runtime.PublishMessageAsync("test", new TopicId(TopicType)); - await runtime.RunUntilIdleAsync(); + await runtime.DisposeAsync(); // Act: Save the state JsonElement savedState = await runtime.SaveStateAsync(); @@ -286,8 +276,7 @@ public class InProcessRuntimeTests() // Act: Start new runtime and restore the state agent = null; - await using InProcessRuntime newRuntime = new(); - await newRuntime.StartAsync(); + await using InProcessRuntime newRuntime = InProcessRuntime.StartNew(); await newRuntime.RegisterActorFactoryAsync(new("MyAgent"), async (id, runtime) => { agent = new MockAgent(id, runtime, "another agent"); diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/MessagingTestFixture.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/MessagingTestFixture.cs similarity index 89% rename from dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/MessagingTestFixture.cs rename to dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/MessagingTestFixture.cs index 234861bb3d..80eb7eb47e 100644 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/MessagingTestFixture.cs +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/MessagingTestFixture.cs @@ -56,10 +56,7 @@ public sealed class ReceiverAgent : TestAgent public ReceiverAgent(ActorId id, IAgentRuntime runtime, string description) : base(id, runtime, description) { - this.RegisterMessageHandler(async (item, messageContext, cancellationToken) => - { - this.Messages.Add(item); - }); + this.RegisterMessageHandler((item, messageContext) => this.Messages.Add(item)); } } @@ -78,10 +75,10 @@ public sealed class CancelAgent : TestAgent { public CancelAgent(ActorId id, IAgentRuntime runtime, string description) : base(id, runtime, description) { - this.RegisterMessageHandler(async (item, messageContext, cancellationToken) => + this.RegisterMessageHandler((item, messageContext) => { - CancellationToken cancelledToken = new(canceled: true); - cancelledToken.ThrowIfCancellationRequested(); + CancellationToken canceledToken = new(canceled: true); + canceledToken.ThrowIfCancellationRequested(); }); } } @@ -90,7 +87,7 @@ public sealed class ErrorAgent : TestAgent { public ErrorAgent(ActorId id, IAgentRuntime runtime, string description) : base(id, runtime, description) { - this.RegisterMessageHandler(async (item, messageContext, cancellationToken) => + this.RegisterMessageHandler((item, messageContext) => { this.DidThrow = true; throw new TestException(); @@ -155,20 +152,20 @@ public sealed class MessagingTestFixture { messageId ??= Guid.NewGuid().ToString(); - await this.Runtime.StartAsync(); + this.Runtime.Start(); await this.Runtime.PublishMessageAsync(message, sendTarget, messageId: messageId); - await this.Runtime.RunUntilIdleAsync(); + await this.Runtime.DisposeAsync(); } public async ValueTask RunSendTestAsync(ActorId sendTarget, object message, string? messageId = null) { messageId ??= Guid.NewGuid().ToString(); - await this.Runtime.StartAsync(); + this.Runtime.Start(); object? result = await this.Runtime.SendMessageAsync(message, sendTarget, messageId: messageId); - await this.Runtime.RunUntilIdleAsync(); + await this.Runtime.DisposeAsync(); return result; } diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/PublishMessageTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/PublishMessageTests.cs similarity index 100% rename from dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/PublishMessageTests.cs rename to dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/PublishMessageTests.cs diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/SendMessageTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/SendMessageTests.cs similarity index 100% rename from dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/SendMessageTests.cs rename to dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/SendMessageTests.cs diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/TestAgents.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/TestAgents.cs similarity index 87% rename from dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/TestAgents.cs rename to dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/TestAgents.cs index b44455d02c..3b35407d67 100644 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/TestAgents.cs +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/TestAgents.cs @@ -26,13 +26,12 @@ public sealed class MockAgent : TestAgent { public MockAgent(ActorId id, IAgentRuntime runtime, string description) : base(id, runtime, description) { - this.RegisterMessageHandler(this.HandleAsync); + this.RegisterMessageHandler(this.Handle); } - public ValueTask HandleAsync(string item, MessageContext messageContext, CancellationToken cancellationToken) + public void Handle(string item, MessageContext messageContext) { this.ReceivedMessages.Add(item); - return default; } public override async ValueTask SaveStateAsync(CancellationToken cancellationToken = default) diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/TestSubscription.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/TestSubscription.cs similarity index 84% rename from dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/TestSubscription.cs rename to dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/TestSubscription.cs index a9f70e151e..4445d114bd 100644 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/TestSubscription.cs +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/TestSubscription.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.Diagnostics.CodeAnalysis; namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess.Tests; @@ -23,7 +22,7 @@ public class TestSubscription(string topicType, ActorType agentType, string? id public bool Equals(ISubscriptionDefinition? other) => this.Id == other?.Id; - public override bool Equals([NotNullWhen(true)] object? obj) => obj is TestSubscription other && other.Equals(this); + public override bool Equals(object? obj) => obj is TestSubscription other && other.Equals(this); public override int GetHashCode() => this.Id.GetHashCode(); diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/MessageEnvelopeTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/MessageEnvelopeTests.cs deleted file mode 100644 index 050dc41e17..0000000000 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/MessageEnvelopeTests.cs +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess.Tests; - -public class MessageEnvelopeTests -{ - [Fact] - public void ConstructAllParametersTest() - { - // Arrange - object message = new { Content = "Test message" }; - const string MessageId = "testid"; - CancellationToken cancellation = new(); - - // Act - MessageEnvelope envelope = new(message, MessageId, cancellation); - - // Assert - Assert.Same(message, envelope.Message); - Assert.Equal(MessageId, envelope.MessageId); - Assert.Equal(cancellation, envelope.Cancellation); - Assert.Null(envelope.Sender); - Assert.Null(envelope.Receiver); - Assert.Null(envelope.Topic); - } - - [Fact] - public void ConstructOnlyRequiredParametersTest() - { - // Arrange & Act - MessageEnvelope envelope = new("test"); - - // Assert - Assert.NotNull(envelope.MessageId); - Assert.NotEmpty(envelope.MessageId); - // Verify it's a valid GUID - Assert.True(Guid.TryParse(envelope.MessageId, out _)); - } - - [Fact] - public void WithSenderTest() - { - // Arrange - MessageEnvelope envelope = new("test"); - ActorId sender = new("testtype", "testkey"); - - // Act - MessageEnvelope result = envelope.WithSender(sender); - - // Assert - Assert.Same(envelope, result); - Assert.Equal(sender, envelope.Sender); - } - - [Fact] - public async Task ForSendTestAsync() - { - // Arrange - MessageEnvelope envelope = new("test"); - ActorId receiver = new("receivertype", "receiverkey"); - object expectedResult = new { Response = "Success" }; - - ValueTask servicer(MessageEnvelope env, CancellationToken ct) => new(expectedResult); - - // Act - MessageDelivery delivery = envelope.ForSend(receiver, servicer); - - // Assert - Assert.NotNull(delivery); - Assert.Same(envelope, delivery.Message); - Assert.Equal(receiver, envelope.Receiver); - - // Invoke the servicer to verify result sink works - await delivery.InvokeAsync(CancellationToken.None); - Assert.True(delivery.ResultTask.IsCompleted); - object? result = await delivery.ResultTask; - Assert.Same(expectedResult, result); - } - - [Fact] - public void ForPublishTest() - { - // Arrange - MessageEnvelope envelope = new("test"); - TopicId topic = new("testtopic"); - - static ValueTask servicer(MessageEnvelope env, CancellationToken ct) => default; - - // Act - MessageDelivery delivery = envelope.ForPublish(topic, servicer); - - // Assert - Assert.NotNull(delivery); - Assert.Same(envelope, delivery.Message); - Assert.Equal(topic, envelope.Topic); - } -} diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests.csproj b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests.csproj deleted file mode 100644 index 39d45f9cd2..0000000000 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - $(ProjectsTargetFrameworks) - $(ProjectsDebugTargetFrameworks) - - - - - - -