From fbf1f10a8a2f662152cf892930a4422b5e9161ff Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 10 Jul 2025 07:57:51 -0400 Subject: [PATCH] First round of cleanup of runtime abstractions (#156) --- .../DynamicallyAccessedMemberTypes.cs | 2 +- .../DynamicallyAccessedMembersAttribute.cs | 4 +- .../RequiresDynamicCodeAttribute.cs | 4 +- .../RequiresUnreferencedCodeAttribute.cs | 4 +- .../UnconditionalSuppressMessageAttribute.cs | 6 +- .../AgentActor.cs | 2 +- .../AgentOrchestration.RequestActor.cs | 12 +- .../AgentOrchestration.ResultActor.cs | 12 +- .../AgentOrchestration.cs | 32 +-- .../Concurrent/ConcurrentActor.cs | 16 +- .../Concurrent/ConcurrentOrchestration.cs | 18 +- .../Concurrent/ConcurrentResultActor.cs | 17 +- .../Extensions/RuntimeExtensions.cs | 16 +- .../GroupChat/GroupChatAgentActor.cs | 40 ++- .../GroupChat/GroupChatManager.cs | 5 - .../GroupChat/GroupChatManagerActor.cs | 42 ++-- .../GroupChat/GroupChatOrchestration.cs | 28 +-- .../GroupChat/RoundRobinGroupChatManager.cs | 12 - .../Handoff/HandoffActor.cs | 51 ++-- .../Handoff/HandoffOrchestration.cs | 18 +- .../Handoff/Handoffs.cs | 4 +- .../Logging/AgentOrchestrationLogMessages.cs | 14 +- .../ConcurrentOrchestrationLogMessages.cs | 6 +- .../GroupChatOrchestrationLogMessages.cs | 20 +- .../HandoffOrchestrationLogMessages.cs | 8 +- .../SequentialOrchestrationLogMessages.cs | 4 +- .../OrchestrationActor.cs | 8 +- .../OrchestrationResult.cs | 32 +-- .../Sequential/SequentialActor.cs | 33 ++- .../Sequential/SequentialOrchestration.cs | 19 +- .../Transforms/DefaultTransforms.cs | 4 - .../ActorId.cs | 116 +++++++++ .../ActorMetadata.cs | 72 ++++++ .../ActorType.cs | 68 +++++ .../AgentId.cs | 135 ---------- .../AgentMetadata.cs | 58 ----- .../AgentProxy.cs | 85 ------- .../AgentType.cs | 103 -------- .../BaseAgent.cs | 149 ----------- .../Exceptions/CantHandleException.cs | 31 --- .../Exceptions/MessageDroppedException.cs | 31 --- .../Exceptions/NotAccessibleException.cs | 31 --- .../Exceptions/UndeliverableException.cs | 30 --- .../HandlerInvoker.cs | 138 ----------- .../IAgentRuntime.cs | 108 ++++---- .../IHandle.cs | 36 --- .../IHostableAgent.cs | 17 -- .../{IAgent.cs => IRuntimeActor.cs} | 21 +- .../ISaveState.cs | 14 +- .../ISubscriptionDefinition.cs | 8 +- .../IdProxyActor.cs | 53 ++++ .../KeyValueParserExtensions.cs | 47 +--- .../MessageContext.cs | 32 +-- ...ions.AI.Agents.Runtime.Abstractions.csproj | 6 +- .../RuntimeActor.cs | 188 ++++++++++++++ .../TopicId.cs | 192 ++++++--------- .../TypeSubscription.cs | 32 +-- .../InProcessRuntime.cs | 233 +++++++++--------- .../MessageDelivery.cs | 9 +- .../MessageEnvelope.cs | 51 ++-- ...ensions.AI.Agents.Runtime.InProcess.csproj | 1 - .../ResultSink.cs | 56 ----- .../AgentIdTests.cs | 61 ++--- .../AgentMetaDataTests.cs | 4 +- .../AgentProxyTests.cs | 20 +- .../AgentTypeTests.cs | 40 +-- .../MessageContextTests.cs | 80 ++---- .../TopicIdTests.cs | 79 +----- .../InProcessRuntimeTests.cs | 94 +++---- .../MessageEnvelopeTests.cs | 8 +- .../MessagingTestFixture.cs | 145 +++++------ .../PublishMessageTests.cs | 4 +- .../ResultSinkTests.cs | 103 -------- .../SendMessageTests.cs | 26 +- .../TestAgents.cs | 19 +- .../TestSubscription.cs | 6 +- 76 files changed, 1254 insertions(+), 2079 deletions(-) create mode 100644 dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorId.cs create mode 100644 dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorMetadata.cs create mode 100644 dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorType.cs delete mode 100644 dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/AgentId.cs delete mode 100644 dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/AgentMetadata.cs delete mode 100644 dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/AgentProxy.cs delete mode 100644 dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/AgentType.cs delete mode 100644 dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/BaseAgent.cs delete mode 100644 dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Exceptions/CantHandleException.cs delete mode 100644 dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Exceptions/MessageDroppedException.cs delete mode 100644 dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Exceptions/NotAccessibleException.cs delete mode 100644 dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Exceptions/UndeliverableException.cs delete mode 100644 dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/HandlerInvoker.cs delete mode 100644 dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IHandle.cs delete mode 100644 dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IHostableAgent.cs rename dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/{IAgent.cs => IRuntimeActor.cs} (66%) create mode 100644 dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IdProxyActor.cs create mode 100644 dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/RuntimeActor.cs delete mode 100644 dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/ResultSink.cs delete mode 100644 dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/ResultSinkTests.cs diff --git a/dotnet/src/LegacySupport/TrimAttributes/DynamicallyAccessedMemberTypes.cs b/dotnet/src/LegacySupport/TrimAttributes/DynamicallyAccessedMemberTypes.cs index 15f7505691..c8663103a5 100644 --- a/dotnet/src/LegacySupport/TrimAttributes/DynamicallyAccessedMemberTypes.cs +++ b/dotnet/src/LegacySupport/TrimAttributes/DynamicallyAccessedMemberTypes.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. namespace System.Diagnostics.CodeAnalysis; diff --git a/dotnet/src/LegacySupport/TrimAttributes/DynamicallyAccessedMembersAttribute.cs b/dotnet/src/LegacySupport/TrimAttributes/DynamicallyAccessedMembersAttribute.cs index 0698067c11..fc8eae8910 100644 --- a/dotnet/src/LegacySupport/TrimAttributes/DynamicallyAccessedMembersAttribute.cs +++ b/dotnet/src/LegacySupport/TrimAttributes/DynamicallyAccessedMembersAttribute.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. namespace System.Diagnostics.CodeAnalysis; @@ -39,7 +39,7 @@ internal sealed class DynamicallyAccessedMembersAttribute : Attribute /// The types of members dynamically accessed. public DynamicallyAccessedMembersAttribute(DynamicallyAccessedMemberTypes memberTypes) { - MemberTypes = memberTypes; + this.MemberTypes = memberTypes; } /// diff --git a/dotnet/src/LegacySupport/TrimAttributes/RequiresDynamicCodeAttribute.cs b/dotnet/src/LegacySupport/TrimAttributes/RequiresDynamicCodeAttribute.cs index b593fc1761..d609038d56 100644 --- a/dotnet/src/LegacySupport/TrimAttributes/RequiresDynamicCodeAttribute.cs +++ b/dotnet/src/LegacySupport/TrimAttributes/RequiresDynamicCodeAttribute.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. namespace System.Diagnostics.CodeAnalysis; @@ -22,7 +22,7 @@ internal sealed class RequiresDynamicCodeAttribute : Attribute /// public RequiresDynamicCodeAttribute(string message) { - Message = message; + this.Message = message; } /// diff --git a/dotnet/src/LegacySupport/TrimAttributes/RequiresUnreferencedCodeAttribute.cs b/dotnet/src/LegacySupport/TrimAttributes/RequiresUnreferencedCodeAttribute.cs index 8b116ff2bc..3464586054 100644 --- a/dotnet/src/LegacySupport/TrimAttributes/RequiresUnreferencedCodeAttribute.cs +++ b/dotnet/src/LegacySupport/TrimAttributes/RequiresUnreferencedCodeAttribute.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. namespace System.Diagnostics.CodeAnalysis; @@ -23,7 +23,7 @@ internal sealed class RequiresUnreferencedCodeAttribute : Attribute /// public RequiresUnreferencedCodeAttribute(string message) { - Message = message; + this.Message = message; } /// diff --git a/dotnet/src/LegacySupport/TrimAttributes/UnconditionalSuppressMessageAttribute.cs b/dotnet/src/LegacySupport/TrimAttributes/UnconditionalSuppressMessageAttribute.cs index 2896a7497f..4e1b380fc2 100644 --- a/dotnet/src/LegacySupport/TrimAttributes/UnconditionalSuppressMessageAttribute.cs +++ b/dotnet/src/LegacySupport/TrimAttributes/UnconditionalSuppressMessageAttribute.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. namespace System.Diagnostics.CodeAnalysis; @@ -23,8 +23,8 @@ internal sealed class UnconditionalSuppressMessageAttribute : Attribute /// The identifier of the analysis rule the attribute applies to. public UnconditionalSuppressMessageAttribute(string category, string checkId) { - Category = category; - CheckId = checkId; + this.Category = category; + this.CheckId = checkId; } /// diff --git a/dotnet/src/Microsoft.Agents.Orchestration/AgentActor.cs b/dotnet/src/Microsoft.Agents.Orchestration/AgentActor.cs index df8903840e..fb83e266d3 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/AgentActor.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/AgentActor.cs @@ -25,7 +25,7 @@ public abstract class AgentActor : OrchestrationActor /// The orchestration context. /// An . /// The logger to use for the actor - protected AgentActor(AgentId id, IAgentRuntime runtime, OrchestrationContext context, Agent agent, ILogger? logger = null) + protected AgentActor(ActorId id, IAgentRuntime runtime, OrchestrationContext context, Agent agent, ILogger? logger = null) : base( id, runtime, diff --git a/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.RequestActor.cs b/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.RequestActor.cs index 651ea3959b..70cbb60d90 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.RequestActor.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.RequestActor.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Microsoft.Agents.Orchestration.Transforms; using Microsoft.Extensions.AI; @@ -15,7 +16,7 @@ public abstract partial class AgentOrchestration /// /// Actor responsible for receiving final message and transforming it into the output type. /// - private sealed class RequestActor : OrchestrationActor, IHandle + private sealed class RequestActor : OrchestrationActor { private readonly OrchestrationInputTransform _transform; private readonly Func, ValueTask> _action; @@ -32,7 +33,7 @@ public abstract partial class AgentOrchestration /// An asynchronous function that processes the resulting source. /// The logger to use for the actor public RequestActor( - AgentId id, + ActorId id, IAgentRuntime runtime, OrchestrationContext context, OrchestrationInputTransform transform, @@ -44,6 +45,8 @@ public abstract partial class AgentOrchestration this._transform = transform; this._action = action; this._completionSource = completionSource; + + this.RegisterMessageHandler(this.HandleAsync); } /// @@ -51,13 +54,14 @@ public abstract partial class AgentOrchestration /// /// The input message of type TInput. /// The context of the message, providing additional details. + /// A token to cancel the operation if needed. /// A ValueTask representing the asynchronous operation. - public async ValueTask HandleAsync(TInput item, MessageContext messageContext) + private async ValueTask HandleAsync(TInput item, MessageContext messageContext, CancellationToken cancellationToken) { this.Logger.LogOrchestrationRequestInvoke(this.Context.Orchestration, this.Id); try { - IEnumerable input = await this._transform.Invoke(item).ConfigureAwait(false); + IEnumerable input = await this._transform.Invoke(item, cancellationToken).ConfigureAwait(false); Task task = this._action.Invoke(input).AsTask(); this.Logger.LogOrchestrationStart(this.Context.Orchestration, this.Id); await task.ConfigureAwait(false); diff --git a/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.ResultActor.cs b/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.ResultActor.cs index edc2e79950..c92d35f6bd 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.ResultActor.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.ResultActor.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Microsoft.Agents.Orchestration.Transforms; using Microsoft.Extensions.AI; @@ -15,7 +16,7 @@ public abstract partial class AgentOrchestration /// /// Actor responsible for receiving the resultant message, transforming it, and handling further orchestration. /// - private sealed class ResultActor : OrchestrationActor, IHandle + private sealed class ResultActor : OrchestrationActor { private readonly TaskCompletionSource _completionSource; private readonly OrchestrationResultTransform _transformResult; @@ -32,7 +33,7 @@ public abstract partial class AgentOrchestration /// Optional TaskCompletionSource to signal orchestration completion. /// The logger to use for the actor public ResultActor( - AgentId id, + ActorId id, IAgentRuntime runtime, OrchestrationContext context, OrchestrationResultTransform transformResult, @@ -44,6 +45,8 @@ public abstract partial class AgentOrchestration this._completionSource = completionSource; this._transformResult = transformResult; this._transform = transformOutput; + + this.RegisterMessageHandler(this.HandleAsync); } /// @@ -53,8 +56,9 @@ public abstract partial class AgentOrchestration /// /// The result item to process. /// The context associated with the message. + /// A token to cancel the operation if needed. /// A ValueTask representing asynchronous operation. - public async ValueTask HandleAsync(TResult item, MessageContext messageContext) + private async ValueTask HandleAsync(TResult item, MessageContext messageContext, CancellationToken cancellationToken) { this.Logger.LogOrchestrationResultInvoke(this.Context.Orchestration, this.Id); @@ -63,7 +67,7 @@ public abstract partial class AgentOrchestration if (!this._completionSource.Task.IsCompleted) { IList result = this._transformResult.Invoke(item); - TOutput output = await this._transform.Invoke(result).ConfigureAwait(false); + TOutput output = await this._transform.Invoke(result, cancellationToken).ConfigureAwait(false); this._completionSource.TrySetResult(output); } } diff --git a/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.cs b/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.cs index 8df9416205..c95961469c 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.cs @@ -129,7 +129,7 @@ public abstract partial class AgentOrchestration TaskCompletionSource completion = new(); - AgentType orchestrationType = await this.RegisterAsync(runtime, context, completion, handoff: null).ConfigureAwait(false); + ActorType orchestrationType = await this.RegisterAsync(runtime, context, completion, handoff: null).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); @@ -149,7 +149,7 @@ public abstract partial class AgentOrchestration /// The unique identifier for the orchestration session. /// The input to be transformed and processed. /// The initial agent type used for starting the orchestration. - protected abstract ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable input, AgentType? entryAgent); + protected abstract ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable input, ActorType? entryAgent); /// /// Orchestration specific registration, including members and returns an optional entry agent. @@ -159,7 +159,7 @@ public abstract partial class AgentOrchestration /// A registration context. /// The logger to use during registration /// The entry AgentType for the orchestration, if any. - protected abstract ValueTask RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger); + protected abstract ValueTask RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger); /// /// Formats and returns a unique AgentType based on the provided topic and suffix. @@ -167,7 +167,7 @@ public abstract partial class AgentOrchestration /// The topic identifier used in formatting the agent type. /// A suffix to differentiate the agent type. /// A formatted AgentType object. - protected AgentType FormatAgentType(TopicId topic, string suffix) => new($"{topic.Type}_{suffix}"); + protected ActorType FormatAgentType(TopicId topic, string suffix) => new($"{topic.Type}_{suffix}"); /// /// Registers the orchestration's root and boot agents, setting up completion and target routing. @@ -177,7 +177,7 @@ public abstract partial class AgentOrchestration /// A TaskCompletionSource for the orchestration. /// The actor type used for handoff. Only defined for nested orchestrations. /// The AgentType representing the orchestration entry point. - private async ValueTask RegisterAsync(IAgentRuntime runtime, OrchestrationContext context, TaskCompletionSource completion, AgentType? handoff) + private async ValueTask RegisterAsync(IAgentRuntime runtime, OrchestrationContext context, TaskCompletionSource completion, ActorType? handoff) { // Create a logger for the orchestration registration. ILogger logger = context.LoggerFactory.CreateLogger(this.GetType()); @@ -185,10 +185,10 @@ public abstract partial class AgentOrchestration // Register orchestration RegistrationContext registrar = new(this.FormatAgentType(context.Topic, "Root"), runtime, context, completion, this.ResultTransform); - AgentType? entryAgent = await this.RegisterOrchestrationAsync(runtime, context, registrar, logger).ConfigureAwait(false); + ActorType? entryAgent = await this.RegisterOrchestrationAsync(runtime, context, registrar, logger).ConfigureAwait(false); // Register actor for orchestration entry-point - AgentType orchestrationEntry = + ActorType orchestrationEntry = await runtime.RegisterOrchestrationAgentAsync( this.FormatAgentType(context.Topic, "Boot"), (agentId, runtime) => @@ -201,11 +201,7 @@ public abstract partial class AgentOrchestration completion, StartAsync, context.LoggerFactory.CreateLogger()); -#if !NETCOREAPP - return new ValueTask(actor); -#else - return ValueTask.FromResult(actor); -#endif + return new ValueTask(actor); }).ConfigureAwait(false); logger.LogOrchestrationRegistrationDone(context.Orchestration, context.Topic); @@ -219,7 +215,7 @@ public abstract partial class AgentOrchestration /// A context used during registration (). /// public sealed class RegistrationContext( - AgentType agentType, + ActorType agentType, IAgentRuntime runtime, OrchestrationContext context, TaskCompletionSource completion, @@ -228,10 +224,10 @@ public abstract partial class AgentOrchestration /// /// Register the final result type. /// - public async ValueTask RegisterResultTypeAsync(OrchestrationResultTransform resultTransform) + public async ValueTask RegisterResultTypeAsync(OrchestrationResultTransform resultTransform) { // Register actor for final result - AgentType registeredType = + ActorType registeredType = await runtime.RegisterOrchestrationAgentAsync( agentType, (agentId, runtime) => @@ -244,11 +240,7 @@ public abstract partial class AgentOrchestration outputTransform, completion, context.LoggerFactory.CreateLogger>()); -#if !NETCOREAPP - return new ValueTask(actor); -#else - return ValueTask.FromResult(actor); -#endif + return new ValueTask(actor); }).ConfigureAwait(false); return registeredType; diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentActor.cs b/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentActor.cs index 5a783c6db4..5a1a10e7b1 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentActor.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentActor.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; @@ -11,9 +12,9 @@ namespace Microsoft.Agents.Orchestration.Concurrent; /// /// An used with the . /// -internal sealed class ConcurrentActor : AgentActor, IHandle +internal sealed class ConcurrentActor : AgentActor { - private readonly AgentType _handoffActor; + private readonly ActorType _handoffActor; /// /// Initializes a new instance of the class. @@ -24,21 +25,22 @@ internal sealed class ConcurrentActor : AgentActor, IHandleAn . /// Identifies the actor collecting results. /// The logger to use for the actor - public ConcurrentActor(AgentId id, IAgentRuntime runtime, OrchestrationContext context, Agent agent, AgentType resultActor, ILogger? logger = null) + public ConcurrentActor(ActorId id, IAgentRuntime runtime, OrchestrationContext context, Agent agent, ActorType resultActor, ILogger? logger = null) : base(id, runtime, context, agent, logger) { this._handoffActor = resultActor; + + this.RegisterMessageHandler(this.HandleAsync); } - /// - public async ValueTask HandleAsync(ConcurrentMessages.Request item, MessageContext messageContext) + private async ValueTask HandleAsync(ConcurrentMessages.Request item, MessageContext messageContext, CancellationToken cancellationToken) { this.Logger.LogConcurrentAgentInvoke(this.Id); - ChatMessage response = await this.InvokeAsync(item.Messages, messageContext.CancellationToken).ConfigureAwait(false); + ChatMessage response = await this.InvokeAsync(item.Messages, cancellationToken).ConfigureAwait(false); this.Logger.LogConcurrentAgentResult(this.Id, response.Text); - await this.PublishMessageAsync(response.AsResultMessage(), this._handoffActor, messageContext.CancellationToken).ConfigureAwait(false); + await this.PublishMessageAsync(response.AsResultMessage(), this._handoffActor, cancellationToken).ConfigureAwait(false); } } diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentOrchestration.cs b/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentOrchestration.cs index 7f94189e14..6e9e19d0a1 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentOrchestration.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentOrchestration.cs @@ -30,18 +30,18 @@ public class ConcurrentOrchestration } /// - protected override ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable input, AgentType? entryAgent) + protected override ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable input, ActorType? entryAgent) { return runtime.PublishMessageAsync(input.AsInputMessage(), topic); } /// - protected override async ValueTask RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger) + protected override async ValueTask RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger) { - AgentType outputType = await registrar.RegisterResultTypeAsync(response => [.. response.Select(r => r.Message)]).ConfigureAwait(false); + ActorType outputType = await registrar.RegisterResultTypeAsync(response => [.. response.Select(r => r.Message)]).ConfigureAwait(false); // Register result actor - AgentType resultType = this.FormatAgentType(context.Topic, "Results"); + ActorType resultType = this.FormatAgentType(context.Topic, "Results"); await runtime.RegisterOrchestrationAgentAsync( resultType, async (agentId, runtime) => @@ -57,17 +57,13 @@ public class ConcurrentOrchestration { ++agentCount; - AgentType agentType = - await runtime.RegisterAgentFactoryAsync( + ActorType agentType = + await runtime.RegisterActorFactoryAsync( this.FormatAgentType(context.Topic, $"Agent_{agentCount}"), (agentId, runtime) => { ConcurrentActor actor = new(agentId, runtime, context, agent, resultType, context.LoggerFactory.CreateLogger()); -#if !NETCOREAPP - return new ValueTask(actor); -#else - return ValueTask.FromResult(actor); -#endif + return new ValueTask(actor); }).ConfigureAwait(false); logger.LogRegisterActor(this.OrchestrationLabel, agentType, "MEMBER", agentCount); diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentResultActor.cs b/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentResultActor.cs index c6b4dbd14d..db5bbcb573 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentResultActor.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentResultActor.cs @@ -11,12 +11,10 @@ namespace Microsoft.Agents.Orchestration.Concurrent; /// /// Actor for capturing each message. /// -internal sealed class ConcurrentResultActor : - OrchestrationActor, - IHandle +internal sealed class ConcurrentResultActor : OrchestrationActor { private readonly ConcurrentQueue _results; - private readonly AgentType _orchestrationType; + private readonly ActorType _orchestrationType; private readonly int _expectedCount; private int _resultCount; @@ -30,10 +28,10 @@ internal sealed class ConcurrentResultActor : /// The expected number of messages to be received. /// The logger to use for the actor public ConcurrentResultActor( - AgentId id, + ActorId id, IAgentRuntime runtime, OrchestrationContext context, - AgentType orchestrationType, + ActorType orchestrationType, int expectedCount, ILogger logger) : base(id, runtime, context, "Captures the results of the ConcurrentOrchestration", logger) @@ -41,10 +39,11 @@ internal sealed class ConcurrentResultActor : this._orchestrationType = orchestrationType; this._expectedCount = expectedCount; this._results = []; + + this.RegisterMessageHandler(this.HandleAsync); } - /// - public async ValueTask HandleAsync(ConcurrentMessages.Result item, MessageContext messageContext) + private async ValueTask HandleAsync(ConcurrentMessages.Result item, MessageContext messageContext, CancellationToken cancellationToken) { this.Logger.LogConcurrentResultCapture(this.Id, this._resultCount + 1, this._expectedCount); @@ -52,7 +51,7 @@ internal sealed class ConcurrentResultActor : if (Interlocked.Increment(ref this._resultCount) == this._expectedCount) { - await this.PublishMessageAsync(this._results.ToArray(), this._orchestrationType, messageContext.CancellationToken).ConfigureAwait(false); + await this.PublishMessageAsync(this._results.ToArray(), this._orchestrationType, cancellationToken).ConfigureAwait(false); } } } diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Extensions/RuntimeExtensions.cs b/dotnet/src/Microsoft.Agents.Orchestration/Extensions/RuntimeExtensions.cs index eadf788292..94400a6746 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Extensions/RuntimeExtensions.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Extensions/RuntimeExtensions.cs @@ -15,9 +15,9 @@ public static class RuntimeExtensions /// /// Sends a message to the specified agent. /// - public static async ValueTask PublishMessageAsync(this IAgentRuntime runtime, object message, AgentType agentType, CancellationToken cancellationToken = default) + public static async ValueTask PublishMessageAsync(this IAgentRuntime runtime, object message, ActorType agentType, CancellationToken cancellationToken = default) { - await runtime.PublishMessageAsync(message, new TopicId(agentType), sender: null, messageId: null, cancellationToken).ConfigureAwait(false); + await runtime.PublishMessageAsync(message, new TopicId(agentType.Name), sender: null, messageId: null, cancellationToken).ConfigureAwait(false); } /// @@ -27,12 +27,12 @@ public static class RuntimeExtensions /// The type of agent to register. /// The factory function for creating the agent. /// The registered agent type. - public static async ValueTask RegisterOrchestrationAgentAsync(this IAgentRuntime runtime, AgentType agentType, Func> factoryFunc) + public static async ValueTask RegisterOrchestrationAgentAsync(this IAgentRuntime runtime, ActorType agentType, Func> factoryFunc) { - AgentType registeredType = await runtime.RegisterAgentFactoryAsync(agentType, factoryFunc).ConfigureAwait(false); + ActorType registeredType = await runtime.RegisterActorFactoryAsync(agentType, factoryFunc).ConfigureAwait(false); // Subscribe agent to its own unique topic - await runtime.SubscribeAsync(registeredType).ConfigureAwait(false); + await runtime.SubscribeAsync(new(registeredType.Name)).ConfigureAwait(false); return registeredType; } @@ -42,9 +42,9 @@ public static class RuntimeExtensions /// /// The runtime for managing the subscription. /// The agent type to subscribe. - public static async Task SubscribeAsync(this IAgentRuntime runtime, string agentType) + public static async Task SubscribeAsync(this IAgentRuntime runtime, ActorType agentType) { - await runtime.AddSubscriptionAsync(new TypeSubscription(agentType, agentType)).ConfigureAwait(false); + await runtime.AddSubscriptionAsync(new TypeSubscription(agentType.Name, agentType)).ConfigureAwait(false); } /// @@ -53,7 +53,7 @@ public static class RuntimeExtensions /// The runtime for managing the subscription. /// The agent type to subscribe. /// A variable list of topics for subscription. - public static async Task SubscribeAsync(this IAgentRuntime runtime, string agentType, params TopicId[] topics) + public static async Task SubscribeAsync(this IAgentRuntime runtime, ActorType agentType, params TopicId[] topics) { for (int index = 0; index < topics.Length; ++index) { diff --git a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatAgentActor.cs b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatAgentActor.cs index a668430182..c71d8dc398 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatAgentActor.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatAgentActor.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; @@ -12,11 +13,7 @@ namespace Microsoft.Agents.Orchestration.GroupChat; /// /// An used with the . /// -internal sealed class GroupChatAgentActor : - AgentActor, - IHandle, - IHandle, - IHandle +internal sealed class GroupChatAgentActor : AgentActor { private readonly List _cache; @@ -28,46 +25,37 @@ internal sealed class GroupChatAgentActor : /// The orchestration context. /// An . /// The logger to use for the actor - public GroupChatAgentActor(AgentId id, IAgentRuntime runtime, OrchestrationContext context, Agent agent, ILogger? logger = null) + public GroupChatAgentActor(ActorId id, IAgentRuntime runtime, OrchestrationContext context, Agent agent, ILogger? logger = null) : base(id, runtime, context, agent, logger) { this._cache = []; + + this.RegisterMessageHandler(this.HandleAsync); + this.RegisterMessageHandler(this.HandleAsync); + this.RegisterMessageHandler(this.HandleAsync); } - /// - public ValueTask HandleAsync(GroupChatMessages.Group item, MessageContext messageContext) + private ValueTask HandleAsync(GroupChatMessages.Group item, MessageContext messageContext, CancellationToken cancellationToken) { this._cache.AddRange(item.Messages); - -#if !NETCOREAPP - return new ValueTask(); -#else - return ValueTask.CompletedTask; -#endif + return default; } - /// - public ValueTask HandleAsync(GroupChatMessages.Reset item, MessageContext messageContext) + private ValueTask HandleAsync(GroupChatMessages.Reset item, MessageContext messageContext, CancellationToken cancellationToken) { this.ResetThread(); - -#if !NETCOREAPP - return new ValueTask(); -#else - return ValueTask.CompletedTask; -#endif + return default; } - /// - public async ValueTask HandleAsync(GroupChatMessages.Speak item, MessageContext messageContext) + private async ValueTask HandleAsync(GroupChatMessages.Speak item, MessageContext messageContext, CancellationToken cancellationToken) { this.Logger.LogChatAgentInvoke(this.Id); - ChatMessage response = await this.InvokeAsync(this._cache, messageContext.CancellationToken).ConfigureAwait(false); + ChatMessage response = await this.InvokeAsync(this._cache, cancellationToken).ConfigureAwait(false); this.Logger.LogChatAgentResult(this.Id, response.Text); this._cache.Clear(); - await this.PublishMessageAsync(response.AsGroupMessage(), this.Context.Topic).ConfigureAwait(false); + await this.PublishMessageAsync(response.AsGroupMessage(), 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 d82e961da0..de6999d928 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatManager.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatManager.cs @@ -96,11 +96,6 @@ public abstract class GroupChatManager } GroupChatManagerResult result = new(resultValue) { Reason = reason }; - -#if !NETCOREAPP return new ValueTask>(result); -#else - return ValueTask.FromResult(result); -#endif } } diff --git a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatManagerActor.cs b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatManagerActor.cs index f7e51017d9..69eb38972b 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatManagerActor.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatManagerActor.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents.Runtime; @@ -11,17 +12,14 @@ namespace Microsoft.Agents.Orchestration.GroupChat; /// /// An used to manage a . /// -internal sealed class GroupChatManagerActor : - OrchestrationActor, - IHandle, - IHandle +internal sealed class GroupChatManagerActor : OrchestrationActor { /// /// A common description for the manager. /// public const string DefaultDescription = "Orchestrates a team of agents to accomplish a defined task."; - private readonly AgentType _orchestrationType; + private readonly ActorType _orchestrationType; private readonly GroupChatManager _manager; private readonly List _chat; private readonly GroupChatTeam _team; @@ -36,65 +34,67 @@ internal sealed class GroupChatManagerActor : /// The team of agents being orchestrated /// Identifies the orchestration agent. /// The logger to use for the actor - public GroupChatManagerActor(AgentId id, IAgentRuntime runtime, OrchestrationContext context, GroupChatManager manager, GroupChatTeam team, AgentType orchestrationType, ILogger? logger = null) + public GroupChatManagerActor(ActorId id, IAgentRuntime runtime, OrchestrationContext context, GroupChatManager manager, GroupChatTeam team, ActorType orchestrationType, ILogger? logger = null) : base(id, runtime, context, DefaultDescription, logger) { this._chat = []; this._manager = manager; this._orchestrationType = orchestrationType; this._team = team; + + this.RegisterMessageHandler(this.HandleAsync); + this.RegisterMessageHandler(this.HandleAsync); } - /// - public async ValueTask HandleAsync(GroupChatMessages.InputTask item, MessageContext messageContext) + private async ValueTask HandleAsync(GroupChatMessages.InputTask item, MessageContext messageContext, CancellationToken cancellationToken) { this.Logger.LogChatManagerInit(this.Id); this._chat.AddRange(item.Messages); - await this.PublishMessageAsync(item.Messages.AsGroupMessage(), this.Context.Topic).ConfigureAwait(false); + await this.PublishMessageAsync(item.Messages.AsGroupMessage(), this.Context.Topic, cancellationToken: cancellationToken).ConfigureAwait(false); - await this.ManageAsync(messageContext).ConfigureAwait(false); + await this.ManageAsync(messageContext, cancellationToken).ConfigureAwait(false); } /// - public async ValueTask HandleAsync(GroupChatMessages.Group item, MessageContext messageContext) + private async ValueTask HandleAsync(GroupChatMessages.Group item, MessageContext messageContext, CancellationToken cancellationToken) { this.Logger.LogChatManagerInvoke(this.Id); this._chat.AddRange(item.Messages); - await this.ManageAsync(messageContext).ConfigureAwait(false); + await this.ManageAsync(messageContext, cancellationToken).ConfigureAwait(false); } - private async ValueTask ManageAsync(MessageContext messageContext) + private async ValueTask ManageAsync(MessageContext messageContext, CancellationToken cancellationToken) { if (this._manager.InteractiveCallback != null) { - GroupChatManagerResult inputResult = await this._manager.ShouldRequestUserInput(this._chat, messageContext.CancellationToken).ConfigureAwait(false); + GroupChatManagerResult inputResult = await this._manager.ShouldRequestUserInput(this._chat, cancellationToken).ConfigureAwait(false); this.Logger.LogChatManagerInput(this.Id, inputResult.Value, inputResult.Reason); if (inputResult.Value) { 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).ConfigureAwait(false); + await this.PublishMessageAsync(input.AsGroupMessage(), this.Context.Topic, cancellationToken: cancellationToken).ConfigureAwait(false); } } - GroupChatManagerResult terminateResult = await this._manager.ShouldTerminate(this._chat, messageContext.CancellationToken).ConfigureAwait(false); + GroupChatManagerResult terminateResult = await this._manager.ShouldTerminate(this._chat, cancellationToken).ConfigureAwait(false); this.Logger.LogChatManagerTerminate(this.Id, terminateResult.Value, terminateResult.Reason); if (terminateResult.Value) { - GroupChatManagerResult filterResult = await this._manager.FilterResults(this._chat, messageContext.CancellationToken).ConfigureAwait(false); + 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, messageContext.CancellationToken).ConfigureAwait(false); + await this.PublishMessageAsync(filterResult.Value.AsResultMessage(), this._orchestrationType, cancellationToken).ConfigureAwait(false); return; } - GroupChatManagerResult selectionResult = await this._manager.SelectNextAgent(this._chat, this._team, messageContext.CancellationToken).ConfigureAwait(false); - AgentType selectionType = this._team[selectionResult.Value].Type; + GroupChatManagerResult selectionResult = await this._manager.SelectNextAgent(this._chat, this._team, cancellationToken).ConfigureAwait(false); + ActorType selectionType = new(this._team[selectionResult.Value].Type); this.Logger.LogChatManagerSelect(this.Id, selectionType); - await this.PublishMessageAsync(new GroupChatMessages.Speak(), selectionType, messageContext.CancellationToken).ConfigureAwait(false); + await this.PublishMessageAsync(new GroupChatMessages.Speak(), selectionType, cancellationToken: cancellationToken).ConfigureAwait(false); } } diff --git a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatOrchestration.cs b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatOrchestration.cs index a5440767a0..e921eb7362 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatOrchestration.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatOrchestration.cs @@ -36,7 +36,7 @@ public class GroupChatOrchestration : } /// - protected override ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable input, AgentType? entryAgent) + protected override ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable input, ActorType? entryAgent) { if (!entryAgent.HasValue) { @@ -46,37 +46,33 @@ public class GroupChatOrchestration : } /// - protected override async ValueTask RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger) + protected override async ValueTask RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger) { - AgentType outputType = await registrar.RegisterResultTypeAsync(response => [response.Message]).ConfigureAwait(false); + ActorType outputType = await registrar.RegisterResultTypeAsync(response => [response.Message]).ConfigureAwait(false); int agentCount = 0; GroupChatTeam team = []; foreach (Agent agent in this.Members) { ++agentCount; - AgentType agentType = await RegisterAgentAsync(agent, agentCount).ConfigureAwait(false); - string name = agent.Name ?? agent.Id ?? agentType; + ActorType agentType = await RegisterAgentAsync(agent, agentCount).ConfigureAwait(false); + string name = agent.Name ?? agent.Id ?? agentType.Name; string? description = agent.Description; - team[name] = (agentType, description ?? DefaultAgentDescription); + team[name] = (agentType.Name, description ?? DefaultAgentDescription); logger.LogRegisterActor(this.OrchestrationLabel, agentType, "MEMBER", agentCount); await runtime.SubscribeAsync(agentType, context.Topic).ConfigureAwait(false); } - AgentType managerType = + ActorType managerType = await runtime.RegisterOrchestrationAgentAsync( this.FormatAgentType(context.Topic, "Manager"), (agentId, runtime) => { GroupChatManagerActor actor = new(agentId, runtime, context, this._manager, team, outputType, context.LoggerFactory.CreateLogger()); -#if !NETCOREAPP - return new ValueTask(actor); -#else - return ValueTask.FromResult(actor); -#endif + return new ValueTask(actor); }).ConfigureAwait(false); logger.LogRegisterActor(this.OrchestrationLabel, managerType, "MANAGER"); @@ -84,17 +80,13 @@ public class GroupChatOrchestration : return managerType; - ValueTask RegisterAgentAsync(Agent agent, int agentCount) => + ValueTask RegisterAgentAsync(Agent agent, int agentCount) => runtime.RegisterOrchestrationAgentAsync( this.FormatAgentType(context.Topic, $"Agent_{agentCount}"), (agentId, runtime) => { GroupChatAgentActor actor = new(agentId, runtime, context, agent, context.LoggerFactory.CreateLogger()); -#if !NETCOREAPP - return new ValueTask(actor); -#else - return ValueTask.FromResult(actor); -#endif + return new ValueTask(actor); }); } } diff --git a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/RoundRobinGroupChatManager.cs b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/RoundRobinGroupChatManager.cs index e0f11475ec..cb65c6c7e9 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/RoundRobinGroupChatManager.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/RoundRobinGroupChatManager.cs @@ -22,11 +22,7 @@ public class RoundRobinGroupChatManager : GroupChatManager public override ValueTask> FilterResults(IReadOnlyCollection history, CancellationToken cancellationToken = default) { GroupChatManagerResult result = new(history.LastOrDefault()?.Text ?? string.Empty) { Reason = "Default result filter provides the final chat message." }; -#if !NETCOREAPP return new ValueTask>(result); -#else - return ValueTask.FromResult(result); -#endif } /// @@ -35,21 +31,13 @@ public class RoundRobinGroupChatManager : GroupChatManager string nextAgent = team.Skip(this._currentAgentIndex).First().Key; this._currentAgentIndex = (this._currentAgentIndex + 1) % team.Count; GroupChatManagerResult result = new(nextAgent) { Reason = $"Selected agent at index: {this._currentAgentIndex}" }; -#if !NETCOREAPP return new ValueTask>(result); -#else - return ValueTask.FromResult(result); -#endif } /// public override ValueTask> ShouldRequestUserInput(IReadOnlyCollection history, CancellationToken cancellationToken = default) { GroupChatManagerResult result = new(false) { Reason = "The default round-robin group chat manager does not request user input." }; -#if !NETCOREAPP return new ValueTask>(result); -#else - return ValueTask.FromResult(result); -#endif } } diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffActor.cs b/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffActor.cs index 7894477385..cbd57e6f29 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffActor.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffActor.cs @@ -14,15 +14,11 @@ namespace Microsoft.Agents.Orchestration.Handoff; /// /// An actor used with the . /// -internal sealed class HandoffActor : - AgentActor, - IHandle, - IHandle, - IHandle +internal sealed class HandoffActor : AgentActor { private readonly ChatClientAgent _chatAgent; private readonly HandoffLookup _handoffs; - private readonly AgentType _resultHandoff; + private readonly ActorType _resultHandoff; private readonly List _cache; private readonly ChatOptions _options; @@ -39,7 +35,7 @@ internal sealed class HandoffActor : /// The handoffs available to this agent /// The handoff agent for capturing the result. /// The logger to use for the actor - public HandoffActor(AgentId id, IAgentRuntime runtime, OrchestrationContext context, ChatClientAgent agent, HandoffLookup handoffs, AgentType resultHandoff, ILogger? logger = null) + public HandoffActor(ActorId id, IAgentRuntime runtime, OrchestrationContext context, ChatClientAgent agent, HandoffLookup handoffs, ActorType resultHandoff, ILogger? logger = null) : base(id, runtime, context, agent, logger) { if (handoffs.ContainsKey(agent.Name ?? agent.Id)) @@ -57,6 +53,10 @@ internal sealed class HandoffActor : Tools = [.. this.CreateHandoffFunctions()], ToolMode = ChatToolMode.Auto }; + + this.RegisterMessageHandler(this.HandleAsync); + this.RegisterMessageHandler(this.HandleAsync); + this.RegisterMessageHandler(this.HandleAsync); } /// @@ -85,33 +85,20 @@ internal sealed class HandoffActor : /// public OrchestrationInteractiveCallback? InteractiveCallback { get; init; } - /// - public ValueTask HandleAsync(HandoffMessages.InputTask item, MessageContext messageContext) + private ValueTask HandleAsync(HandoffMessages.InputTask item, MessageContext messageContext, CancellationToken cancellationToken) { this._taskSummary = null; this._cache.AddRange(item.Messages); - -#if !NETCOREAPP - return new ValueTask(); -#else - return ValueTask.CompletedTask; -#endif + return default; } - /// - public ValueTask HandleAsync(HandoffMessages.Response item, MessageContext messageContext) + private ValueTask HandleAsync(HandoffMessages.Response item, MessageContext messageContext, CancellationToken cancellationToken) { this._cache.Add(item.Message); - -#if !NETCOREAPP - return new ValueTask(); -#else - return ValueTask.CompletedTask; -#endif + return default; } - /// - public async ValueTask HandleAsync(HandoffMessages.Request item, MessageContext messageContext) + private async ValueTask HandleAsync(HandoffMessages.Request item, MessageContext messageContext, CancellationToken cancellationToken) { try { @@ -122,7 +109,7 @@ internal sealed class HandoffActor : ChatMessage response; try { - response = await this.InvokeAsync(this._cache, messageContext.CancellationToken).ConfigureAwait(false); + response = await this.InvokeAsync(this._cache, cancellationToken).ConfigureAwait(false); } catch (Exception exception) { @@ -139,13 +126,13 @@ internal sealed class HandoffActor : // 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, messageContext.CancellationToken).ConfigureAwait(false); + await this.PublishMessageAsync(new HandoffMessages.Response { Message = response }, this.Context.Topic, messageId: null, cancellationToken).ConfigureAwait(false); } if (this._handoffAgent != null) { - AgentType handoffType = this._handoffs[this._handoffAgent].AgentType; - await this.PublishMessageAsync(new HandoffMessages.Request(), handoffType, messageContext.CancellationToken).ConfigureAwait(false); + ActorType handoffType = this._handoffs[this._handoffAgent].AgentType; + await this.PublishMessageAsync(new HandoffMessages.Request(), handoffType, cancellationToken).ConfigureAwait(false); this._handoffAgent = null; break; @@ -154,12 +141,12 @@ internal sealed class HandoffActor : 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, messageContext.CancellationToken).ConfigureAwait(false); + await this.PublishMessageAsync(new HandoffMessages.Response { Message = input }, this.Context.Topic, messageId: null, cancellationToken).ConfigureAwait(false); this._cache.Add(input); continue; } - await this.EndAsync(response.Text ?? "No handoff or human response function requested. Ending task.", messageContext.CancellationToken).ConfigureAwait(false); + await this.EndAsync(response.Text ?? "No handoff or human response function requested. Ending task.", cancellationToken).ConfigureAwait(false); } } catch (Exception exception) @@ -176,7 +163,7 @@ internal sealed class HandoffActor : name: "end_task", description: "Complete the task with a summary when no further requests are given."); - foreach (KeyValuePair handoff in this._handoffs) + foreach (KeyValuePair handoff in this._handoffs) { AIFunction handoffFunction = AIFunctionFactory.Create( diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffOrchestration.cs b/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffOrchestration.cs index a98a40e972..b90063aa81 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffOrchestration.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffOrchestration.cs @@ -50,7 +50,7 @@ public class HandoffOrchestration : AgentOrchestration - protected override async ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable input, AgentType? entryAgent) + protected override async ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable input, ActorType? entryAgent) { if (!entryAgent.HasValue) { @@ -61,14 +61,14 @@ public class HandoffOrchestration : AgentOrchestration - protected override async ValueTask RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger) + protected override async ValueTask RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger) { - AgentType outputType = await registrar.RegisterResultTypeAsync(response => [response.Message]).ConfigureAwait(false); + ActorType outputType = await registrar.RegisterResultTypeAsync(response => [response.Message]).ConfigureAwait(false); // Each agent handsoff its result to the next agent. - Dictionary agentMap = []; + Dictionary agentMap = []; Dictionary handoffMap = []; - AgentType agentType = outputType; + ActorType agentType = outputType; for (int index = this.Members.Count - 1; index >= 0; --index) { Agent agent = this.Members[index]; @@ -84,11 +84,7 @@ public class HandoffOrchestration : AgentOrchestration(actor); -#else - return ValueTask.FromResult(actor); -#endif + return new ValueTask(actor); }).ConfigureAwait(false); agentMap[agent.Name ?? agent.Id] = agentType; @@ -112,5 +108,5 @@ public class HandoffOrchestration : AgentOrchestration this.FormatAgentType(topic, $"Agent_{index + 1}"); + private ActorType GetAgentType(TopicId topic, int index) => this.FormatAgentType(topic, $"Agent_{index + 1}"); } diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Handoff/Handoffs.cs b/dotnet/src/Microsoft.Agents.Orchestration/Handoff/Handoffs.cs index 9a562ee7c9..d779abbe27 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Handoff/Handoffs.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Handoff/Handoffs.cs @@ -142,6 +142,6 @@ public static class OrchestrationHandoffsExtensions /// /// Handoff relationships post-processed into a name-based lookup table that includes the agent type and handoff description. -/// Maps agent names/IDs to a tuple of and handoff description. +/// Maps agent names/IDs to a tuple of and handoff description. /// -internal sealed class HandoffLookup : Dictionary; +internal sealed class HandoffLookup : Dictionary; diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Logging/AgentOrchestrationLogMessages.cs b/dotnet/src/Microsoft.Agents.Orchestration/Logging/AgentOrchestrationLogMessages.cs index 88efced26b..9008ff8e35 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Logging/AgentOrchestrationLogMessages.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Logging/AgentOrchestrationLogMessages.cs @@ -37,7 +37,7 @@ internal static partial class AgentOrchestrationLogMessages public static partial void LogRegisterActor( this ILogger logger, string orchestration, - AgentType agentType, + ActorType agentType, string label); /// @@ -49,7 +49,7 @@ internal static partial class AgentOrchestrationLogMessages public static partial void LogRegisterActor( this ILogger logger, string orchestration, - AgentType agentType, + ActorType agentType, string label, int count); @@ -96,7 +96,7 @@ internal static partial class AgentOrchestrationLogMessages public static partial void LogOrchestrationStart( this ILogger logger, string orchestration, - AgentId agentId); + ActorId agentId); /// /// Logs that orchestration request actor is active @@ -107,7 +107,7 @@ internal static partial class AgentOrchestrationLogMessages public static partial void LogOrchestrationRequestInvoke( this ILogger logger, string orchestration, - AgentId agentId); + ActorId agentId); /// /// Logs that orchestration request actor experienced an unexpected failure. @@ -118,7 +118,7 @@ internal static partial class AgentOrchestrationLogMessages public static partial void LogOrchestrationRequestFailure( this ILogger logger, string orchestration, - AgentId agentId, + ActorId agentId, Exception exception); /// @@ -130,7 +130,7 @@ internal static partial class AgentOrchestrationLogMessages public static partial void LogOrchestrationResultInvoke( this ILogger logger, string orchestration, - AgentId agentId); + ActorId agentId); /// /// Logs that orchestration result actor experienced an unexpected failure. @@ -141,6 +141,6 @@ internal static partial class AgentOrchestrationLogMessages public static partial void LogOrchestrationResultFailure( this ILogger logger, string orchestration, - AgentId agentId, + ActorId agentId, Exception exception); } diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Logging/ConcurrentOrchestrationLogMessages.cs b/dotnet/src/Microsoft.Agents.Orchestration/Logging/ConcurrentOrchestrationLogMessages.cs index b266310669..eb76da967b 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Logging/ConcurrentOrchestrationLogMessages.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Logging/ConcurrentOrchestrationLogMessages.cs @@ -22,14 +22,14 @@ internal static partial class ConcurrentOrchestrationLogMessages Message = "REQUEST Concurrent agent [{AgentId}]")] public static partial void LogConcurrentAgentInvoke( this ILogger logger, - AgentId agentId); + ActorId agentId); [LoggerMessage( Level = LogLevel.Trace, Message = "RESULT Concurrent agent [{AgentId}]: {Message}")] public static partial void LogConcurrentAgentResult( this ILogger logger, - AgentId agentId, + ActorId agentId, string? message); /// @@ -40,7 +40,7 @@ internal static partial class ConcurrentOrchestrationLogMessages Message = "COLLECT Concurrent result [{AgentId}]: #{ResultCount} / {ExpectedCount}")] public static partial void LogConcurrentResultCapture( this ILogger logger, - AgentId agentId, + ActorId agentId, int resultCount, int expectedCount); } diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Logging/GroupChatOrchestrationLogMessages.cs b/dotnet/src/Microsoft.Agents.Orchestration/Logging/GroupChatOrchestrationLogMessages.cs index 76a60c270a..6f06c39dee 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Logging/GroupChatOrchestrationLogMessages.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Logging/GroupChatOrchestrationLogMessages.cs @@ -22,14 +22,14 @@ internal static partial class GroupChatOrchestrationLogMessages Message = "CHAT AGENT invoked [{AgentId}]")] public static partial void LogChatAgentInvoke( this ILogger logger, - AgentId agentId); + ActorId agentId); [LoggerMessage( Level = LogLevel.Trace, Message = "CHAT AGENT result [{AgentId}]: {Message}")] public static partial void LogChatAgentResult( this ILogger logger, - AgentId agentId, + ActorId agentId, string? message); [LoggerMessage( @@ -37,21 +37,21 @@ internal static partial class GroupChatOrchestrationLogMessages Message = "CHAT MANAGER initialized [{AgentId}]")] public static partial void LogChatManagerInit( this ILogger logger, - AgentId agentId); + ActorId agentId); [LoggerMessage( Level = LogLevel.Debug, Message = "CHAT MANAGER invoked [{AgentId}]")] public static partial void LogChatManagerInvoke( this ILogger logger, - AgentId agentId); + ActorId agentId); [LoggerMessage( Level = LogLevel.Debug, Message = "CHAT MANAGER terminate? [{AgentId}]: {Result} ({Reason})")] public static partial void LogChatManagerTerminate( this ILogger logger, - AgentId agentId, + ActorId agentId, bool result, string reason); @@ -60,15 +60,15 @@ internal static partial class GroupChatOrchestrationLogMessages Message = "CHAT MANAGER select: {NextAgent} [{AgentId}]")] public static partial void LogChatManagerSelect( this ILogger logger, - AgentId agentId, - AgentType nextAgent); + ActorId agentId, + ActorType nextAgent); [LoggerMessage( Level = LogLevel.Debug, Message = "CHAT MANAGER result [{AgentId}]: '{Result}' ({Reason})")] public static partial void LogChatManagerResult( this ILogger logger, - AgentId agentId, + ActorId agentId, string result, string reason); @@ -77,7 +77,7 @@ internal static partial class GroupChatOrchestrationLogMessages Message = "CHAT MANAGER user-input? [{AgentId}]: {Result} ({Reason})")] public static partial void LogChatManagerInput( this ILogger logger, - AgentId agentId, + ActorId agentId, bool result, string reason); @@ -86,6 +86,6 @@ internal static partial class GroupChatOrchestrationLogMessages Message = "CHAT AGENT user-input [{AgentId}]: {Message}")] public static partial void LogChatManagerUserInput( this ILogger logger, - AgentId agentId, + ActorId agentId, string? message); } diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Logging/HandoffOrchestrationLogMessages.cs b/dotnet/src/Microsoft.Agents.Orchestration/Logging/HandoffOrchestrationLogMessages.cs index 132167e6a1..2eb0a4e0ce 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Logging/HandoffOrchestrationLogMessages.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Logging/HandoffOrchestrationLogMessages.cs @@ -22,14 +22,14 @@ internal static partial class HandoffOrchestrationLogMessages Message = "REQUEST Handoff agent [{AgentId}]")] public static partial void LogHandoffAgentInvoke( this ILogger logger, - AgentId agentId); + ActorId agentId); [LoggerMessage( Level = LogLevel.Trace, Message = "RESULT Handoff agent [{AgentId}]: {Message}")] public static partial void LogHandoffAgentResult( this ILogger logger, - AgentId agentId, + ActorId agentId, string? message); [LoggerMessage( @@ -37,7 +37,7 @@ internal static partial class HandoffOrchestrationLogMessages Message = "TOOL Handoff [{AgentId}]: {Name}")] public static partial void LogHandoffFunctionCall( this ILogger logger, - AgentId agentId, + ActorId agentId, string name); [LoggerMessage( @@ -45,6 +45,6 @@ internal static partial class HandoffOrchestrationLogMessages Message = "RESULT Handoff summary [{AgentId}]: {Summary}")] public static partial void LogHandoffSummary( this ILogger logger, - AgentId agentId, + ActorId agentId, string? summary); } diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Logging/SequentialOrchestrationLogMessages.cs b/dotnet/src/Microsoft.Agents.Orchestration/Logging/SequentialOrchestrationLogMessages.cs index 39425ff792..93d28eef8a 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Logging/SequentialOrchestrationLogMessages.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Logging/SequentialOrchestrationLogMessages.cs @@ -22,13 +22,13 @@ internal static partial class SequentialOrchestrationLogMessages Message = "REQUEST Sequential agent [{AgentId}]")] public static partial void LogSequentialAgentInvoke( this ILogger logger, - AgentId agentId); + ActorId agentId); [LoggerMessage( Level = LogLevel.Trace, Message = "RESULT Sequential agent [{AgentId}]: {Message}")] public static partial void LogSequentialAgentResult( this ILogger logger, - AgentId agentId, + ActorId agentId, string? message); } diff --git a/dotnet/src/Microsoft.Agents.Orchestration/OrchestrationActor.cs b/dotnet/src/Microsoft.Agents.Orchestration/OrchestrationActor.cs index 5b1366c1a7..d3f3f53629 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/OrchestrationActor.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/OrchestrationActor.cs @@ -10,12 +10,12 @@ namespace Microsoft.Agents.Orchestration; /// /// Base abstractions for any actor that participates in an orchestration. /// -public abstract class OrchestrationActor : BaseAgent +public abstract class OrchestrationActor : RuntimeActor { /// /// Initializes a new instance of the class. /// - protected OrchestrationActor(AgentId id, IAgentRuntime runtime, OrchestrationContext context, string description, ILogger? logger = null) + protected OrchestrationActor(ActorId id, IAgentRuntime runtime, OrchestrationContext context, string description, ILogger? logger = null) : base(id, runtime, description, logger) { this.Context = context; @@ -35,9 +35,9 @@ public abstract class OrchestrationActor : BaseAgent /// The agent identifier, if it exists. protected async ValueTask PublishMessageAsync( object message, - AgentType agentType, + ActorType agentType, CancellationToken cancellationToken = default) { - await base.PublishMessageAsync(message, new TopicId(agentType), messageId: null, cancellationToken).ConfigureAwait(false); + await base.PublishMessageAsync(message, new TopicId(agentType.Name), messageId: null, cancellationToken).ConfigureAwait(false); } } diff --git a/dotnet/src/Microsoft.Agents.Orchestration/OrchestrationResult.cs b/dotnet/src/Microsoft.Agents.Orchestration/OrchestrationResult.cs index 2ecf0e2c75..c5de6b2e8d 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/OrchestrationResult.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/OrchestrationResult.cs @@ -34,8 +34,11 @@ public sealed class OrchestrationResult : IDisposable /// public void Dispose() { - this.Dispose(disposing: true); - GC.SuppressFinalize(this); + if (!this._isDisposed) + { + this._cancelSource.Dispose(); + this._isDisposed = true; + } } /// @@ -60,13 +63,13 @@ public sealed class OrchestrationResult : IDisposable /// Thrown if the orchestration does not complete within the specified timeout period. public async ValueTask GetValueAsync(TimeSpan? timeout = null, CancellationToken cancellationToken = default) { -#if !NETCOREAPP +#if NET + ObjectDisposedException.ThrowIf(this._isDisposed, this); +#else if (this._isDisposed) { throw new ObjectDisposedException(this.GetType().Name); } -#else - ObjectDisposedException.ThrowIf(this._isDisposed, this); #endif this._logger.LogOrchestrationResultAwait(this.Orchestration, this.Topic); @@ -96,30 +99,17 @@ public sealed class OrchestrationResult : IDisposable /// public void Cancel() { -#if !NETCOREAPP +#if NET + ObjectDisposedException.ThrowIf(this._isDisposed, this); +#else if (this._isDisposed) { throw new ObjectDisposedException(this.GetType().Name); } -#else - ObjectDisposedException.ThrowIf(this._isDisposed, this); #endif this._logger.LogOrchestrationResultCancelled(this.Orchestration, this.Topic); this._cancelSource.Cancel(); this._completion.SetCanceled(); } - - private void Dispose(bool disposing) - { - if (!this._isDisposed) - { - if (disposing) - { - this._cancelSource.Dispose(); - } - - this._isDisposed = true; - } - } } diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Sequential/SequentialActor.cs b/dotnet/src/Microsoft.Agents.Orchestration/Sequential/SequentialActor.cs index 59876e6cd0..6be2d40fdc 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Sequential/SequentialActor.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Sequential/SequentialActor.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; @@ -12,12 +13,9 @@ namespace Microsoft.Agents.Orchestration.Sequential; /// /// An actor used with the . /// -internal sealed class SequentialActor : - AgentActor, - IHandle, - IHandle +internal sealed class SequentialActor : AgentActor { - private readonly AgentType _nextAgent; + private readonly ActorType _nextAgent; /// /// Initializes a new instance of the class. @@ -28,35 +26,32 @@ internal sealed class SequentialActor : /// An . /// The identifier of the next agent for which to handoff the result /// The logger to use for the actor - public SequentialActor(AgentId id, IAgentRuntime runtime, OrchestrationContext context, Agent agent, AgentType nextAgent, ILogger? logger = null) + public SequentialActor(ActorId id, IAgentRuntime runtime, OrchestrationContext context, Agent agent, ActorType nextAgent, ILogger? logger = null) : base(id, runtime, context, agent, logger) { logger?.LogInformation("ACTOR {ActorId} {NextAgent}", this.Id, nextAgent); this._nextAgent = nextAgent; + + this.RegisterMessageHandler(this.HandleAsync); + this.RegisterMessageHandler(this.HandleAsync); } - /// - public async ValueTask HandleAsync(SequentialMessages.Request item, MessageContext messageContext) - { - await this.InvokeAgentAsync(item.Messages, messageContext).ConfigureAwait(false); - } + public ValueTask HandleAsync(SequentialMessages.Request item, MessageContext messageContext, CancellationToken cancellationToken) => + this.InvokeAgentAsync(item.Messages, messageContext, cancellationToken); - /// - public async ValueTask HandleAsync(SequentialMessages.Response item, MessageContext messageContext) - { - await this.InvokeAgentAsync([item.Message], messageContext).ConfigureAwait(false); - } + public ValueTask HandleAsync(SequentialMessages.Response item, MessageContext messageContext, CancellationToken cancellationToken) => + this.InvokeAgentAsync([item.Message], messageContext, cancellationToken); - private async ValueTask InvokeAgentAsync(IList input, MessageContext messageContext) + private async ValueTask InvokeAgentAsync(IList input, MessageContext messageContext, CancellationToken cancellationToken) { this.Logger.LogInformation("INVOKE {ActorId} {NextAgent}", this.Id, this._nextAgent); this.Logger.LogSequentialAgentInvoke(this.Id); - ChatMessage response = await this.InvokeAsync(input, messageContext.CancellationToken).ConfigureAwait(false); + ChatMessage response = await this.InvokeAsync(input, cancellationToken).ConfigureAwait(false); this.Logger.LogSequentialAgentResult(this.Id, response.Text); - await this.PublishMessageAsync(response.AsResponseMessage(), this._nextAgent).ConfigureAwait(false); + await this.PublishMessageAsync(response.AsResponseMessage(), this._nextAgent, cancellationToken: cancellationToken).ConfigureAwait(false); } } diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Sequential/SequentialOrchestration.cs b/dotnet/src/Microsoft.Agents.Orchestration/Sequential/SequentialOrchestration.cs index ba53494ffa..c8dba0116e 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Sequential/SequentialOrchestration.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Sequential/SequentialOrchestration.cs @@ -27,7 +27,7 @@ public class SequentialOrchestration : AgentOrchestration - protected override async ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable input, AgentType? entryAgent) + protected override async ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable input, ActorType? entryAgent) { if (!entryAgent.HasValue) { @@ -37,12 +37,12 @@ public class SequentialOrchestration : AgentOrchestration - protected override async ValueTask RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger) + protected override async ValueTask RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger) { - AgentType outputType = await registrar.RegisterResultTypeAsync(response => [response.Message]).ConfigureAwait(false); + ActorType outputType = await registrar.RegisterResultTypeAsync(response => [response.Message]).ConfigureAwait(false); // Each agent handsoff its result to the next agent. - AgentType nextAgent = outputType; + ActorType nextAgent = outputType; for (int index = this.Members.Count - 1; index >= 0; --index) { Agent agent = this.Members[index]; @@ -53,20 +53,15 @@ public class SequentialOrchestration : AgentOrchestration RegisterAgentAsync(Agent agent, int index, AgentType nextAgent) => + ValueTask RegisterAgentAsync(Agent agent, int index, ActorType nextAgent) => runtime.RegisterOrchestrationAgentAsync( this.GetAgentType(context.Topic, index), (agentId, runtime) => { SequentialActor actor = new(agentId, runtime, context, agent, nextAgent, context.LoggerFactory.CreateLogger()); - -#if !NETCOREAPP - return new ValueTask(actor); -#else - return ValueTask.FromResult(actor); -#endif + return new ValueTask(actor); }); } - private AgentType GetAgentType(TopicId topic, int index) => this.FormatAgentType(topic, $"Agent_{index + 1}"); + private ActorType GetAgentType(TopicId topic, int index) => this.FormatAgentType(topic, $"Agent_{index + 1}"); } diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Transforms/DefaultTransforms.cs b/dotnet/src/Microsoft.Agents.Orchestration/Transforms/DefaultTransforms.cs index e5b080188f..b9e082f4bf 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Transforms/DefaultTransforms.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Transforms/DefaultTransforms.cs @@ -13,11 +13,7 @@ internal static class DefaultTransforms { public static ValueTask> FromInput(TInput input, CancellationToken cancellationToken = default) { -#if !NETCOREAPP return new ValueTask>(TransformInput()); -#else - return ValueTask.FromResult(TransformInput()); -#endif IEnumerable TransformInput() => input switch diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorId.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorId.cs new file mode 100644 index 0000000000..8a4c433fc8 --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorId.cs @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.AI.Agents.Runtime; + +/// +/// Provides a unique identifier for an actor instance within an agent runtime, +/// serving as the "address" of the actor instance for receiving messages. +/// +public readonly struct ActorId : IEquatable +{ + /// + /// Initializes a new instance of the struct from an . + /// + /// The actor type. + /// Actor instance identifier. + public ActorId(string type, string key) : this(new ActorType(type), key) + { + } + + /// + /// Initializes a new instance of the struct from an . + /// + /// The actor type. + /// Actor instance identifier. + public ActorId(ActorType type, string key) + { + if (!IsValidKey(key)) + { + throw new ArgumentException($"Invalid {nameof(ActorId)} key.", nameof(key)); + } + + this.Type = type; + this.Key = key; + } + + /// + /// Gets an identifier that associates an actor with a specific factory function. + /// + /// + /// Strings may only be composed of alphanumeric letters (a-z) and (0-9), or underscores (_). + /// + public ActorType Type { get; } + + /// + /// Gets an actor instance identifier. + /// + /// + /// Strings may only be composed of alphanumeric letters (a-z) and (0-9), or underscores (_). + /// + public string Key { get; } + + /// + /// Convert a string of the format "type/key" into an . + /// + /// The actor ID string. + /// An instance of . + public static ActorId Parse(string actorId) + { + if (!KeyValueParser.TryParse(actorId, out string? type, out string? key)) + { + throw new FormatException($"Invalid actor ID: '{actorId}'. Expected format is 'type/key'."); + } + + return new ActorId(type, key); + } + + /// + public override readonly string ToString() => $"{this.Type}/{this.Key}"; + + /// + public override readonly bool Equals([NotNullWhen(true)] object? obj) => + obj is ActorId other && this.Equals(other); + + /// + public readonly bool Equals(ActorId other) => + this.Type == other.Type && this.Key == other.Key; + + /// + public override readonly int GetHashCode() => + HashCode.Combine(this.Type, this.Key); + + /// + public static bool operator ==(ActorId left, ActorId right) => + left.Equals(right); + + /// + public static bool operator !=(ActorId left, ActorId right) => + !left.Equals(right); + + /// Determines whether the specified key is valid. + /// It must be non-null, not be only whitespace, and only contain printable ASCII characters. + internal static bool IsValidKey(string key) + { + if (string.IsNullOrWhiteSpace(key)) + { + return false; + } + +#if NET + return !key.AsSpan().ContainsAnyExceptInRange((char)32, (char)126); +#else + foreach (char c in key) + { + if ((int)c is < 32 or > 126) + { + return false; + } + } + + return true; +#endif + } +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorMetadata.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorMetadata.cs new file mode 100644 index 0000000000..6e28c69f93 --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorMetadata.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; + +namespace Microsoft.Extensions.AI.Agents.Runtime; + +/// +/// Represents metadata associated with an actor, including its type, unique key, and description. +/// +public readonly struct ActorMetadata : IEquatable +{ + /// + /// Initializes a new instance of the class with the specified type, key, and description. + /// + /// The type of the actor. + /// The unique key associated with the actor. + /// A brief description of the actor. + public ActorMetadata(ActorType type, string key, string description) + { + if (!ActorId.IsValidKey(key)) + { + 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; + } + + /// + /// Gets an identifier that associates an actor with a specific factory function. + /// + public ActorType Type { get; } + + /// + /// A unique key identifying the actor instance. + /// Strings may only be composed of alphanumeric letters (a-z, 0-9), or underscores (_). + /// + public string Key { get; } + + /// + /// A brief description of the actor's purpose or functionality. + /// + public string Description { get; } + + /// + public override readonly bool Equals(object? obj) => + obj is ActorMetadata actorMetadata && this.Equals(actorMetadata); + + /// + public readonly bool Equals(ActorMetadata other) => + this.Type == other.Type && + this.Key == other.Key && + this.Description == other.Description; + + /// + public override readonly int GetHashCode() => + HashCode.Combine(this.Type, this.Key, this.Description); + + /// + public static bool operator ==(ActorMetadata left, ActorMetadata right) => + left.Equals(right); + + /// + public static bool operator !=(ActorMetadata left, ActorMetadata right) => + !(left == right); +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorType.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorType.cs new file mode 100644 index 0000000000..6f101b46f4 --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorType.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Text.RegularExpressions; + +namespace Microsoft.Extensions.AI.Agents.Runtime; + +/// +/// Represents the type of an actor. +/// +public readonly partial struct ActorType : IEquatable +{ + /// + /// Initializes a new instance of the struct. + /// + /// The actor type. + public ActorType(string type) + { + if (!IsValid(type)) + { + throw new ArgumentException($"Invalid type: '{type}'. Must be alphanumeric (a-z, 0-9, _) and cannot start with a number or contain spaces."); + } + + this.Name = type; + } + + /// + /// The string representation of this actor type. + /// + public string Name { get; } + + /// + /// Returns the string representation of the . + /// + /// A string in the format "type/source". + public override readonly string ToString() => + this.Name; + + /// + public override bool Equals(object? obj) => + obj is ActorType other && this.Equals(other); + + /// + public bool Equals(ActorType other) => + this.Name.Equals(other.Name, StringComparison.Ordinal); + + /// + public override int GetHashCode() => + this.Name.GetHashCode(); + + /// + public static bool operator ==(ActorType left, ActorType right) => + left.Equals(right); + + /// + public static bool operator !=(ActorType left, ActorType right) => + !(left == right); + + internal static bool IsValid(string type) => + type is not null && TypeRegex().IsMatch(type); + +#if NET + [GeneratedRegex("^[a-zA-Z_][a-zA-Z_0-9]*$")] + private static partial Regex TypeRegex(); +#else + private static Regex TypeRegex() => new("^[a-zA-Z_][a-zA-Z_0-9]*$", RegexOptions.Compiled); +#endif +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/AgentId.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/AgentId.cs deleted file mode 100644 index 7b6b462568..0000000000 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/AgentId.cs +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Text.RegularExpressions; - -namespace Microsoft.Extensions.AI.Agents.Runtime; - -/// -/// Agent ID uniquely identifies an agent instance within an agent runtime, including a distributed runtime. -/// It serves as the "address" of the agent instance for receiving messages. -/// \ -/// -/// See the Python equivalent: -/// AgentId in AutoGen (Python). -/// -[DebuggerDisplay($"AgentId(type=\"{{{nameof(Type)}}}\", key=\"{{{nameof(Key)}}}\")")] -public struct AgentId : IEquatable -{ - /// - /// The default source value used when no source is explicitly provided. - /// - public const string DefaultKey = "default"; - - private static readonly Regex KeyRegex = new(@"^[\x20-\x7E]+$", RegexOptions.Compiled); // ASCII 32-126 - - /// - /// An identifier that associates an agent with a specific factory function. - /// Strings may only be composed of alphanumeric letters (a-z) and (0-9), or underscores (_). - /// - public string Type { get; } - - /// - /// Agent instance identifier. - /// Strings may only be composed of alphanumeric letters (a-z) and (0-9), or underscores (_). - /// - public string Key { get; } - - internal static Regex KeyRegex1 => KeyRegex2; - - internal static Regex KeyRegex2 => KeyRegex; - - /// - /// Initializes a new instance of the struct. - /// - /// The agent type. - /// Agent instance identifier. - public AgentId(string type, string key) - { - AgentType.Validate(type); - - if (string.IsNullOrWhiteSpace(key) || !KeyRegex.IsMatch(key)) - { - throw new ArgumentException($"Invalid AgentId key: '{key}'. Must only contain ASCII characters 32-126."); - } - - this.Type = type; - this.Key = key; - } - - /// - /// Initializes a new instance of the struct from a tuple. - /// - /// A tuple containing the agent type and key. - public AgentId((string Type, string Key) kvPair) - : this(kvPair.Type, kvPair.Key) - { - } - - /// - /// Initializes a new instance of the struct from an . - /// - /// The agent type. - /// Agent instance identifier. - public AgentId(AgentType type, string key) - : this(type.Name, key) - { - } - - /// - /// Convert a string of the format "type/key" into an . - /// - /// The agent ID string. - /// An instance of . - public static AgentId FromStr(string maybeAgentId) => new(maybeAgentId.ToKeyValuePair(nameof(Type), nameof(Key))); - - /// - /// Returns the string representation of the . - /// - /// A string in the format "type/key". - public override readonly string ToString() => $"{this.Type}/{this.Key}"; - - /// - /// Determines whether the specified object is equal to the current . - /// - /// The object to compare with the current instance. - /// true if the specified object is equal to the current ; otherwise, false. - public override readonly bool Equals([NotNullWhen(true)] object? obj) - { - return (obj is AgentId other && this.Equals(other)); - } - - /// - public readonly bool Equals(AgentId other) - { - return this.Type == other.Type && this.Key == other.Key; - } - - /// - /// Returns a hash code for this . - /// - /// A hash code for the current instance. - public override readonly int GetHashCode() - { - return HashCode.Combine(this.Type, this.Key); - } - - /// - /// Explicitly converts a string to an . - /// - /// The string representation of an agent ID. - /// An instance of . - public static explicit operator AgentId(string id) => FromStr(id); - - /// - /// Equality operator for . - /// - public static bool operator ==(AgentId left, AgentId right) => left.Equals(right); - - /// - /// Inequality operator for . - /// - public static bool operator !=(AgentId left, AgentId right) => !left.Equals(right); -} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/AgentMetadata.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/AgentMetadata.cs deleted file mode 100644 index f90b95a048..0000000000 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/AgentMetadata.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; - -namespace Microsoft.Extensions.AI.Agents.Runtime; - -/// -/// Represents metadata associated with an agent, including its type, unique key, and description. -/// -public readonly struct AgentMetadata(string type, string key, string description) : IEquatable -{ - /// - /// An identifier that associates an agent with a specific factory function. - /// Strings may only be composed of alphanumeric letters (a-z, 0-9), or underscores (_). - /// - public string Type { get; } = type; - - /// - /// A unique key identifying the agent instance. - /// Strings may only be composed of alphanumeric letters (a-z, 0-9), or underscores (_). - /// - public string Key { get; } = key; - - /// - /// A brief description of the agent's purpose or functionality. - /// - public string Description { get; } = description; - - /// - public override readonly bool Equals(object? obj) - { - return obj is AgentMetadata agentMetadata && this.Equals(agentMetadata); - } - - /// - public readonly bool Equals(AgentMetadata other) - { - return this.Type.Equals(other.Type, StringComparison.Ordinal) && this.Key.Equals(other.Key, StringComparison.Ordinal); - } - - /// - public override readonly int GetHashCode() - { - return HashCode.Combine(this.Type, this.Key); - } - - /// - public static bool operator ==(AgentMetadata left, AgentMetadata right) - { - return left.Equals(right); - } - - /// - public static bool operator !=(AgentMetadata left, AgentMetadata right) - { - return !(left == right); - } -} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/AgentProxy.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/AgentProxy.cs deleted file mode 100644 index 9ece29fda3..0000000000 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/AgentProxy.cs +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Extensions.AI.Agents.Runtime; - -/// -/// A proxy that allows you to use an in place of its associated . -/// -public class AgentProxy -{ - /// - /// The runtime instance used to interact with agents. - /// - private readonly IAgentRuntime _runtime; - private AgentMetadata? _metadata; - - /// - /// Initializes a new instance of the class. - /// - public AgentProxy(AgentId agentId, IAgentRuntime runtime) - { - this.Id = agentId; - this._runtime = runtime; - } - - /// - /// The target agent for this proxy. - /// - public AgentId Id { get; } - - /// - /// Gets the metadata of the agent. - /// - /// - /// An instance of containing details about the agent. - /// - public AgentMetadata Metadata => this._metadata ??= this.QueryMetadataAndUnwrap(); - - /// - /// Sends a message to the agent and processes the response. - /// - /// The message to send to the agent. - /// The agent that is sending the message. - /// - /// The message ID. If null, a new message ID will be generated. - /// This message ID must be unique and is recommended to be a UUID. - /// - /// - /// A token used to cancel an in-progress operation. Defaults to null. - /// - /// A task representing the asynchronous operation, returning the response from the agent. - public ValueTask SendMessageAsync(object message, AgentId sender, string? messageId = null, CancellationToken cancellationToken = default) - { - return this._runtime.SendMessageAsync(message, this.Id, sender, messageId, cancellationToken); - } - - /// - /// Loads the state of the agent from a previously saved state. - /// - /// A dictionary representing the state of the agent. Must be JSON serializable. - /// A task representing the asynchronous operation. - public ValueTask LoadStateAsync(JsonElement state) - { - return this._runtime.LoadAgentStateAsync(this.Id, state); - } - - /// - /// Saves the state of the agent. The result must be JSON serializable. - /// - /// A task representing the asynchronous operation, returning a dictionary containing the saved state. - public ValueTask SaveStateAsync() - { - return this._runtime.SaveAgentStateAsync(this.Id); - } - - private AgentMetadata QueryMetadataAndUnwrap() - { -#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits - return this._runtime.GetAgentMetadataAsync(this.Id).AsTask().ConfigureAwait(false).GetAwaiter().GetResult(); -#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits - } -} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/AgentType.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/AgentType.cs deleted file mode 100644 index 12dcc0cfcc..0000000000 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/AgentType.cs +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Text.RegularExpressions; - -namespace Microsoft.Extensions.AI.Agents.Runtime; - -/// -/// Represents the type of an agent as a string. -/// This is a strongly-typed wrapper around a string, ensuring type safety when working with agent types. -/// -/// -/// This struct is immutable and provides implicit conversion to and from . -/// -public readonly partial struct AgentType : IEquatable -{ -#if NET - [GeneratedRegex("^[a-zA-Z_][a-zA-Z0-9_]*$")] - private static partial Regex TypeRegex(); -#else - private static Regex TypeRegex() => new("^[a-zA-Z_][a-zA-Z0-9_]*$", RegexOptions.Compiled); -#endif - - internal static void Validate(string type) - { - if (string.IsNullOrWhiteSpace(type) || !TypeRegex().IsMatch(type)) - { - throw new ArgumentException($"Invalid AgentId type: '{type}'. Must be alphanumeric (a-z, 0-9, _) and cannot start with a number or contain spaces."); - } - } - - /// - /// Initializes a new instance of the struct. - /// - /// The agent type. - public AgentType(string type) - { - Validate(type); - this.Name = type; - } - - /// - /// The string representation of this agent type. - /// - public string Name { get; } - - /// - /// Returns the string representation of the . - /// - /// A string in the format "type/source". - public override readonly string ToString() => this.Name; - - /// - /// Explicitly converts a to an . - /// - /// The .NET to convert. - /// An instance with the name of the provided type. - public static explicit operator AgentType(Type type) => new(type.Name); - - /// - /// Implicitly converts a to an . - /// - /// The string representation of the agent type. - /// An instance with the given name. - public static implicit operator AgentType(string type) => new(type); - - /// - /// Implicitly converts an to a . - /// - /// The instance. - /// The string representation of the agent type. - public static implicit operator string(AgentType type) => type.ToString(); - - /// - public override bool Equals(object? obj) - { - return obj is AgentType other && this.Equals(other); - } - - /// - public bool Equals(AgentType other) - { - return this.Name.Equals(other.Name, StringComparison.Ordinal); - } - - /// - public override int GetHashCode() - { - return this.Name.GetHashCode(); - } - - /// - public static bool operator ==(AgentType left, AgentType right) - { - return left.Equals(right); - } - - /// - public static bool operator !=(AgentType left, AgentType right) - { - return !(left == right); - } -} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/BaseAgent.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/BaseAgent.cs deleted file mode 100644 index 5c658cb743..0000000000 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/BaseAgent.cs +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; - -namespace Microsoft.Extensions.AI.Agents.Runtime; - -/// -/// Represents the base class for an agent in the AutoGen system. -/// -public abstract class BaseAgent : IHostableAgent -{ - private static readonly JsonElement s_emptyElement = JsonDocument.Parse("{}").RootElement; - - /// - /// The activity source for tracing. - /// - public static readonly ActivitySource TraceSource = new($"{typeof(IAgent).Namespace}"); - - private readonly Dictionary _handlerInvokers; - private readonly IAgentRuntime _runtime; - - /// - /// Provides logging capabilities used for diagnostic and operational information. - /// - protected internal ILogger Logger { get; } - - /// - /// Gets the description of the agent. - /// - protected string Description { get; } - - /// - /// Gets the unique identifier of the agent. - /// - public AgentId Id { get; } - - /// - /// Gets the metadata of the agent. - /// - public AgentMetadata Metadata { get; } - - /// - /// Initializes a new instance of the BaseAgent class with the specified identifier, runtime, description, and optional logger. - /// - /// The unique identifier of the agent. - /// The runtime environment in which the agent operates. - /// A brief description of the agent's purpose. - /// An optional logger for recording diagnostic information. - protected BaseAgent( - AgentId id, - IAgentRuntime runtime, - string description, - ILogger? logger = null) - { - this.Logger = logger ?? NullLogger.Instance; - - this.Id = id; - this.Description = description; - this.Metadata = new AgentMetadata(this.Id.Type, this.Id.Key, this.Description); - - this._runtime = runtime; - this._handlerInvokers = HandlerInvoker.ReflectAgentHandlers(this); - } - - /// - /// Handles an incoming message by determining its type and invoking the corresponding handler method if available. - /// - /// The message object to be handled. - /// The context associated with the message. - /// A ValueTask that represents the asynchronous operation, containing the response object or null. - public async ValueTask OnMessageAsync(object message, MessageContext messageContext) - { - // Determine type of message, then get handler method and invoke it - Type messageType = message.GetType(); - if (this._handlerInvokers.TryGetValue(messageType, out HandlerInvoker? handlerInvoker)) - { - return await handlerInvoker.InvokeAsync(message, messageContext).ConfigureAwait(false); - } - - return null; - } - - /// - public virtual ValueTask SaveStateAsync() - { - return new ValueTask(s_emptyElement); - } - - /// - public virtual ValueTask LoadStateAsync(JsonElement state) => - default; - - /// - /// Closes this agent gracefully by releasing allocated resources and performing any necessary cleanup. - /// - public virtual ValueTask CloseAsync() => - default; - - /// - /// Sends a message to a specified recipient agent through the runtime. - /// - /// The requested agent's type. - /// A token used to cancel the operation if needed. - /// A ValueTask that represents the asynchronous operation, returning the response object or null. - protected async ValueTask GetAgentAsync(AgentType agent, CancellationToken cancellationToken = default) - { - try - { - return await this._runtime.GetAgentAsync(agent, lazy: false).ConfigureAwait(false); - } - catch (InvalidOperationException) - { - return null; - } - } - - /// - /// Sends a message to a specified recipient agent through the runtime. - /// - /// The message object to send. - /// The recipient agent's identifier. - /// An optional identifier for the message. - /// A token used to cancel the operation if needed. - /// A ValueTask that represents the asynchronous operation, returning the response object or null. - protected ValueTask SendMessageAsync(object message, AgentId recipient, string? messageId = null, CancellationToken cancellationToken = default) - { - return this._runtime.SendMessageAsync(message, recipient, sender: this.Id, messageId, cancellationToken); - } - - /// - /// Publishes a message to all agents subscribed to a specific topic through the runtime. - /// - /// The message object to publish. - /// The topic identifier to which the message is published. - /// An optional identifier for the message. - /// A token used to cancel the operation if needed. - /// A ValueTask that represents the asynchronous publish operation. - protected ValueTask PublishMessageAsync(object message, TopicId topic, string? messageId = null, CancellationToken cancellationToken = default) - { - return this._runtime.PublishMessageAsync(message, topic, sender: this.Id, messageId, cancellationToken); - } -} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Exceptions/CantHandleException.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Exceptions/CantHandleException.cs deleted file mode 100644 index 89f4348f38..0000000000 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Exceptions/CantHandleException.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.Extensions.AI.Agents.Runtime; - -/// -/// Exception thrown when a handler cannot process the given message. -/// -[ExcludeFromCodeCoverage] -public class CantHandleException : Exception -{ - /// - /// Initializes a new instance of the class. - /// - public CantHandleException() : base("The handler cannot process the given message.") { } - - /// - /// Initializes a new instance of the class with a custom error message. - /// - /// The custom error message. - public CantHandleException(string message) : base(message) { } - - /// - /// Initializes a new instance of the class with a custom error message and an inner exception. - /// - /// The custom error message. - /// The inner exception that caused this error. - public CantHandleException(string message, Exception innerException) : base(message, innerException) { } -} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Exceptions/MessageDroppedException.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Exceptions/MessageDroppedException.cs deleted file mode 100644 index 72139afc1d..0000000000 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Exceptions/MessageDroppedException.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.Extensions.AI.Agents.Runtime; - -/// -/// Exception thrown when a message is dropped. -/// -[ExcludeFromCodeCoverage] -public class MessageDroppedException : Exception -{ - /// - /// Initializes a new instance of the class. - /// - public MessageDroppedException() : base("The message was dropped.") { } - - /// - /// Initializes a new instance of the class with a custom error message. - /// - /// The custom error message. - public MessageDroppedException(string message) : base(message) { } - - /// - /// Initializes a new instance of the class with a custom error message and an inner exception. - /// - /// The custom error message. - /// The inner exception that caused this error. - public MessageDroppedException(string message, Exception innerException) : base(message, innerException) { } -} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Exceptions/NotAccessibleException.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Exceptions/NotAccessibleException.cs deleted file mode 100644 index fe6d18c1d2..0000000000 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Exceptions/NotAccessibleException.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.Extensions.AI.Agents.Runtime; - -/// -/// Exception thrown when an attempt is made to access an unavailable value, such as a remote resource. -/// -[ExcludeFromCodeCoverage] -public class NotAccessibleException : Exception -{ - /// - /// Initializes a new instance of the class. - /// - public NotAccessibleException() : base("The requested value is not accessible.") { } - - /// - /// Initializes a new instance of the class with a custom error message. - /// - /// The custom error message. - public NotAccessibleException(string message) : base(message) { } - - /// - /// Initializes a new instance of the class with a custom error message and an inner exception. - /// - /// The custom error message. - /// The inner exception that caused this error. - public NotAccessibleException(string message, Exception innerException) : base(message, innerException) { } -} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Exceptions/UndeliverableException.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Exceptions/UndeliverableException.cs deleted file mode 100644 index 3ecc046a60..0000000000 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Exceptions/UndeliverableException.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Diagnostics.CodeAnalysis; -namespace Microsoft.Extensions.AI.Agents.Runtime; - -/// -/// Exception thrown when a message cannot be delivered. -/// -[ExcludeFromCodeCoverage] -public class UndeliverableException : Exception -{ - /// - /// Initializes a new instance of the class. - /// - public UndeliverableException() : base("The message cannot be delivered.") { } - - /// - /// Initializes a new instance of the class with a custom error message. - /// - /// The custom error message. - public UndeliverableException(string message) : base(message) { } - - /// - /// Initializes a new instance of the class with a custom error message and an inner exception. - /// - /// The custom error message. - /// The inner exception that caused this error. - public UndeliverableException(string message, Exception innerException) : base(message, innerException) { } -} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/HandlerInvoker.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/HandlerInvoker.cs deleted file mode 100644 index 8a18243d80..0000000000 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/HandlerInvoker.cs +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; - -namespace Microsoft.Extensions.AI.Agents.Runtime; - -/// -/// Invokes handler methods asynchronously using reflection. -/// The target methods must return either a ValueTask or a ValueTask{T}. -/// This class wraps the reflection call and provides a unified asynchronous invocation interface. -/// -internal sealed class HandlerInvoker -{ - /// - /// Scans the provided agent for implemented handler interfaces (IHandle<> and IHandle<,>) via reflection, - /// creates a corresponding for each handler method, and returns a dictionary that maps - /// the message type (first generic argument of the interface) to its invoker. - /// - /// The agent instance whose handler interfaces will be reflected. - /// A dictionary mapping message types to their corresponding instances. - public static Dictionary ReflectAgentHandlers(BaseAgent agent) - { - Type realType = agent.GetType(); - - IEnumerable candidateInterfaces = - realType.GetInterfaces() - .Where(i => i.IsGenericType && - (i.GetGenericTypeDefinition() == typeof(IHandle<>) || - (i.GetGenericTypeDefinition() == typeof(IHandle<,>)))); - - Dictionary invokers = []; - foreach (Type interface_ in candidateInterfaces) - { - MethodInfo handleAsync = - interface_.GetMethod(nameof(IHandle.HandleAsync), BindingFlags.Instance | BindingFlags.Public) ?? - throw new InvalidOperationException($"No handler method found for interface {interface_.FullName}"); - - HandlerInvoker invoker = new(handleAsync, agent); - invokers.Add(interface_.GetGenericArguments()[0], invoker); - } - - return invokers; - } - - /// - /// Represents the asynchronous invocation function. - /// - private Func> Invocation { get; } - - /// - /// Initializes a new instance of the class with the specified method information and target object. - /// - /// The MethodInfo representing the handler method to be invoked. - /// The target instance of the agent. - /// Thrown if the target is missing for a non-static method or if the method's return type is not supported. - private HandlerInvoker(MethodInfo methodInfo, BaseAgent target) - { - object? invocation(object? message, MessageContext messageContext) => methodInfo.Invoke(target, [message, messageContext]); - - Func> getResultAsync; - // Check if the method returns a non-generic ValueTask - if (methodInfo.ReturnType.IsAssignableFrom(typeof(ValueTask))) - { - getResultAsync = async (message, messageContext) => - { - // Await the ValueTask and return null as there is no result value. - await ((ValueTask)invocation(message, messageContext)!).ConfigureAwait(false); - return null; - }; - } - // Check if the method returns a generic ValueTask - else if (methodInfo.ReturnType.IsGenericType && methodInfo.ReturnType.GetGenericTypeDefinition() == typeof(ValueTask<>)) - { - // Obtain the generic type argument for ValueTask - MethodInfo typeEraseAwait = typeof(HandlerInvoker) - .GetMethod(nameof(TypeEraseAwaitAsync), BindingFlags.NonPublic | BindingFlags.Static)! - .MakeGenericMethod(methodInfo.ReturnType.GetGenericArguments()[0]); - - getResultAsync = async (message, messageContext) => - { - // Execute the invocation and then type-erase the ValueTask to ValueTask - object valueTask = invocation(message, messageContext)!; - object? typelessValueTask = typeEraseAwait.Invoke(null, [valueTask]); - - Debug.Assert(typelessValueTask is ValueTask, "Expected ValueTask after type erasure."); - - return await ((ValueTask)typelessValueTask).ConfigureAwait(false); - }; - } - else - { - throw new InvalidOperationException($"Method {methodInfo.Name} must return a ValueTask or ValueTask"); - } - - this.Invocation = getResultAsync; - } - - /// - /// Invokes the handler method asynchronously with the provided message and context. - /// - /// The message to be passed as the first argument to the handler. - /// The contextual information associated with the message. - /// A ValueTask representing the asynchronous operation, which yields the handler's result. - public async ValueTask InvokeAsync(object? obj, MessageContext messageContext) - { - try - { - return await this.Invocation.Invoke(obj, messageContext).ConfigureAwait(false); - } - catch (TargetInvocationException ex) - { - // Unwrap the exception to get the original exception thrown by the handler method. - Exception? innerException = ex.InnerException; - if (innerException != null) - { - throw innerException; - } - throw; - } - } - - /// - /// Awaits a generic ValueTask and returns its result as an object. - /// This method is used to convert a ValueTask{T} to ValueTask{object?}. - /// - /// The type of the result contained in the ValueTask. - /// The ValueTask to be awaited. - /// A ValueTask containing the result as an object. - private static async ValueTask TypeEraseAwaitAsync(ValueTask vt) - { - return await vt.ConfigureAwait(false); - } -} 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 b99f882760..4dcafd8b40 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IAgentRuntime.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IAgentRuntime.cs @@ -9,23 +9,22 @@ using System.Threading.Tasks; namespace Microsoft.Extensions.AI.Agents.Runtime; /// -/// Defines the runtime environment for agents, managing message sending, subscriptions, agent resolution, and state persistence. +/// Defines the runtime environment for actors, managing message sending, subscriptions, actor resolution, and state persistence, +/// all in support of agent-based architectures. /// public interface IAgentRuntime : ISaveState { /// - /// Sends a message to an agent and gets a response. - /// This method should be used to communicate directly with an agent. + /// Sends a message to an actor and gets a response. + /// This method should be used to communicate directly with an actor. /// /// The message to send. - /// The agent to send the message to. - /// The agent sending the message. Should be null if sent from an external source. + /// The actor to send the message to. + /// The actor sending the message. Should be null if sent from an external source. /// A unique identifier for the message. If null, a new ID will be generated. /// A token to cancel the operation if needed. - /// A task representing the asynchronous operation, returning the response from the agent. - /// Thrown if the recipient cannot handle the message. - /// Thrown if the message cannot be delivered. - ValueTask SendMessageAsync(object message, AgentId recipient, AgentId? sender = null, string? messageId = null, CancellationToken cancellationToken = default); + /// A task representing the asynchronous operation, returning the response from the actor. + ValueTask SendMessageAsync(object message, ActorId recipient, ActorId? sender = null, string? messageId = null, CancellationToken cancellationToken = default); /// /// Publishes a message to all agents subscribed to the given topic. @@ -33,90 +32,99 @@ public interface IAgentRuntime : ISaveState /// /// The message to publish. /// The topic to publish the message to. - /// The agent sending the message. Defaults to null. + /// The actor sending the message. Defaults to null. /// A unique message ID. If null, a new one will be generated. /// A token to cancel the operation if needed. /// A task representing the asynchronous operation. - /// Thrown if the message cannot be delivered. - ValueTask PublishMessageAsync(object message, TopicId topic, AgentId? sender = null, string? messageId = null, CancellationToken cancellationToken = default); + ValueTask PublishMessageAsync(object message, TopicId topic, ActorId? sender = null, string? messageId = null, CancellationToken cancellationToken = default); /// - /// Retrieves an agent by its unique identifier. + /// Retrieves an actor by its unique identifier. /// - /// The unique identifier of the agent. - /// If true, the agent is fetched lazily. - /// A task representing the asynchronous operation, returning the agent's ID. - ValueTask GetAgentAsync(AgentId agentId, bool lazy = true/*, CancellationToken? = default*/); + /// The unique identifier of the actor. + /// 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(ActorId actorId, bool lazy = true, CancellationToken cancellationToken = default); /// - /// Retrieves an agent by its type. + /// Retrieves an actor by its type. /// - /// The type of the agent. - /// An optional key to specify variations of the agent. Defaults to "default". - /// If true, the agent is fetched lazily. - /// A task representing the asynchronous operation, returning the agent's ID. - ValueTask GetAgentAsync(AgentType agentType, string key = "default", bool lazy = true/*, CancellationToken? = default*/); + /// 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 agent by its string representation. + /// Retrieves an actor by its string representation. /// - /// The string representation of the agent. - /// An optional key to specify variations of the agent. Defaults to "default". - /// If true, the agent is fetched lazily. - /// A task representing the asynchronous operation, returning the agent's ID. - ValueTask GetAgentAsync(string agent, string key = "default", bool lazy = true/*, CancellationToken? = default*/); + /// 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 agent. + /// Saves the state of an actor. /// The result must be JSON serializable. /// - /// The ID of the agent whose state is being saved. + /// The ID of the actor whose state is being saved. + /// A token to cancel the operation if needed. /// A task representing the asynchronous operation, returning a dictionary of the saved state. - ValueTask SaveAgentStateAsync(AgentId agentId/*, CancellationToken? cancellationToken = default*/); + ValueTask SaveActorStateAsync(ActorId actorId, CancellationToken cancellationToken = default); /// - /// Loads the saved state into an agent. + /// Loads the saved state into an actor. /// - /// The ID of the agent whose state is being restored. + /// The ID of the actor whose state is being restored. /// The state dictionary to restore. + /// A token to cancel the operation if needed. /// A task representing the asynchronous operation. - ValueTask LoadAgentStateAsync(AgentId agentId, JsonElement state/*, CancellationToken? cancellationToken = default*/); + ValueTask LoadActorStateAsync(ActorId actorId, JsonElement state, CancellationToken cancellationToken = default); /// - /// Retrieves metadata for an agent. + /// Retrieves metadata for an actor. /// - /// The ID of the agent. - /// A task representing the asynchronous operation, returning the agent's metadata. - ValueTask GetAgentMetadataAsync(AgentId agentId/*, CancellationToken? cancellationToken = default*/); + /// The ID of the actor. + /// A token to cancel the operation if needed. + /// A task representing the asynchronous operation, returning the actor's metadata. + ValueTask GetActorMetadataAsync(ActorId actorId, CancellationToken cancellationToken = default); /// /// Adds a new subscription for the runtime to handle when processing published messages. /// /// The subscription to add. + /// A token to cancel the operation if needed. /// A task representing the asynchronous operation. - ValueTask AddSubscriptionAsync(ISubscriptionDefinition subscription/*, CancellationToken? cancellationToken = default*/); + ValueTask AddSubscriptionAsync(ISubscriptionDefinition subscription, CancellationToken cancellationToken = default); /// /// Removes a subscription from the runtime. /// /// The unique identifier of the subscription to remove. + /// A token to cancel the operation if needed. /// A task representing the asynchronous operation. /// Thrown if the subscription does not exist. - ValueTask RemoveSubscriptionAsync(string subscriptionId/*, CancellationToken? cancellationToken = default*/); + ValueTask RemoveSubscriptionAsync(string subscriptionId, CancellationToken cancellationToken = default); /// - /// Registers an agent factory with the runtime, associating it with a specific agent type. + /// Registers an actor factory with the runtime, associating it with a specific actor type. /// The type must be unique. /// - /// The agent type to associate with the factory. - /// A function that asynchronously creates the agent instance. - /// A task representing the asynchronous operation, returning the registered . - ValueTask RegisterAgentFactoryAsync(AgentType type, Func> factoryFunc); + /// 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 . + ValueTask RegisterActorFactoryAsync(ActorType type, Func> factoryFunc, CancellationToken cancellationToken = default); /// - /// Attempts to retrieve an for the specified agent. + /// Attempts to retrieve an for the specified actor. /// - /// The ID of the agent. - /// A task representing the asynchronous operation, returning an if successful. - ValueTask TryGetAgentProxyAsync(AgentId agentId); + /// The ID of the actor. + /// A token to cancel the operation if needed. + /// A task representing the asynchronous operation, returning an if successful. + ValueTask TryGetActorProxyAsync(ActorId actorId, CancellationToken cancellationToken = default); } diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IHandle.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IHandle.cs deleted file mode 100644 index bdfb926a85..0000000000 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IHandle.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Threading.Tasks; - -namespace Microsoft.Extensions.AI.Agents.Runtime; - -/// -/// Defines a handler interface for processing items of type . -/// -/// The type of item to be handled. -public interface IHandle -{ - /// - /// Handles the specified item asynchronously. - /// - /// The item to be handled. - /// The context of the message being handled. - /// A task that represents the asynchronous operation. - ValueTask HandleAsync(T item, MessageContext messageContext); -} - -/// -/// Defines a handler interface for processing items of type and . -/// -/// The input type -/// The output type -public interface IHandle -{ - /// - /// Handles the specified item asynchronously. - /// - /// The item to be handled. - /// The context of the message being handled. - /// A task that represents the asynchronous operation. - ValueTask HandleAsync(TIn item, MessageContext messageContext); -} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IHostableAgent.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IHostableAgent.cs deleted file mode 100644 index cac3727b07..0000000000 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IHostableAgent.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Threading.Tasks; - -namespace Microsoft.Extensions.AI.Agents.Runtime; - -/// -/// Represents an agent that can be explicitly hosted and closed when the runtime shuts down. -/// -public interface IHostableAgent : IAgent -{ - /// - /// Called when the runtime is closing. - /// - /// A task representing the asynchronous operation. - ValueTask CloseAsync(); -} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IAgent.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IRuntimeActor.cs similarity index 66% rename from dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IAgent.cs rename to dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IRuntimeActor.cs index bb9a2376ed..d94871f644 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IAgent.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IRuntimeActor.cs @@ -1,36 +1,37 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Threading; using System.Threading.Tasks; namespace Microsoft.Extensions.AI.Agents.Runtime; /// -/// Represents an agent within the runtime that can process messages, maintain state, and be closed when no longer needed. +/// Represents an actor within the runtime that can process messages, maintain state, and be closed when no longer needed. /// -public interface IAgent : ISaveState +public interface IRuntimeActor : ISaveState { /// - /// Gets the unique identifier of the agent. + /// Gets the unique identifier of the actor. /// - AgentId Id { get; } + ActorId Id { get; } /// - /// Gets metadata associated with the agent. + /// Gets metadata associated with the actor. /// - AgentMetadata Metadata { get; } + ActorMetadata Metadata { get; } /// - /// Handles an incoming message for the agent. - /// This should only be called by the runtime, not by other agents. + /// Handles an incoming message for the actor. + /// This should only be called by the runtime, not by other actors. /// /// The received message. The type should match one of the expected subscription types. /// The context of the message, providing additional metadata. + /// A token to cancel the operation if needed. /// /// 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 agent cannot handle the message. - ValueTask OnMessageAsync(object message, MessageContext messageContext); // TODO: How do we express this properly in .NET? + ValueTask OnMessageAsync(object message, MessageContext messageContext, CancellationToken cancellationToken = default); } diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ISaveState.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ISaveState.cs index ca75467831..d9776db5f4 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ISaveState.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ISaveState.cs @@ -1,25 +1,30 @@ // Copyright (c) Microsoft. All rights reserved. using System.Text.Json; +using System.Threading; using System.Threading.Tasks; namespace Microsoft.Extensions.AI.Agents.Runtime; +// TODO: Why is this interface needed? It's inherited by IAgentRuntime and IRuntimeActor. +// Is the former needed (does IAgentRuntime need to not only persist every actor but do so via +// this interface)? If not, these methods could be moved to IRuntimeActor. + /// -/// Defines a contract for saving and loading the state of an object. -/// The state must be JSON serializable. +/// Defines a contract for saving and loading the state of an object as JSON. /// public interface ISaveState { /// /// Saves the current state of the object. /// + /// A token to cancel the operation if needed. /// /// A task representing the asynchronous operation, returning a dictionary /// containing the saved state. The structure of the state is implementation-defined /// but must be JSON serializable. /// - ValueTask SaveStateAsync(); + ValueTask SaveStateAsync(CancellationToken cancellationToken = default); /// /// Loads a previously saved state into the object. @@ -28,6 +33,7 @@ public interface ISaveState /// A dictionary representing the saved state. The structure of the state /// is implementation-defined but must be JSON serializable. /// + /// A token to cancel the operation if needed. /// A task representing the asynchronous operation. - ValueTask LoadStateAsync(JsonElement state); + ValueTask LoadStateAsync(JsonElement state, CancellationToken cancellationToken = default); } diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ISubscriptionDefinition.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ISubscriptionDefinition.cs index 5fa4245e97..32844f597a 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ISubscriptionDefinition.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ISubscriptionDefinition.cs @@ -5,7 +5,7 @@ using System.Diagnostics.CodeAnalysis; namespace Microsoft.Extensions.AI.Agents.Runtime; /// -/// Defines a subscription that matches topics and maps them to agents. +/// Defines a subscription that matches topics and maps them to actors. /// public interface ISubscriptionDefinition { @@ -42,10 +42,10 @@ public interface ISubscriptionDefinition bool Matches(TopicId topic); /// - /// Maps a to an . + /// Maps a to an . /// Should only be called if returns true. /// /// The topic to map. - /// The that should handle the topic. - AgentId MapToAgent(TopicId topic); + /// The that should handle the topic. + ActorId MapToActor(TopicId topic); } diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IdProxyActor.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IdProxyActor.cs new file mode 100644 index 0000000000..250927bdae --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IdProxyActor.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.AI.Agents.Runtime; + +/// +/// Provides an actor proxy that allows you to use an in place of its associated . +/// +public sealed class IdProxyActor : IRuntimeActor +{ + /// The runtime instance used to interact with actors. + private readonly IAgentRuntime _runtime; + /// The metadata for the actor, lazy-loaded. + private ActorMetadata? _metadata; + + /// + /// Initializes a new instance of the class. + /// + public IdProxyActor(IAgentRuntime runtime, ActorId actorId) + { + this.Id = actorId; + this._runtime = runtime; + } + + /// + public ActorId Id { get; } + + /// + public ActorMetadata Metadata => + this._metadata ??= +#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits + this._runtime.GetActorMetadataAsync(this.Id).AsTask().GetAwaiter().GetResult(); +#pragma warning restore VSTHRD002 + + /// + public ValueTask SendMessageAsync(object message, ActorId sender, string? messageId = null, CancellationToken cancellationToken = default) => + this._runtime.SendMessageAsync(message, this.Id, sender, messageId, cancellationToken); + + /// + public ValueTask LoadStateAsync(JsonElement state, CancellationToken cancellationToken = default) => + this._runtime.LoadActorStateAsync(this.Id, state, cancellationToken); + + /// + public ValueTask SaveStateAsync(CancellationToken cancellationToken = default) => + this._runtime.SaveActorStateAsync(this.Id, cancellationToken); + + /// + ValueTask IRuntimeActor.OnMessageAsync(object message, MessageContext messageContext, CancellationToken cancellationToken) => + new((object?)null); +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/KeyValueParserExtensions.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/KeyValueParserExtensions.cs index 68ee6ad95e..8bd840a45d 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/KeyValueParserExtensions.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/KeyValueParserExtensions.cs @@ -1,52 +1,31 @@ // Copyright (c) Microsoft. All rights reserved. -using System; -using System.Text.RegularExpressions; +using System.Diagnostics.CodeAnalysis; namespace Microsoft.Extensions.AI.Agents.Runtime; /// /// Provides helper methods for parsing key-value string representations. /// -internal static class KeyValueParserExtensions +internal static class KeyValueParser { - /// - /// The regular expression pattern used to match key-value pairs in the format "key/value". - /// - private const string KVPairPattern = @"^(?\w+)/(?\w+)$"; - - /// - /// The compiled regex used for extracting key-value pairs from a string. - /// - private static readonly Regex KVPairRegex = new(KVPairPattern, RegexOptions.Compiled); - /// /// Parses a string in the format "key/value" into a tuple containing the key and value. /// - /// The input string containing a key-value pair. - /// The expected name of the key component. - /// The expected name of the value component. - /// A tuple containing the extracted key and value. - /// - /// Thrown if the input string does not match the expected "key/value" format. - /// - /// - /// Example usage: - /// - /// string input = "agent1/12345"; - /// var result = input.ToKVPair("Type", "Key"); - /// Console.WriteLine(result.Item1); // Outputs: agent1 - /// Console.WriteLine(result.Item2); // Outputs: 12345 - /// - /// - public static (string, string) ToKeyValuePair(this string inputPair, string keyName, string valueName) + public static bool TryParse(string input, [NotNullWhen(true)] out string? key, [NotNullWhen(true)] out string? value) { - Match match = KVPairRegex.Match(inputPair); - if (match.Success) + if (!string.IsNullOrEmpty(input)) { - return (match.Groups["key"].Value, match.Groups["value"].Value); + int separatorIndex = input.IndexOf('/'); + if (separatorIndex >= 0) + { + key = input.Substring(0, separatorIndex); + value = input.Substring(separatorIndex + 1); + return true; + } } - throw new FormatException($"Invalid key-value pair format: {inputPair}; expecting \"{{{keyName}}}/{{{valueName}}}\""); + key = value = null; + return false; } } diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/MessageContext.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/MessageContext.cs index c3d7b9c956..3e3d3482f4 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/MessageContext.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/MessageContext.cs @@ -7,32 +7,36 @@ namespace Microsoft.Extensions.AI.Agents.Runtime; /// /// Represents the context of a message being sent within the agent runtime. -/// This includes metadata such as the sender, topic, RPC status, and cancellation handling. /// -public class MessageContext(string messageId, CancellationToken cancellationToken) +/// +/// This includes metadata such as the sender, topic, ahd RPC status. +/// +public sealed class MessageContext { - /// - /// Initializes a new instance of the class. - /// - public MessageContext(CancellationToken cancellation) : this(Guid.NewGuid().ToString(), cancellation) - { } + private string? _messageId; /// /// Gets or sets the unique identifier for this message. /// - public string MessageId { get; } = messageId; + public string MessageId + { + get => this._messageId ?? Interlocked.CompareExchange(ref this._messageId, Guid.NewGuid().ToString(), null) ?? this._messageId; + set + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentException("MessageId cannot be null or empty.", nameof(value)); + } - /// - /// Gets or sets the cancellation token associated with this message. - /// This can be used to cancel the operation if necessary. - /// - public CancellationToken CancellationToken { get; } = cancellationToken; + this._messageId = value; + } + } /// /// Gets or sets the sender of the message. /// If null, the sender is unspecified. /// - public AgentId? Sender { get; set; } + public ActorId? Sender { get; set; } /// /// Gets or sets the topic associated with the message. 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 f28086b0da..781c7bf2db 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 @@ -5,15 +5,19 @@ $(ProjectsDebugTargetFrameworks) $(NoWarn);IDE1006;IDE0130 alpha - false true + true + + + + diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/RuntimeActor.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/RuntimeActor.cs new file mode 100644 index 0000000000..44187a196b --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/RuntimeActor.cs @@ -0,0 +1,188 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Microsoft.Extensions.AI.Agents.Runtime; + +/// +/// Provides a base implementation of . +/// +public abstract class RuntimeActor : IRuntimeActor +{ + private static readonly JsonElement s_emptyElement = JsonDocument.Parse("{}").RootElement; + + /// + /// The activity source for tracing. + /// + public static readonly ActivitySource TraceSource = new($"{typeof(IRuntimeActor).Namespace}"); + + private readonly Dictionary _handlerInvokers = []; + private readonly IAgentRuntime _runtime; + + private delegate ValueTask HandlerInvoker(object? message, MessageContext messageContext, CancellationToken cancellationToken); + + /// + /// Provides logging capabilities used for diagnostic and operational information. + /// + protected internal ILogger Logger { get; } + + /// + /// Gets the unique identifier of the actor. + /// + public ActorId Id { get; } + + /// + /// Gets the metadata of the actor. + /// + public ActorMetadata Metadata { get; } + + /// + /// Initializes a new instance of the RuntimeActor class with the specified identifier, runtime, description, and optional logger. + /// + /// The unique identifier of the actor. + /// The runtime environment in which the actor operates. + /// A brief description of the actor's purpose. + /// An optional logger for recording diagnostic information. + protected RuntimeActor( + ActorId id, + IAgentRuntime runtime, + string description, + ILogger? logger = null) + { + this.Id = id; + this._runtime = runtime; + this.Logger = logger ?? NullLogger.Instance; + + 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(Func messageHandler) + { + if (messageHandler is null) + { + throw new ArgumentNullException(nameof(messageHandler)); + } + + if (this._handlerInvokers.ContainsKey(typeof(TInput))) + { + throw new InvalidOperationException($"A handler for type {typeof(TInput)} is already registered."); + } + + 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 that produces a . + /// 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) + { + if (messageHandler is null) + { + throw new ArgumentNullException(nameof(messageHandler)); + } + + if (this._handlerInvokers.ContainsKey(typeof(TInput))) + { + throw new InvalidOperationException($"A handler for type {typeof(TInput)} is already registered."); + } + + this._handlerInvokers.Add( + typeof(TInput), + async (message, messageContext, cancellationToken) => + { + TOutput? result = await messageHandler((TInput)message!, messageContext, cancellationToken).ConfigureAwait(false); + return (object?)result; + }); + } + + /// + /// Handles an incoming message by determining its type and invoking the corresponding handler method if available. + /// + /// The message object to be handled. + /// The context associated with the message. + /// A token used to cancel the operation if needed. + /// A ValueTask that represents the asynchronous operation, containing the response object or null. + public ValueTask OnMessageAsync(object message, MessageContext messageContext, CancellationToken cancellationToken = default) + { + // Get the handler for the message type, and invoke it, if it exists. + if (message is not null && this._handlerInvokers.TryGetValue(message.GetType(), out HandlerInvoker? handlerInvoker)) + { + return handlerInvoker(message, messageContext, cancellationToken); + } + + return new((object?)null); + } + + /// + public virtual ValueTask SaveStateAsync(CancellationToken cancellationToken = default) => + new(s_emptyElement); + + /// + public virtual ValueTask LoadStateAsync(JsonElement state, CancellationToken cancellationToken = default) => + default; + + /// + /// Sends a message to a specified recipient actor through the runtime. + /// + /// The requested actor's type. + /// A token used to cancel the operation if needed. + /// A ValueTask that represents the asynchronous operation, returning the response object or null. + protected async ValueTask GetActorAsync(ActorType actor, CancellationToken cancellationToken = default) + { + try + { + return await this._runtime.GetActorAsync(actor, lazy: false, cancellationToken: cancellationToken).ConfigureAwait(false); + } + catch (InvalidOperationException) + { + return null; + } + } + + /// + /// Sends a message to a specified recipient actor through the runtime. + /// + /// The message object to send. + /// The recipient actor's identifier. + /// An optional identifier for the message. + /// A token used to cancel the operation if needed. + /// A ValueTask that represents the asynchronous operation, returning the response object or null. + protected ValueTask SendMessageAsync(object message, ActorId recipient, string? messageId = null, CancellationToken cancellationToken = default) => + this._runtime.SendMessageAsync(message, recipient, sender: this.Id, messageId, cancellationToken); + + /// + /// Publishes a message to all actors subscribed to a specific topic through the runtime. + /// + /// The message object to publish. + /// The topic identifier to which the message is published. + /// An optional identifier for the message. + /// A token used to cancel the operation if needed. + /// A ValueTask that represents the asynchronous publish operation. + protected ValueTask PublishMessageAsync(object message, TopicId topic, string? messageId = null, CancellationToken cancellationToken = default) => + this._runtime.PublishMessageAsync(message, topic, sender: this.Id, messageId, cancellationToken); +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/TopicId.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/TopicId.cs index 781fe76874..ce16f39355 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/TopicId.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/TopicId.cs @@ -2,147 +2,111 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Text.RegularExpressions; namespace Microsoft.Extensions.AI.Agents.Runtime; /// -/// Represents a topic identifier that defines the scope of a broadcast message. +/// Provides a topic identifier that defines the scope of a broadcast message. +/// +/// /// The agent runtime implements a publish-subscribe model through its broadcast API, /// where messages must be published with a specific topic. -/// -/// See the Python equivalent: -/// CloudEvents Type Specification. -/// -public struct TopicId : IEquatable +/// +public readonly partial struct TopicId : IEquatable { - /// - /// The default source value used when no source is explicitly provided. - /// - public const string DefaultSource = "default"; + private const string TypePattern = @"^[\w-.:=]+$"; - /// - /// The separator character for the string representation of the topic. - /// - public const string Separator = "/"; - - /// - /// Gets the type of the event that this represents. - /// This adheres to the CloudEvents specification. - /// - /// Must match the pattern: ^[\w\-\.\:\=]+$. - /// - /// Learn more here: - /// CloudEvents Type. - /// - public string Type { get; } - - /// - /// Gets the source that identifies the context in which an event happened. - /// This adheres to the CloudEvents specification. - /// - /// Learn more here: - /// CloudEvents Source. - /// - public string Source { get; } +#if NET + [GeneratedRegex(TypePattern)] + private static partial Regex TypeRegex(); +#else + private static Regex TypeRegex() => s_typeRegex; + private static readonly Regex s_typeRegex = new(TypePattern, RegexOptions.Compiled); +#endif /// /// Initializes a new instance of the struct. /// - /// The type of the topic. - /// The source of the event. Defaults to if not specified. - public TopicId(string type, string source = DefaultSource) + /// The type of the topic. Must match the pattern: ^[\w-.:=]+$ + /// The source of the event. + public TopicId(string type, string? source = null) { - this.Type = type; - this.Source = source; - } - - /// - /// Initializes a new instance of the struct from a tuple. - /// - /// A tuple containing the topic type and source. - public TopicId((string Type, string Source) kvPair) : this(kvPair.Type, kvPair.Source) - { - } - - /// - /// Converts a string in the format "type/source" into a . - /// - /// The topic ID string. - /// An instance of . - /// Thrown when the string is not in the valid "type/source" format. - public static TopicId FromStr(string maybeTopicId) => new(maybeTopicId.ToKeyValuePair(nameof(Type), nameof(Source))); - - /// - /// Returns the string representation of the . - /// - /// A string in the format "type/source". - public override readonly string ToString() => $"{this.Type}{Separator}{this.Source}"; - - /// - /// Determines whether the specified object is equal to the current . - /// - /// The object to compare with the current instance. - /// true if the specified object is equal to the current ; otherwise, false. - public override readonly bool Equals([NotNullWhen(true)] object? obj) - { - if (obj is TopicId other) + if (type is null) { - return this.Type == other.Type && this.Source == other.Source; + throw new ArgumentNullException(nameof(type)); } - return false; + if (!TypeRegex().IsMatch(type)) + { + throw new ArgumentException("Invalid type format.", nameof(type)); + } + + // TODO: What validation should be performed on source? The cited cloudevents spec suggests it should be a URI reference. + + this.Type = type; + this.Source = source ?? "default"; } /// - /// Determines whether the specified object is equal to the current . + /// Gets the type of the event that this represents. /// - /// The object to compare with the current instance. - /// true if the specified object is equal to the current ; otherwise, false. - public readonly bool Equals([NotNullWhen(true)] TopicId other) - { - return this.Type == other.Type && this.Source == other.Source; - } + /// + /// This adheres to the CloudEvents specification. + /// CloudEvents Type. + /// + public string Type { get; } /// - /// Returns a hash code for this . + /// Gets the source that identifies the context in which an event happened. /// - /// A hash code for the current instance. - public override readonly int GetHashCode() - { - return HashCode.Combine(this.Type, this.Source); - } + /// + /// This adheres to the CloudEvents specification. + /// CloudEvents Source. + /// + public string Source { get; } /// - /// Explicitly converts a string to a . + /// Convert a string of the format "type/key" into an . /// - /// The string representation of a topic ID. + /// The actor ID string. /// An instance of . - public static explicit operator TopicId(string id) => FromStr(id); + public static TopicId Parse(string TopicId) + { + if (!KeyValueParser.TryParse(TopicId, out string? type, out string? key)) + { + throw new FormatException($"Invalid TopicId format: '{TopicId}'. Expected format is 'type/key'."); + } + + return new TopicId(type, key); + } + + /// + public override readonly string ToString() => $"{this.Type}/{this.Source}"; + + /// + public override readonly bool Equals([NotNullWhen(true)] object? obj) => + obj is TopicId other && this.Equals(other); + + /// + public readonly bool Equals(TopicId other) => + this.Type == other.Type && this.Source == other.Source; + + /// + public override readonly int GetHashCode() => + HashCode.Combine(this.Type, this.Source); + + /// + public static bool operator ==(TopicId left, TopicId right) => + left.Equals(right); + + /// + public static bool operator !=(TopicId left, TopicId right) => + !left.Equals(right); // TODO: Implement < for wildcard matching (type, *) - // == => < - // Type == other.Type => < - /// - /// Determines whether the given matches another topic. - /// - /// The topic ID to compare against. - /// - /// true if the topic types are equal; otherwise, false. - /// - public readonly bool IsWildcardMatch(TopicId other) - { - return this.Type == other.Type; - } - - /// - public static bool operator ==(TopicId left, TopicId right) - { - return left.Equals(right); - } - - /// - public static bool operator !=(TopicId left, TopicId right) - { - return !(left == right); - } + //public readonly bool IsWildcardMatch(TopicId other) + //{ + // return this.Type == other.Type; + //} } diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/TypeSubscription.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/TypeSubscription.cs index 75f03a864a..23f23ded9b 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/TypeSubscription.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/TypeSubscription.cs @@ -6,8 +6,8 @@ using System.Diagnostics.CodeAnalysis; namespace Microsoft.Extensions.AI.Agents.Runtime; /// -/// This subscription matches on topics based on the exact type and maps to agents using the source of the topic as the agent key. -/// This subscription causes each source to have its own agent instance. +/// This subscription matches on topics based on the exact type and maps to actors using the source of the topic as the actor key. +/// This subscription causes each source to have its own actor instance. /// /// /// Example: @@ -15,21 +15,21 @@ namespace Microsoft.Extensions.AI.Agents.Runtime; /// var subscription = new TypeSubscription("t1", "a1"); /// /// In this case: -/// - A with type `"t1"` and source `"s1"` will be handled by an agent of type `"a1"` with key `"s1"`. -/// - A with type `"t1"` and source `"s2"` will be handled by an agent of type `"a1"` with key `"s2"`. +/// - A with type `"t1"` and source `"s1"` will be handled by an actor of type `"a1"` with key `"s1"`. +/// - A with type `"t1"` and source `"s2"` will be handled by an actor of type `"a1"` with key `"s2"`. /// -public class TypeSubscription : ISubscriptionDefinition +public sealed class TypeSubscription : ISubscriptionDefinition { /// /// Initializes a new instance of the class. /// /// The exact topic type to match against. - /// Agent type to handle this subscription. + /// Actor type to handle this subscription. /// Unique identifier for the subscription. If not provided, a new UUID will be generated. - public TypeSubscription(string topicType, AgentType agentType, string? id = null) + public TypeSubscription(string topicType, ActorType actorType, string? id = null) { this.TopicType = topicType; - this.AgentType = agentType; + this.ActorType = actorType; this.Id = id ?? Guid.NewGuid().ToString(); } @@ -44,9 +44,9 @@ public class TypeSubscription : ISubscriptionDefinition public string TopicType { get; } /// - /// Gets the agent type that handles this subscription. + /// Gets the actor type that handles this subscription. /// - public AgentType AgentType { get; } + public ActorType ActorType { get; } /// /// Checks if a given matches the subscription based on an exact type match. @@ -59,19 +59,19 @@ public class TypeSubscription : ISubscriptionDefinition } /// - /// Maps a to an . Should only be called if returns true. + /// Maps a to an . Should only be called if returns true. /// /// The topic to map. - /// An representing the agent that should handle the topic. + /// An representing the actor that should handle the topic. /// Thrown if the topic does not match the subscription. - public AgentId MapToAgent(TopicId topic) + public ActorId MapToActor(TopicId topic) { if (!this.Matches(topic)) { throw new InvalidOperationException("TopicId does not match the subscription."); } - return new AgentId(this.AgentType, topic.Source); + return new ActorId(this.ActorType, topic.Source); } /// @@ -84,7 +84,7 @@ public class TypeSubscription : ISubscriptionDefinition return obj is TypeSubscription other && (this.Id == other.Id || - (this.AgentType == other.AgentType && + (this.ActorType == other.ActorType && this.TopicType == other.TopicType)); } @@ -101,6 +101,6 @@ public class TypeSubscription : ISubscriptionDefinition /// A hash code for this instance, suitable for use in hashing algorithms and data structures. public override int GetHashCode() { - return HashCode.Combine(this.Id, this.AgentType, this.TopicType); + return HashCode.Combine(this.Id, this.ActorType, this.TopicType); } } diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/InProcessRuntime.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/InProcessRuntime.cs index 172d7f50ba..87aa7e817a 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/InProcessRuntime.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/InProcessRuntime.cs @@ -5,21 +5,19 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Reflection; using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; -#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess; /// /// Provides an in-process/in-memory implementation of the agent runtime. /// -public sealed class InProcessRuntime : IAgentRuntime, IAsyncDisposable +public sealed partial class InProcessRuntime : IAgentRuntime, IAsyncDisposable { - private readonly Dictionary>> _agentFactories = []; + private readonly Dictionary>> _actorFactories = []; private readonly Dictionary _subscriptions = []; private readonly ConcurrentQueue _messageDeliveryQueue = new(); @@ -29,13 +27,13 @@ public sealed class InProcessRuntime : IAgentRuntime, IAsyncDisposable private Func _shouldContinue = () => true; // Exposed for testing purposes. - internal int messageQueueCount; - internal readonly Dictionary agentInstances = []; + internal int _messageQueueCount; + internal readonly Dictionary _actorInstances = []; /// - /// Gets or sets a value indicating whether agents should receive messages they send themselves. + /// Gets or sets a value indicating whether actors should receive messages they send themselves. /// - public bool DeliverToSelf { get; set; } //= false; + public bool DeliverToSelf { get; set; } /// public async ValueTask DisposeAsync() @@ -90,7 +88,7 @@ public sealed class InProcessRuntime : IAgentRuntime, IAsyncDisposable /// /// This will run until the message queue is empty and then stop the runtime. /// - public async Task RunUntilIdleAsync() + public async Task RunUntilIdleAsync(CancellationToken cancellationToken = default) { Func oldShouldContinue = this._shouldContinue; this._shouldContinue = () => !this._messageDeliveryQueue.IsEmpty; @@ -102,7 +100,7 @@ public sealed class InProcessRuntime : IAgentRuntime, IAsyncDisposable } /// - public ValueTask PublishMessageAsync(object message, TopicId topic, AgentId? sender = null, string? messageId = null, CancellationToken cancellationToken = default) + public ValueTask PublishMessageAsync(object message, TopicId topic, ActorId? sender = null, string? messageId = null, CancellationToken cancellationToken = default) { return this.ExecuteTracedAsync(async () => { @@ -112,14 +110,14 @@ public sealed class InProcessRuntime : IAgentRuntime, IAsyncDisposable .ForPublish(topic, this.PublishMessageServicerAsync); this._messageDeliveryQueue.Enqueue(delivery); - Interlocked.Increment(ref this.messageQueueCount); + Interlocked.Increment(ref this._messageQueueCount); - await delivery.ResultSink.Future.ConfigureAwait(false); + await delivery.ResultTask.ConfigureAwait(false); }); } /// - public async ValueTask SendMessageAsync(object message, AgentId recipient, AgentId? sender = null, string? messageId = null, CancellationToken cancellationToken = default) + public async ValueTask SendMessageAsync(object message, ActorId recipient, ActorId? sender = null, string? messageId = null, CancellationToken cancellationToken = default) { return await this.ExecuteTracedAsync(async () => { @@ -129,74 +127,67 @@ public sealed class InProcessRuntime : IAgentRuntime, IAsyncDisposable .ForSend(recipient, this.SendMessageServicerAsync); this._messageDeliveryQueue.Enqueue(delivery); - Interlocked.Increment(ref this.messageQueueCount); + Interlocked.Increment(ref this._messageQueueCount); - try - { - return await delivery.ResultSink.Future.ConfigureAwait(false); - } - catch (TargetInvocationException ex) when (ex.InnerException is OperationCanceledException innerOCEx) - { - throw new OperationCanceledException($"Delivery of message {messageId} was cancelled.", innerOCEx); - } + return await delivery.ResultTask.ConfigureAwait(false); }).ConfigureAwait(false); } /// - public async ValueTask GetAgentAsync(AgentId agentId, bool lazy = true) + public async ValueTask GetActorAsync(ActorId actorId, bool lazy = true, CancellationToken cancellationToken = default) { if (!lazy) { - await this.EnsureAgentAsync(agentId).ConfigureAwait(false); + await this.EnsureActorAsync(actorId, cancellationToken).ConfigureAwait(false); } - return agentId; + return actorId; } /// - public ValueTask GetAgentAsync(AgentType agentType, string key = AgentId.DefaultKey, bool lazy = true) - => this.GetAgentAsync(new AgentId(agentType, key), lazy); + public ValueTask GetActorAsync(ActorType actorType, string? key = null, bool lazy = true, CancellationToken cancellationToken = default) + => this.GetActorAsync(actorType.Name, key, lazy, cancellationToken); /// - public ValueTask GetAgentAsync(string agent, string key = AgentId.DefaultKey, bool lazy = true) - => this.GetAgentAsync(new AgentId(agent, key), lazy); + 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 GetAgentMetadataAsync(AgentId agentId) + public async ValueTask GetActorMetadataAsync(ActorId actorId, CancellationToken cancellationToken = default) { - IHostableAgent agent = await this.EnsureAgentAsync(agentId).ConfigureAwait(false); - return agent.Metadata; + IRuntimeActor actor = await this.EnsureActorAsync(actorId, cancellationToken).ConfigureAwait(false); + return actor.Metadata; } /// - public async ValueTask TryGetUnderlyingAgentInstanceAsync(AgentId agentId) where TAgent : IHostableAgent + public async ValueTask TryGetUnderlyingActorInstanceAsync(ActorId actorId, CancellationToken cancellationToken = default) where TActor : IRuntimeActor { - IHostableAgent agent = await this.EnsureAgentAsync(agentId).ConfigureAwait(false); + IRuntimeActor actor = await this.EnsureActorAsync(actorId, cancellationToken).ConfigureAwait(false); - if (agent is not TAgent concreteAgent) + if (actor is not TActor concreteActor) { - throw new InvalidOperationException($"Agent with name {agentId.Type} is not of type {typeof(TAgent).Name}."); + throw new InvalidOperationException($"Actor with name {actorId.Type} is not of type {typeof(TActor).Name}."); } - return concreteAgent; + return concreteActor; } /// - public async ValueTask LoadAgentStateAsync(AgentId agentId, JsonElement state) + public async ValueTask LoadActorStateAsync(ActorId actorId, JsonElement state, CancellationToken cancellationToken = default) { - IHostableAgent agent = await this.EnsureAgentAsync(agentId).ConfigureAwait(false); - await agent.LoadStateAsync(state).ConfigureAwait(false); + IRuntimeActor actor = await this.EnsureActorAsync(actorId, cancellationToken).ConfigureAwait(false); + await actor.LoadStateAsync(state, cancellationToken).ConfigureAwait(false); } /// - public async ValueTask SaveAgentStateAsync(AgentId agentId) + public async ValueTask SaveActorStateAsync(ActorId actorId, CancellationToken cancellationToken = default) { - IHostableAgent agent = await this.EnsureAgentAsync(agentId).ConfigureAwait(false); - return await agent.SaveStateAsync().ConfigureAwait(false); + IRuntimeActor actor = await this.EnsureActorAsync(actorId, cancellationToken).ConfigureAwait(false); + return await actor.SaveStateAsync(cancellationToken).ConfigureAwait(false); } /// - public ValueTask AddSubscriptionAsync(ISubscriptionDefinition subscription) + public ValueTask AddSubscriptionAsync(ISubscriptionDefinition subscription, CancellationToken cancellationToken = default) { if (this._subscriptions.ContainsKey(subscription.Id)) { @@ -209,7 +200,7 @@ public sealed class InProcessRuntime : IAgentRuntime, IAsyncDisposable } /// - public ValueTask RemoveSubscriptionAsync(string subscriptionId) + public ValueTask RemoveSubscriptionAsync(string subscriptionId, CancellationToken cancellationToken = default) { if (!this._subscriptions.ContainsKey(subscriptionId)) { @@ -222,61 +213,62 @@ public sealed class InProcessRuntime : IAgentRuntime, IAsyncDisposable } /// - public async ValueTask LoadStateAsync(JsonElement state) + public async ValueTask LoadStateAsync(JsonElement state, CancellationToken cancellationToken = default) { - foreach (JsonProperty agentIdStr in state.EnumerateObject()) + foreach (JsonProperty actorIdStr in state.EnumerateObject()) { - AgentId agentId = AgentId.FromStr(agentIdStr.Name); + ActorId actorId = ActorId.Parse(actorIdStr.Name); - if (this._agentFactories.ContainsKey(agentId.Type)) + if (this._actorFactories.ContainsKey(actorId.Type)) { - IHostableAgent agent = await this.EnsureAgentAsync(agentId).ConfigureAwait(false); - await agent.LoadStateAsync(agentIdStr.Value).ConfigureAwait(false); + IRuntimeActor actor = await this.EnsureActorAsync(actorId, cancellationToken).ConfigureAwait(false); + await actor.LoadStateAsync(actorIdStr.Value, cancellationToken).ConfigureAwait(false); } } } /// - public async ValueTask SaveStateAsync() + public async ValueTask SaveStateAsync(CancellationToken cancellationToken = default) { Dictionary state = []; - foreach (AgentId agentId in this.agentInstances.Keys) + foreach (ActorId actorId in this._actorInstances.Keys) { - JsonElement agentState = await this.agentInstances[agentId].SaveStateAsync().ConfigureAwait(false); - state[agentId.ToString()] = agentState; + JsonElement actorState = await this._actorInstances[actorId].SaveStateAsync(cancellationToken).ConfigureAwait(false); + state[actorId.ToString()] = actorState; } - return JsonSerializer.SerializeToElement(state); + return JsonSerializer.SerializeToElement(state, InProcessRuntimeContext.Default.DictionaryStringJsonElement); } /// - /// Registers an agent factory with the runtime, associating it with a specific agent type. + /// Registers an actor factory with the runtime, associating it with a specific actor type. /// - /// The type of agent created by the factory. - /// The agent type to associate with the factory. - /// A function that asynchronously creates the agent instance. - /// A task representing the asynchronous operation, returning the registered agent type. - public ValueTask RegisterAgentFactoryAsync(AgentType type, Func> factoryFunc) where TAgent : IHostableAgent - // Declare the lambda return type explicitly, as otherwise the compiler will infer 'ValueTask' + /// 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.RegisterAgentFactoryAsync(type, async ValueTask (agentId, runtime) => await factoryFunc(agentId, runtime).ConfigureAwait(false)); + => this.RegisterActorFactoryAsync(type, async ValueTask (actorId, runtime) => await factoryFunc(actorId, runtime).ConfigureAwait(false), cancellationToken); /// - public async ValueTask RegisterAgentFactoryAsync(AgentType type, Func> factoryFunc) + public async ValueTask RegisterActorFactoryAsync(ActorType type, Func> factoryFunc, CancellationToken cancellationToken = default) { - if (this._agentFactories.ContainsKey(type)) + if (this._actorFactories.ContainsKey(type)) { - throw new InvalidOperationException($"Agent with type {type} already exists."); + throw new InvalidOperationException($"Actor with type {type} already exists."); } - this._agentFactories.Add(type, factoryFunc); + this._actorFactories.Add(type, factoryFunc); return type; } /// - public async ValueTask TryGetAgentProxyAsync(AgentId agentId) + public async ValueTask TryGetActorProxyAsync(ActorId actorId, CancellationToken cancellationToken = default) { - AgentProxy proxy = new(agentId, this); + IdProxyActor proxy = new(this, actorId); return proxy; } @@ -285,7 +277,7 @@ public sealed class InProcessRuntime : IAgentRuntime, IAsyncDisposable { if (this._messageDeliveryQueue.TryDequeue(out MessageDelivery? delivery)) { - Interlocked.Decrement(ref this.messageQueueCount); + Interlocked.Decrement(ref this._messageQueueCount); Debug.WriteLine($"Processing message {delivery.Message.MessageId}..."); await delivery.InvokeAsync(cancellation).ConfigureAwait(false); } @@ -296,26 +288,18 @@ public sealed class InProcessRuntime : IAgentRuntime, IAsyncDisposable ConcurrentDictionary pendingTasks = []; while (!cancellation.IsCancellationRequested && this._shouldContinue()) { - // Get a unique task id - Guid taskId; - do - { - taskId = Guid.NewGuid(); - } while (pendingTasks.ContainsKey(taskId)); + // 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(); - // Check if the task is already completed - if (processTask.IsCompleted) + if (!processTask.IsCompleted) { - continue; + pendingTasks.TryAdd(taskId, processTask.AsTask().ContinueWith(t => pendingTasks.TryRemove(taskId, out _), TaskScheduler.Current)); } - - Task actualTask = processTask.AsTask(); - pendingTasks.TryAdd(taskId, actualTask.ContinueWith(t => pendingTasks.TryRemove(taskId, out _), TaskScheduler.Current)); } // The pending task dictionary may contain null values when a race condition is experienced during @@ -332,7 +316,7 @@ public sealed class InProcessRuntime : IAgentRuntime, IAsyncDisposable throw new InvalidOperationException("Message must have a topic to be published."); } - List exceptions = []; + List? exceptions = null; TopicId topic = envelope.Topic.Value; foreach (ISubscriptionDefinition subscription in this._subscriptions.Values.Where(subscription => subscription.Matches(topic))) { @@ -340,36 +324,36 @@ public sealed class InProcessRuntime : IAgentRuntime, IAsyncDisposable { deliveryToken.ThrowIfCancellationRequested(); - AgentId? sender = envelope.Sender; + ActorId? sender = envelope.Sender; using CancellationTokenSource combinedSource = CancellationTokenSource.CreateLinkedTokenSource(envelope.Cancellation, deliveryToken); - MessageContext messageContext = new(envelope.MessageId, combinedSource.Token) + + 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 }; - AgentId agentId = subscription.MapToAgent(topic); - if (!this.DeliverToSelf && sender.HasValue && sender == agentId) - { - continue; - } + IRuntimeActor actor = await this.EnsureActorAsync(actorId, combinedSource.Token).ConfigureAwait(false); - IHostableAgent agent = await this.EnsureAgentAsync(agentId).ConfigureAwait(false); - - // TODO: Cancellation propagation! - await agent.OnMessageAsync(envelope.Message, messageContext).ConfigureAwait(false); + await actor.OnMessageAsync(envelope.Message, messageContext, combinedSource.Token).ConfigureAwait(false); } catch (Exception ex) { - exceptions.Add(ex); + (exceptions ??= []).Add(ex); } } - if (exceptions.Count > 0) + if (exceptions is not null) { - // TODO: Unwrap TargetInvocationException? throw new AggregateException("One or more exceptions occurred while processing the message.", exceptions); } } @@ -382,63 +366,72 @@ public sealed class InProcessRuntime : IAgentRuntime, IAsyncDisposable } using CancellationTokenSource combinedSource = CancellationTokenSource.CreateLinkedTokenSource(envelope.Cancellation, deliveryToken); - MessageContext messageContext = new(envelope.MessageId, combinedSource.Token) + MessageContext messageContext = new() { + MessageId = envelope.MessageId, Sender = envelope.Sender, IsRpc = false }; - AgentId receiver = envelope.Receiver.Value; - IHostableAgent agent = await this.EnsureAgentAsync(receiver).ConfigureAwait(false); + ActorId receiver = envelope.Receiver.Value; + IRuntimeActor actor = await this.EnsureActorAsync(receiver, combinedSource.Token).ConfigureAwait(false); - return await agent.OnMessageAsync(envelope.Message, messageContext).ConfigureAwait(false); + return await actor.OnMessageAsync(envelope.Message, messageContext, combinedSource.Token).ConfigureAwait(false); } - private async ValueTask EnsureAgentAsync(AgentId agentId) + private async ValueTask EnsureActorAsync(ActorId actorId, CancellationToken cancellationToken) { - if (!this.agentInstances.TryGetValue(agentId, out IHostableAgent? agent)) + if (!this._actorInstances.TryGetValue(actorId, out IRuntimeActor? actor)) { - if (!this._agentFactories.TryGetValue(agentId.Type, out Func>? factoryFunc)) + if (!this._actorFactories.TryGetValue(actorId.Type, out Func>? factoryFunc)) { - throw new InvalidOperationException($"Agent with name {agentId.Type} not found."); + throw new InvalidOperationException($"Actor with name {actorId.Type} not found."); } - agent = await factoryFunc(agentId, this).ConfigureAwait(false); - this.agentInstances.Add(agentId, agent); + actor = await factoryFunc(actorId, this).ConfigureAwait(false); + this._actorInstances.Add(actorId, actor); } - return this.agentInstances[agentId]; + return actor; } private async Task FinishAsync(CancellationToken token) { - foreach (IHostableAgent agent in this.agentInstances.Values) + foreach (IRuntimeActor actor in this._actorInstances.Values) { - if (!token.IsCancellationRequested) + if (!token.IsCancellationRequested && actor is IAsyncDisposable closeableActor) { - await agent.CloseAsync().ConfigureAwait(false); + await closeableActor.DisposeAsync().ConfigureAwait(false); } } - this._shutdownSource?.Dispose(); - this._finishSource?.Dispose(); - this._finishSource = null; - this._shutdownSource = null; + 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) -#pragma warning restore CA1822 // Mark members as static { // TODO: Bind tracing return func(); } -#pragma warning disable CA1822 // Mark members as static private ValueTask ExecuteTracedAsync(Func func) -#pragma warning restore CA1822 // Mark members as static { // 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 index 0080eada0c..52669e9a01 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/MessageDelivery.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/MessageDelivery.cs @@ -6,14 +6,11 @@ using System.Threading.Tasks; namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess; -internal sealed class MessageDelivery(MessageEnvelope message, Func servicer, IResultSink resultSink) +internal sealed class MessageDelivery(MessageEnvelope message, Func servicer, Task resultTask) { public MessageEnvelope Message { get; } = message; public Func Servicer { get; } = servicer; - public IResultSink ResultSink { get; } = resultSink; + public Task ResultTask { get; } = resultTask; - public ValueTask InvokeAsync(CancellationToken cancellation) - { - return this.Servicer(this.Message, cancellation); - } + 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 index d4fa500d92..8186252d96 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/MessageEnvelope.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/MessageEnvelope.cs @@ -6,73 +6,60 @@ using System.Threading.Tasks; namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess; -internal sealed class MessageEnvelope +internal sealed class MessageEnvelope(object message, string? messageId = null, CancellationToken cancellation = default) { - public object Message { get; } - public string MessageId { get; } + public object Message { get; } = message; + public string MessageId { get; } = messageId ?? Guid.NewGuid().ToString(); public TopicId? Topic { get; private set; } - public AgentId? Sender { get; private set; } - public AgentId? Receiver { get; private set; } - public CancellationToken Cancellation { get; } + public ActorId? Sender { get; private set; } + public ActorId? Receiver { get; private set; } + public CancellationToken Cancellation { get; } = cancellation; - public MessageEnvelope(object message, string? messageId = null, CancellationToken cancellation = default) - { - this.Message = message; - this.MessageId = messageId ?? Guid.NewGuid().ToString(); - this.Cancellation = cancellation; - } - - public MessageEnvelope WithSender(AgentId? sender) + public MessageEnvelope WithSender(ActorId? sender) { this.Sender = sender; return this; } - public MessageDelivery ForSend(AgentId receiver, Func> servicer) + public MessageDelivery ForSend(ActorId receiver, Func> servicer) { this.Receiver = receiver; - ResultSink resultSink = new(); - - return new MessageDelivery(this, BoundServicer, resultSink); - - async ValueTask BoundServicer(MessageEnvelope envelope, CancellationToken cancellation) + TaskCompletionSource tcs = new(); + return new MessageDelivery(this, async (MessageEnvelope envelope, CancellationToken cancellation) => { try { object? result = await servicer(envelope, cancellation).ConfigureAwait(false); - resultSink.SetResult(result); + tcs.SetResult(result); } catch (OperationCanceledException exception) { - resultSink.SetCancelled(exception); + tcs.TrySetCanceled(exception.CancellationToken); } catch (Exception exception) { - resultSink.SetException(exception); + tcs.SetException(exception); } - } + }, tcs.Task); } public MessageDelivery ForPublish(TopicId topic, Func servicer) { this.Topic = topic; - ResultSink waitForPublish = new(); - - async ValueTask BoundServicer(MessageEnvelope envelope, CancellationToken cancellation) + TaskCompletionSource tcs = new(); + return new MessageDelivery(this, async (envelope, cancellation) => { try { await servicer(envelope, cancellation).ConfigureAwait(false); - waitForPublish.SetResult(null); + tcs.SetResult(null); } catch (Exception ex) { - waitForPublish.SetException(ex); + tcs.SetException(ex); } - } - - return new MessageDelivery(this, BoundServicer, waitForPublish); + }, 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 index 40cb8f1339..2169aa805f 100644 --- 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 @@ -4,7 +4,6 @@ $(ProjectsTargetFrameworks) $(ProjectsDebugTargetFrameworks) alpha - false diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/ResultSink.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/ResultSink.cs deleted file mode 100644 index b9c44f2171..0000000000 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/ResultSink.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Threading.Tasks; -using System.Threading.Tasks.Sources; - -namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess; - -internal interface IResultSink : IValueTaskSource -{ - void SetResult(TResult result); - void SetException(Exception exception); - void SetCancelled(OperationCanceledException? exception = null); - - ValueTask Future { get; } -} - -internal sealed class ResultSink : IResultSink -{ - private ManualResetValueTaskSourceCore _core; - - public bool IsCancelled { get; private set; } - - public TResult GetResult(short token) - { - return this._core.GetResult(token); - } - - public ValueTaskSourceStatus GetStatus(short token) - { - return this._core.GetStatus(token); - } - - public void OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) - { - this._core.OnCompleted(continuation, state, token, flags); - } - - public void SetCancelled(OperationCanceledException? exception = null) - { - this.IsCancelled = true; - this._core.SetException(exception ?? new OperationCanceledException()); - } - - public void SetException(Exception exception) - { - this._core.SetException(exception); - } - - public void SetResult(TResult result) - { - this._core.SetResult(result); - } - - public ValueTask Future => new(this, this._core.Version); -} diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/AgentIdTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/AgentIdTests.cs index a22ae1b32f..ec3a3a902d 100644 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/AgentIdTests.cs +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/AgentIdTests.cs @@ -17,54 +17,34 @@ public class AgentIdTests() public void AgentIdShouldThrowArgumentExceptionWithInvalidKey(string? invalidKey) { // Act & Assert - ArgumentException exception = Assert.Throws(() => new AgentId("validType", invalidKey!)); - Assert.Contains("Invalid AgentId key", exception.Message); + ArgumentException exception = Assert.Throws(() => new ActorId("validType", invalidKey!)); + Assert.Contains("Invalid ActorId key", exception.Message); } [Fact] public void AgentIdShouldInitializeCorrectlyTest() { - AgentId agentId = new("TestType", "TestKey"); + ActorId agentId = new("TestType", "TestKey"); - Assert.Equal("TestType", agentId.Type); - Assert.Equal("TestKey", agentId.Key); - } - - [Fact] - public void AgentIdShouldConvertFromTupleTest() - { - (string, string) agentTuple = ("TupleType", "TupleKey"); - AgentId agentId = new(agentTuple); - - Assert.Equal("TupleType", agentId.Type); - Assert.Equal("TupleKey", agentId.Key); - } - - [Fact] - public void AgentIdShouldConvertFromAgentType() - { - AgentType agentType = "TestType"; - AgentId agentId = new(agentType, "TestKey"); - - Assert.Equal("TestType", agentId.Type); + Assert.Equal("TestType", agentId.Type.Name); Assert.Equal("TestKey", agentId.Key); } [Fact] public void AgentIdShouldParseFromStringTest() { - AgentId agentId = AgentId.FromStr("ParsedType/ParsedKey"); + ActorId agentId = ActorId.Parse("ParsedType/ParsedKey"); - Assert.Equal("ParsedType", agentId.Type); + Assert.Equal("ParsedType", agentId.Type.Name); Assert.Equal("ParsedKey", agentId.Key); } [Fact] public void AgentIdShouldCompareEqualityCorrectlyTest() { - AgentId agentId1 = new("SameType", "SameKey"); - AgentId agentId2 = new("SameType", "SameKey"); - AgentId agentId3 = new("DifferentType", "DifferentKey"); + ActorId agentId1 = new("SameType", "SameKey"); + ActorId agentId2 = new("SameType", "SameKey"); + ActorId agentId3 = new("DifferentType", "DifferentKey"); Assert.Equal(agentId2, agentId1); Assert.NotEqual(agentId3, agentId1); @@ -75,27 +55,18 @@ public class AgentIdTests() [Fact] public void AgentIdShouldGenerateCorrectHashCodeTest() { - AgentId agentId1 = new("HashType", "HashKey"); - AgentId agentId2 = new("HashType", "HashKey"); - AgentId agentId3 = new("DifferentType", "DifferentKey"); + ActorId agentId1 = new("HashType", "HashKey"); + ActorId agentId2 = new("HashType", "HashKey"); + ActorId agentId3 = new("DifferentType", "DifferentKey"); Assert.Equal(agentId2.GetHashCode(), agentId1.GetHashCode()); Assert.NotEqual(agentId3.GetHashCode(), agentId1.GetHashCode()); } - [Fact] - public void AgentIdShouldConvertExplicitlyFromStringTest() - { - AgentId agentId = (AgentId)"ConvertedType/ConvertedKey"; - - Assert.Equal("ConvertedType", agentId.Type); - Assert.Equal("ConvertedKey", agentId.Key); - } - [Fact] public void AgentIdShouldReturnCorrectToStringTest() { - AgentId agentId = new("ToStringType", "ToStringKey"); + ActorId agentId = new("ToStringType", "ToStringKey"); Assert.Equal("ToStringType/ToStringKey", agentId.ToString()); } @@ -103,7 +74,7 @@ public class AgentIdTests() [Fact] public void AgentIdShouldCompareInequalityForWrongTypeTest() { - AgentId agentId1 = new("Type1", "Key1"); + ActorId agentId1 = new("Type1", "Key1"); Assert.False(agentId1.Equals(Guid.NewGuid())); } @@ -111,8 +82,8 @@ public class AgentIdTests() [Fact] public void AgentIdShouldCompareInequalityCorrectlyTest() { - AgentId agentId1 = new("Type1", "Key1"); - AgentId agentId2 = new("Type2", "Key2"); + ActorId agentId1 = new("Type1", "Key1"); + ActorId agentId2 = new("Type2", "Key2"); Assert.True(agentId1 != agentId2); } diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/AgentMetaDataTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/AgentMetaDataTests.cs index 7bec3a33b1..c64c1f3a02 100644 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/AgentMetaDataTests.cs +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/AgentMetaDataTests.cs @@ -8,10 +8,10 @@ public class AgentMetadataTests() public void AgentMetadataShouldInitializeCorrectlyTest() { // Arrange & Act - AgentMetadata metadata = new("TestType", "TestKey", "TestDescription"); + ActorMetadata metadata = new(new ActorType("TestType"), "TestKey", "TestDescription"); // Assert - Assert.Equal("TestType", metadata.Type); + Assert.Equal("TestType", metadata.Type.Name); Assert.Equal("TestKey", metadata.Key); Assert.Equal("TestDescription", metadata.Description); } diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/AgentProxyTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/AgentProxyTests.cs index 084c8981d8..7e6f9bcd5d 100644 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/AgentProxyTests.cs +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/AgentProxyTests.cs @@ -10,14 +10,14 @@ namespace Microsoft.Extensions.AI.Agents.Runtime.Abstractions.Tests; public class AgentProxyTests { private readonly Mock _mockRuntime; - private readonly AgentId _agentId; - private readonly AgentProxy _agentProxy; + private readonly ActorId _agentId; + private readonly IdProxyActor _agentProxy; public AgentProxyTests() { this._mockRuntime = new Mock(); - this._agentId = new AgentId("testType", "testKey"); - this._agentProxy = new AgentProxy(this._agentId, this._mockRuntime.Object); + this._agentId = new ActorId("testType", "testKey"); + this._agentProxy = new IdProxyActor(this._mockRuntime.Object, this._agentId); } [Fact] @@ -30,8 +30,8 @@ public class AgentProxyTests [Fact] public void MetadataShouldMatchAgentTest() { - AgentMetadata expectedMetadata = new("testType", "testKey", "testDescription"); - this._mockRuntime.Setup(r => r.GetAgentMetadataAsync(this._agentId)) + ActorMetadata expectedMetadata = new(new("testType"), "testKey", "testDescription"); + this._mockRuntime.Setup(r => r.GetActorMetadataAsync(this._agentId, default)) .ReturnsAsync(expectedMetadata); Assert.Equal(expectedMetadata, this._agentProxy.Metadata); @@ -42,7 +42,7 @@ public class AgentProxyTests { // Arrange object message = new { Content = "Hello" }; - AgentId sender = new("senderType", "senderKey"); + ActorId sender = new("senderType", "senderKey"); object response = new { Content = "Response" }; this._mockRuntime.Setup(r => r.SendMessageAsync(message, this._agentId, sender, null, It.IsAny())) @@ -61,14 +61,14 @@ public class AgentProxyTests // Arrange JsonElement state = JsonDocument.Parse("{\"key\":\"value\"}").RootElement; - this._mockRuntime.Setup(r => r.LoadAgentStateAsync(this._agentId, state)) + this._mockRuntime.Setup(r => r.LoadActorStateAsync(this._agentId, state, default)) .Returns(default(ValueTask)); // Act await this._agentProxy.LoadStateAsync(state); // Assert - this._mockRuntime.Verify(r => r.LoadAgentStateAsync(this._agentId, state), Times.Once); + this._mockRuntime.Verify(r => r.LoadActorStateAsync(this._agentId, state, default), Times.Once); } [Fact] @@ -77,7 +77,7 @@ public class AgentProxyTests // Arrange JsonElement expectedState = JsonDocument.Parse("{\"key\":\"value\"}").RootElement; - this._mockRuntime.Setup(r => r.SaveAgentStateAsync(this._agentId)) + this._mockRuntime.Setup(r => r.SaveActorStateAsync(this._agentId, default)) .ReturnsAsync(expectedState); // Act diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/AgentTypeTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/AgentTypeTests.cs index 32bce85a7d..eedb8c9733 100644 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/AgentTypeTests.cs +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/AgentTypeTests.cs @@ -17,46 +17,18 @@ public class AgentTypeTests public void AgentIdShouldThrowArgumentExceptionWithInvalidType(string? invalidType) { // Act & Assert - ArgumentException exception = Assert.Throws(() => new AgentType(invalidType!)); - Assert.Contains("Invalid AgentId type", exception.Message); + ArgumentException exception = Assert.Throws(() => new ActorType(invalidType!)); + Assert.Contains("Invalid type", exception.Message); } [Fact] - public void ImplicitConversionFromStringTest() + public void ConversionToStringTest() { // Arrange - string agentTypeName = "TestAgent"; - - // Act - AgentType agentType = agentTypeName; + ActorType agentType = new("TestAgent"); // Assert - Assert.Equal(agentTypeName, agentType.Name); - } - - [Fact] - public void ImplicitConversionToStringTest() - { - // Arrange - AgentType agentType = "TestAgent"; - - // Act - string agentTypeName = agentType; - - // Assert - Assert.Equal("TestAgent", agentTypeName); - } - - [Fact] - public void ExplicitConversionFromTypeTest() - { - // Arrange - Type type = typeof(string); - - // Act - AgentType agentType = (AgentType)type; - - // Assert - Assert.Equal(type.Name, agentType.Name); + Assert.Equal("TestAgent", agentType.Name); + Assert.Equal("TestAgent", agentType.ToString()); } } diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/MessageContextTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/MessageContextTests.cs index 5a1f1074f0..68a14b8569 100644 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/MessageContextTests.cs +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/MessageContextTests.cs @@ -1,80 +1,34 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.Threading; namespace Microsoft.Extensions.AI.Agents.Runtime.Abstractions.Tests; public class MessageContextTests { [Fact] - public void ConstructWithMessageIdAndCancellationTokenTest() + public void Properties_Roundtrip() { - // Arrange - string messageId = Guid.NewGuid().ToString(); - CancellationToken cancellationToken = new(); + MessageContext ctx = new(); - // Act - MessageContext messageContext = new(messageId, cancellationToken); + string id = ctx.MessageId; + Assert.NotNull(id); + Assert.True(Guid.TryParse(id, out _)); + ctx.MessageId = "newid"; + Assert.Equal("newid", ctx.MessageId); - // Assert - Assert.Equal(messageId, messageContext.MessageId); - Assert.Equal(cancellationToken, messageContext.CancellationToken); - } + Assert.False(ctx.IsRpc); + ctx.IsRpc = true; + Assert.True(ctx.IsRpc); - [Fact] - public void ConstructWithCancellationTokenTest() - { - // Arrange - CancellationToken cancellationToken = new(); + Assert.Null(ctx.Sender); + ActorId sender = new("type", "key"); + ctx.Sender = sender; + Assert.Equal(sender, ctx.Sender); - // Act - MessageContext messageContext = new(cancellationToken); - - // Assert - Assert.NotNull(messageContext.MessageId); - Assert.Equal(cancellationToken, messageContext.CancellationToken); - } - - [Fact] - public void AssignSenderTest() - { - // Arrange - MessageContext messageContext = new(new CancellationToken()); - AgentId sender = new("type", "key"); - - // Act - messageContext.Sender = sender; - - // Assert - Assert.Equal(sender, messageContext.Sender); - } - - [Fact] - public void AssignTopicTest() - { - // Arrange - MessageContext messageContext = new(new CancellationToken()); + Assert.Null(ctx.Topic); TopicId topic = new("type", "source"); - - // Act - messageContext.Topic = topic; - - // Assert - Assert.Equal(topic, messageContext.Topic); - } - - [Fact] - public void AssignIsRpcPropertyTest() - { - // Arrange - MessageContext messageContext = new(new CancellationToken()) - { - // Act - IsRpc = true - }; - - // Assert - Assert.True(messageContext.IsRpc); + ctx.Topic = topic; + Assert.Equal(topic, ctx.Topic); } } diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/TopicIdTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/TopicIdTests.cs index 51ac6f7981..d94dfcc57a 100644 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/TopicIdTests.cs +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/TopicIdTests.cs @@ -14,7 +14,6 @@ public class TopicIdTests // Assert Assert.Equal("testtype", topicId.Type); - Assert.Equal(TopicId.DefaultSource, topicId.Source); } [Fact] @@ -28,42 +27,28 @@ public class TopicIdTests Assert.Equal("customsource", topicId.Source); } - [Fact] - public void ConstructWithTupleTest() + [Theory] + [InlineData("testtype/https://github.com/cloudevents", "testtype", "https://github.com/cloudevents")] + [InlineData("testtype/mailto:cncf-wg-serverless@lists.cncf.io", "testtype", "mailto:cncf-wg-serverless@lists.cncf.io")] + [InlineData("testtype/urn:uuid:6e8bc430-9c3a-11d9-9669-0800200c9a66", "testtype", "urn:uuid:6e8bc430-9c3a-11d9-9669-0800200c9a66")] + [InlineData("testtype//cloudevents/spec/pull/123", "testtype", "/cloudevents/spec/pull/123")] + [InlineData("testtype//sensors/tn-1234567/alerts", "testtype", "/sensors/tn-1234567/alerts")] + [InlineData("testtype/1-555-123-4567", "testtype", "1-555-123-4567")] + public void ParseTest(string input, string expectedType, string expectedSource) { - // Arrange - (string, string) tuple = ("testtype", "customsource"); + TopicId topicId = TopicId.Parse(input); - // Act - TopicId topicId = new(tuple); - - // Assert - Assert.Equal("testtype", topicId.Type); - Assert.Equal("customsource", topicId.Source); - } - - [Fact] - public void ConvertFromStringTest() - { - // Arrange - const string TopicIdStr = "testtype/customsource"; - - // Act - TopicId topicId = TopicId.FromStr(TopicIdStr); - - // Assert - Assert.Equal("testtype", topicId.Type); - Assert.Equal("customsource", topicId.Source); + Assert.Equal(expectedType, topicId.Type); + Assert.Equal(expectedSource, topicId.Source); } [Theory] [InlineData("invalid-format")] - [InlineData("too/many/parts")] [InlineData("")] - public void InvalidFormatFromStringThrowsTest(string invalidInput) + public void InvalidFormatParseThrowsTest(string invalidInput) { // Act & Assert - Assert.Throws(() => TopicId.FromStr(invalidInput)); + Assert.Throws(() => TopicId.Parse(invalidInput)); } [Fact] @@ -141,42 +126,4 @@ public class TopicIdTests // Assert Assert.Equal(hash1, hash2); } - - [Fact] - public void ExplicitConversionTest() - { - // Arrange - string topicIdStr = "testtype/customsource"; - - // Act - TopicId topicId = (TopicId)topicIdStr; - - // Assert - Assert.Equal("testtype", topicId.Type); - Assert.Equal("customsource", topicId.Source); - } - - [Fact] - public void IsWildcardMatchTest() - { - // Arrange - TopicId topicId1 = new("testtype", "source1"); - TopicId topicId2 = new("testtype", "source2"); - - // Act & Assert - Assert.True(topicId1.IsWildcardMatch(topicId2)); - Assert.True(topicId2.IsWildcardMatch(topicId1)); - } - - [Fact] - public void IsWildcardMismatchTest() - { - // Arrange - TopicId topicId1 = new("testtype1", "source"); - TopicId topicId2 = new("testtype2", "source"); - - // Act & Assert - Assert.False(topicId1.IsWildcardMatch(topicId2)); - Assert.False(topicId2.IsWildcardMatch(topicId1)); - } } diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/InProcessRuntimeTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/InProcessRuntimeTests.cs index edfb51a171..3a49bc43cf 100644 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/InProcessRuntimeTests.cs +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/InProcessRuntimeTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess.Tests; @@ -18,7 +19,7 @@ public class InProcessRuntimeTests() // Assert Assert.False(runtime.DeliverToSelf); - Assert.Equal(0, runtime.messageQueueCount); + Assert.Equal(0, runtime._messageQueueCount); // Act await runtime.StopAsync(); // Already stopped @@ -29,13 +30,13 @@ public class InProcessRuntimeTests() // Assert // Invalid to start runtime that is already started await Assert.ThrowsAsync(() => runtime.StartAsync()); - Assert.Equal(0, runtime.messageQueueCount); + Assert.Equal(0, runtime._messageQueueCount); // Act await runtime.StopAsync(); // Assert - Assert.Equal(0, runtime.messageQueueCount); + Assert.Equal(0, runtime._messageQueueCount); } [Fact] @@ -43,7 +44,7 @@ public class InProcessRuntimeTests() { // Arrange await using InProcessRuntime runtime = new(); - TestSubscription subscription = new("TestTopic", "MyAgent"); + TestSubscription subscription = new("TestTopic", new("MyAgent")); // Act & Assert await Assert.ThrowsAsync(async () => await runtime.RemoveSubscriptionAsync(subscription.Id)); @@ -68,44 +69,44 @@ public class InProcessRuntimeTests() await using InProcessRuntime runtime = new(); // Act & Assert - await Assert.ThrowsAsync(async () => await runtime.GetAgentAsync(AgentType, lazy: false)); + await Assert.ThrowsAsync(async () => await runtime.GetActorAsync(AgentType, lazy: false)); // Arrange - await runtime.RegisterAgentFactoryAsync(AgentType, factoryFunc); + await runtime.RegisterActorFactoryAsync(new(AgentType), factoryFunc); // Act & Assert - await Assert.ThrowsAsync(async () => await runtime.RegisterAgentFactoryAsync(AgentType, factoryFunc)); + await Assert.ThrowsAsync(async () => await runtime.RegisterActorFactoryAsync(new(AgentType), factoryFunc)); // Act: Lookup by type - AgentId agentId = await runtime.GetAgentAsync(AgentType, lazy: false); + ActorId agentId = await runtime.GetActorAsync(AgentType, lazy: false); // Assert Assert.Single(agents); - Assert.Single(runtime.agentInstances); + Assert.Single(runtime._actorInstances); // Act - MockAgent agent = await runtime.TryGetUnderlyingAgentInstanceAsync(agentId); + MockAgent agent = await runtime.TryGetUnderlyingActorInstanceAsync(agentId); // Assert Assert.Equal(agentId, agent.Id); // Act & Assert - await Assert.ThrowsAsync(async () => await runtime.TryGetUnderlyingAgentInstanceAsync(agentId)); + await Assert.ThrowsAsync(async () => await runtime.TryGetUnderlyingActorInstanceAsync(agentId)); // Act: Lookup by ID - AgentId sameId = await runtime.GetAgentAsync(agentId, lazy: false); + ActorId sameId = await runtime.GetActorAsync(agentId, lazy: false); // Assert Assert.Equal(agentId, sameId); // Act: Lookup by Type - sameId = await runtime.GetAgentAsync((AgentType)agent.Id.Type, lazy: false); + sameId = await runtime.GetActorAsync((ActorType)agent.Id.Type, lazy: false); // Assert Assert.Equal(agentId, sameId); // Act: Lookup metadata - AgentMetadata metadata = await runtime.GetAgentMetadataAsync(agentId); + ActorMetadata metadata = await runtime.GetActorMetadataAsync(agentId); // Assert Assert.Equal(agentId.Type, metadata.Type); @@ -113,15 +114,16 @@ public class InProcessRuntimeTests() Assert.Equal(agentId.Key, metadata.Key); // Act: Access proxy - AgentProxy proxy = await runtime.TryGetAgentProxyAsync(agentId); + IdProxyActor? proxy = await runtime.TryGetActorProxyAsync(agentId); // Assert + Assert.NotNull(proxy); Assert.Equal(agentId, proxy.Id); Assert.Equal(metadata.Type, proxy.Metadata.Type); Assert.Equal(metadata.Description, proxy.Metadata.Description); Assert.Equal(metadata.Key, proxy.Metadata.Key); - async ValueTask factoryFunc(AgentId id, IAgentRuntime runtime) + async ValueTask factoryFunc(ActorId id, IAgentRuntime runtime) { MockAgent agent = new(id, runtime, AgentDescription); agents.Add(agent); @@ -137,35 +139,35 @@ public class InProcessRuntimeTests() const string TestMessage = "test message"; await using InProcessRuntime firstRuntime = new(); - await firstRuntime.RegisterAgentFactoryAsync(AgentType, factoryFunc); + await firstRuntime.RegisterActorFactoryAsync(new(AgentType), factoryFunc); // Act - AgentId agentId = await firstRuntime.GetAgentAsync(AgentType, lazy: false); + ActorId agentId = await firstRuntime.GetActorAsync(AgentType, lazy: false); // Assert - Assert.Single(firstRuntime.agentInstances); + Assert.Single(firstRuntime._actorInstances); // Arrange - MockAgent agent = (MockAgent)firstRuntime.agentInstances[agentId]; + MockAgent agent = (MockAgent)firstRuntime._actorInstances[agentId]; agent.ReceivedMessages.Add(TestMessage); // Act - JsonElement agentState = await firstRuntime.SaveAgentStateAsync(agentId); + JsonElement agentState = await firstRuntime.SaveActorStateAsync(agentId); // Arrange await using InProcessRuntime secondRuntime = new(); - await secondRuntime.RegisterAgentFactoryAsync(AgentType, factoryFunc); + await secondRuntime.RegisterActorFactoryAsync(new(AgentType), factoryFunc); // Act - await secondRuntime.LoadAgentStateAsync(agentId, agentState); + await secondRuntime.LoadActorStateAsync(agentId, agentState); // Assert - Assert.Single(secondRuntime.agentInstances); - MockAgent copy = (MockAgent)secondRuntime.agentInstances[agentId]; + Assert.Single(secondRuntime._actorInstances); + MockAgent copy = (MockAgent)secondRuntime._actorInstances[agentId]; Assert.Single(copy.ReceivedMessages); Assert.Equal(TestMessage, copy.ReceivedMessages.Single().ToString()); - static async ValueTask factoryFunc(AgentId id, IAgentRuntime runtime) + static async ValueTask factoryFunc(ActorId id, IAgentRuntime runtime) { MockAgent agent = new(id, runtime, "A test agent"); return agent; @@ -178,14 +180,14 @@ public class InProcessRuntimeTests() // Arrange await using InProcessRuntime runtime = new(); MockAgent? agent = null; - await runtime.RegisterAgentFactoryAsync("MyAgent", async (id, runtime) => + await runtime.RegisterActorFactoryAsync(new("MyAgent"), async (id, runtime) => { agent = new MockAgent(id, runtime, "A test agent"); return agent; }); // Act: Ensure the agent is actually created - AgentId agentId = await runtime.GetAgentAsync("MyAgent", lazy: false); + ActorId agentId = await runtime.GetActorAsync("MyAgent", lazy: false); // Assert Assert.NotNull(agent); @@ -197,7 +199,7 @@ public class InProcessRuntimeTests() await runtime.RunUntilIdleAsync(); // Assert - Assert.Equal(0, runtime.messageQueueCount); + Assert.Equal(0, runtime._messageQueueCount); Assert.Single(agent.ReceivedMessages); } @@ -214,21 +216,21 @@ public class InProcessRuntimeTests() }; MockAgent? agent = null; - await runtime.RegisterAgentFactoryAsync("MyAgent", async (id, runtime) => + await runtime.RegisterActorFactoryAsync(new("MyAgent"), async (id, runtime) => { agent = new MockAgent(id, runtime, "A test agent"); return agent; }); // Assert - Assert.Empty(runtime.agentInstances); + Assert.Empty(runtime._actorInstances); // Act: Ensure the agent is actually created - AgentId agentId = await runtime.GetAgentAsync("MyAgent", lazy: false); + ActorId agentId = await runtime.GetActorAsync("MyAgent", lazy: false); // Assert Assert.NotNull(agent); - Assert.Single(runtime.agentInstances); + Assert.Single(runtime._actorInstances); const string TopicType = "TestTopic"; @@ -250,14 +252,14 @@ public class InProcessRuntimeTests() // Arrange: Create a runtime and register an agent await using InProcessRuntime runtime = new(); MockAgent? agent = null; - await runtime.RegisterAgentFactoryAsync("MyAgent", async (id, runtime) => + await runtime.RegisterActorFactoryAsync(new("MyAgent"), async (id, runtime) => { agent = new MockAgent(id, runtime, "test agent"); return agent; }); // Get agent ID and instantiate agent by publishing - AgentId agentId = await runtime.GetAgentAsync("MyAgent", lazy: false); + ActorId agentId = await runtime.GetActorAsync("MyAgent", lazy: false); const string TopicType = "TestTopic"; await runtime.AddSubscriptionAsync(new TestSubscription(TopicType, agentId.Type)); @@ -286,46 +288,44 @@ public class InProcessRuntimeTests() agent = null; await using InProcessRuntime newRuntime = new(); await newRuntime.StartAsync(); - await newRuntime.RegisterAgentFactoryAsync("MyAgent", async (id, runtime) => + await newRuntime.RegisterActorFactoryAsync(new("MyAgent"), async (id, runtime) => { agent = new MockAgent(id, runtime, "another agent"); return agent; }); // Assert: Show that no agent instances exist in the new runtime - Assert.Empty(newRuntime.agentInstances); + Assert.Empty(newRuntime._actorInstances); // Act: Load the state into the new runtime and show that agent is now instantiated await newRuntime.LoadStateAsync(savedState); // Assert Assert.NotNull(agent); - Assert.Single(newRuntime.agentInstances); - Assert.True(newRuntime.agentInstances.ContainsKey(agentId)); + Assert.Single(newRuntime._actorInstances); + Assert.True(newRuntime._actorInstances.ContainsKey(agentId)); Assert.Single(agent.ReceivedMessages); } #pragma warning disable CA1812 // Avoid uninstantiated internal classes - private sealed class WrongAgent : IHostableAgent + private sealed class WrongAgent : IRuntimeActor #pragma warning restore CA1812 { - public AgentId Id => throw new NotImplementedException(); + public ActorId Id => throw new NotImplementedException(); - public AgentMetadata Metadata => throw new NotImplementedException(); + public ActorMetadata Metadata => throw new NotImplementedException(); - public ValueTask CloseAsync() => default; - - public ValueTask LoadStateAsync(JsonElement state) + public ValueTask LoadStateAsync(JsonElement state, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } - public ValueTask OnMessageAsync(object message, MessageContext messageContext) + public ValueTask OnMessageAsync(object message, MessageContext messageContext, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } - public ValueTask SaveStateAsync() + public ValueTask SaveStateAsync(CancellationToken cancellationToken = default) { throw new NotImplementedException(); } 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 index 55c0c2d632..050dc41e17 100644 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/MessageEnvelopeTests.cs +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/MessageEnvelopeTests.cs @@ -46,7 +46,7 @@ public class MessageEnvelopeTests { // Arrange MessageEnvelope envelope = new("test"); - AgentId sender = new("testtype", "testkey"); + ActorId sender = new("testtype", "testkey"); // Act MessageEnvelope result = envelope.WithSender(sender); @@ -61,7 +61,7 @@ public class MessageEnvelopeTests { // Arrange MessageEnvelope envelope = new("test"); - AgentId receiver = new("receivertype", "receiverkey"); + ActorId receiver = new("receivertype", "receiverkey"); object expectedResult = new { Response = "Success" }; ValueTask servicer(MessageEnvelope env, CancellationToken ct) => new(expectedResult); @@ -76,8 +76,8 @@ public class MessageEnvelopeTests // Invoke the servicer to verify result sink works await delivery.InvokeAsync(CancellationToken.None); - Assert.True(delivery.ResultSink.Future.IsCompleted); - object? result = await delivery.ResultSink.Future; + Assert.True(delivery.ResultTask.IsCompleted); + object? result = await delivery.ResultTask; Assert.Same(expectedResult, result); } diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/MessagingTestFixture.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/MessagingTestFixture.cs index 95466f08a1..234861bb3d 100644 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/MessagingTestFixture.cs +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/MessagingTestFixture.cs @@ -16,114 +16,87 @@ public sealed class BasicMessage public sealed class TestException : Exception; #pragma warning restore RCS1194 // Implement exception constructors -public sealed class PublisherAgent : TestAgent, IHandle +public sealed class PublisherAgent : TestAgent { - private readonly IList _targetTopics; - - public PublisherAgent(AgentId id, IAgentRuntime runtime, string description, IList targetTopics) - : base(id, runtime, description) + public PublisherAgent(ActorId id, IAgentRuntime runtime, string description, IList targetTopics) : base(id, runtime, description) { - this._targetTopics = targetTopics; - } - - public async ValueTask HandleAsync(BasicMessage item, MessageContext messageContext) - { - this.ReceivedMessages.Add(item); - foreach (TopicId targetTopic in this._targetTopics) + this.RegisterMessageHandler(async (item, messageContext, cancellationToken) => { - await this.PublishMessageAsync( - new BasicMessage { Content = $"@{targetTopic}: {item.Content}" }, - targetTopic); - } + this.ReceivedMessages.Add(item); + foreach (TopicId targetTopic in targetTopics) + { + await this.PublishMessageAsync( + new BasicMessage { Content = $"@{targetTopic}: {item.Content}" }, + targetTopic, + cancellationToken: cancellationToken); + } + }); } } -public sealed class SendOnAgent : TestAgent, IHandle +public sealed class SendOnAgent : TestAgent { - private readonly IList _targetKeys; - - public SendOnAgent(AgentId id, IAgentRuntime runtime, string description, IList targetKeys) - : base(id, runtime, description) + public SendOnAgent(ActorId id, IAgentRuntime runtime, string description, IList targetKeys) : base(id, runtime, description) { - this._targetKeys = targetKeys; - } - - public async ValueTask HandleAsync(BasicMessage item, MessageContext messageContext) - { - foreach (Guid targetKey in this._targetKeys) + this.RegisterMessageHandler(async (item, messageContext, cancellationToken) => { - AgentId targetId = new(nameof(ReceiverAgent), targetKey.ToString()); - BasicMessage response = new() { Content = $"@{targetKey}: {item.Content}" }; - await this.SendMessageAsync(response, targetId); - } + foreach (Guid targetKey in targetKeys) + { + ActorId targetId = new(nameof(ReceiverAgent), targetKey.ToString()); + BasicMessage response = new() { Content = $"@{targetKey}: {item.Content}" }; + await this.SendMessageAsync(response, targetId, cancellationToken: cancellationToken); + } + }); } } -public sealed class ReceiverAgent : TestAgent, IHandle +public sealed class ReceiverAgent : TestAgent { public List Messages { get; } = []; - public ReceiverAgent(AgentId id, IAgentRuntime runtime, string description) - : base(id, runtime, description) + public ReceiverAgent(ActorId id, IAgentRuntime runtime, string description) : base(id, runtime, description) { - } - - public ValueTask HandleAsync(BasicMessage item, MessageContext messageContext) - { - this.Messages.Add(item); - return default; + this.RegisterMessageHandler(async (item, messageContext, cancellationToken) => + { + this.Messages.Add(item); + }); } } -public sealed class ProcessorAgent : TestAgent, IHandle +public sealed class ProcessorAgent : TestAgent { - private Func ProcessFunc { get; } - - public ProcessorAgent(AgentId id, IAgentRuntime runtime, Func processFunc, string description) - : base(id, runtime, description) + public ProcessorAgent(ActorId id, IAgentRuntime runtime, Func processFunc, string description) : base(id, runtime, description) { - this.ProcessFunc = processFunc; - } - - public ValueTask HandleAsync(BasicMessage item, MessageContext messageContext) - { - BasicMessage result = new() { Content = this.ProcessFunc.Invoke(((BasicMessage)item).Content) }; - - return new(result); + this.RegisterMessageHandler(async (item, messageContext, cancellationtoken) => + { + return new BasicMessage() { Content = processFunc.Invoke(((BasicMessage)item).Content) }; + }); } } -public sealed class CancelAgent : TestAgent, IHandle +public sealed class CancelAgent : TestAgent { - public CancelAgent(AgentId id, IAgentRuntime runtime, string description) - : base(id, runtime, description) + public CancelAgent(ActorId id, IAgentRuntime runtime, string description) : base(id, runtime, description) { - } - - public ValueTask HandleAsync(BasicMessage item, MessageContext messageContext) - { - CancellationToken cancelledToken = new(canceled: true); - cancelledToken.ThrowIfCancellationRequested(); - - return default; + this.RegisterMessageHandler(async (item, messageContext, cancellationToken) => + { + CancellationToken cancelledToken = new(canceled: true); + cancelledToken.ThrowIfCancellationRequested(); + }); } } -public sealed class ErrorAgent : TestAgent, IHandle +public sealed class ErrorAgent : TestAgent { - public ErrorAgent(AgentId id, IAgentRuntime runtime, string description) - : base(id, runtime, description) + public ErrorAgent(ActorId id, IAgentRuntime runtime, string description) : base(id, runtime, description) { + this.RegisterMessageHandler(async (item, messageContext, cancellationToken) => + { + this.DidThrow = true; + throw new TestException(); + }); } - public bool DidThrow { get; private set; } - - public ValueTask HandleAsync(BasicMessage item, MessageContext messageContext) - { - this.DidThrow = true; - - throw new TestException(); - } } public sealed class MessagingTestFixture @@ -131,23 +104,23 @@ public sealed class MessagingTestFixture private Dictionary AgentsTypeMap { get; } = []; public InProcessRuntime Runtime { get; } = new(); - public ValueTask RegisterFactoryMapInstances(AgentType type, Func> factory) - where TAgent : IHostableAgent + public ValueTask RegisterFactoryMapInstances(ActorType type, Func> factory) + where TAgent : IRuntimeActor { - async ValueTask WrappedFactory(AgentId id, IAgentRuntime runtime) + async ValueTask WrappedFactory(ActorId id, IAgentRuntime runtime) { TAgent agent = await factory(id, runtime); this.GetAgentInstances()[id] = agent; return agent; } - return this.Runtime.RegisterAgentFactoryAsync(type, WrappedFactory); + return this.Runtime.RegisterActorFactoryAsync(type, WrappedFactory); } - public Dictionary GetAgentInstances() where TAgent : IHostableAgent + public Dictionary GetAgentInstances() where TAgent : IRuntimeActor { if (!this.AgentsTypeMap.TryGetValue(typeof(TAgent), out object? maybeAgentMap) || - maybeAgentMap is not Dictionary result) + maybeAgentMap is not Dictionary result) { this.AgentsTypeMap[typeof(TAgent)] = result = []; } @@ -157,24 +130,24 @@ public sealed class MessagingTestFixture public async ValueTask RegisterReceiverAgentAsync(string? agentNameSuffix = null, params string[] topicTypes) { await this.RegisterFactoryMapInstances( - $"{nameof(ReceiverAgent)}{agentNameSuffix ?? string.Empty}", + new($"{nameof(ReceiverAgent)}{agentNameSuffix ?? string.Empty}"), (id, runtime) => new ValueTask(new ReceiverAgent(id, runtime, string.Empty))); foreach (string topicType in topicTypes) { - await this.Runtime.AddSubscriptionAsync(new TestSubscription(topicType, $"{nameof(ReceiverAgent)}{agentNameSuffix ?? string.Empty}")); + await this.Runtime.AddSubscriptionAsync(new TestSubscription(topicType, new($"{nameof(ReceiverAgent)}{agentNameSuffix ?? string.Empty}"))); } } public async ValueTask RegisterErrorAgentAsync(string? agentNameSuffix = null, params string[] topicTypes) { await this.RegisterFactoryMapInstances( - $"{nameof(ErrorAgent)}{agentNameSuffix ?? string.Empty}", + new($"{nameof(ErrorAgent)}{agentNameSuffix ?? string.Empty}"), (id, runtime) => new ValueTask(new ErrorAgent(id, runtime, string.Empty))); foreach (string topicType in topicTypes) { - await this.Runtime.AddSubscriptionAsync(new TestSubscription(topicType, $"{nameof(ErrorAgent)}{agentNameSuffix ?? string.Empty}")); + await this.Runtime.AddSubscriptionAsync(new TestSubscription(topicType, new($"{nameof(ErrorAgent)}{agentNameSuffix ?? string.Empty}"))); } } @@ -187,7 +160,7 @@ public sealed class MessagingTestFixture await this.Runtime.RunUntilIdleAsync(); } - public async ValueTask RunSendTestAsync(AgentId sendTarget, object message, string? messageId = null) + public async ValueTask RunSendTestAsync(ActorId sendTarget, object message, string? messageId = null) { messageId ??= Guid.NewGuid().ToString(); diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/PublishMessageTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/PublishMessageTests.cs index 87e2d25fb7..4f03c1e962 100644 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/PublishMessageTests.cs +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/PublishMessageTests.cs @@ -93,10 +93,10 @@ public class PublishMessageTests MessagingTestFixture fixture = new(); await fixture.RegisterFactoryMapInstances( - nameof(PublisherAgent), + new(nameof(PublisherAgent)), (id, runtime) => new ValueTask(new PublisherAgent(id, runtime, string.Empty, [new TopicId("TestTopic")]))); - await fixture.Runtime.AddSubscriptionAsync(new TestSubscription("RunTest", nameof(PublisherAgent))); + await fixture.Runtime.AddSubscriptionAsync(new TestSubscription("RunTest", new(nameof(PublisherAgent)))); await fixture.RegisterReceiverAgentAsync(topicTypes: "TestTopic"); await fixture.RegisterReceiverAgentAsync("2", topicTypes: "TestTopic"); diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/ResultSinkTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/ResultSinkTests.cs deleted file mode 100644 index 604e359a90..0000000000 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/ResultSinkTests.cs +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Threading.Tasks; -using System.Threading.Tasks.Sources; - -namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess.Tests; - -public class ResultSinkTests -{ - [Fact] - public void GetResultTest() - { - // Arrange - ResultSink sink = new(); - const int ExpectedResult = 42; - - // Act - sink.SetResult(ExpectedResult); - int result = sink.GetResult(0); - - // Assert - Assert.Equal(ExpectedResult, result); - Assert.Equal(ValueTaskSourceStatus.Succeeded, sink.GetStatus(0)); - } - - [Fact] - public async Task FutureResultTestAsync() - { - // Arrange - ResultSink sink = new(); - const string ExpectedResult = "test"; - - // Act - sink.SetResult(ExpectedResult); - string result = await sink.Future; - - // Assert - Assert.Equal(ExpectedResult, result); - Assert.Equal(ValueTaskSourceStatus.Succeeded, sink.GetStatus(0)); - } - - [Fact] - public async Task SetExceptionTestAsync() - { - // Arrange - ResultSink sink = new(); - InvalidOperationException expectedException = new("Test exception"); - - // Act - sink.SetException(expectedException); - - // Assert - Exception exception = await Assert.ThrowsAsync(async () => await sink.Future); - Assert.Equal(expectedException.Message, exception.Message); - exception = Assert.Throws(() => sink.GetResult(0)); - Assert.Equal(expectedException.Message, exception.Message); - Assert.Equal(ValueTaskSourceStatus.Faulted, sink.GetStatus(0)); - } - - [Fact] - public async Task SetCancelledTestAsync() - { - // Arrange - ResultSink sink = new(); - - // Act - sink.SetCancelled(); - - // Assert - Assert.True(sink.IsCancelled); - Assert.Throws(() => sink.GetResult(0)); - await Assert.ThrowsAsync(async () => await sink.Future); - Assert.Equal(ValueTaskSourceStatus.Canceled, sink.GetStatus(0)); - } - - [Fact] - public void OnCompletedTest() - { - // Arrange - ResultSink sink = new(); - bool continuationCalled = false; - const int ExpectedResult = 42; - - // Register the continuation - sink.OnCompleted( - state => continuationCalled = true, - state: null, - token: 0, - ValueTaskSourceOnCompletedFlags.None); - - // Assert - Assert.False(continuationCalled, "Continuation should have been called"); - - // Act - sink.SetResult(ExpectedResult); - - // Assert - Assert.Equal(ExpectedResult, sink.GetResult(0)); - Assert.Equal(ValueTaskSourceStatus.Succeeded, sink.GetStatus(0)); - Assert.True(continuationCalled, "Continuation should have been called"); - } -} diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/SendMessageTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/SendMessageTests.cs index d2e1223e85..a6b93a9532 100644 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/SendMessageTests.cs +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/SendMessageTests.cs @@ -16,10 +16,10 @@ public class SendMessageTests MessagingTestFixture fixture = new(); - await fixture.RegisterFactoryMapInstances(nameof(ProcessorAgent), + await fixture.RegisterFactoryMapInstances(new(nameof(ProcessorAgent)), (id, runtime) => new ValueTask(new ProcessorAgent(id, runtime, ProcessFunc, string.Empty))); - AgentId targetAgent = new(nameof(ProcessorAgent), Guid.NewGuid().ToString()); + ActorId targetAgent = new(nameof(ProcessorAgent), Guid.NewGuid().ToString()); object? maybeResult = await fixture.RunSendTestAsync(targetAgent, new BasicMessage { Content = "1" }); Assert.Equal("Processed(1)", Assert.IsType(maybeResult).Content); @@ -30,12 +30,12 @@ public class SendMessageTests { MessagingTestFixture fixture = new(); - await fixture.RegisterFactoryMapInstances(nameof(CancelAgent), + await fixture.RegisterFactoryMapInstances(new(nameof(CancelAgent)), (id, runtime) => new ValueTask(new CancelAgent(id, runtime, string.Empty))); - AgentId targetAgent = new(nameof(CancelAgent), Guid.NewGuid().ToString()); + ActorId targetAgent = new(nameof(CancelAgent), Guid.NewGuid().ToString()); - await Assert.ThrowsAsync(() => fixture.RunSendTestAsync(targetAgent, new BasicMessage { Content = "1" }).AsTask()); + await Assert.ThrowsAnyAsync(() => fixture.RunSendTestAsync(targetAgent, new BasicMessage { Content = "1" }).AsTask()); } [Fact] @@ -43,10 +43,10 @@ public class SendMessageTests { MessagingTestFixture fixture = new(); - await fixture.RegisterFactoryMapInstances(nameof(ErrorAgent), + await fixture.RegisterFactoryMapInstances(new(nameof(ErrorAgent)), (id, runtime) => new ValueTask(new ErrorAgent(id, runtime, string.Empty))); - AgentId targetAgent = new(nameof(ErrorAgent), Guid.NewGuid().ToString()); + ActorId targetAgent = new(nameof(ErrorAgent), Guid.NewGuid().ToString()); await Assert.ThrowsAsync(() => fixture.RunSendTestAsync(targetAgent, new BasicMessage { Content = "1" }).AsTask()); } @@ -58,16 +58,16 @@ public class SendMessageTests MessagingTestFixture fixture = new(); - Dictionary sendAgents = fixture.GetAgentInstances(); - Dictionary receiverAgents = fixture.GetAgentInstances(); + Dictionary sendAgents = fixture.GetAgentInstances(); + Dictionary receiverAgents = fixture.GetAgentInstances(); - await fixture.RegisterFactoryMapInstances(nameof(SendOnAgent), + await fixture.RegisterFactoryMapInstances(new(nameof(SendOnAgent)), (id, runtime) => new ValueTask(new SendOnAgent(id, runtime, string.Empty, targetGuids))); - await fixture.RegisterFactoryMapInstances(nameof(ReceiverAgent), + await fixture.RegisterFactoryMapInstances(new(nameof(ReceiverAgent)), (id, runtime) => new ValueTask(new ReceiverAgent(id, runtime, string.Empty))); - AgentId targetAgent = new(nameof(SendOnAgent), Guid.NewGuid().ToString()); + ActorId targetAgent = new(nameof(SendOnAgent), Guid.NewGuid().ToString()); BasicMessage input = new() { Content = "Hello" }; Task testTask = fixture.RunSendTestAsync(targetAgent, input).AsTask(); @@ -82,7 +82,7 @@ public class SendMessageTests // Check that each of the target agents received the message foreach (Guid targetKey in targetGuids) { - AgentId targetId = new(nameof(ReceiverAgent), targetKey.ToString()); + ActorId targetId = new(nameof(ReceiverAgent), targetKey.ToString()); Assert.Single(receiverAgents[targetId].Messages); Assert.Contains(receiverAgents[targetId].Messages, m => m.Content == $"@{targetKey}: {input.Content}"); } diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/TestAgents.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/TestAgents.cs index 577c9479d9..b44455d02c 100644 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/TestAgents.cs +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/TestAgents.cs @@ -3,15 +3,16 @@ using System; using System.Collections.Generic; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess.Tests; -public abstract class TestAgent : BaseAgent +public abstract class TestAgent : RuntimeActor { internal List ReceivedMessages = []; - protected TestAgent(AgentId id, IAgentRuntime runtime, string description) + protected TestAgent(ActorId id, IAgentRuntime runtime, string description) : base(id, runtime, description) { } @@ -21,24 +22,26 @@ public abstract class TestAgent : BaseAgent /// A test agent that captures the messages it receives and /// is able to save and load its state. /// -public sealed class MockAgent : TestAgent, IHandle +public sealed class MockAgent : TestAgent { - public MockAgent(AgentId id, IAgentRuntime runtime, string description) - : base(id, runtime, description) { } + public MockAgent(ActorId id, IAgentRuntime runtime, string description) : base(id, runtime, description) + { + this.RegisterMessageHandler(this.HandleAsync); + } - public ValueTask HandleAsync(string item, MessageContext messageContext) + public ValueTask HandleAsync(string item, MessageContext messageContext, CancellationToken cancellationToken) { this.ReceivedMessages.Add(item); return default; } - public override async ValueTask SaveStateAsync() + public override async ValueTask SaveStateAsync(CancellationToken cancellationToken = default) { JsonElement json = JsonSerializer.SerializeToElement(this.ReceivedMessages); return json; } - public override ValueTask LoadStateAsync(JsonElement state) + public override ValueTask LoadStateAsync(JsonElement state, CancellationToken cancellationToken = default) { this.ReceivedMessages = JsonSerializer.Deserialize>(state) ?? throw new InvalidOperationException("Failed to deserialize state"); return default; diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/TestSubscription.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/TestSubscription.cs index 9c926b9db9..a9f70e151e 100644 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/TestSubscription.cs +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/TestSubscription.cs @@ -5,20 +5,20 @@ using System.Diagnostics.CodeAnalysis; namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess.Tests; -public class TestSubscription(string topicType, string agentType, string? id = null) : ISubscriptionDefinition +public class TestSubscription(string topicType, ActorType agentType, string? id = null) : ISubscriptionDefinition { public string Id { get; } = id ?? Guid.NewGuid().ToString(); public string TopicType { get; } = topicType; - public AgentId MapToAgent(TopicId topic) + public ActorId MapToActor(TopicId topic) { if (!this.Matches(topic)) { throw new InvalidOperationException("TopicId does not match the subscription."); } - return new AgentId(agentType, topic.Source); + return new ActorId(agentType, topic.Source); } public bool Equals(ISubscriptionDefinition? other) => this.Id == other?.Id;