diff --git a/.github/workflows/dotnet-cosmosdb-integration-tests.yml b/.github/workflows/dotnet-cosmosdb-integration-tests.yml deleted file mode 100644 index 5c247360c2..0000000000 --- a/.github/workflows/dotnet-cosmosdb-integration-tests.yml +++ /dev/null @@ -1,158 +0,0 @@ -# -# This workflow runs Cosmos DB integration tests using the Cosmos DB emulator. -# - -name: dotnet-cosmosdb-integration-tests - -on: - workflow_dispatch: - pull_request: - branches: ["main", "feature*"] - paths: - - dotnet/tests/CosmosDB.IntegrationTests/** - - dotnet/src/Microsoft.Agents.AI.Runtime.Storage.CosmosDB/** - - '.github/workflows/dotnet-cosmosdb-integration-tests.yml' - merge_group: - branches: ["main"] - push: - branches: ["main", "feature*"] - paths: - - dotnet/tests/CosmosDB.IntegrationTests/** - - dotnet/src/Microsoft.Agents.AI.Runtime.Storage.CosmosDB/** - - '.github/workflows/dotnet-cosmosdb-integration-tests.yml' - schedule: - - cron: "0 2 * * *" # Run at 2 AM UTC daily - -env: - COSMOSDB_TESTS_USE_EMULATOR_CICD: "true" - -jobs: - build-and-test: - runs-on: ${{ matrix.os }} - - strategy: - fail-fast: false - matrix: - include: - - { targetFramework: "net9.0", os: "ubuntu-latest", configuration: Release } - # - { targetFramework: "net9.0", os: "ubuntu-latest", configuration: Debug } - - services: - cosmosdb: - image: mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:latest - ports: - - 8081:8081 - env: - AZURE_COSMOS_EMULATOR_ENABLE_DATA_PERSISTENCE: "false" - AZURE_COSMOS_EMULATOR_PARTITION_COUNT: "20" # the more the better for stable tests - - steps: - - uses: actions/checkout@v5 - with: - persist-credentials: false - sparse-checkout: | - . - .github - dotnet - - name: Setup dotnet - uses: actions/setup-dotnet@v5.0.0 - with: - global-json-file: ${{ github.workspace }}/dotnet/global.json - - name: Build dotnet solutions - shell: bash - run: | - export SOLUTIONS=$(find ./dotnet/ -type f -name "*.slnx" | tr '\n' ' ') - for solution in $SOLUTIONS; do - dotnet build $solution -c ${{ matrix.configuration }} --warnaserror - done - - name: Package install check - shell: bash - # All frameworks are only built for the release configuration, so we only run this step for the release configuration - # and dotnet new doesn't support net472 - if: matrix.configuration == 'Release' && matrix.targetFramework != 'net472' - run: | - TEMP_DIR=$(mktemp -d) - - export SOLUTIONS=$(find ./dotnet/ -type f -name "*.slnx" | tr '\n' ' ') - for solution in $SOLUTIONS; do - dotnet pack $solution /property:TargetFrameworks=${{ matrix.targetFramework }} -c ${{ matrix.configuration }} --no-build --no-restore --output "$TEMP_DIR/artifacts" - done - - pushd "$TEMP_DIR" - - # Create a new console app to test the package installation - dotnet new console -f ${{ matrix.targetFramework }} --name packcheck --output consoleapp - - # Create minimal nuget.config and use only dotnet nuget commands - echo '' > consoleapp/nuget.config - - # Add sources with local first using dotnet nuget commands - dotnet nuget add source ../artifacts --name local --configfile consoleapp/nuget.config - dotnet nuget add source https://api.nuget.org/v3/index.json --name nuget.org --configfile consoleapp/nuget.config - - # Change to project directory to ensure local nuget.config is used - pushd consoleapp - dotnet add packcheck.csproj package Microsoft.Agents.AI --prerelease - dotnet build -f ${{ matrix.targetFramework }} -c ${{ matrix.configuration }} packcheck.csproj - - # Clean up - popd - popd - rm -rf "$TEMP_DIR" - - - name: Wait for Cosmos DB Emulator to be ready - run: | - set -e - for i in $(seq 1 120); do - if curl -sk https://localhost:8081/_explorer/emulator.pem -o /dev/null; then - echo "Emulator is up." - break - fi - echo "Waiting for emulator... ($i/120)" - sleep 2 - done - - - name: Install emulator TLS certificate into system trust store - run: | - set -e - sudo apt-get update - sudo apt-get install -y ca-certificates curl openssl - # Fetch the PEM directly from the emulator's explorer endpoint - curl -sk https://localhost:8081/_explorer/emulator.pem -o cosmos-emulator.crt - # Install with the correct .crt extension so update-ca-certificates picks it up - sudo cp cosmos-emulator.crt /usr/local/share/ca-certificates/cosmos-emulator.crt - sudo update-ca-certificates - - - name: Verify TLS now trusts the emulator - run: | - # Use -servername to avoid SNI warning and check verification - echo | openssl s_client -connect localhost:8081 -servername localhost 2>/dev/null | grep -E "Verify return code|subject=|issuer=" - # Expect: "Verify return code: 0 (ok)" - - - name: Run Cosmos DB Integration Tests - shell: bash - run: | - # Run the specific CosmosDB integration tests - dotnet test ./dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Agents.AI.Runtime.Storage.CosmosDB.Tests/Microsoft.Agents.AI.Runtime.Storage.CosmosDB.Tests.csproj \ - -f ${{ matrix.targetFramework }} \ - -c ${{ matrix.configuration }} \ - --no-build \ - -v Normal \ - --logger trx \ - --collect:"XPlat Code Coverage" \ - --results-directory:"TestResults/Coverage/" \ - -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.ExcludeByAttribute=GeneratedCodeAttribute,CompilerGeneratedAttribute,ExcludeFromCodeCoverageAttribute - - # Generate test reports and check coverage - - name: Generate test reports - uses: danielpalme/ReportGenerator-GitHub-Action@5.4.13 - with: - reports: "./TestResults/Coverage/**/coverage.cobertura.xml" - targetdir: "./TestResults/Reports" - reporttypes: "HtmlInline;JsonSummary" - - - name: Upload coverage report artifact - uses: actions/upload-artifact@v4 - with: - name: CosmosDB-CoverageReport-${{ matrix.os }}-${{ matrix.targetFramework }}-${{ matrix.configuration }} - path: ./TestResults/Reports diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index a0b980cca1..2359f90324 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -14,7 +14,6 @@ - diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 3cf3ca3d2f..eea5eba27c 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -166,7 +166,6 @@ - @@ -274,15 +273,8 @@ - - - - - - - @@ -302,6 +294,5 @@ - diff --git a/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/AgentWebChat.AgentHost.csproj b/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/AgentWebChat.AgentHost.csproj index cc814aed5e..9843a1144d 100644 --- a/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/AgentWebChat.AgentHost.csproj +++ b/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/AgentWebChat.AgentHost.csproj @@ -11,11 +11,8 @@ - - - diff --git a/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/HttpActorApiRouteBuilderExtensions.cs b/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/HttpActorApiRouteBuilderExtensions.cs deleted file mode 100644 index d69f8dc457..0000000000 --- a/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/HttpActorApiRouteBuilderExtensions.cs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Diagnostics.CodeAnalysis; -using Microsoft.Agents.AI.Runtime; -using Microsoft.AspNetCore.Mvc; - -namespace AgentWebChat.AgentHost; - -internal static partial class HttpActorApiRouteBuilderExtensions -{ - private const string BasePath = "/actors/v1"; - - public static void MapActors(this IEndpointRouteBuilder endpoints, IActorClient? actorClient = null, [StringSyntax("Route")] string? path = default) - { - path ??= BasePath; - actorClient ??= endpoints.ServiceProvider.GetRequiredService(); - - var routeGroup = endpoints.MapGroup(path); - - // GET /actors/v1/{actorType}/{actorKey}/{messageId} - routeGroup.MapGet( - "/{actorType}/{actorKey}/{messageId}", async ( - string actorType, - string actorKey, - string messageId, - [FromQuery] bool? blocking, - [FromQuery] bool? streaming, - HttpContext context, - CancellationToken cancellationToken) => - await HttpActorProcessor.GetResponseAsync( - actorType, - actorKey, - messageId, - blocking: blocking, - streaming: streaming, - context, - actorClient, - cancellationToken)) - .WithName("GetActorResponse"); - - // POST /actors/v1/{actorType}/{actorKey}/{messageId} - routeGroup.MapPost( - "/{actorType}/{actorKey}/{messageId}", async ( - string actorType, - string actorKey, - string messageId, - [FromQuery] bool? blocking, - [FromQuery] bool? streaming, - [FromBody] ActorRequest request, - CancellationToken cancellationToken) => - await HttpActorProcessor.SendRequestAsync( - actorType, - actorKey, - messageId, - blocking: blocking, - streaming: streaming, - request, - actorClient, - cancellationToken)) - .WithName("SendActorRequest"); - - // POST /actors/v1/{actorType}/{actorKey}/{messageId}:cancel - routeGroup.MapPost( - "/{actorType}/{actorKey}/{messageId}:cancel", async ( - string actorType, - string actorKey, - string messageId, - CancellationToken cancellationToken) => - await HttpActorProcessor.CancelRequestAsync(actorType, actorKey, messageId, actorClient, cancellationToken)) - .WithName("CancelActorRequest"); - } -} diff --git a/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/HttpActorProcessor.cs b/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/HttpActorProcessor.cs deleted file mode 100644 index cd39514cc8..0000000000 --- a/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/HttpActorProcessor.cs +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json; -using Microsoft.Agents.AI.Hosting; -using Microsoft.Agents.AI.Runtime; -using Microsoft.AspNetCore.Http.Features; - -namespace AgentWebChat.AgentHost; - -internal static class HttpActorProcessor -{ - public static async Task GetResponseAsync( - string actorType, - string actorKey, - string messageId, - bool? blocking, - bool? streaming, - HttpContext context, - IActorClient actorClient, - CancellationToken cancellationToken) - { - var actorId = new ActorId(actorType, actorKey); - - var responseHandle = await actorClient.GetResponseAsync(actorId, messageId, cancellationToken); - - if (responseHandle.TryGetResponse(out var response)) - { - return GetResult(response); - } - - if (streaming is true) - { - return new ActorUpdateStreamingResult(responseHandle); - } - - if (blocking is true) - { - response = await responseHandle.GetResponseAsync(cancellationToken); - return GetResult(response); - } - - return Results.Ok(new ActorResponse - { - ActorId = actorId, - MessageId = messageId, - Status = RequestStatus.Pending, - Data = JsonSerializer.Deserialize("{}"), - }); - } - - public static async Task SendRequestAsync( - string actorType, - string actorKey, - string messageId, - bool? blocking, - bool? streaming, - ActorRequest request, - IActorClient actorClient, - CancellationToken cancellationToken) - { - var responseHandle = await actorClient.SendRequestAsync(request, cancellationToken); - if (responseHandle.TryGetResponse(out var response)) - { - return GetResult(response); - } - - if (streaming is true) - { - return new ActorUpdateStreamingResult(responseHandle); - } - - if (blocking is true) - { - response = await responseHandle.GetResponseAsync(cancellationToken); - return GetResult(response); - } - - return Results.Accepted(); - } - - private static IResult GetResult(ActorResponse response) - { - if (response.Status == RequestStatus.NotFound) - { - return Results.NotFound(); - } - - return Results.Ok(response); - } - - public static async Task CancelRequestAsync( - string actorType, - string actorKey, - string messageId, - IActorClient actorClient, - CancellationToken cancellationToken) - { - var actorId = new ActorId(actorType, actorKey); - var responseHandle = await actorClient.GetResponseAsync(actorId, messageId, cancellationToken); - - if (responseHandle.TryGetResponse(out var response)) - { - if (response.Status is RequestStatus.NotFound) - { - return Results.NotFound(); - } - else if (response.Status is RequestStatus.Completed or RequestStatus.Failed) - { - return Results.Conflict("The request has already completed and cannot be cancelled."); - } - } - - await responseHandle.CancelAsync(cancellationToken); - return Results.NoContent(); - } - - private sealed class ActorUpdateStreamingResult( - ActorResponseHandle responseHandle) : IResult - { - public async Task ExecuteAsync(HttpContext httpContext) - { - var cancellationToken = httpContext.RequestAborted; - var response = httpContext.Response; - response.Headers.ContentType = "text/event-stream"; - response.Headers.CacheControl = "no-cache,no-store"; - response.Headers.Connection = "keep-alive"; - - // Make sure we disable all response buffering for SSE. - response.Headers.ContentEncoding = "identity"; - httpContext.Features.GetRequiredFeature().DisableBuffering(); - await response.Body.FlushAsync(cancellationToken); - - var updateTypeInfo = AgentHostingJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ActorRequestUpdate)); - - await foreach (var update in responseHandle.WatchUpdatesAsync(cancellationToken).ConfigureAwait(false)) - { - var eventData = JsonSerializer.Serialize(update, updateTypeInfo); - var eventText = $"data: {eventData}\n\n"; - - await response.WriteAsync(eventText, cancellationToken); - await response.Body.FlushAsync(cancellationToken); - } - } - } -} diff --git a/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Log.cs b/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Log.cs deleted file mode 100644 index 469fcea62b..0000000000 --- a/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Log.cs +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.Agents.AI.Runtime; - -namespace AgentWebChat.AgentHost; - -/// -/// High-performance logging messages using LoggerMessage source generator. -/// -internal static partial class Log -{ - // API endpoint logging - [LoggerMessage( - Level = LogLevel.Information, - Message = "Actor invocation started: Name={ActorName}, SessionId={SessionId}, RequestId={RequestId}, Stream={StreamRequested}")] - public static partial void ActorInvocationStarted(ILogger logger, string actorName, string sessionId, string requestId, bool streamRequested); - - [LoggerMessage( - Level = LogLevel.Information, - Message = "Actor invocation completed: Name={ActorName}, SessionId={SessionId}, RequestId={RequestId}, Status={Status}, Duration={DurationMs}ms")] - public static partial void ActorInvocationCompleted(ILogger logger, string actorName, string sessionId, string requestId, RequestStatus status, long durationMs); - - [LoggerMessage( - Level = LogLevel.Warning, - Message = "Actor invocation failed: Name={ActorName}, SessionId={SessionId}, RequestId={RequestId}, Duration={DurationMs}ms")] - public static partial void ActorInvocationFailed(ILogger logger, Exception exception, string actorName, string sessionId, string requestId, long durationMs); - - // SSE streaming logging - [LoggerMessage( - Level = LogLevel.Debug, - Message = "SSE streaming started for request: {RequestId}")] - public static partial void SseStreamingStarted(ILogger logger, string requestId); - - [LoggerMessage( - Level = LogLevel.Debug, - Message = "SSE progress update sent: RequestId={RequestId}, UpdateCount={UpdateCount}")] - public static partial void SseProgressUpdateSent(ILogger logger, string requestId, int updateCount); - - [LoggerMessage( - Level = LogLevel.Debug, - Message = "SSE streaming completed: RequestId={RequestId}, TotalUpdates={TotalUpdates}")] - public static partial void SseStreamingCompleted(ILogger logger, string requestId, int totalUpdates); - - [LoggerMessage( - Level = LogLevel.Warning, - Message = "SSE streaming cancelled: RequestId={RequestId}")] - public static partial void SseStreamingCanceled(ILogger logger, string requestId); - - [LoggerMessage( - Level = LogLevel.Error, - Message = "SSE streaming error: RequestId={RequestId}")] - public static partial void SseStreamingError(ILogger logger, Exception exception, string requestId); - - // Response processing logging - [LoggerMessage( - Level = LogLevel.Debug, - Message = "Processing actor response: RequestId={RequestId}, Status={Status}, IsStreaming={IsStreaming}")] - public static partial void ProcessingActorResponse(ILogger logger, string requestId, RequestStatus status, bool isStreaming); - - [LoggerMessage( - Level = LogLevel.Information, - Message = "Actor response processed successfully: RequestId={RequestId}, ResponseType={ResponseType}")] - public static partial void ActorResponseProcessed(ILogger logger, string requestId, string responseType); - - // Request/Response logging - [LoggerMessage( - Level = LogLevel.Debug, - Message = "Actor request received: RequestId={RequestId}, PayloadSize={PayloadSize} bytes, Stream={StreamRequested}")] - public static partial void ActorRequestReceived(ILogger logger, string requestId, int payloadSize, bool streamRequested); - - [LoggerMessage( - Level = LogLevel.Debug, - Message = "Actor request sent to runtime: RequestId={RequestId}, ActorName={ActorName}, SessionId={SessionId}")] - public static partial void ActorRequestSent(ILogger logger, string requestId, string actorName, string sessionId); - - [LoggerMessage( - Level = LogLevel.Debug, - Message = "Actor response handle obtained: RequestId={RequestId}, HasImmediateResponse={HasImmediateResponse}")] - public static partial void ActorResponseHandleObtained(ILogger logger, string requestId, bool hasImmediateResponse); - - [LoggerMessage( - Level = LogLevel.Information, - Message = "Waiting for actor response: RequestId={RequestId}")] - public static partial void WaitingForActorResponse(ILogger logger, string requestId); - - [LoggerMessage( - Level = LogLevel.Debug, - Message = "Actor response received: RequestId={RequestId}, Status={Status}")] - public static partial void ActorResponseReceived(ILogger logger, string requestId, RequestStatus status); - - // ChatClientAgentActor logging - [LoggerMessage( - Level = LogLevel.Information, - Message = "Actor started: ActorId={ActorId}, AgentName={AgentName}")] - public static partial void ActorStarted(ILogger logger, string actorId, string agentName); - - [LoggerMessage( - Level = LogLevel.Debug, - Message = "Thread state restored: ActorId={ActorId}, HasExistingThread={HasExistingThread}")] - public static partial void ThreadStateRestored(ILogger logger, string actorId, bool hasExistingThread); - - [LoggerMessage( - Level = LogLevel.Information, - Message = "Processing agent request: RequestId={RequestId}, ActorId={ActorId}, MessageCount={MessageCount}")] - public static partial void ProcessingAgentRequest(ILogger logger, string requestId, string actorId, int messageCount); - - [LoggerMessage( - Level = LogLevel.Debug, - Message = "Agent streaming update: RequestId={RequestId}, UpdateNumber={UpdateNumber}")] - public static partial void AgentStreamingUpdate(ILogger logger, string requestId, int updateNumber); - - [LoggerMessage( - Level = LogLevel.Information, - Message = "Agent request completed: RequestId={RequestId}, TotalUpdates={TotalUpdates}")] - public static partial void AgentRequestCompleted(ILogger logger, string requestId, int totalUpdates); - - [LoggerMessage( - Level = LogLevel.Error, - Message = "Agent request failed: RequestId={RequestId}, ActorId={ActorId}")] - public static partial void AgentRequestFailed(ILogger logger, Exception exception, string requestId, string actorId); - - [LoggerMessage( - Level = LogLevel.Warning, - Message = "Unknown message type received: MessageType={MessageType}, ActorId={ActorId}")] - public static partial void UnknownMessageType(ILogger logger, string messageType, string actorId); - - [LoggerMessage( - Level = LogLevel.Warning, - Message = "Error processing messages: ActorId={ActorId}")] - public static partial void ErrorProcessingMessages(ILogger logger, Exception exception, string actorId); - - [LoggerMessage( - Level = LogLevel.Error, - Message = "Write operation failed: ActorId={ActorId}, RequestId={RequestId}")] - public static partial void WriteOperationFailed(ILogger logger, string actorId, string requestId); -} diff --git a/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs b/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs index 030ceafe59..2224bc1b40 100644 --- a/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs +++ b/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs @@ -1,14 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Text.Json; using AgentWebChat.AgentHost; using AgentWebChat.AgentHost.Utilities; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Hosting; using Microsoft.Agents.AI.Hosting.A2A.AspNetCore; -using Microsoft.Agents.AI.Runtime.Storage.CosmosDB; using Microsoft.Agents.AI.Workflows; -using Microsoft.Azure.Cosmos; using Microsoft.Extensions.AI; var builder = WebApplication.CreateBuilder(args); @@ -20,19 +17,6 @@ builder.Services.AddOpenApi(); // Add services to the container. builder.Services.AddProblemDetails(); -// Add CosmosDB client integration -builder.AddAzureCosmosClient("agent-web-chat-cosmosdb", null, CosmosClientOptions => -{ - CosmosClientOptions.ApplicationName = "AgentWebChat"; - CosmosClientOptions.ConnectionMode = ConnectionMode.Direct; - CosmosClientOptions.ConsistencyLevel = ConsistencyLevel.Session; - CosmosClientOptions.UseSystemTextJsonSerializerWithOptions = new JsonSerializerOptions() - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - TypeInfoResolver = CosmosActorStateJsonContext.Default - }; -}); - // Configure the chat model and our agent. builder.AddKeyedChatClient("chat-model"); @@ -80,9 +64,6 @@ builder.AddAIAgent("knights-and-knaves", (sp, key) => #pragma warning restore VSTHRD002 }); -// Add CosmosDB state storage to override default storage -builder.Services.AddCosmosActorStateStorage("actor-state-db", "ActorState"); - var app = builder.Build(); app.MapOpenApi(); @@ -91,8 +72,6 @@ app.UseSwaggerUI(options => options.SwaggerEndpoint("/openapi/v1.json", "Agents // Configure the HTTP request pipeline. app.UseExceptionHandler(); -app.MapActors(); - // attach a2a with simple message communication app.MapA2A(agentName: "pirate", path: "/a2a/pirate"); app.MapA2A(agentName: "knights-and-knaves", path: "/a2a/knights-and-knaves", agentCard: new() diff --git a/dotnet/samples/AgentWebChat/AgentWebChat.AppHost/AgentWebChat.AppHost.csproj b/dotnet/samples/AgentWebChat/AgentWebChat.AppHost/AgentWebChat.AppHost.csproj index 0fbad8daee..464ba54db8 100644 --- a/dotnet/samples/AgentWebChat/AgentWebChat.AppHost/AgentWebChat.AppHost.csproj +++ b/dotnet/samples/AgentWebChat/AgentWebChat.AppHost/AgentWebChat.AppHost.csproj @@ -14,7 +14,6 @@ - diff --git a/dotnet/samples/AgentWebChat/AgentWebChat.AppHost/Program.cs b/dotnet/samples/AgentWebChat/AgentWebChat.AppHost/Program.cs index 9b25fe3488..a28b3e1902 100644 --- a/dotnet/samples/AgentWebChat/AgentWebChat.AppHost/Program.cs +++ b/dotnet/samples/AgentWebChat/AgentWebChat.AppHost/Program.cs @@ -8,15 +8,8 @@ var azOpenAiResource = builder.AddParameterFromConfiguration("AzureOpenAIName", var azOpenAiResourceGroup = builder.AddParameterFromConfiguration("AzureOpenAIResourceGroup", "AzureOpenAI:ResourceGroup"); var chatModel = builder.AddAIModel("chat-model").AsAzureOpenAI("gpt-4o", o => o.AsExisting(azOpenAiResource, azOpenAiResourceGroup)); -var cosmosDbResource = builder.AddParameterFromConfiguration("CosmosDbName", "CosmosDb:Name"); -var cosmosDbResourceGroup = builder.AddParameterFromConfiguration("CosmosDbResourceGroup", "CosmosDb:ResourceGroup"); -var cosmos = builder.AddAzureCosmosDB("agent-web-chat-cosmosdb").RunAsExisting(cosmosDbResource, cosmosDbResourceGroup); - -var stateDb = cosmos.AddCosmosDatabase("actor-state-db"); - var agentHost = builder.AddProject("agenthost") - .WithReference(chatModel) - .WithReference(cosmos).WaitFor(cosmos); + .WithReference(chatModel); builder.AddProject("webfrontend") .WithExternalHttpEndpoints() diff --git a/dotnet/samples/AgentWebChat/AgentWebChat.Web/A2AActorClient.cs b/dotnet/samples/AgentWebChat/AgentWebChat.Web/A2AActorClient.cs deleted file mode 100644 index 8ccf08c54c..0000000000 --- a/dotnet/samples/AgentWebChat/AgentWebChat.Web/A2AActorClient.cs +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using System.Text.Json; -using A2A; -using Microsoft.Agents.AI.Hosting; -using Microsoft.Agents.AI.Hosting.A2A.Converters; -using Microsoft.Agents.AI.Runtime; -using Microsoft.Extensions.AI; - -namespace AgentWebChat.Web; - -internal sealed class A2AActorClient : IActorClient -{ - private readonly ILogger _logger; - private readonly Uri _uri; - - // because A2A sdk does not provide a client which can handle multiple agents, we need a client per agent - // for this app the convention is "baseUri/" - private readonly ConcurrentDictionary _clients = []; - - public A2AActorClient(ILogger logger, Uri baseUri) - { - this._logger = logger; - this._uri = baseUri; - } - - public Task GetAgentCardAsync(string agent, CancellationToken cancellationToken = default) - { - this._logger.LogInformation("Retrieving agent card for {Agent}", agent); - - var (_, a2aCardResolver) = this.ResolveClient(agent); - return a2aCardResolver.GetAgentCardAsync(cancellationToken); - } - - public ValueTask GetResponseAsync(ActorId actorId, string messageId, CancellationToken cancellationToken) => throw new NotImplementedException(); - - public ValueTask SendRequestAsync(ActorRequest request, CancellationToken cancellationToken) - { - var agentName = request.ActorId.Type; - var (a2aClient, _) = this.ResolveClient(agentName); - - return new ValueTask(new A2AActorResponseHandle(a2aClient, request)); - } - - private (A2AClient, A2ACardResolver) ResolveClient(ActorType agentName) - => this.ResolveClient(agentName.Name); - - private (A2AClient, A2ACardResolver) ResolveClient(string agentName) => - this._clients.GetOrAdd(agentName, name => - { - var uri = new Uri($"{this._uri}/{name}/"); - var a2aClient = new A2AClient(uri); - - // /v1/card is a default path for A2A agent card discovery - var a2aCardResolver = new A2ACardResolver(uri, agentCardPath: "/v1/card/"); - - this._logger.LogInformation("Built clients for agent {Agent} with baseUri {Uri}", name, uri); - return (a2aClient, a2aCardResolver); - }); - - private sealed class A2AActorResponseHandle : ActorResponseHandle - { - private readonly A2AClient _a2aClient; - private readonly ActorRequest _request; - - public A2AActorResponseHandle(A2AClient a2aClient, ActorRequest request) - { - this._a2aClient = a2aClient; - this._request = request; - } - - public override ValueTask CancelAsync(CancellationToken cancellationToken) => throw new NotImplementedException(); - - public override ValueTask GetResponseAsync(CancellationToken cancellationToken) => throw new NotImplementedException(); - - public override bool TryGetResponse([NotNullWhen(true)] out ActorResponse? response) => throw new NotImplementedException(); - - public override async IAsyncEnumerable WatchUpdatesAsync([EnumeratorCancellation] CancellationToken cancellationToken) - { - var agentRunRequestData = this._request.Params.Deserialize(AgentHostingJsonUtilities.DefaultOptions.GetTypeInfo(typeof(AgentRunRequest))) as AgentRunRequest; - var messageTexts = agentRunRequestData!.Messages!.SelectMany(x => x.Contents.OfType()).Select(x => x.Text); - var parts = messageTexts.Select(text => new TextPart { Text = text }); - var messageSendParams = new MessageSendParams - { - Message = new() - { - Role = MessageRole.User, - MessageId = this._request.MessageId, - ContextId = this._request.ActorId.Key, - Parts = [.. parts] - } - }; - - await foreach (var upd in this._a2aClient.SendMessageStreamAsync(messageSendParams, cancellationToken)) - { - var @event = upd.Data; - if (@event is not Message message) - { - throw new NotSupportedException("Only message is supported in A2A processing, but got: " + @event.GetType()); - } - - // handling of message on agentProxy side expects the - yield return message.ToActorRequestUpdate(status: RequestStatus.Pending); - } - - // complete request after all updates are sent - yield return new ActorRequestUpdate(status: RequestStatus.Completed, data: default); - } - } -} diff --git a/dotnet/samples/AgentWebChat/AgentWebChat.Web/A2AAgentClient.cs b/dotnet/samples/AgentWebChat/AgentWebChat.Web/A2AAgentClient.cs new file mode 100644 index 0000000000..eec82ee37c --- /dev/null +++ b/dotnet/samples/AgentWebChat/AgentWebChat.Web/A2AAgentClient.cs @@ -0,0 +1,205 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Concurrent; +using System.Runtime.CompilerServices; +using System.Text.Json; +using A2A; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Hosting.A2A.Converters; +using Microsoft.Extensions.AI; + +namespace AgentWebChat.Web; + +internal sealed class A2AAgentClient : IAgentClient +{ + private readonly ILogger _logger; + private readonly Uri _uri; + + // because A2A sdk does not provide a client which can handle multiple agents, we need a client per agent + // for this app the convention is "baseUri/" + private readonly ConcurrentDictionary _clients = []; + + public A2AAgentClient(ILogger logger, Uri baseUri) + { + this._logger = logger; + this._uri = baseUri; + } + + public async IAsyncEnumerable RunStreamingAsync( + string agentName, + IList messages, + string? threadId = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + this._logger.LogInformation("Running agent {AgentName} with {MessageCount} messages via A2A", agentName, messages.Count); + + var (a2aClient, _) = this.ResolveClient(agentName); + var contextId = threadId ?? Guid.NewGuid().ToString("N"); + + // Convert and send messages via A2A without try-catch in yield method + var results = new List(); + + try + { + // Convert all messages to A2A parts and create a single message + var parts = messages.ToParts(); + var a2aMessage = new Message + { + MessageId = Guid.NewGuid().ToString("N"), + ContextId = contextId, + Role = MessageRole.User, + Parts = parts + }; + + var messageSendParams = new MessageSendParams { Message = a2aMessage }; + var a2aResponse = await a2aClient.SendMessageAsync(messageSendParams, cancellationToken); + + // Handle different response types + if (a2aResponse is Message message) + { + var responseMessage = MessageConverter.ToChatMessage(message); + if (responseMessage is not null) + { + results.Add(new AgentRunResponseUpdate(responseMessage.Role, responseMessage.Contents) + { + MessageId = message.MessageId, + CreatedAt = DateTimeOffset.UtcNow + }); + } + } + else if (a2aResponse is AgentTask agentTask) + { + // Manually convert AgentTask artifacts to ChatMessages since the extension method is internal + if (agentTask.Artifacts is not null) + { + foreach (var artifact in agentTask.Artifacts) + { + List? aiContents = null; + + foreach (var part in artifact.Parts) + { + var aiContent = ConvertPartToAIContent(part); + if (aiContent != null) + { + (aiContents ??= []).Add(aiContent); + } + } + + if (aiContents is not null) + { + var additionalProperties = ConvertMetadataToAdditionalProperties(artifact.Metadata); + var chatMessage = new ChatMessage(ChatRole.Assistant, aiContents) + { + AdditionalProperties = additionalProperties, + RawRepresentation = artifact, + }; + + results.Add(new AgentRunResponseUpdate(chatMessage.Role, chatMessage.Contents) + { + MessageId = agentTask.Id, + CreatedAt = DateTimeOffset.UtcNow + }); + } + } + } + } + else + { + this._logger.LogWarning("Unsupported A2A response type: {ResponseType}", a2aResponse?.GetType().FullName ?? "null"); + } + } + catch (Exception ex) + { + this._logger.LogError(ex, "Error running agent {AgentName} via A2A", agentName); + + results.Add(new AgentRunResponseUpdate(ChatRole.Assistant, $"Error: {ex.Message}") + { + MessageId = Guid.NewGuid().ToString("N"), + CreatedAt = DateTimeOffset.UtcNow + }); + } + + // Yield the results + foreach (var result in results) + { + yield return result; + } + } + + public async Task GetAgentCardAsync(string agentName, CancellationToken cancellationToken = default) + { + this._logger.LogInformation("Retrieving agent card for {Agent}", agentName); + + var (_, a2aCardResolver) = this.ResolveClient(agentName); + try + { + return await a2aCardResolver.GetAgentCardAsync(cancellationToken); + } + catch (Exception ex) + { + this._logger.LogError(ex, "Failed to get agent card for {AgentName}", agentName); + return null; + } + } + + private (A2AClient, A2ACardResolver) ResolveClient(string agentName) => + this._clients.GetOrAdd(agentName, name => + { + var uri = new Uri($"{this._uri}/{name}/"); + var a2aClient = new A2AClient(uri); + + // /v1/card is a default path for A2A agent card discovery + var a2aCardResolver = new A2ACardResolver(uri, agentCardPath: "/v1/card/"); + + this._logger.LogInformation("Built clients for agent {Agent} with baseUri {Uri}", name, uri); + return (a2aClient, a2aCardResolver); + }); + + private static AIContent? ConvertPartToAIContent(Part part) => + part switch + { + TextPart textPart => new TextContent(textPart.Text) + { + RawRepresentation = textPart + }, + FilePart filePart when filePart.File is FileWithUri fileWithUrl => new HostedFileContent(fileWithUrl.Uri) + { + RawRepresentation = filePart + }, + _ => null + }; + + private static AdditionalPropertiesDictionary? ConvertMetadataToAdditionalProperties(Dictionary? metadata) + { + if (metadata is not { Count: > 0 }) + { + return null; + } + + var additionalProperties = new AdditionalPropertiesDictionary(); + foreach (var kvp in metadata) + { + additionalProperties[kvp.Key] = kvp.Value; + } + return additionalProperties; + } +} + +// Extension method to convert multiple chat messages to A2A messages +internal static class ChatMessageExtensions +{ + public static List ToA2AMessages(this IList chatMessages) + { + if (chatMessages is null || chatMessages.Count == 0) + { + return []; + } + + var result = new List(); + foreach (var chatMessage in chatMessages) + { + result.Add(MessageConverter.ToA2AMessage(chatMessage)); + } + return result; + } +} diff --git a/dotnet/samples/AgentWebChat/AgentWebChat.Web/AgentDiscoveryClient.cs b/dotnet/samples/AgentWebChat/AgentWebChat.Web/AgentDiscoveryClient.cs index 08f4d07213..09e3c9d630 100644 --- a/dotnet/samples/AgentWebChat/AgentWebChat.Web/AgentDiscoveryClient.cs +++ b/dotnet/samples/AgentWebChat/AgentWebChat.Web/AgentDiscoveryClient.cs @@ -16,7 +16,6 @@ public class AgentDiscoveryClient(HttpClient httpClient, ILogger>(json, AgentHostingJsonUtilities.DefaultOptions) ?? []; logger.LogInformation("Retrieved {AgentCount} agents from the API", agents.Count); - _ = new HttpActorClient(null!); return agents; } diff --git a/dotnet/samples/AgentWebChat/AgentWebChat.Web/Components/Pages/Home.razor b/dotnet/samples/AgentWebChat/AgentWebChat.Web/Components/Pages/Home.razor index bb27fed21d..13c3f2711f 100644 --- a/dotnet/samples/AgentWebChat/AgentWebChat.Web/Components/Pages/Home.razor +++ b/dotnet/samples/AgentWebChat/AgentWebChat.Web/Components/Pages/Home.razor @@ -3,14 +3,12 @@ @inject AgentDiscoveryClient AgentClient @inject IJSRuntime JSRuntime @inject ILogger Logger -@inject IActorClient ActorClient -@inject A2AActorClient A2AActorClient +@inject A2AAgentClient A2AActorClient @rendermode InteractiveServer @using System.Text @using System.Text.Json @using Microsoft.Extensions.AI @using Microsoft.Agents.AI.Hosting -@using Microsoft.Agents.AI.Runtime @using A2A Agent Web Chat @@ -882,204 +880,215 @@ @code { - private string currentMessage = ""; - private bool isStreaming = false; - private bool isLoadingAgents = true; - private string currentStreamedMessage = ""; - private string selectedAgentName = ""; - private List availableAgents = new(); - private List conversations = new(); - private Conversation? currentConversation; - // protocol - private Protocol selectedProtocol; + private string currentMessage = ""; + private bool isStreaming = false; + private bool isLoadingAgents = true; + private string currentStreamedMessage = ""; + private string selectedAgentName = ""; + private List availableAgents = new(); + private List conversations = new(); + private Conversation? currentConversation; - // a2a agent card - private bool isA2AExpanded = false; - private bool isDiscoveringCard = false; - private string? discoveredAgentCardJson = null; - private string? discoveryError = null; + // protocol + private Protocol selectedProtocol; - private enum Protocol - { - AgenticFramework, - A2A // Agent-to-Agent protocol - } + // a2a agent card + private bool isA2AExpanded = false; + private bool isDiscoveringCard = false; + private string? discoveredAgentCardJson = null; + private string? discoveryError = null; - private sealed class Conversation - { - public string SessionId { get; set; } = Guid.NewGuid().ToString("N"); - public string AgentName { get; set; } = ""; - public List Messages { get; set; } = new(); - } + private enum Protocol + { + AgenticFramework, + A2A // Agent-to-Agent protocol + } - protected override async Task OnInitializedAsync() - { - Logger.LogDebug("Initializing Agent Chat component"); + private sealed class Conversation + { + public string SessionId { get; set; } = Guid.NewGuid().ToString("N"); + public string AgentName { get; set; } = ""; + public List Messages { get; set; } = new(); + } - // Load agents - try - { - availableAgents = await AgentClient.GetAgentsAsync(); - Logger.LogInformation("Loaded {AgentCount} agents", availableAgents.Count); - Logger.LogInformation("Loaded Agents info: {AgentData}", JsonSerializer.Serialize(availableAgents, new JsonSerializerOptions() { WriteIndented = true })); + protected override async Task OnInitializedAsync() + { + Logger.LogDebug("Initializing Agent Chat component"); - // Default to first agent and start a conversation - if (availableAgents.Any()) - { - selectedAgentName = availableAgents.First().Name!; - StartNewConversation(); - } - } - catch (Exception ex) - { - Logger.LogError(ex, "Failed to load agents"); - } - finally - { - isLoadingAgents = false; - } - - // Conversations start fresh on page load - } - - private string GetAgentIcon(string agentName) => agentName?.ToLower() switch + // Load agents + try { - "pirate" => "🏴‍☠️", - "knights-and-knaves" => "⚔️", - _ => "🤖" - }; + availableAgents = await AgentClient.GetAgentsAsync(); + Logger.LogInformation("Loaded {AgentCount} agents", availableAgents.Count); + Logger.LogInformation("Loaded Agents info: {AgentData}", JsonSerializer.Serialize(availableAgents, new JsonSerializerOptions() { WriteIndented = true })); - private string GetAgentDisplayName(string agentName) => agentName?.ToLower() switch + // Default to first agent and start a conversation + if (availableAgents.Any()) + { + selectedAgentName = availableAgents.First().Name!; + StartNewConversation(); + } + } + catch (Exception ex) { - "pirate" => "Pirate", - "knights-and-knaves" => "Knights & Knaves", - _ => agentName ?? "Agent" - }; + Logger.LogError(ex, "Failed to load agents"); + } + finally + { + isLoadingAgents = false; + } - private void ToggleA2AExpanded() => isA2AExpanded = !isA2AExpanded; + // Conversations start fresh on page load + } - private async Task DiscoverAgentCard() - { - if (string.IsNullOrEmpty(selectedAgentName) || isDiscoveringCard) - return; + private string GetAgentIcon(string agentName) => agentName?.ToLower() switch + { + "pirate" => "🏴‍☠️", + "knights-and-knaves" => "⚔️", + _ => "🤖" + }; - isDiscoveringCard = true; - discoveryError = null; - discoveredAgentCardJson = null; - StateHasChanged(); + private string GetAgentDisplayName(string agentName) => agentName?.ToLower() switch + { + "pirate" => "Pirate", + "knights-and-knaves" => "Knights & Knaves", + _ => agentName ?? "Agent" + }; - try - { - Logger.LogInformation("Discovering agent card for agent: {AgentName}", selectedAgentName); - var agentCard = await A2AActorClient.GetAgentCardAsync(selectedAgentName); - discoveredAgentCardJson = JsonSerializer.Serialize(agentCard, new JsonSerializerOptions() { WriteIndented = true }); - Logger.LogInformation("Successfully discovered agent card for {AgentName}: {CardData}", selectedAgentName, discoveredAgentCardJson); - } - catch (Exception ex) - { - Logger.LogError(ex, "Failed to discover agent card for {AgentName}", selectedAgentName); - discoveryError = $"Failed to discover agent card: {ex.Message}"; - } - finally - { - isDiscoveringCard = false; - StateHasChanged(); - } - } + private void ToggleA2AExpanded() => isA2AExpanded = !isA2AExpanded; - private void StartNewConversation() - { - if (string.IsNullOrEmpty(selectedAgentName)) - return; + private async Task DiscoverAgentCard() + { + if (string.IsNullOrEmpty(selectedAgentName) || isDiscoveringCard) + return; - var newConversation = new Conversation + isDiscoveringCard = true; + discoveryError = null; + discoveredAgentCardJson = null; + StateHasChanged(); + + try + { + Logger.LogInformation("Discovering agent card for agent: {AgentName}", selectedAgentName); + var agentCard = await A2AActorClient.GetAgentCardAsync(selectedAgentName); + if (agentCard is not null) + { + discoveredAgentCardJson = JsonSerializer.Serialize(agentCard, new JsonSerializerOptions() { WriteIndented = true }); + Logger.LogInformation("Successfully discovered agent card for {AgentName}: {CardData}", selectedAgentName, discoveredAgentCardJson); + } + else + { + discoveryError = "No agent card found for this agent."; + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to discover agent card for {AgentName}", selectedAgentName); + discoveryError = $"Failed to discover agent card: {ex.Message}"; + } + finally + { + isDiscoveringCard = false; + StateHasChanged(); + } + } + + private void StartNewConversation() + { + if (string.IsNullOrEmpty(selectedAgentName)) + return; + + var newConversation = new Conversation { AgentName = selectedAgentName }; - conversations.Add(newConversation); - currentConversation = newConversation; + conversations.Add(newConversation); + currentConversation = newConversation; - Logger.LogInformation("Started new conversation with agent: {AgentName}, session: {SessionId}", - newConversation.AgentName, newConversation.SessionId); + Logger.LogInformation("Started new conversation with agent: {AgentName}, session: {SessionId}", + newConversation.AgentName, newConversation.SessionId); - StateHasChanged(); - } + StateHasChanged(); + } - private void SelectConversation(string sessionId) - { - currentConversation = conversations.FirstOrDefault(c => c.SessionId == sessionId); - if (currentConversation is not null) - { - selectedAgentName = currentConversation.AgentName; - Logger.LogDebug("Selected conversation with session: {SessionId}", sessionId); - } - StateHasChanged(); - } + private void SelectConversation(string sessionId) + { + currentConversation = conversations.FirstOrDefault(c => c.SessionId == sessionId); + if (currentConversation is not null) + { + selectedAgentName = currentConversation.AgentName; + Logger.LogDebug("Selected conversation with session: {SessionId}", sessionId); + } + StateHasChanged(); + } - private void CloseConversation(string sessionId) - { - var conversationToRemove = conversations.FirstOrDefault(c => c.SessionId == sessionId); - if (conversationToRemove is not null) - { - conversations.Remove(conversationToRemove); + private void CloseConversation(string sessionId) + { + var conversationToRemove = conversations.FirstOrDefault(c => c.SessionId == sessionId); + if (conversationToRemove is not null) + { + conversations.Remove(conversationToRemove); - if (currentConversation?.SessionId == sessionId) - { - currentConversation = conversations.FirstOrDefault(); - if (currentConversation is not null) - { - selectedAgentName = currentConversation.AgentName; - } - } + if (currentConversation?.SessionId == sessionId) + { + currentConversation = conversations.FirstOrDefault(); + if (currentConversation is not null) + { + selectedAgentName = currentConversation.AgentName; + } + } - Logger.LogInformation("Closed conversation with session: {SessionId}", sessionId); - } - StateHasChanged(); - } + Logger.LogInformation("Closed conversation with session: {SessionId}", sessionId); + } + StateHasChanged(); + } - private async Task SendMessage() - { - if (string.IsNullOrWhiteSpace(currentMessage) || isStreaming || currentConversation is null) - return; + private async Task SendMessage() + { + if (string.IsNullOrWhiteSpace(currentMessage) || isStreaming || currentConversation is null) + return; - var userMessage = currentMessage.Trim(); - currentMessage = ""; + var userMessage = currentMessage.Trim(); + currentMessage = ""; - Logger.LogInformation("User sending message: '{UserMessage}' to agent {AgentName} in session {SessionId}", - userMessage, currentConversation.AgentName, currentConversation.SessionId); + Logger.LogInformation("User sending message: '{UserMessage}' to agent {AgentName} in session {SessionId}", + userMessage, currentConversation.AgentName, currentConversation.SessionId); - // Add user message to chat - currentConversation.Messages.Add(new ChatMessage(ChatRole.User, userMessage)); - StateHasChanged(); - await ScrollToBottom(); + // Add user message to chat + currentConversation.Messages.Add(new ChatMessage(ChatRole.User, userMessage)); + StateHasChanged(); + await ScrollToBottom(); - // Start streaming response - isStreaming = true; - currentStreamedMessage = ""; - StateHasChanged(); + // Start streaming response + isStreaming = true; + currentStreamedMessage = ""; + StateHasChanged(); - StringBuilder responseContent = new(); - var hasReceivedContent = false; + StringBuilder responseContent = new(); + var hasReceivedContent = false; - using var timeoutCts = new CancellationTokenSource( + using var timeoutCts = new CancellationTokenSource( #if DEBUG TimeSpan.FromSeconds(120) #else - TimeSpan.FromSeconds(20) + TimeSpan.FromSeconds(20) #endif - ); + ); - try - { - var actorClient = (selectedProtocol is Protocol.A2A) ? A2AActorClient : ActorClient; - var agent = new AgentProxy(currentConversation.AgentName, actorClient); - var thread = agent.GetNewThread(currentConversation.SessionId); + try + { - await foreach (var update in agent.RunStreamingAsync( - [new ChatMessage(ChatRole.User, userMessage)], - thread, + // Select the appropriate client based on protocol + + var agentClient = A2AActorClient; + var messages = new List { new(ChatRole.User, userMessage) }; + + await foreach (var update in agentClient.RunStreamingAsync( + currentConversation.AgentName, + messages, + currentConversation.SessionId, cancellationToken: timeoutCts.Token)) { var content = update.Text ?? ""; diff --git a/dotnet/samples/AgentWebChat/AgentWebChat.Web/HttpActorClient.cs b/dotnet/samples/AgentWebChat/AgentWebChat.Web/HttpActorClient.cs deleted file mode 100644 index cf7cba5843..0000000000 --- a/dotnet/samples/AgentWebChat/AgentWebChat.Web/HttpActorClient.cs +++ /dev/null @@ -1,177 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Diagnostics.CodeAnalysis; -using System.Net.ServerSentEvents; -using System.Runtime.CompilerServices; -using System.Text.Json; -using Microsoft.Agents.AI; -using Microsoft.Agents.AI.Hosting; -using Microsoft.Agents.AI.Runtime; - -namespace AgentWebChat.Web; - -internal sealed class HttpActorClient(HttpClient httpClient) : IActorClient -{ - private const string BaseUri = "/actors/v1"; - - public async ValueTask GetResponseAsync(ActorId actorId, string messageId, CancellationToken cancellationToken) - { - var uri = new Uri($"{BaseUri}/{actorId.Type}/{actorId.Key}/{messageId}", UriKind.Relative); - var response = await httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); - return new HttpActorResponseHandle(httpClient, actorId, messageId, initialResponseMessage: response); - } - - public async ValueTask SendRequestAsync(ActorRequest request, CancellationToken cancellationToken) - { - var actorId = request.ActorId; - var messageId = request.MessageId; - var uri = new Uri($"{BaseUri}/{actorId.Type}/{actorId.Key}/{messageId}?streaming=true", UriKind.Relative); - var jsonContent = JsonContent.Create(request, AgentHostingJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ActorRequest))); - var message = new HttpRequestMessage(HttpMethod.Post, uri) { Content = jsonContent }; - var response = await httpClient.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); - return new HttpActorResponseHandle(httpClient, actorId, messageId, response); - } - - private sealed class HttpActorResponseHandle( - HttpClient httpClient, - ActorId actorId, - string messageId, - HttpResponseMessage? initialResponseMessage) : ActorResponseHandle - { - private HttpResponseMessage? _responseMessage = initialResponseMessage; - private ActorResponse? _lastResponse; - - public override async ValueTask CancelAsync(CancellationToken cancellationToken) - { - this._responseMessage?.Dispose(); - this._responseMessage = null; - - var uri = new Uri($"{BaseUri}/{actorId.Type}/{actorId.Key}/{messageId}:cancel", UriKind.Relative); - await httpClient.PostAsync(uri, null, cancellationToken).ConfigureAwait(false); - } - - public override async ValueTask GetResponseAsync(CancellationToken cancellationToken) - { - try - { - // If the response is already completed, don't bother requesting the response again; - if (this._lastResponse is { } response && response.Status.IsTerminated()) - { - return response; - } - - if (IsStreamingResponse(this._responseMessage)) - { - try - { - var updates = new List(); - await foreach (var update in EnumerateAsync(this._responseMessage, cancellationToken).ConfigureAwait(false)) - { - if (!update.Status.IsTerminated()) - { - continue; - } - - response = new ActorResponse { ActorId = actorId, MessageId = messageId, Status = update.Status, Data = update.Data }; - this._lastResponse = response; - return response; - } - } - finally - { - this._responseMessage?.Dispose(); - this._responseMessage = null; - } - } - - var uri = new Uri($"{BaseUri}/{actorId.Type}/{actorId.Key}/{messageId}?blocking=true", UriKind.Relative); - using var responseMessage = this._responseMessage ?? await httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); - response = await this.ReadResponseAsync(responseMessage, cancellationToken).ConfigureAwait(false); - this._lastResponse = response; - return response; - } - finally - { - this._responseMessage = null; - } - } - - public override bool TryGetResponse([NotNullWhen(true)] out ActorResponse? response) - { - response = this._lastResponse; - return response is not null; - } - - public override async IAsyncEnumerable WatchUpdatesAsync([EnumeratorCancellation] CancellationToken cancellationToken) - { - // If the response is already completed, don't bother streaming anything. - if (this._lastResponse is { } response && response.Status.IsTerminated()) - { - yield return new ActorRequestUpdate(response.Status, response.Data); - yield break; - } - - try - { - var uri = new Uri($"{BaseUri}/{actorId.Type}/{actorId.Key}/{messageId}?streaming=true", UriKind.Relative); - using var responseMessage = this._responseMessage ?? await httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); - if (IsJsonResponse(responseMessage)) - { - // If the response is JSON, read it as a single response and yield it. - response = await this.ReadResponseAsync(responseMessage, cancellationToken).ConfigureAwait(false); - - yield return new ActorRequestUpdate(response.Status, response.Data); - yield break; - } - - await foreach (var update in EnumerateAsync(responseMessage, cancellationToken).ConfigureAwait(false)) - { - yield return update; - } - } - finally - { - this._responseMessage = null; - } - } - - private static async IAsyncEnumerable EnumerateAsync(HttpResponseMessage responseMessage, [EnumeratorCancellation] CancellationToken cancellationToken) - { - var responseStream = await responseMessage.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - var sseParser = SseParser.Create(responseStream, (eventType, data) => - { - if (eventType != "message") - { - // Only process default message events - return null; - } - - var reader = new Utf8JsonReader(data); - return JsonSerializer.Deserialize(ref reader, AgentHostingJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ActorRequestUpdate))) as ActorRequestUpdate; - }); - - await foreach (var item in sseParser.EnumerateAsync(cancellationToken).ConfigureAwait(false)) - { - if (item.Data is not null) - { - yield return item.Data; - } - } - } - - private async Task ReadResponseAsync(HttpResponseMessage responseMessage, CancellationToken cancellationToken) => - await responseMessage.Content.ReadFromJsonAsync(AgentRuntimeJsonUtilities.DefaultOptions, cancellationToken).ConfigureAwait(false) ?? - throw new InvalidOperationException($"No response found for actor '{actorId}' with message ID '{messageId}'."); - - private static bool IsJsonResponse([NotNullWhen(true)] HttpResponseMessage? response) => response?.Content.Headers.ContentType?.MediaType == "application/json"; - - private static bool IsStreamingResponse([NotNullWhen(true)] HttpResponseMessage? response) => response?.Content.Headers.ContentType?.MediaType == "text/event-stream"; - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - this._responseMessage?.Dispose(); - this._responseMessage = null; - } - } -} diff --git a/dotnet/samples/AgentWebChat/AgentWebChat.Web/IAgentClient.cs b/dotnet/samples/AgentWebChat/AgentWebChat.Web/IAgentClient.cs new file mode 100644 index 0000000000..13f1824c64 --- /dev/null +++ b/dotnet/samples/AgentWebChat/AgentWebChat.Web/IAgentClient.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft. All rights reserved. + +using A2A; +using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; + +namespace AgentWebChat.Web; + +/// +/// Interface for clients that can interact with agents and provide streaming responses. +/// +public interface IAgentClient +{ + /// + /// Runs an agent with the specified messages and returns a streaming response. + /// + /// The name of the agent to run. + /// The messages to send to the agent. + /// Optional thread identifier for conversation continuity. + /// Cancellation token. + /// An asynchronous enumerable of agent response updates. + IAsyncEnumerable RunStreamingAsync( + string agentName, + IList messages, + string? threadId = null, + CancellationToken cancellationToken = default); + + /// + /// Gets the agent card for the specified agent (A2A protocol only). + /// + /// The name of the agent. + /// Cancellation token. + /// The agent card if supported, null otherwise. + Task GetAgentCardAsync(string agentName, CancellationToken cancellationToken = default); +} + +/// +/// Helper class to create a thread-like wrapper for agent clients. +/// +public class AgentClientThread +{ + public string ThreadId { get; } + + public AgentClientThread(string? threadId = null) + { + this.ThreadId = threadId ?? Guid.NewGuid().ToString("N"); + } +} diff --git a/dotnet/samples/AgentWebChat/AgentWebChat.Web/Program.cs b/dotnet/samples/AgentWebChat/AgentWebChat.Web/Program.cs index c8e53aff31..0467990e1f 100644 --- a/dotnet/samples/AgentWebChat/AgentWebChat.Web/Program.cs +++ b/dotnet/samples/AgentWebChat/AgentWebChat.Web/Program.cs @@ -2,7 +2,6 @@ using AgentWebChat.Web; using AgentWebChat.Web.Components; -using Microsoft.Agents.AI.Runtime; var builder = WebApplication.CreateBuilder(args); @@ -23,8 +22,7 @@ Uri baseAddress = new("https+http://agenthost"); Uri a2aAddress = new("http://localhost:5390/a2a"); builder.Services.AddHttpClient(client => client.BaseAddress = baseAddress); -builder.Services.AddHttpClient(client => client.BaseAddress = baseAddress); -builder.Services.AddSingleton(sp => new A2AActorClient(sp.GetRequiredService>(), a2aAddress)); +builder.Services.AddSingleton(sp => new A2AAgentClient(sp.GetRequiredService>(), a2aAddress)); var app = builder.Build(); diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/WebApplicationExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/WebApplicationExtensions.cs index a54342098f..2bdb48081f 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/WebApplicationExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/WebApplicationExtensions.cs @@ -2,7 +2,6 @@ using A2A; using A2A.AspNetCore; -using Microsoft.Agents.AI.Runtime; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -24,9 +23,8 @@ public static class WebApplicationExtensions { var agent = app.Services.GetRequiredKeyedService(agentName); var loggerFactory = app.Services.GetRequiredService(); - var actorClient = app.Services.GetRequiredService(); - var taskManager = agent.MapA2A(actorClient, loggerFactory: loggerFactory); + var taskManager = agent.MapA2A(loggerFactory: loggerFactory); app.MapA2A(taskManager, path); } @@ -45,9 +43,8 @@ public static class WebApplicationExtensions { var agent = app.Services.GetRequiredKeyedService(agentName); var loggerFactory = app.Services.GetRequiredService(); - var actorClient = app.Services.GetRequiredService(); - var taskManager = agent.MapA2A(actorClient, agentCard: agentCard, loggerFactory: loggerFactory); + var taskManager = agent.MapA2A(agentCard: agentCard, loggerFactory: loggerFactory); app.MapA2A(taskManager, path); } diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AIAgentExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AIAgentExtensions.cs index 2de1200a49..b2d3e9ce1f 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AIAgentExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AIAgentExtensions.cs @@ -1,10 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Threading; using System.Threading.Tasks; using A2A; -using Microsoft.Agents.AI.Hosting.A2A.Internal; -using Microsoft.Agents.AI.Runtime; +using Microsoft.Agents.AI.Hosting.A2A.Converters; using Microsoft.Extensions.Logging; namespace Microsoft.Agents.AI.Hosting.A2A; @@ -18,46 +18,56 @@ public static class AIAgentExtensions /// Attaches A2A (Agent-to-Agent) messaging capabilities via Message processing to the specified . /// /// Agent to attach A2A messaging processing capabilities to. - /// The actor client implementation to use. /// Instance of to configure for A2A messaging. New instance will be created if not passed. /// The logger factory to use for creating instances. /// The configured . public static TaskManager MapA2A( this AIAgent agent, - IActorClient actorClient, TaskManager? taskManager = null, ILoggerFactory? loggerFactory = null) { ArgumentNullException.ThrowIfNull(agent, nameof(agent)); ArgumentNullException.ThrowIfNull(agent.Name, nameof(agent.Name)); - ArgumentNullException.ThrowIfNull(actorClient, nameof(actorClient)); taskManager ??= new(); - var a2aAgentWrapper = new A2AAgentWrapper(actorClient, agent, loggerFactory); - - taskManager.OnMessageReceived += a2aAgentWrapper.ProcessMessageAsync; + taskManager.OnMessageReceived += OnMessageReceivedAsync; return taskManager; + + async Task OnMessageReceivedAsync(MessageSendParams messageSendParams, CancellationToken cancellationToken) + { + var response = await agent.RunAsync( + messageSendParams.ToChatMessages(), + cancellationToken: cancellationToken).ConfigureAwait(false); + var contextId = messageSendParams.Message.ContextId ?? Guid.NewGuid().ToString("N"); + var parts = response.Messages.ToParts(); + + return new Message + { + MessageId = response.ResponseId ?? Guid.NewGuid().ToString("N"), + ContextId = contextId, + Role = MessageRole.Agent, + Parts = parts + }; + } } /// /// Attaches A2A (Agent-to-Agent) messaging capabilities via Message processing to the specified . /// /// Agent to attach A2A messaging processing capabilities to. - /// The actor client implementation to use. /// The agent card to return on query. /// Instance of to configure for A2A messaging. New instance will be created if not passed. /// The logger factory to use for creating instances. /// The configured . public static TaskManager MapA2A( this AIAgent agent, - IActorClient actorClient, AgentCard agentCard, TaskManager? taskManager = null, ILoggerFactory? loggerFactory = null) { - taskManager = agent.MapA2A(actorClient, taskManager, loggerFactory); + taskManager = agent.MapA2A(taskManager, loggerFactory); taskManager.OnAgentCardQuery += (context, query) => { diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/Converters/ActorEntitiesConverter.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/Converters/ActorEntitiesConverter.cs deleted file mode 100644 index e20f43cf22..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/Converters/ActorEntitiesConverter.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Text.Json; -using A2A; -using Microsoft.Agents.AI.Runtime; -using Microsoft.Extensions.AI; - -namespace Microsoft.Agents.AI.Hosting.A2A.Converters; - -internal static class ActorEntitiesConverter -{ - public static Message ToMessage(this AgentRunResponse response, string contextId) - { - var parts = response.Messages.ToParts(); - - return new Message - { - MessageId = response.ResponseId ?? Guid.NewGuid().ToString("N"), - ContextId = contextId, - Role = MessageRole.Agent, - Parts = parts - }; - } - - public static ActorRequestUpdate ToActorRequestUpdate(this Message message, RequestStatus status = RequestStatus.Completed) - { - // maybe we need to split to chatmessage-per-part, but the idea to map is clear - var chatMessage = - message.ToChatMessage() ?? - throw new ArgumentException("The Message could not be converted to a ChatMessage.", nameof(message)); - - var agentRunResponseUpdate = new AgentRunResponseUpdate(ChatRole.Assistant, chatMessage.Contents); - var updateTypeInfo = AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(AgentRunResponseUpdate)); - var jsonElement = JsonSerializer.SerializeToElement(agentRunResponseUpdate, updateTypeInfo); - return new ActorRequestUpdate(status, jsonElement); - } - - public static AgentRunResponse ToAgentRunResponse(this Message message) - { - // maybe we need to split to chatmessage-per-part, but the idea to map is clear - var chatMessage = - message.ToChatMessage() ?? - throw new ArgumentException("The Message could not be converted to a ChatMessage.", nameof(message)); - - return new AgentRunResponse(chatMessage); - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/Internal/A2AAgentWrapper.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/Internal/A2AAgentWrapper.cs deleted file mode 100644 index f9a9f7387a..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/Internal/A2AAgentWrapper.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Threading; -using System.Threading.Tasks; -using A2A; -using Microsoft.Agents.AI.Hosting.A2A.Converters; -using Microsoft.Agents.AI.Runtime; -using Microsoft.Extensions.Logging; -using Microsoft.Shared.Diagnostics; - -namespace Microsoft.Agents.AI.Hosting.A2A.Internal; - -/// -/// A2A agent that wraps an existing AIAgent and provides A2A-specific thread wrapping. -/// -internal sealed class A2AAgentWrapper -{ - private readonly AgentProxy _agentProxy; - - public A2AAgentWrapper( - IActorClient actorClient, - AIAgent innerAgent, - ILoggerFactory? loggerFactory = null) - { - Throw.IfNullOrEmpty(innerAgent.Name); - - this._agentProxy = new AgentProxy(innerAgent.Name, actorClient); - } - - public async Task ProcessMessageAsync(MessageSendParams messageSendParams, CancellationToken cancellationToken) - { - var contextId = messageSendParams.Message.ContextId ?? Guid.NewGuid().ToString("N"); - var chatMessages = messageSendParams.ToChatMessages(); - - var thread = this._agentProxy.GetNewThread(contextId); - var response = await this._agentProxy.RunAsync(messages: chatMessages, thread: thread, options: null, cancellationToken: cancellationToken).ConfigureAwait(false); - - return response.ToMessage(contextId); - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/Microsoft.Agents.AI.Hosting.A2A.csproj b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/Microsoft.Agents.AI.Hosting.A2A.csproj index 980cfc5dee..11b53eb130 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/Microsoft.Agents.AI.Hosting.A2A.csproj +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/Microsoft.Agents.AI.Hosting.A2A.csproj @@ -22,7 +22,6 @@ - diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/AgentActor.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/AgentActor.cs deleted file mode 100644 index f8d9902c6b..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Hosting/AgentActor.cs +++ /dev/null @@ -1,151 +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.Agents.AI.Runtime; -using Microsoft.Extensions.AI; -using Microsoft.Extensions.Logging; - -namespace Microsoft.Agents.AI.Hosting; - -internal sealed class AgentActor( - AIAgent agent, - IActorRuntimeContext context, - ILogger logger) : IActor -{ - private const string ThreadStateKey = "thread"; - private string? _etag; - private AgentThread? _thread; - - public ValueTask DisposeAsync() => default; - - public async ValueTask RunAsync(CancellationToken cancellationToken) - { - Log.ActorStarted(logger, context.ActorId.ToString(), agent.Name ?? "Unknown"); - await Task.Yield(); - - // Restore thread state - var response = await context.ReadAsync( - new ActorReadOperationBatch([new GetValueOperation(ThreadStateKey)]), - cancellationToken).ConfigureAwait(false); - - this._etag = response.ETag; - var hasExistingThread = false; - if (response.Results[0] is GetValueResult { Value: { } threadJson }) - { - // Deserialize the thread state if it exists - this._thread = agent.DeserializeThread(threadJson); - hasExistingThread = true; - } - - this._thread ??= agent.GetNewThread(); - Log.ThreadStateRestored(logger, context.ActorId.ToString(), hasExistingThread); - - while (!cancellationToken.IsCancellationRequested) - { - try - { - await foreach (var message in context.WatchMessagesAsync(cancellationToken).ConfigureAwait(false)) - { - switch (message.Type) - { - case ActorMessageType.Request: - await this.HandleAgentRequestAsync((ActorRequestMessage)message, cancellationToken).ConfigureAwait(false); - break; - case ActorMessageType.Response: - // Handle response messages if needed - break; - default: - Log.UnknownMessageType(logger, message.Type.ToString(), context.ActorId.ToString()); - break; - } - } - } - catch (Exception ex) - { - if (cancellationToken.IsCancellationRequested && ex is OperationCanceledException) - { - return; - } - - Log.ErrorProcessingMessages(logger, ex, context.ActorId.ToString()); - } - } - } - - private async Task HandleAgentRequestAsync(ActorRequestMessage message, CancellationToken cancellationToken) - { - var requestId = message.MessageId; - Debug.Assert(this._thread is not null); - Debug.Assert(this._etag is not null); - - if (message.Method is not AgentActorConstants.RunMethodName) - { - // Unsupported method, we can only handle "Run" requests. - var data = JsonSerializer.SerializeToElement("Unsupported method.", AgentHostingJsonUtilities.DefaultOptions.GetTypeInfo(typeof(string))); - await context.WriteAsync( - new(this._etag, [ - new UpdateRequestOperation( - requestId, - RequestStatus.Failed, - data)]), - cancellationToken).ConfigureAwait(false); - return; - } - - // Parse the request to get the agent run parameters - List? messages; - if (message.Params is { } payload) - { - var arg = payload.Deserialize(AgentHostingJsonUtilities.DefaultOptions.GetTypeInfo(typeof(AgentRunRequest))) as AgentRunRequest; - messages = arg?.Messages; - } - - messages ??= []; - - Log.ProcessingAgentRequest(logger, requestId, context.ActorId.ToString(), messages.Count); - try - { - var i = 0; - var updates = new List(); - await foreach (var update in agent.RunStreamingAsync(messages, this._thread, cancellationToken: cancellationToken).ConfigureAwait(false)) - { - var updateJson = JsonSerializer.SerializeToElement(update, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(AgentRunResponseUpdate))); - context.OnProgressUpdate(requestId, i++, updateJson); - updates.Add(update); - Log.AgentStreamingUpdate(logger, requestId, i); - } - - var serializedRunResponse = JsonSerializer.SerializeToElement(updates.ToAgentRunResponse(), AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(AgentRunResponse))); - var updatedThread = this._thread.Serialize(AgentHostingJsonUtilities.DefaultOptions); - - var writeResponse = await context.WriteAsync( - new(this._etag, - [ - new UpdateRequestOperation(requestId, RequestStatus.Completed, serializedRunResponse), - new SetValueOperation(ThreadStateKey, updatedThread) - ]), cancellationToken) - .ConfigureAwait(false); - if (!writeResponse.Success) - { - Log.WriteOperationFailed(logger, context.ActorId.ToString(), requestId); - } - else - { - Log.AgentRequestCompleted(logger, requestId, updates.Count); - } - - this._etag = writeResponse.ETag; - } - catch (Exception exception) - { - Log.AgentRequestFailed(logger, exception, requestId, context.ActorId.ToString()); - - // TODO: Retry later? - } - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/AgentActorConstants.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/AgentActorConstants.cs deleted file mode 100644 index ac1b2d99c9..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Hosting/AgentActorConstants.cs +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -namespace Microsoft.Agents.AI.Hosting; - -internal static class AgentActorConstants -{ - public const string RunMethodName = "Run"; -} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/AgentHostingJsonUtilities.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/AgentHostingJsonUtilities.cs index 527525d92d..95a930a9a2 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting/AgentHostingJsonUtilities.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting/AgentHostingJsonUtilities.cs @@ -3,7 +3,6 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; -using Microsoft.Agents.AI.Runtime; namespace Microsoft.Agents.AI.Hosting; @@ -42,7 +41,6 @@ public static partial class AgentHostingJsonUtilities // Chain with all supported types from Microsoft.Agents.AI.Abstractions. options.TypeInfoResolverChain.Add(AgentAbstractionsJsonUtilities.DefaultOptions.TypeInfoResolver!); - options.TypeInfoResolverChain.Add(AgentRuntimeAbstractionsJsonUtilities.DefaultOptions.TypeInfoResolver!); options.MakeReadOnly(); return options; @@ -52,8 +50,6 @@ public static partial class AgentHostingJsonUtilities [JsonSourceGenerationOptions(JsonSerializerDefaults.Web, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, NumberHandling = JsonNumberHandling.AllowReadingFromString)] - [JsonSerializable(typeof(AgentRunRequest))] - [JsonSerializable(typeof(AgentProxyThread))] [JsonSerializable(typeof(AgentThread))] [ExcludeFromCodeCoverage] internal sealed partial class JsonContext : JsonSerializerContext; diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/AgentProxy.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/AgentProxy.cs deleted file mode 100644 index 287c77bb14..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Hosting/AgentProxy.cs +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Agents.AI.Runtime; -using Microsoft.Extensions.AI; -using Microsoft.Shared.Diagnostics; - -namespace Microsoft.Agents.AI.Hosting; - -/// -/// Represents a proxy for an AI agent that communicates with the agent runtime via an actor client. -/// -public sealed class AgentProxy : AIAgent -{ - private readonly IActorClient _client; - - /// - /// Initializes a new instance of the class with the specified agent name and actor client. - /// - /// The name of the agent. - /// The actor client used to communicate with the agent. - public AgentProxy(string name, IActorClient client) - { - this._client = Throw.IfNull(client, nameof(client)); - this.Name = Throw.IfNullOrEmpty(name, nameof(name)); - } - - /// - public override string Name { get; } - - /// - public override AgentThread GetNewThread() => new AgentProxyThread(); - - /// - public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null) - => new AgentProxyThread(serializedThread, jsonSerializerOptions); - - /// - /// Gets a thread by its . - /// - /// The thread identifier. - /// The thread. - public AgentThread GetNewThread(string conversationId) => new AgentProxyThread(conversationId); - - /// - public override async Task RunAsync( - IEnumerable messages, - AgentThread? thread = null, - AgentRunOptions? options = null, - CancellationToken cancellationToken = default) - { - Throw.IfNull(messages, nameof(messages)); - string agentThreadId = GetAgentThreadId(thread); - return await this.RunAsync(messages, agentThreadId, cancellationToken).ConfigureAwait(false); - } - - /// - public override async IAsyncEnumerable RunStreamingAsync( - IEnumerable messages, - AgentThread? thread = null, - AgentRunOptions? options = null, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - Throw.IfNull(messages, nameof(messages)); - string agentThreadId = GetAgentThreadId(thread); - await foreach (var item in this.RunStreamingAsync(messages, agentThreadId, cancellationToken).ConfigureAwait(false)) - { - yield return item; - } - } - - private async Task RunAsync(IEnumerable messages, string threadId, CancellationToken cancellationToken) - { - var handle = await this.RunCoreAsync(messages, threadId, cancellationToken).ConfigureAwait(false); - var response = await handle.GetResponseAsync(cancellationToken).ConfigureAwait(false); - return response.Status switch - { - RequestStatus.Completed => (AgentRunResponse)response.Data.Deserialize( - AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(AgentRunResponse)))!, - RequestStatus.Failed => throw new InvalidOperationException($"The agent run request failed: {response.Data}"), - RequestStatus.Pending => throw new InvalidOperationException("The agent run request is still pending."), - _ => throw new NotSupportedException($"The agent run request returned an unsupported status: {response.Status}.") - }; - } - - private async IAsyncEnumerable RunStreamingAsync( - IEnumerable messages, - string threadId, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - var response = await this.RunCoreAsync(messages, threadId, cancellationToken).ConfigureAwait(false); - var updateTypeInfo = AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(AgentRunResponseUpdate)); - await foreach (var update in response.WatchUpdatesAsync(cancellationToken).ConfigureAwait(false)) - { - if (update.Status is RequestStatus.Failed) - { - throw new InvalidOperationException($"The agent run request failed: {update.Data}"); - } - - if (update.Status is RequestStatus.Completed) - { - yield break; - } - - yield return (AgentRunResponseUpdate)update.Data.Deserialize(updateTypeInfo)!; - } - } - - private static string GetAgentThreadId(AgentThread? thread) - { - if (thread is null) - { - return AgentProxyThread.CreateId(); - } - - if (thread is not AgentProxyThread agentProxyThread) - { - throw new ArgumentException("The thread must be an instance of AgentProxyThread.", nameof(thread)); - } - - return agentProxyThread.ConversationId!; - } - - private async Task RunCoreAsync(IEnumerable messages, string threadId, CancellationToken cancellationToken) - { - List newMessages = [.. messages]; - - var runRequest = new AgentRunRequest - { - Messages = newMessages - }; - - string messageId = newMessages.LastOrDefault()?.MessageId ?? Guid.NewGuid().ToString("N"); - ActorRequest actorRequest = new( - actorId: new ActorId(this.Name, threadId), - messageId, - method: AgentActorConstants.RunMethodName, - @params: JsonSerializer.SerializeToElement(runRequest, AgentHostingJsonUtilities.DefaultOptions.GetTypeInfo(typeof(AgentRunRequest)))); - return await this._client.SendRequestAsync(actorRequest, cancellationToken).ConfigureAwait(false); - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/AgentProxyThread.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/AgentProxyThread.cs deleted file mode 100644 index b3bbc162df..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Hosting/AgentProxyThread.cs +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Text.Json; -using System.Text.RegularExpressions; -using Microsoft.Shared.Diagnostics; - -namespace Microsoft.Agents.AI.Hosting; - -/// -/// Represents an agent thread for a . -/// -internal sealed partial class AgentProxyThread : ServiceIdAgentThread -{ -#if NET7_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.StringSyntax("Regex")] -#endif - private const string ThreadValidationRegex = "^[a-zA-Z0-9_.\\-~]+$"; - -#if NET7_0_OR_GREATER - /// - /// Regular expression pattern for valid thread IDs. - /// Thread IDs must be alphanumeric and can contain hyphens, underscores, dots, and tildes (RFC 3986 unreserved characters). - /// - [GeneratedRegex(ThreadValidationRegex, RegexOptions.Compiled)] - private static partial Regex ValidIdPattern(); -#else - /// - /// Regular expression pattern for valid thread IDs. - /// Thread IDs must be alphanumeric and can contain hyphens, underscores, dots, and tildes (RFC 3986 unreserved characters). - /// - private static readonly Regex s_validIdPattern = new(ThreadValidationRegex, RegexOptions.Compiled); - - /// - /// Regular expression pattern for valid thread IDs. - /// Thread IDs must be alphanumeric and can contain hyphens, underscores, dots, and tildes (RFC 3986 unreserved characters). - /// - private static Regex ValidIdPattern() => s_validIdPattern; -#endif - - /// - /// Initializes a new instance of the class with the specified identifier. - /// - /// The unique identifier for the agent proxy thread. - internal AgentProxyThread(string id) - { - Throw.IfNullOrEmpty(id); - ValidateId(id); - this.ConversationId = id; - } - - /// - /// Initializes a new instance of the class with the specified identifier. - /// - internal AgentProxyThread() : this(CreateId()) - { - } - - /// - /// Initializes a new instance of the class from serialized state. - /// - /// A representing the serialized state of the thread. - /// Optional settings for customizing the JSON deserialization process. - internal AgentProxyThread(JsonElement serializedThreadState, JsonSerializerOptions? jsonSerializerOptions = null) - : base(serializedThreadState, jsonSerializerOptions) - { - } - - /// - /// Gets the ID that the conversation state is stored under for the agent. - /// - public string? ConversationId - { - get => this.ServiceThreadId; - private set => this.ServiceThreadId = value; - } - - internal static string CreateId() => Guid.NewGuid().ToString("N"); - - /// - /// Validates that the provided ID matches the required pattern for thread IDs. - /// - /// The ID to validate. - /// Thrown when the ID is not valid. - private static void ValidateId(string id) - { - if (!ValidIdPattern().IsMatch(id)) - { - throw new ArgumentException($"Thread ID '{id}' is not valid. Thread IDs must contain only alphanumeric characters, hyphens, underscores, dots, and tildes.", nameof(id)); - } - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/AgentRunRequest.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/AgentRunRequest.cs deleted file mode 100644 index fe1b3a1651..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Hosting/AgentRunRequest.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Text.Json.Serialization; -using Microsoft.Extensions.AI; - -namespace Microsoft.Agents.AI.Hosting; - -/// -/// Represents a request to run an agent with a collection of chat messages. -/// -public sealed class AgentRunRequest -{ - /// - /// Gets or sets the collection of chat messages to be processed by the agent. - /// - [JsonPropertyName("messages")] - public List? Messages { get; set; } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/HostApplicationBuilderAgentExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/HostApplicationBuilderAgentExtensions.cs index 982f20f488..6d5f5283fe 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting/HostApplicationBuilderAgentExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting/HostApplicationBuilderAgentExtensions.cs @@ -2,11 +2,9 @@ using System; using System.Linq; -using Microsoft.Agents.AI.Runtime; using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI.Hosting; @@ -24,10 +22,10 @@ public static class HostApplicationBuilderAgentExtensions /// The instructions for the agent. /// The configured host application builder. /// Thrown when , , or is null. - public static IHostApplicationBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string instructions) + public static IHostApplicationBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions) { Throw.IfNull(builder); - Throw.IfNull(name); + Throw.IfNullOrEmpty(name); return builder.AddAIAgent(name, instructions, chatClientServiceKey: null); } @@ -40,10 +38,10 @@ public static class HostApplicationBuilderAgentExtensions /// The chat client which the agent will use for inference. /// The configured host application builder. /// Thrown when , , or is null. - public static IHostApplicationBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string instructions, IChatClient chatClient) + public static IHostApplicationBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions, IChatClient chatClient) { Throw.IfNull(builder); - Throw.IfNull(name); + Throw.IfNullOrEmpty(name); return builder.AddAIAgent(name, (sp, key) => new ChatClientAgent(chatClient, instructions, key)); } @@ -57,10 +55,10 @@ public static class HostApplicationBuilderAgentExtensions /// The key to use when resolving the chat client from the service provider. If null, a non-keyed service will be resolved. /// The configured host application builder. /// Thrown when , , or is null. - public static IHostApplicationBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string instructions, string description, object? chatClientServiceKey) + public static IHostApplicationBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions, string? description, object? chatClientServiceKey) { Throw.IfNull(builder); - Throw.IfNull(name); + Throw.IfNullOrEmpty(name); return builder.AddAIAgent(name, (sp, key) => { var chatClient = chatClientServiceKey is null ? sp.GetRequiredService() : sp.GetRequiredKeyedService(chatClientServiceKey); @@ -77,10 +75,10 @@ public static class HostApplicationBuilderAgentExtensions /// The key to use when resolving the chat client from the service provider. If null, a non-keyed service will be resolved. /// The configured host application builder. /// Thrown when , , or is null. - public static IHostApplicationBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string instructions, object? chatClientServiceKey) + public static IHostApplicationBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions, object? chatClientServiceKey) { Throw.IfNull(builder); - Throw.IfNull(name); + Throw.IfNullOrEmpty(name); return builder.AddAIAgent(name, (sp, key) => { var chatClient = chatClientServiceKey is null ? sp.GetRequiredService() : sp.GetRequiredKeyedService(chatClientServiceKey); @@ -108,7 +106,7 @@ public static class HostApplicationBuilderAgentExtensions var keyString = key as string; Throw.IfNullOrEmpty(keyString); var agent = createAgentDelegate(sp, keyString) ?? throw new InvalidOperationException($"The agent factory did not return a valid {nameof(AIAgent)} instance for key '{keyString}'."); - if (agent.Name != keyString) + if (!string.Equals(agent.Name, keyString, StringComparison.Ordinal)) { throw new InvalidOperationException($"The agent factory returned an agent with name '{agent.Name}', but the expected name is '{keyString}'."); } @@ -116,25 +114,9 @@ public static class HostApplicationBuilderAgentExtensions return agent; }); - return builder.AddAgentActor(name); - } - - private static IHostApplicationBuilder AddAgentActor(this IHostApplicationBuilder builder, string name) - { - Throw.IfNull(builder); - // Register the agent by name for discovery. var agentHostBuilder = GetAgentRegistry(builder); agentHostBuilder.AgentNames.Add(name); - - // Add the actor runtime and register the agent actor type. - var actorBuilder = builder.AddActorRuntime(); - actorBuilder.AddActorType( - new ActorType(name), - (sp, ctx) => new AgentActor( - sp.GetRequiredKeyedService(name), - ctx, - sp.GetRequiredService>())); return builder; } diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/Log.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/Log.cs deleted file mode 100644 index 98f66328e6..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Hosting/Log.cs +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using Microsoft.Extensions.Logging; - -namespace Microsoft.Agents.AI.Hosting; - -/// -/// High-performance logging messages using LoggerMessage source generator. -/// -internal static partial class Log -{ - [LoggerMessage( - Level = LogLevel.Information, - Message = "Actor started: ActorId={ActorId}, AgentName={AgentName}")] - public static partial void ActorStarted(ILogger logger, string actorId, string agentName); - - [LoggerMessage( - Level = LogLevel.Debug, - Message = "Thread state restored: ActorId={ActorId}, HasExistingThread={HasExistingThread}")] - public static partial void ThreadStateRestored(ILogger logger, string actorId, bool hasExistingThread); - - [LoggerMessage( - Level = LogLevel.Information, - Message = "Processing agent request: RequestId={RequestId}, ActorId={ActorId}, MessageCount={MessageCount}")] - public static partial void ProcessingAgentRequest(ILogger logger, string requestId, string actorId, int messageCount); - - [LoggerMessage( - Level = LogLevel.Debug, - Message = "Agent streaming update: RequestId={RequestId}, UpdateNumber={UpdateNumber}")] - public static partial void AgentStreamingUpdate(ILogger logger, string requestId, int updateNumber); - - [LoggerMessage( - Level = LogLevel.Information, - Message = "Agent request completed: RequestId={RequestId}, TotalUpdates={TotalUpdates}")] - public static partial void AgentRequestCompleted(ILogger logger, string requestId, int totalUpdates); - - [LoggerMessage( - Level = LogLevel.Error, - Message = "Agent request failed: RequestId={RequestId}, ActorId={ActorId}")] - public static partial void AgentRequestFailed(ILogger logger, Exception exception, string requestId, string actorId); - - [LoggerMessage( - Level = LogLevel.Warning, - Message = "Unknown message type received: MessageType={MessageType}, ActorId={ActorId}")] - public static partial void UnknownMessageType(ILogger logger, string messageType, string actorId); - - [LoggerMessage( - Level = LogLevel.Warning, - Message = "Error processing messages: ActorId={ActorId}")] - public static partial void ErrorProcessingMessages(ILogger logger, Exception exception, string actorId); - - [LoggerMessage( - Level = LogLevel.Error, - Message = "Write operation failed: ActorId={ActorId}, RequestId={RequestId}")] - public static partial void WriteOperationFailed(ILogger logger, string actorId, string requestId); -} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/Microsoft.Agents.AI.Hosting.csproj b/dotnet/src/Microsoft.Agents.AI.Hosting/Microsoft.Agents.AI.Hosting.csproj index 75319ed617..d593a8005a 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting/Microsoft.Agents.AI.Hosting.csproj +++ b/dotnet/src/Microsoft.Agents.AI.Hosting/Microsoft.Agents.AI.Hosting.csproj @@ -16,8 +16,6 @@ - - diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorId.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorId.cs deleted file mode 100644 index 11293b67de..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorId.cs +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Diagnostics.CodeAnalysis; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Provides a unique identifier for an actor instance within an agent runtime, -/// serving as the "address" of the actor instance for receiving messages. -/// -[JsonConverter(typeof(Converter))] -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 value) - { - if (!TryParse(value, out var result)) - { - throw new FormatException($"Invalid actor ID: '{value}'. Expected format is 'type/key'."); - } - - return result; - } - - private static bool TryParse(string input, out ActorId actorId) - { - if (!string.IsNullOrEmpty(input)) - { - int separatorIndex = input.IndexOf('/'); - if (separatorIndex >= 0) - { - var type = input.Substring(0, separatorIndex); - var key = input.Substring(separatorIndex + 1); - actorId = new ActorId(type, key); - return true; - } - } - - actorId = default; - return false; - } - - /// - 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 - } - - /// - /// JSON converter for . - /// - public sealed class Converter : JsonConverter - { - /// - public override ActorId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType != JsonTokenType.String) - { - throw new JsonException("Expected string value for ActorId"); - } - - string? actorIdString = reader.GetString() ?? throw new JsonException("ActorId cannot be null"); - return Parse(actorIdString); - } - - /// - public override void Write(Utf8JsonWriter writer, ActorId value, JsonSerializerOptions options) => - writer.WriteStringValue(value.ToString()); - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorMessage.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorMessage.cs deleted file mode 100644 index 87eb8b930c..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorMessage.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Base class for all actor messages that can be sent between actors. -/// -/// -/// This abstract class serves as the foundation for all actor message types. -/// Each concrete implementation represents a specific type of message, -/// such as request messages or response messages. -/// -//[JsonConverter(typeof(Converter))] -[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")] -[JsonDerivedType(typeof(ActorRequestMessage), "request")] -[JsonDerivedType(typeof(ActorResponseMessage), "response")] -public abstract class ActorMessage -{ - /// Prevent external derivations. - private protected ActorMessage() - { - } - - /// - /// Gets the type of the message. - /// - [JsonIgnore] - public abstract ActorMessageType Type { get; } - - /// - /// Additional properties that can be used to extend the message with custom data. - /// - [JsonExtensionData] - public Dictionary? ExtensionData { get; set; } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorMessageType.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorMessageType.cs deleted file mode 100644 index d17f103866..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorMessageType.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json.Serialization; - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Specifies the type of actor message. -/// -public enum ActorMessageType -{ - /// - /// Represents a request message sent to an actor. - /// - [JsonStringEnumMemberName("request")] - Request, - - /// - /// Represents a response message sent from an actor. - /// - [JsonStringEnumMemberName("response")] - Response -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorMessageWriteOperation.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorMessageWriteOperation.cs deleted file mode 100644 index ef757c7bb1..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorMessageWriteOperation.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Base class for write operations that modify an actor's messaging (inbox/outbox). -/// -public abstract class ActorMessageWriteOperation : ActorWriteOperation -{ - /// Prevent external derivations. - private protected ActorMessageWriteOperation() - { - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorReadOperation.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorReadOperation.cs deleted file mode 100644 index bac70e2468..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorReadOperation.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Base class for all actor read operations that can query actor state or messaging. -/// -/// -/// This abstract class serves as the foundation for all actor read operation types. -/// Each concrete implementation represents a specific type of read operation, -/// such as querying actor state or messaging information. -/// -[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")] -[JsonDerivedType(typeof(ListKeysOperation), "list_keys")] -[JsonDerivedType(typeof(GetValueOperation), "get_value")] -public abstract class ActorReadOperation -{ - /// Prevent external derivations. - private protected ActorReadOperation() - { - } - - /// - /// Gets the type of the read operation. - /// - [JsonIgnore] - public abstract ActorReadOperationType Type { get; } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorReadOperationBatch.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorReadOperationBatch.cs deleted file mode 100644 index 98c350ebfa..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorReadOperationBatch.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Represents a batch of read operations to be performed on an actor. -/// -/// The collection of read operations to perform. -public sealed class ActorReadOperationBatch(IReadOnlyList operations) -{ - /// - /// Gets the collection of read operations to perform. - /// - [JsonPropertyName("operations")] - public IReadOnlyList Operations { get; } = operations; -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorReadOperationType.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorReadOperationType.cs deleted file mode 100644 index 0a8c520291..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorReadOperationType.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json.Serialization; - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Specifies the type of actor read operation. -/// -public enum ActorReadOperationType -{ - /// - /// Represents a list keys operation. - /// - [JsonStringEnumMemberName("list_keys")] - ListKeys, - - /// - /// Represents a get value operation. - /// - [JsonStringEnumMemberName("get_value")] - GetValue -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorReadResult.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorReadResult.cs deleted file mode 100644 index 60dab08cd9..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorReadResult.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Base class for all actor read operation results. -/// -/// -/// This abstract class serves as the foundation for all actor read operation result types. -/// Each concrete implementation represents a specific type of read operation result, -/// such as listing keys or retrieving values from an actor's state. -/// -[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")] -[JsonDerivedType(typeof(ListKeysResult), "list_keys")] -[JsonDerivedType(typeof(GetValueOperation), "get_value")] -public abstract class ActorReadResult -{ - /// Prevent external derivations. - private protected ActorReadResult() - { - } - - /// - /// Gets the type of the read result operation. - /// - [JsonIgnore] - public abstract ActorReadResultType Type { get; } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorReadResultType.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorReadResultType.cs deleted file mode 100644 index 6fd510e217..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorReadResultType.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json.Serialization; - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Specifies the type of actor read result operation. -/// -public enum ActorReadResultType -{ - /// - /// Represents a list keys operation result. - /// - [JsonStringEnumMemberName("list_keys")] - ListKeys, - - /// - /// Represents a get value operation result. - /// - [JsonStringEnumMemberName("get_value")] - GetValue -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorRequest.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorRequest.cs deleted file mode 100644 index c771ca5d2e..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorRequest.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Represents a request to be sent to an actor. -/// -public sealed class ActorRequest(ActorId actorId, string messageId, string method, JsonElement @params) -{ - /// - /// Gets or sets the identifier of the target actor. - /// - [JsonPropertyName("actorId")] - public ActorId ActorId { get; } = actorId; - - /// - /// Gets or sets the unique identifier for this request. - /// - [JsonPropertyName("messageId")] - public string MessageId { get; } = messageId; - - /// - /// Gets or sets the method name to invoke on the actor. - /// - [JsonPropertyName("method")] - public string Method { get; } = method; - - /// - /// Gets or sets the parameters for the method invocation. - /// - [JsonPropertyName("params")] - public JsonElement Params { get; } = @params; -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorRequestMessage.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorRequestMessage.cs deleted file mode 100644 index 5c8c68b6c8..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorRequestMessage.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Base class for request messages sent to actors. -/// -public sealed class ActorRequestMessage(string MessageId) : ActorMessage -{ - /// - public override ActorMessageType Type => ActorMessageType.Request; - - /// - /// Gets or sets the actor ID of the sender. - /// - [JsonPropertyName("sender")] - public ActorId? SenderId { get; init; } - - /// - /// Gets or sets the unique identifier for the request. - /// - [JsonPropertyName("messageId")] - public string MessageId { get; } = MessageId; - - /// - /// Name of the method to invoke. - /// - [JsonPropertyName("method")] - public string? Method { get; init; } - - /// - /// Optional parameters for the method invocation. - /// - [JsonPropertyName("params")] - public JsonElement Params { get; init; } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorRequestUpdate.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorRequestUpdate.cs deleted file mode 100644 index 0685cfda65..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorRequestUpdate.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Microsoft.Agents.AI.Runtime; - -// External (client) interface. - -/// -/// Represents an update to an actor request's status and data. -/// -public sealed class ActorRequestUpdate(RequestStatus status, JsonElement data) -{ - /// - /// Gets the updated status of the request. - /// - [JsonPropertyName("status")] - public RequestStatus Status { get; } = status; - - /// - /// Gets the updated data associated with the request. - /// - [JsonPropertyName("data")] - public JsonElement Data { get; } = data; -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorResponse.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorResponse.cs deleted file mode 100644 index 9558f19194..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorResponse.cs +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Represents a response handle for an actor request, providing access to the result and status updates. -/// -public sealed class ActorResponse -{ - /// - /// Gets the identifier of the actor that is processing the request. - /// - [JsonPropertyName("actorId")] - public ActorId ActorId { get; init; } - - /// - /// Gets the unique identifier of the message/request. - /// - [JsonPropertyName("messageId")] - public string? MessageId { get; init; } - - /// - /// Gets the response data from the actor. - /// - [JsonPropertyName("data")] - public JsonElement Data { get; init; } - - /// - /// Gets or sets the current status of the request. - /// - [JsonPropertyName("status")] - public RequestStatus Status { get; init; } - - /// - public override string ToString() - { - string dataString; - if (this.Data.ValueKind is JsonValueKind.Undefined) - { - dataString = "undefined"; - } - else - { - var rawText = this.Data.GetRawText(); - dataString = rawText.Length switch - { - > 250 => $"{rawText.Substring(0, 250)}...", - _ => rawText, - }; - } - - return $"ActorResponse(ActorId: {this.ActorId}, Status: {this.Status}, MessageId: {this.MessageId ?? "null"}, Data: {dataString})"; - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorResponseHandle.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorResponseHandle.cs deleted file mode 100644 index 4ed4a24fca..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorResponseHandle.cs +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Represents a handle to an actor response, allowing retrieval of the response data and status updates. -/// -public abstract class ActorResponseHandle : IDisposable -{ - /// - /// Attempts to get the response from the request if it is immediately available. - /// - /// When this method returns , contains the actor response; otherwise, . - /// if the response is immediately available; otherwise, . - /// - /// This method does not block and returns immediately. If the request is still pending or processing, - /// this method returns . - /// Use to wait asynchronously for the response to become available. - /// - public abstract bool TryGetResponse([NotNullWhen(true)] out ActorResponse? response); - - /// - /// Gets the response from the completed request. - /// - /// The to monitor for cancellation requests. The default is . - /// A task that completes when the request is finished. - public abstract ValueTask GetResponseAsync(CancellationToken cancellationToken); - - /// - /// Cancels the request if it is still pending. - /// - /// A task representing the cancellation operation. - public abstract ValueTask CancelAsync(CancellationToken cancellationToken); - - /// - /// Watches for status and data updates to the request. - /// - /// The to monitor for cancellation requests. The default is . - /// An asynchronous enumerable of request updates. - public abstract IAsyncEnumerable WatchUpdatesAsync(CancellationToken cancellationToken); - - /// - public void Dispose() - { - this.Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Disposes of the resources used by the class. - /// - /// A boolean indicating whether the method is being called from the method. - protected virtual void Dispose(bool disposing) { } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorResponseMessage.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorResponseMessage.cs deleted file mode 100644 index 56684ddbfd..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorResponseMessage.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Base class for response messages sent from actors. -/// -public sealed class ActorResponseMessage(string MessageId) : ActorMessage -{ - /// - public override ActorMessageType Type => ActorMessageType.Response; - - /// - /// Gets or sets the actor ID of the sender. - /// - [JsonPropertyName("senderId")] - public ActorId SenderId { get; init; } - - /// - /// Gets or sets the unique identifier for the request. - /// - [JsonPropertyName("messageId")] - public string MessageId { get; } = MessageId; - - /// - /// Gets or sets the status of the request. - /// - [JsonPropertyName("status")] - public RequestStatus Status { get; init; } - - /// - /// Gets or sets the response data (result or error information). - /// - [JsonPropertyName("data")] - public JsonElement Data { get; init; } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorRuntimeJsonUtilities.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorRuntimeJsonUtilities.cs deleted file mode 100644 index 9cabb79cd2..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorRuntimeJsonUtilities.cs +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Diagnostics.CodeAnalysis; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Microsoft.Agents.AI.Runtime; - -/// Provides a collection of utility methods for working with JSON data in the context of actor runtime abstractions. -public static partial class AgentRuntimeAbstractionsJsonUtilities -{ - /// - /// Gets the singleton used as the default in JSON serialization operations. - /// - /// - /// - /// For Native AOT or applications disabling , this instance - /// includes source generated contracts for all common exchange types contained in this library. - /// - /// - /// It additionally turns on the following settings: - /// - /// Enables defaults. - /// Enables as the default ignore condition for properties. - /// Enables as the default number handling for number types. - /// Enables for enum serialization. - /// - /// - /// - public static JsonSerializerOptions DefaultOptions { get; } = CreateDefaultOptions(); - - /// - /// Creates default options to use for actor runtime-related serialization. - /// - /// The configured options. - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050:RequiresDynamicCode", Justification = "Converter is guarded by IsReflectionEnabledByDefault check.")] - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access", Justification = "Converter is guarded by IsReflectionEnabledByDefault check.")] - private static JsonSerializerOptions CreateDefaultOptions() - { - // Copy the configuration from the source generated context. - JsonSerializerOptions options = new(JsonContext.Default.Options); - - options.MakeReadOnly(); - return options; - } - - /// - /// Source-generated JSON type information for use by all agent runtime abstractions. - /// - [JsonSourceGenerationOptions( - JsonSerializerDefaults.Web, - UseStringEnumConverter = true, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - WriteIndented = false)] - [JsonSerializable(typeof(ActorId))] - [JsonSerializable(typeof(ActorMessage))] - [JsonSerializable(typeof(ActorReadOperation))] - [JsonSerializable(typeof(ActorReadOperationBatch))] - [JsonSerializable(typeof(ActorReadResult))] - [JsonSerializable(typeof(ActorRequest))] - [JsonSerializable(typeof(ActorRequestMessage))] - [JsonSerializable(typeof(ActorRequestUpdate))] - [JsonSerializable(typeof(ActorResponse))] - [JsonSerializable(typeof(ActorResponseMessage))] - [JsonSerializable(typeof(ActorType))] - [JsonSerializable(typeof(ActorWriteOperation))] - [JsonSerializable(typeof(ActorWriteOperationBatch))] - [JsonSerializable(typeof(GetValueOperation))] - [JsonSerializable(typeof(GetValueResult))] - [JsonSerializable(typeof(JsonElement))] - [JsonSerializable(typeof(ListKeysOperation))] - [JsonSerializable(typeof(ListKeysResult))] - [JsonSerializable(typeof(ReadResponse))] - [JsonSerializable(typeof(RemoveKeyOperation))] - [JsonSerializable(typeof(RequestStatus))] - [JsonSerializable(typeof(SendRequestOperation))] - [JsonSerializable(typeof(SetValueOperation))] - [JsonSerializable(typeof(UpdateRequestOperation))] - [JsonSerializable(typeof(WriteResponse))] - internal sealed partial class JsonContext : JsonSerializerContext; -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorStateReadOperation.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorStateReadOperation.cs deleted file mode 100644 index 604e10a793..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorStateReadOperation.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Base class for read operations that query an actor's internal state. -/// -public abstract class ActorStateReadOperation : ActorReadOperation -{ - /// Prevent external derivations. - private protected ActorStateReadOperation() - { - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorStateWriteOperation.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorStateWriteOperation.cs deleted file mode 100644 index c651c70ab6..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorStateWriteOperation.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Base class for write operations that modify an actor's internal state. -/// -/// -/// This abstract class serves as the foundation for all actor state write operation types. -/// Each concrete implementation represents a specific type of state modification operation, -/// such as setting or removing key-value pairs in an actor's state. -/// -public abstract class ActorStateWriteOperation : ActorWriteOperation -{ - /// Prevent external derivations. - private protected ActorStateWriteOperation() - { - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorType.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorType.cs deleted file mode 100644 index 23f0d7a1df..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorType.cs +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Text.RegularExpressions; -using Microsoft.Shared.Diagnostics; - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Represents the type of an actor. -/// -[JsonConverter(typeof(Converter))] -public readonly partial struct ActorType : IEquatable -{ -#if NET7_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.StringSyntax("Regex")] -#endif - private const string AgentTypeValidationRegex = "^[a-zA-Z_][a-zA-Z._:\\-0-9]*$"; - - /// - /// Initializes a new instance of the struct. - /// - /// The actor type. - public ActorType(string type) - { - Throw.IfNullOrEmpty(type); - if (!IsValidType(type)) - { - throw new ArgumentException($"Invalid type: '{type}'. Must start with a letter or underscore, and can only contain letters, dots, underscores, colons, hyphens, and numbers.", nameof(type)); - } - - 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); - -#if NET7_0_OR_GREATER - [GeneratedRegex(AgentTypeValidationRegex)] - private static partial Regex TypeRegex(); -#else - private static readonly Regex s_typeRegex = new(AgentTypeValidationRegex, RegexOptions.Compiled); - private static Regex TypeRegex() => s_typeRegex; -#endif - - /// - /// Validates whether the provided type string is a valid actor type. - /// - public static bool IsValidType(string type) - { - Throw.IfNullOrEmpty(type); - return TypeRegex().IsMatch(type); - } - - /// - /// JSON converter for . - /// - public sealed class Converter : JsonConverter - { - /// - public override ActorType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType != JsonTokenType.String) - { - throw new JsonException("Expected string value for ActorType"); - } - - string? actorTypeString = reader.GetString() ?? throw new JsonException("ActorType cannot be null"); - return new ActorType(actorTypeString); - } - - /// - public override void Write(Utf8JsonWriter writer, ActorType value, JsonSerializerOptions options) => - writer.WriteStringValue(value.Name); - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorWriteOperation.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorWriteOperation.cs deleted file mode 100644 index 8eeea51362..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorWriteOperation.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json.Serialization; - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Base class for all actor write operations that can modify actor state or messaging. -/// -/// -/// This abstract class serves as the foundation for all actor write operation types. -/// Each concrete implementation represents a specific type of write operation, -/// such as modifying actor state or sending messages. -/// -[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")] -[JsonDerivedType(typeof(SetValueOperation), "set_value")] -[JsonDerivedType(typeof(RemoveKeyOperation), "remove_key")] -[JsonDerivedType(typeof(UpdateRequestOperation), "update_request")] -[JsonDerivedType(typeof(SendRequestOperation), "send_request")] -public abstract class ActorWriteOperation -{ - /// Prevent external derivations. - private protected ActorWriteOperation() - { - } - - /// - /// Gets the type of the write operation. - /// - [JsonIgnore] - public abstract ActorWriteOperationType Type { get; } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorWriteOperationBatch.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorWriteOperationBatch.cs deleted file mode 100644 index c6c14637e0..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorWriteOperationBatch.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Represents a batch of write operations to be performed atomically on an actor. -/// -/// The ETag for optimistic concurrency control. -/// The collection of write operations to perform. -public sealed class ActorWriteOperationBatch(string eTag, IReadOnlyCollection operations) -{ - /// - /// Gets the collection of write operations to perform. - /// - [JsonPropertyName("operations")] - public IReadOnlyCollection Operations { get; } = operations; - - /// - /// Gets the ETag for optimistic concurrency control. - /// - [JsonPropertyName("etag")] - public string ETag { get; } = eTag; -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorWriteOperationType.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorWriteOperationType.cs deleted file mode 100644 index 3ae0b495f8..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ActorWriteOperationType.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json.Serialization; - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Specifies the type of actor write operation. -/// -public enum ActorWriteOperationType -{ - /// - /// Represents a set key-value operation. - /// - [JsonStringEnumMemberName("set_value")] - SetValue, - - /// - /// Represents a remove key operation. - /// - [JsonStringEnumMemberName("remove_key")] - RemoveKey, - - /// - /// Represents a send request operation. - /// - [JsonStringEnumMemberName("send_request")] - SendRequest, - - /// - /// Represents an update request operation. - /// - [JsonStringEnumMemberName("update_request")] - UpdateRequest -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/GetValueOperation.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/GetValueOperation.cs deleted file mode 100644 index 6d52ee6788..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/GetValueOperation.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json.Serialization; - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Represents a request to read a value from the actor's state by its key. -/// -/// The key corresponding to the value to read from the actor's state. -public sealed class GetValueOperation(string key) : ActorStateReadOperation -{ - /// - /// Gets the key corresponding to the value to read from the actor's state. - /// - [JsonPropertyName("key")] - public string Key { get; } = key; - - /// - /// Gets the type of the read operation. - /// - public override ActorReadOperationType Type => ActorReadOperationType.GetValue; -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/GetValueResult.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/GetValueResult.cs deleted file mode 100644 index 4aa50e195d..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/GetValueResult.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Represents the result of a get value operation containing the retrieved value. -/// -/// The value retrieved from the actor's state, or null if not found. -public sealed class GetValueResult(JsonElement? value) : ActorReadResult -{ - /// - /// Gets the value retrieved from the actor's state. - /// - [JsonPropertyName("value")] - public JsonElement? Value { get; } = value; - - /// - /// Gets the type of the read result operation. - /// - public override ActorReadResultType Type => ActorReadResultType.GetValue; -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/IActor.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/IActor.cs deleted file mode 100644 index df7de4c3ef..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/IActor.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Agents.AI.Runtime; - -// Implemented by the Agent Framework (eg, Agent, Orchestration, Process, etc) -/// -/// Represents an actor in the actor system that can process messages and maintain state. -/// -public interface IActor : IAsyncDisposable -{ - /// - /// Runs the actor. - /// When the value returned from this method completes, the actor is considered stopped. - /// IActor is expected to call IActorContext.WatchMessagesAsync() to receive messages. - /// - /// The to monitor for cancellation requests. The default is . - /// A task representing the start operation. - ValueTask RunAsync(CancellationToken cancellationToken); -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/IActorClient.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/IActorClient.cs deleted file mode 100644 index 87ad973dfe..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/IActorClient.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Interface for sending requests to actors and managing responses. -/// -public interface IActorClient -{ - /// - /// Submits a request to an actor and gets a handle for the response. - /// This method is idempotent: if the request is already in progress, it will return the existing response. - /// - /// The request to send to the actor. - /// The to monitor for cancellation requests. The default is . - /// A task representing the actor response handle. - ValueTask SendRequestAsync(ActorRequest request, CancellationToken cancellationToken); - - /// - /// Gets an already-running request by its identifier. - /// - /// The identifier of the actor processing the request. - /// The unique identifier of the request message. - /// The to monitor for cancellation requests. The default is . - /// A task representing the actor response handle. - ValueTask GetResponseAsync(ActorId actorId, string messageId, CancellationToken cancellationToken); -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/IActorRuntimeBuilder.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/IActorRuntimeBuilder.cs deleted file mode 100644 index 4270e87d67..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/IActorRuntimeBuilder.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Builder interface for configuring actor types in the runtime. -/// -public interface IActorRuntimeBuilder -{ - /// - /// Registers an actor type with its factory method. - /// - /// The actor type to register. - /// The factory method to create instances of the actor. - void AddActorType(ActorType type, Func activator); -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/IActorRuntimeContext.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/IActorRuntimeContext.cs deleted file mode 100644 index 61ed7638a3..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/IActorRuntimeContext.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Provides the runtime context for an actor, enabling it to interact with the actor system. -/// -public interface IActorRuntimeContext -{ - /// - /// Gets the identifier of the actor. - /// - ActorId ActorId { get; } - - /// - /// Watches for incoming requests and responses in the actor's inbox and outbox. - /// - /// The to monitor for cancellation requests. The default is . - /// An asynchronous enumerable of actor notifications. - IAsyncEnumerable WatchMessagesAsync(CancellationToken cancellationToken = default); - - /// - /// Performs a batch of write operations atomically. - /// - /// The batch of write operations to perform. - /// The to monitor for cancellation requests. The default is . - /// A task representing the write response. - ValueTask WriteAsync(ActorWriteOperationBatch operations, CancellationToken cancellationToken = default); - - /// - /// Performs a batch of read operations. - /// - /// The batch of read operations to perform. - /// The to monitor for cancellation requests. The default is . - /// A task representing the read response. - ValueTask ReadAsync(ActorReadOperationBatch operations, CancellationToken cancellationToken = default); - - /// - /// Reports progress updates for streaming responses. - /// The messageId must correspond to a non-terminated request in the actor's inbox (Status is Pending). - /// - /// The identifier of the message being updated. - /// The sequence number for ordering progress updates. - /// The progress data. - void OnProgressUpdate(string messageId, int sequenceNumber, JsonElement data); -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/IActorStateStorage.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/IActorStateStorage.cs deleted file mode 100644 index e8bd011d25..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/IActorStateStorage.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Interface for actor state storage operations, providing persistence for actor state data. -/// -public interface IActorStateStorage -{ - /// - /// Writes state changes to the actor's persistent storage. - /// - /// The identifier of the actor whose state is being modified. - /// The collection of write operations to perform. - /// The expected ETag for optimistic concurrency control. - /// The to monitor for cancellation requests. The default is . - /// A task representing the write response with success status and updated ETag. - ValueTask WriteStateAsync(ActorId actorId, IReadOnlyCollection operations, string etag, CancellationToken cancellationToken = default); - - /// - /// Reads state data from the actor's persistent storage. - /// - /// The identifier of the actor whose state is being read. - /// The collection of read operations to perform. - /// The to monitor for cancellation requests. The default is . - /// A task representing the read response with results and current ETag. - ValueTask ReadStateAsync(ActorId actorId, IReadOnlyCollection operations, CancellationToken cancellationToken = default); -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/InMemoryActorStateStorage.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/InMemoryActorStateStorage.cs deleted file mode 100644 index 3c4f4dbe35..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/InMemoryActorStateStorage.cs +++ /dev/null @@ -1,369 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Provides an in-memory implementation of for testing and development scenarios. -/// -/// -/// -/// This implementation stores all actor state in memory using concurrent dictionaries for thread safety. -/// State is not persisted across application restarts and is lost when the application terminates. -/// -/// -/// The implementation provides optimistic concurrency control using ETags. Each write operation must -/// provide the current ETag, and the operation will fail if the ETag has changed since the last read. -/// This ensures that concurrent modifications to the same actor state are handled correctly. -/// -/// -/// Supported operations: -/// -/// - Sets a key-value pair in the actor's state -/// - Removes a key from the actor's state -/// - Retrieves a value by key from the actor's state -/// - Lists keys in the actor's state with optional prefix filtering -/// -/// -/// -/// This implementation is suitable for: -/// -/// Unit testing scenarios -/// Development and prototyping -/// Single-process applications where persistence is not required -/// -/// -/// -/// For production scenarios requiring persistence, consider implementing a custom storage provider -/// that uses a database or other persistent storage mechanism. -/// -/// -/// -/// -/// // Create storage instance -/// var storage = new InMemoryActorStateStorage(); -/// var actorId = new ActorId("MyActor", "instance1"); -/// -/// // Write some state -/// var writeOps = new List<ActorStateWriteOperation> -/// { -/// new SetValueOperation("name", JsonSerializer.SerializeToElement("John")), -/// new SetValueOperation("age", JsonSerializer.SerializeToElement(30)) -/// }; -/// var writeResult = await storage.WriteStateAsync(actorId, writeOps, "0"); -/// -/// // Read the state back -/// var readOps = new List<ActorStateReadOperation> -/// { -/// new GetValueOperation("name"), -/// new ListKeysOperation(null), // List all keys -/// new ListKeysOperation(null, "prefix_") // List keys starting with "prefix_" -/// }; -/// var readResult = await storage.ReadStateAsync(actorId, readOps); -/// -/// -public sealed class InMemoryActorStateStorage : IActorStateStorage -{ - private static readonly ActivitySource ActivitySource = new("Microsoft.Agents.AI.Runtime.Abstractions.InMemoryActorStateStorage"); - - private readonly ConcurrentDictionary _actorStates = []; - private readonly object _lockObject = new(); - private long _globalETagCounter; - - /// - /// Represents the internal state of an actor including its key-value pairs and ETag. - /// - private sealed class ActorState - { - public ConcurrentDictionary Data { get; } = []; - public string ETag { get; set; } = "0"; - } - - /// - public ValueTask WriteStateAsync(ActorId actorId, IReadOnlyCollection operations, string etag, CancellationToken cancellationToken = default) - { - using var activity = ActivitySource.StartActivity("actor.state write"); - - if (operations is null) - { - throw new ArgumentNullException(nameof(operations)); - } - - if (etag is null) - { - throw new ArgumentNullException(nameof(etag)); - } - - cancellationToken.ThrowIfCancellationRequested(); - - // Set telemetry attributes - SetActorAttributes(activity, actorId); - SetStateAttributes(activity, "write", operations.Count, etag); - - try - { - lock (this._lockObject) - { - var actorState = this._actorStates.GetOrAdd(actorId, _ => new ActorState()); - - // Check ETag for optimistic concurrency control - if (actorState.ETag != etag) - { - activity? - .SetTag("state.success", false) - .SetTag("error.type", "etag_mismatch") - .SetStatus(ActivityStatusCode.Error, "ETag mismatch - concurrent modification detected"); - - // Return failure with current ETag - return new ValueTask(new WriteResponse(actorState.ETag, success: false)); - } - - // Apply all operations - var operationTypes = new List(); - foreach (var operation in operations) - { - switch (operation) - { - case SetValueOperation setValue: - actorState.Data[setValue.Key] = setValue.Value; - operationTypes.Add("set"); - break; - - case RemoveKeyOperation removeKey: - actorState.Data.TryRemove(removeKey.Key, out _); - operationTypes.Add("remove"); - break; - - default: - var errorMessage = $"Unsupported write operation type: {operation.GetType().Name}"; - var exception = new InvalidOperationException(errorMessage); - SetErrorAttributes(activity, exception); - throw exception; - } - } - - // Update ETag - var newETag = Interlocked.Increment(ref this._globalETagCounter).ToString(); - actorState.ETag = newETag; - - // Set success attributes - SetOperationStatus(activity, true); - activity? - .SetTag("state.success", true) - .SetTag("state.new_etag", newETag) - .SetTag("state.operations", string.Join(",", operationTypes)); - - return new ValueTask(new WriteResponse(newETag, success: true)); - } - } - catch (Exception ex) - { - SetErrorAttributes(activity, ex); - throw; - } - } - - /// - public ValueTask ReadStateAsync(ActorId actorId, IReadOnlyCollection operations, CancellationToken cancellationToken = default) - { - using var activity = ActivitySource.StartActivity("actor.state read"); - - if (operations is null) - { - throw new ArgumentNullException(nameof(operations)); - } - - cancellationToken.ThrowIfCancellationRequested(); - - // Set telemetry attributes - SetActorAttributes(activity, actorId); - SetStateAttributes(activity, "read", operations.Count); - - try - { - var actorState = this._actorStates.GetOrAdd(actorId, _ => new ActorState()); - var results = new List(); - var operationTypes = new List(); - - foreach (var operation in operations) - { - switch (operation) - { - case GetValueOperation getValue: - var hasValue = actorState.Data.TryGetValue(getValue.Key, out var value); - results.Add(new GetValueResult(hasValue ? value : null)); - operationTypes.Add($"get:{getValue.Key}"); - break; - - case ListKeysOperation listKeys: - var keys = actorState.Data.Keys.ToList(); - - // Filter keys by prefix if provided - if (!string.IsNullOrEmpty(listKeys.KeyPrefix)) - { - keys = [.. keys.Where(key => key.StartsWith(listKeys.KeyPrefix, StringComparison.Ordinal))]; - } - - // Handle pagination if continuation token is provided - if (!string.IsNullOrEmpty(listKeys.ContinuationToken)) - { - // For this simple implementation, we'll parse the continuation token as an index - if (int.TryParse(listKeys.ContinuationToken, out int startIndex) && startIndex < keys.Count) - { - keys = [.. keys.Skip(startIndex)]; - } - else - { - keys = []; - } - } - - // For simplicity, we'll return all keys without pagination - // In a real implementation, you might want to implement proper pagination - results.Add(new ListKeysResult(keys.AsReadOnly(), continuationToken: null)); - operationTypes.Add($"list:{listKeys.KeyPrefix ?? "*"}"); - break; - - default: - var errorMessage = $"Unsupported read operation type: {operation.GetType().Name}"; - var exception = new InvalidOperationException(errorMessage); - SetErrorAttributes(activity, exception); - throw exception; - } - } - - // Set success attributes - SetOperationStatus(activity, true); - activity? - .SetTag("state.etag", actorState.ETag) - .SetTag("state.operations", string.Join(",", operationTypes)) - .SetTag("state.success", true); - - return new ValueTask(new ReadResponse(actorState.ETag, results.AsReadOnly())); - } - catch (Exception ex) - { - SetErrorAttributes(activity, ex); - throw; - } - } - - /// - /// Clears all stored actor state. This method is primarily intended for testing scenarios. - /// - public void Clear() - { - lock (this._lockObject) - { - this._actorStates.Clear(); - Interlocked.Exchange(ref this._globalETagCounter, 0); - } - } - - /// - /// Gets the current count of actors that have state stored. - /// - /// The number of actors with stored state. - public int ActorCount => this._actorStates.Count; - - /// - /// Gets the current count of keys stored for a specific actor. - /// - /// The actor identifier. - /// The number of keys stored for the specified actor, or 0 if the actor has no state. - public int GetKeyCount(ActorId actorId) => - this._actorStates.TryGetValue(actorId, out var state) ? state.Data.Count : 0; - - /// - /// Gets the current ETag for a specific actor. - /// - /// The actor identifier. - /// The current ETag for the specified actor, or "0" if the actor has no state. - public string GetETag(ActorId actorId) => - this._actorStates.TryGetValue(actorId, out var state) ? state.ETag : "0"; - - /// - /// Sets actor attributes on an activity. - /// - /// The activity to set attributes on. - /// The actor ID. - private static void SetActorAttributes(Activity? activity, ActorId actorId) => - activity? - .SetTag("actor.id", actorId.ToString()) - .SetTag("actor.type", actorId.Type.Name); - - /// - /// Sets state operation attributes on an activity. - /// - /// The activity to set attributes on. - /// The type of state operation. - /// Optional count of operations. - /// Optional ETag value. - private static void SetStateAttributes(Activity? activity, string operationType, int? operationCount = null, string? etag = null) - { - if (activity is null) - { - return; - } - - activity.SetTag("state.operation.type", operationType); - - if (operationCount.HasValue) - { - activity.SetTag("state.operation.count", operationCount.Value); - } - - if (!string.IsNullOrEmpty(etag)) - { - activity.SetTag("state.etag", etag); - } - } - - /// - /// Sets success/failure status on an activity. - /// - /// The activity to set status on. - /// Whether the operation was successful. - /// Optional error message for failures. - private static void SetOperationStatus(Activity? activity, bool success, string? errorMessage = null) - { - if (activity is null) - { - return; - } - - if (success) - { - activity.SetStatus(ActivityStatusCode.Ok); - } - else - { - activity.SetStatus(ActivityStatusCode.Error, errorMessage); - } - } - - /// - /// Sets error attributes on an activity. - /// - /// The activity to set error attributes on. - /// The exception that occurred. - private static void SetErrorAttributes(Activity? activity, Exception exception) => - activity? - .SetTag("error.type", exception.GetType().Name) - .SetTag("error.message", exception.Message) - .SetStatus(ActivityStatusCode.Error, exception.Message) - .AddEvent(new ActivityEvent("exception", DateTimeOffset.UtcNow, new ActivityTagsCollection - { - ["error.type"] = exception.GetType().Name, - ["error.message"] = exception.Message, - ["error.stack_trace"] = exception.StackTrace - })); -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ListKeysOperation.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ListKeysOperation.cs deleted file mode 100644 index 1f71b6d8c9..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ListKeysOperation.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json.Serialization; - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Represents an operation to list keys from an actor's state, with optional pagination support. -/// -/// Optional token for pagination to continue listing from a previous operation. -/// Optional prefix to filter keys. Only keys starting with this prefix will be returned. -public sealed class ListKeysOperation(string? continuationToken, string? keyPrefix = null) : ActorStateReadOperation -{ - /// - /// Gets the continuation token for pagination. - /// - [JsonPropertyName("continuationToken")] - public string? ContinuationToken { get; } = continuationToken; - - /// - /// Gets the key prefix for filtering. Only keys starting with this prefix will be returned. - /// - [JsonPropertyName("keyPrefix")] - public string? KeyPrefix { get; } = keyPrefix; - - /// - /// Gets the type of the read operation. - /// - public override ActorReadOperationType Type => ActorReadOperationType.ListKeys; -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ListKeysResult.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ListKeysResult.cs deleted file mode 100644 index a96fe7107e..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ListKeysResult.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Represents the result of a list keys operation containing the found keys and optional continuation token. -/// -/// The collection of keys found in the actor's state. -/// Optional token for pagination to retrieve additional keys. -public sealed class ListKeysResult(IReadOnlyCollection keys, string? continuationToken) : ActorReadResult -{ - /// - /// Gets the collection of keys found in the actor's state. - /// - [JsonPropertyName("keys")] - public IReadOnlyCollection Keys { get; } = keys; - - /// - /// Gets the continuation token for pagination. - /// - [JsonPropertyName("continuationToken")] - public string? ContinuationToken { get; } = continuationToken; - - /// - /// Gets the type of the read result operation. - /// - public override ActorReadResultType Type => ActorReadResultType.ListKeys; -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/Microsoft.Agents.AI.Runtime.Abstractions.csproj b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/Microsoft.Agents.AI.Runtime.Abstractions.csproj deleted file mode 100644 index 7c753596f3..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/Microsoft.Agents.AI.Runtime.Abstractions.csproj +++ /dev/null @@ -1,40 +0,0 @@ - - - - $(ProjectsTargetFrameworks) - $(ProjectsDebugTargetFrameworks) - $(NoWarn);IDE1006;IDE0130 - preview - Microsoft.Agents.AI.Runtime - - - - true - true - true - true - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ReadResponse.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ReadResponse.cs deleted file mode 100644 index 88d1bfcd40..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/ReadResponse.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// The response of a read request for an actor. -/// -/// The actor's last-known ETag value. -/// The ordered collection of results. -public sealed class ReadResponse(string eTag, IReadOnlyList results) -{ - /// - /// Gets the version of the state update. - /// - [JsonPropertyName("etag")] - public string ETag { get; } = eTag; - - /// - /// Gets the ordered collection of read operation results. - /// - [JsonPropertyName("results")] - public IReadOnlyList Results { get; } = results; -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/RemoveKeyOperation.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/RemoveKeyOperation.cs deleted file mode 100644 index 88bf0ce873..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/RemoveKeyOperation.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json.Serialization; - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Represents an operation to remove a key from an actor's state. -/// -/// The key to remove from the actor's state. -public sealed class RemoveKeyOperation(string Key) : ActorStateWriteOperation -{ - /// - /// Gets the key for the state operation. - /// - [JsonPropertyName("key")] - public string Key { get; } = Key; - - /// - /// Gets the type of the write operation. - /// - public override ActorWriteOperationType Type => ActorWriteOperationType.RemoveKey; -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/RequestStatus.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/RequestStatus.cs deleted file mode 100644 index 118044672d..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/RequestStatus.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json.Serialization; - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Represents the status of a request in the actor system. -/// -public enum RequestStatus -{ - /// - /// The request is pending and has not yet been processed. - /// - [JsonStringEnumMemberName("pending")] - Pending, - - /// - /// The request has been completed successfully. - /// - [JsonStringEnumMemberName("completed")] - Completed, - - /// - /// The request has failed. - /// - [JsonStringEnumMemberName("failed")] - Failed, - - /// - /// The request was not found, possibly due to it being deleted or never existing. - /// - [JsonStringEnumMemberName("not_found")] - NotFound, -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/RequestStatusExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/RequestStatusExtensions.cs deleted file mode 100644 index b3b96203dd..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/RequestStatusExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Provides extension methods for the enumeration. -/// -public static class RequestStatusExtensions -{ - /// - /// Determines if the request status indicates that the request has terminated. - /// - /// The request status to check. - /// if the request has terminated; otherwise, . - public static bool IsTerminated(this RequestStatus status) => status != RequestStatus.Pending; -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/SendRequestOperation.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/SendRequestOperation.cs deleted file mode 100644 index 79dc9992f0..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/SendRequestOperation.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json.Serialization; - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Represents an operation to send a request message to another actor. -/// -/// The request message to send. -public sealed class SendRequestOperation(ActorRequestMessage Message) : ActorMessageWriteOperation -{ - /// - /// Gets the message to send. - /// - [JsonPropertyName("message")] - public ActorRequestMessage Message { get; } = Message; - - /// - /// Gets the type of the write operation. - /// - public override ActorWriteOperationType Type => ActorWriteOperationType.SendRequest; -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/SetValueOperation.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/SetValueOperation.cs deleted file mode 100644 index 2acc110570..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/SetValueOperation.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Represents an operation to set a key-value pair in an actor's state. -/// -/// The key to set in the actor's state. -/// The value to associate with the key. -public sealed class SetValueOperation(string Key, JsonElement Value) : ActorStateWriteOperation -{ - /// - /// Gets the key for the state operation. - /// - [JsonPropertyName("key")] - public string Key { get; } = Key; - - /// - /// Gets the value for the state operation. - /// - [JsonPropertyName("value")] - public JsonElement Value { get; } = Value; - - /// - /// Gets the type of the write operation. - /// - public override ActorWriteOperationType Type => ActorWriteOperationType.SetValue; -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/UpdateRequestOperation.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/UpdateRequestOperation.cs deleted file mode 100644 index 660ab719d4..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/UpdateRequestOperation.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Represents an operation to update the status of an incoming request, possibly with a result. -/// The MessageId must correspond to a non-terminated request in the actor's inbox (Status is Pending). -/// -/// The identifier of the message to update. -/// The new status for the request. -/// The data associated with the status update (e.g., result for completed requests). -public sealed class UpdateRequestOperation(string MessageId, RequestStatus Status, JsonElement Data) : ActorMessageWriteOperation -{ - /// - /// Gets the identifier of the message to update. - /// - [JsonPropertyName("messageId")] - public string MessageId { get; } = MessageId; - - /// - /// Gets the new status for the request. - /// - [JsonPropertyName("status")] - public RequestStatus Status { get; } = Status; - - /// - /// Gets the data associated with the status update. - /// - [JsonPropertyName("data")] - public JsonElement Data { get; } = Data; - - /// - /// Gets the type of the write operation. - /// - public override ActorWriteOperationType Type => ActorWriteOperationType.UpdateRequest; -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/WriteResponse.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/WriteResponse.cs deleted file mode 100644 index 6b9f7f02fd..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Abstractions/WriteResponse.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json.Serialization; - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Represents the response of a write request for an actor. -/// -/// The actor's updated ETag value after the write operation. -/// Whether the write operation was successful. -public sealed class WriteResponse(string eTag, bool success) -{ - /// - /// Gets the version of the state update. - /// - [JsonPropertyName("etag")] - public string ETag { get; } = eTag; - - /// - /// Whether the write operation was successful. - /// - /// - /// If false, the write operation may have failed due to a concurrency conflict or other issue. - /// In either case the property will contain the last known ETag value of the actor's state. - /// - [JsonPropertyName("success")] - public bool Success { get; } = success; -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Storage.CosmosDB/ActorDocuments.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Storage.CosmosDB/ActorDocuments.cs deleted file mode 100644 index 275497d43f..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Storage.CosmosDB/ActorDocuments.cs +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Text.Json; - -namespace Microsoft.Agents.AI.Runtime.Storage.CosmosDB; - -/// -/// Root document for each actor that provides actor-level ETag semantics. -/// Every write operation updates this document to ensure a single ETag represents -/// the entire actor's state for optimistic concurrency control. -/// This document contains no actor state data. It only serves to track last modified -/// time and provide a single ETag for the actor's state. -/// -public sealed class ActorRootDocument -{ - /// - /// The document ID. - /// - public string Id { get; set; } = default!; - - /// - /// The actor type. - /// - public string ActorType { get; set; } = default!; - /// - /// The actor key. - /// - public string ActorKey { get; set; } = default!; - - /// - /// The last modified timestamp. - /// - public DateTimeOffset LastModified { get; set; } -} - -/// -/// Actor state document that represents a single key-value pair in the actor's state. -/// Document Structure (one per actor key): -/// { -/// "id": "state_sanitizedkey", // Unique document ID for the state entry -/// "actorId": "actor-123", // Partition key (actor ID) -/// "key": "foo", // Logical key for the state entry -/// "value": { "bar": 42, "baz": "hello" } // Arbitrary JsonElement payload -/// } -/// -public sealed class ActorStateDocument -{ - /// - /// The document ID. - /// - public string Id { get; set; } = default!; - - /// - /// The actor type. - /// - public string ActorType { get; set; } = default!; - /// - /// The actor key. - /// - public string ActorKey { get; set; } = default!; - - /// - /// The logical key for the state entry. - /// - public string Key { get; set; } = default!; - - /// - /// The value payload. - /// - public JsonElement Value { get; set; } = default!; -} - -/// -/// Projection class for Cosmos DB queries to retrieve keys. -/// -public sealed class KeyProjection -{ - /// - /// The key value. - /// - public string Key { get; set; } = default!; -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Storage.CosmosDB/CosmosActorStateJsonContext.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Storage.CosmosDB/CosmosActorStateJsonContext.cs deleted file mode 100644 index 3899e102b0..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Storage.CosmosDB/CosmosActorStateJsonContext.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Microsoft.Agents.AI.Runtime.Storage.CosmosDB; - -/// -/// Source-generated JSON type information for Cosmos DB actor state documents. -/// -[JsonSourceGenerationOptions( - JsonSerializerDefaults.Web, - UseStringEnumConverter = true, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - WriteIndented = false)] -[JsonSerializable(typeof(ActorStateDocument))] -[JsonSerializable(typeof(ActorRootDocument))] -[JsonSerializable(typeof(KeyProjection))] -[JsonSerializable(typeof(KeyProjection[]))] -[JsonSerializable(typeof(JsonElement))] -public sealed partial class CosmosActorStateJsonContext : JsonSerializerContext; diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Storage.CosmosDB/CosmosActorStateStorage.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Storage.CosmosDB/CosmosActorStateStorage.cs deleted file mode 100644 index 32d11106df..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Storage.CosmosDB/CosmosActorStateStorage.cs +++ /dev/null @@ -1,264 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Azure.Cosmos; - -namespace Microsoft.Agents.AI.Runtime.Storage.CosmosDB; - -/// -/// Cosmos DB implementation of actor state storage. -/// -public class CosmosActorStateStorage : IActorStateStorage, IAsyncDisposable -{ - private readonly LazyCosmosContainer _lazyContainer; - private const string InitialEtag = "0"; // Initial ETag value when no state exists - - /// - /// Constructs a new instance of with the specified Cosmos DB container. - /// - /// The Cosmos DB container to use for storage. - /// Thrown when is null. - public CosmosActorStateStorage(Container container) => this._lazyContainer = new LazyCosmosContainer(container); - - /// - /// This constructor is used by dependency injection to create an instance of - /// with a lazy-loaded Cosmos container whose initialization is deferred until first access. - /// - /// The lazy-loaded Cosmos container. - /// Thrown when is null. - internal CosmosActorStateStorage(LazyCosmosContainer lazyContainer) => - this._lazyContainer = lazyContainer ?? throw new ArgumentNullException(nameof(lazyContainer)); - - /// - /// Writes state changes to the actor's persistent storage. - /// - public async ValueTask WriteStateAsync( - ActorId actorId, - IReadOnlyCollection operations, - string etag, - CancellationToken cancellationToken = default) - { - if (operations.Count == 0) - { - throw new InvalidOperationException("No operations provided for write. At least one operation is required."); - } - - var container = await this._lazyContainer.GetContainerAsync().ConfigureAwait(false); - var (partitionKey, actorType, actorKey) = BuildPartitionKey(actorId); - var batch = container.CreateTransactionalBatch(partitionKey); - - // Add data operations to batch - foreach (var op in operations) - { - switch (op) - { - case SetValueOperation set: - var docId = GetDocumentId(set.Key); - - var item = new ActorStateDocument - { - Id = docId, - ActorType = actorType, - ActorKey = actorKey, - Key = set.Key, - Value = set.Value - }; - - batch.UpsertItem(item); - break; - - case RemoveKeyOperation remove: - var docToRemove = GetDocumentId(remove.Key); - batch.DeleteItem(docToRemove); - break; - - default: - throw new ArgumentException($"Unsupported write operation: {op.GetType().Name}"); - } - } - - // Add root document update to batch - var newRoot = new ActorRootDocument - { - Id = RootDocumentId, - ActorType = actorType, - ActorKey = actorKey, - LastModified = DateTimeOffset.UtcNow, - }; - - if (string.IsNullOrEmpty(etag) || etag == InitialEtag) - { - // No eTag provided or initial eTag - create new root document (will fail if it already exists) - batch.CreateItem(newRoot); - } - else - { - // eTag provided - replace existing root document with eTag check - batch.ReplaceItem(RootDocumentId, newRoot, new TransactionalBatchItemRequestOptions { IfMatchEtag = etag }); - } - - try - { - var result = await batch.ExecuteAsync(cancellationToken).ConfigureAwait(false); - if (!result.IsSuccessStatusCode) - { - _ = result.ErrorMessage; - return new WriteResponse(eTag: string.Empty, success: false); - } - - // Get the ETag from the root document operation (last operation in batch) - var rootResult = result[result.Count - 1]; - return new WriteResponse(eTag: rootResult.ETag, success: true); - } - catch (CosmosException) - { - // If any operation in the batch fails, we return failure - return new WriteResponse(eTag: string.Empty, success: false); - } - } - - /// - /// Reads state data from the actor's persistent storage. - /// - public async ValueTask ReadStateAsync( - ActorId actorId, - IReadOnlyCollection operations, - CancellationToken cancellationToken = default) - { - if (operations.Count == 0) - { - throw new InvalidOperationException("No operations provided for read. At least one operation is required."); - } - - var container = await this._lazyContainer.GetContainerAsync().ConfigureAwait(false); - var results = new List(); - - // Read root document first to get actor-level ETag - string actorETag = await GetActorETagAsync(container, actorId, cancellationToken).ConfigureAwait(false); - var actorType = actorId.Type.ToString(); - var actorKey = actorId.Key; - - foreach (var op in operations) - { - switch (op) - { - case GetValueOperation get: - var id = GetDocumentId(get.Key); - try - { - var response = await container.ReadItemAsync( - id, - GetPartitionKey(actorId), - cancellationToken: cancellationToken) - .ConfigureAwait(false); - - results.Add(new GetValueResult(response.Resource.Value)); - } - catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound) - { - results.Add(new GetValueResult(null)); - } - break; - - case ListKeysOperation list: - QueryDefinition query; - if (!string.IsNullOrEmpty(list.KeyPrefix)) - { - query = new QueryDefinition("SELECT c.key FROM c WHERE c.actorType = @actorType AND c.actorKey = @actorKey AND c.key != null AND STARTSWITH(c.key, @keyPrefix)") - .WithParameter("@actorType", actorType) - .WithParameter("@actorKey", actorKey) - .WithParameter("@keyPrefix", list.KeyPrefix); - } - else - { - query = new QueryDefinition("SELECT c.key FROM c WHERE c.actorType = @actorType AND c.actorKey = @actorKey AND c.key != null") - .WithParameter("@actorType", actorType) - .WithParameter("@actorKey", actorKey); - } - - var requestOptions = new QueryRequestOptions - { - PartitionKey = GetPartitionKey(actorId), - MaxItemCount = -1 // Use dynamic page size - }; - - var iterator = container.GetItemQueryIterator( - query, - list.ContinuationToken, - requestOptions); - - var keys = new List(); - string? continuationToken = null; - - while (iterator.HasMoreResults) - { - var page = await iterator.ReadNextAsync(cancellationToken).ConfigureAwait(false); - foreach (var projection in page) - { - keys.Add(projection.Key); - } - - continuationToken = page.ContinuationToken; - } - - results.Add(new ListKeysResult(keys, continuationToken)); - break; - - default: - throw new NotSupportedException($"Unsupported read operation: {op.GetType().Name}"); - } - } - - return new ReadResponse(actorETag, results); - } - - private static string GetDocumentId(string key) => $"state_{CosmosIdSanitizer.Sanitize(key)}"; - private const string RootDocumentId = "rootdoc"; - - private static PartitionKey GetPartitionKey(ActorId actorId) - { - var (partitionKey, _, _) = BuildPartitionKey(actorId); - return partitionKey; - } - - private static (PartitionKey partitionKey, string actorType, string actorKey) BuildPartitionKey(ActorId actorId) - { - var actorType = actorId.Type.ToString(); - var actorKey = actorId.Key; - var partitionKey = new PartitionKeyBuilder().Add(actorType).Add(actorKey).Build(); - return (partitionKey, actorType, actorKey); - } - - /// - /// Gets the current ETag for the actor's root document. - /// Returns a generated ETag if no root document exists. - /// - private static async ValueTask GetActorETagAsync(Container container, ActorId actorId, CancellationToken cancellationToken) - { - try - { - var rootResponse = await container.ReadItemAsync( - RootDocumentId, - GetPartitionKey(actorId), - cancellationToken: cancellationToken).ConfigureAwait(false); - return rootResponse.ETag; - } - catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound) - { - // No root document means no actor state exists - return initial ETag - return InitialEtag; - } - } - - /// - /// Disposes the Cosmos DB container asynchronously. - /// - public async ValueTask DisposeAsync() - { - await this._lazyContainer.DisposeAsync().ConfigureAwait(false); - GC.SuppressFinalize(this); - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Storage.CosmosDB/CosmosIdSanitizer.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Storage.CosmosDB/CosmosIdSanitizer.cs deleted file mode 100644 index f338514a16..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Storage.CosmosDB/CosmosIdSanitizer.cs +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; - -namespace Microsoft.Agents.AI.Runtime.Storage.CosmosDB; - -// CosmosIdSanitizer is used to sanitize Cosmos DB IDs by replacing characters that are -// not allowed in Cosmos DB IDs with a safe escape sequence. This implementation was -// inspired heavily by the one in Orleans, with some modifications made to enable -// targeting NET472. -internal static class CosmosIdSanitizer -{ - private const char EscapeChar = '~'; - public const char SeparatorChar = '_'; - - private static ReadOnlySpan SanitizedCharacters => ['/', '\\', '?', '#', SeparatorChar, EscapeChar]; - private static ReadOnlySpan ReplacementCharacters => ['0', '1', '2', '3', '4', '5']; - - public static string Sanitize(string input) - { - int extraChars = CountSanitizedCharacters(input.AsSpan()); - - if (extraChars == 0) - { - return input; - } - -#if NET8_0_OR_GREATER - return string.Create(input.Length + extraChars, input, (output, state) => - Encode(state.AsSpan(), output)); -#else - var result = new char[input.Length + extraChars]; - Encode(input.AsSpan(), result); - return new string(result, 0, input.Length + extraChars); -#endif - } - - public static string Unsanitize(string input) - { - int escapeCount = CountEscapeCharacters(input.AsSpan()); - - if (escapeCount == 0) - { - return input; - } - -#if NET8_0_OR_GREATER - return string.Create(input.Length - escapeCount, input, (output, state) => - Decode(state.AsSpan(), output)); -#else - var result = new char[input.Length - escapeCount]; - Decode(input.AsSpan(), result); - return new string(result, 0, input.Length - escapeCount); -#endif - } - - private static int CountSanitizedCharacters(ReadOnlySpan input) - { - int count = 0; - foreach (var c in input) - { - if (SanitizedCharacters.IndexOf(c) >= 0) - { - count++; - } - } - return count; - } - - private static int CountEscapeCharacters(ReadOnlySpan input) - { - int count = 0; - foreach (var c in input) - { - if (c == EscapeChar) - { - count++; - } - } - return count; - } - - private static void Encode(ReadOnlySpan input, Span output) - { - int j = 0; - foreach (var c in input) - { - int idx = SanitizedCharacters.IndexOf(c); - if (idx < 0) - { - output[j++] = c; - } - else - { - output[j++] = EscapeChar; - output[j++] = ReplacementCharacters[idx]; - } - } - } - - private static void Decode(ReadOnlySpan input, Span output) - { - int j = 0; - bool isEscaped = false; - - foreach (var c in input) - { - if (isEscaped) - { - int idx = ReplacementCharacters.IndexOf(c); - if (idx < 0) - { - throw new ArgumentException("Input is not in a valid format: Encountered unsupported escape sequence"); - } - - output[j++] = SanitizedCharacters[idx]; - isEscaped = false; - } - else if (c == EscapeChar) - { - isEscaped = true; - } - else - { - output[j++] = c; - } - } - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Storage.CosmosDB/LazyCosmosContainer.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Storage.CosmosDB/LazyCosmosContainer.cs deleted file mode 100644 index 78db567e95..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Storage.CosmosDB/LazyCosmosContainer.cs +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Azure.Cosmos; -namespace Microsoft.Agents.AI.Runtime.Storage.CosmosDB; - -/// -/// A lazy wrapper around a Cosmos DB Container. -/// This avoids performing async I/O-bound operations (i.e. Cosmos DB setup) during -/// DI registration, deferring them until first access. -/// -internal sealed class LazyCosmosContainer : IAsyncDisposable -{ -#if !NET - [ThreadStatic] - private static Random? t_random; -#endif - - private readonly CosmosClient? _cosmosClient; - private readonly string? _databaseName; - private readonly string? _containerName; - - private readonly CancellationTokenSource _cts = new(); - private Task? _initTask; - - // internal for testing - internal static readonly string[] CosmosPartitionKeyPaths = ["/actorType", "/actorKey"]; - - /// - /// LazyCosmosContainer constructor that initializes the container lazily. - /// - public LazyCosmosContainer(CosmosClient cosmosClient, string databaseName, string containerName) - { - this._cosmosClient = cosmosClient ?? throw new ArgumentNullException(nameof(cosmosClient)); - this._databaseName = databaseName ?? throw new ArgumentNullException(nameof(databaseName)); - this._containerName = containerName ?? throw new ArgumentNullException(nameof(containerName)); - } - /// - /// LazyCosmosContainer constructor that accepts an existing Container instance. - /// - public LazyCosmosContainer(Container container) - { - if (container is null) - { - throw new ArgumentNullException(nameof(container)); - } - - this._initTask = Task.FromResult(container); - } - - /// - /// Gets the Container, initializing it if necessary. - /// - public Task GetContainerAsync() - => this._initTask ??= this.InitializeWithRetryAsync(this._cts.Token); - - private async Task InitializeWithRetryAsync(CancellationToken cancellationToken) - { - var baseDelay = TimeSpan.FromSeconds(1); - var maxDelay = TimeSpan.FromSeconds(30); - var previousDelay = baseDelay; - - while (true) - { - cancellationToken.ThrowIfCancellationRequested(); - try - { - return await this.InitializeContainerAsync(cancellationToken).ConfigureAwait(false); - } - catch (CosmosException ex) when (IsTransient(ex)) - { - // If server provided RetryAfter, respect it but add a small jitter so clients don't retry in perfect sync. - if (ex.RetryAfter is not null && ex.RetryAfter > TimeSpan.Zero) - { - var retry = ex.RetryAfter.Value; - var jitterMs = RandomNextDouble() * retry.TotalMilliseconds; // 0..retry - var delay = retry + TimeSpan.FromMilliseconds(jitterMs); - await Task.Delay(delay, cancellationToken).ConfigureAwait(false); - previousDelay = delay; - continue; - } - - // sleep = min(maxDelay, random(baseDelay, previousDelay * 3)) - var minMs = baseDelay.TotalMilliseconds; - var maxMs = Math.Min(maxDelay.TotalMilliseconds, Math.Max(minMs, previousDelay.TotalMilliseconds * 3)); - var sleepMs = (RandomNextDouble() * (maxMs - minMs)) + minMs; - var jitterDelay = TimeSpan.FromMilliseconds(sleepMs); - - await Task.Delay(jitterDelay, cancellationToken).ConfigureAwait(false); - previousDelay = jitterDelay; - } - } - } - - private async Task InitializeContainerAsync(CancellationToken cancellationToken) - { - // Create database if it doesn't exist - var database = await this._cosmosClient!.CreateDatabaseIfNotExistsAsync(this._databaseName!, cancellationToken: cancellationToken).ConfigureAwait(false); - - var containerProperties = new ContainerProperties(this._containerName!, CosmosPartitionKeyPaths) - { - Id = this._containerName!, - IndexingPolicy = new IndexingPolicy - { - IndexingMode = IndexingMode.Consistent, - Automatic = true - }, - PartitionKeyPaths = CosmosPartitionKeyPaths - }; - - // Add composite index for efficient queries - containerProperties.IndexingPolicy.CompositeIndexes.Add( - [ - new() { Path = "/actorType", Order = CompositePathSortOrder.Ascending }, - new() { Path = "/actorKey", Order = CompositePathSortOrder.Ascending }, - new() { Path = "/key", Order = CompositePathSortOrder.Ascending } - ]); - - var container = await database.Database.CreateContainerIfNotExistsAsync(containerProperties, cancellationToken: cancellationToken).ConfigureAwait(false); - return container.Container; - } - - private static bool IsTransient(Exception exception) => exception switch - { - CosmosException cosmosEx => cosmosEx.StatusCode switch - { -#if NET9_0_OR_GREATER - HttpStatusCode.TooManyRequests => true, // 429 - Rate limited -#endif - HttpStatusCode.InternalServerError => true, // 500 - Server error - HttpStatusCode.BadGateway => true, // 502 - Bad gateway - HttpStatusCode.ServiceUnavailable => true, // 503 - Service unavailable - HttpStatusCode.GatewayTimeout => true, // 504 - Gateway timeout - HttpStatusCode.RequestTimeout => true, // 408 - Request timeout - _ => false - }, - OperationCanceledException or ArgumentException => false, - _ => true // Retry other exceptions (network issues, etc.) - }; - - private static double RandomNextDouble() => -#if NET - Random.Shared.NextDouble(); -#else - (t_random ??= new()).NextDouble(); -#endif - - public ValueTask DisposeAsync() - { - this._cts?.Cancel(); - this._cts?.Dispose(); - return default; - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Storage.CosmosDB/Microsoft.Agents.AI.Runtime.Storage.CosmosDB.csproj b/dotnet/src/Microsoft.Agents.AI.Runtime.Storage.CosmosDB/Microsoft.Agents.AI.Runtime.Storage.CosmosDB.csproj deleted file mode 100644 index bac406453c..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Storage.CosmosDB/Microsoft.Agents.AI.Runtime.Storage.CosmosDB.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - $(ProjectsTargetFrameworks) - $(ProjectsDebugTargetFrameworks) - $(NoWarn);IDE1006;IDE0130 - preview - - - - - - - - - - - - - - - - - diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime.Storage.CosmosDB/ServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Runtime.Storage.CosmosDB/ServiceCollectionExtensions.cs deleted file mode 100644 index c135ba140d..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime.Storage.CosmosDB/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json; -using Microsoft.Azure.Cosmos; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.Agents.AI.Runtime.Storage.CosmosDB; - -#pragma warning disable VSTHRD002 - -/// -/// Extension methods for configuring Cosmos DB actor state storage in dependency injection. -/// -public static class ServiceCollectionExtensions -{ - /// - /// Adds Cosmos DB actor state storage to the service collection. - /// - /// The service collection to add services to. - /// The Cosmos DB connection string. - /// The database name to use for actor state storage. - /// The container name to use for actor state storage. Defaults to "ActorState". - /// The service collection for chaining. - public static IServiceCollection AddCosmosActorStateStorage( - this IServiceCollection services, - string connectionString, - string databaseName, - string containerName = "ActorState") - { - // Register CosmosClient as singleton - services.AddSingleton(serviceProvider => - { - var cosmosClientOptions = new CosmosClientOptions - { - ApplicationName = "AgentFramework", - ConnectionMode = ConnectionMode.Direct, - ConsistencyLevel = ConsistencyLevel.Session, - UseSystemTextJsonSerializerWithOptions = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - TypeInfoResolver = CosmosActorStateJsonContext.Default - } - }; - - return new CosmosClient(connectionString, cosmosClientOptions); - }); - - // Register LazyCosmosContainer as singleton - services.AddSingleton(serviceProvider => - { - var cosmosClient = serviceProvider.GetRequiredService(); - return new LazyCosmosContainer(cosmosClient, databaseName, containerName); - }); - - // Register the storage implementation - services.AddSingleton(serviceProvider => - { - var lazyContainer = serviceProvider.GetRequiredService(); - return new CosmosActorStateStorage(lazyContainer); - }); - - return services; - } - - /// - /// Adds Cosmos DB actor state storage to the service collection using an existing CosmosClient from DI. - /// - /// The service collection to add services to. - /// The database name to use for actor state storage. - /// The container name to use for actor state storage. Defaults to "ActorState". - /// The service collection for chaining. - public static IServiceCollection AddCosmosActorStateStorage( - this IServiceCollection services, - string databaseName, - string containerName = "ActorState") - { - // Register LazyCosmosContainer as singleton using existing CosmosClient - services.AddSingleton(serviceProvider => - { - var cosmosClient = serviceProvider.GetRequiredService(); - return new LazyCosmosContainer(cosmosClient, databaseName, containerName); - }); - - // Register the storage implementation - services.AddSingleton(serviceProvider => - { - var lazyContainer = serviceProvider.GetRequiredService(); - return new CosmosActorStateStorage(lazyContainer); - }); - - return services; - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime/ActivityExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Runtime/ActivityExtensions.cs deleted file mode 100644 index d07cae8b81..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime/ActivityExtensions.cs +++ /dev/null @@ -1,396 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using static Microsoft.Agents.AI.Runtime.ActorRuntimeOpenTelemetryConsts; - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Helper methods for setting common telemetry attributes on activities. -/// -internal static class ActivityExtensions -{ - public const string ActorCreated = EventInfo.Names.ActorCreated; - public const string ActorStarted = EventInfo.Names.ActorStarted; - public const string MessageSent = EventInfo.Names.MessageSent; - public const string MessageReceived = EventInfo.Names.MessageReceived; - public const string RequestCompleted = EventInfo.Names.RequestCompleted; - - // Re-export common status values for convenience - public const string Started = "started"; - public const string Sent = "sent"; - public const string Enqueued = "enqueued"; - public const string Created = "created"; - public const string Found = "found"; - public const string HandleCreated = "handle_created"; - - /// - /// Sets common actor attributes on an activity. - /// - /// The activity to set attributes on. - /// The actor ID. - /// Optional operation name. - public static void SetActorAttributes(this System.Diagnostics.Activity? activity, ActorId actorId, string? operation = null) - { - if (activity is null) - { - return; - } - - activity - .SetTag(Actor.Id, actorId.ToString()) - .SetTag(Actor.Type, actorId.Type.Name) - .SetTag(Actor.RpcSystem, Actor.SystemName); - - if (!string.IsNullOrEmpty(operation)) - { - activity.SetTag(Actor.Operation, operation); - } - } - - /// - /// Sets common message attributes on an activity. - /// - /// The activity to set attributes on. - /// The message ID. - /// Optional message type. - /// Optional message method. - public static void SetMessageAttributes(this System.Diagnostics.Activity? activity, string messageId, string? messageType = null, string? method = null) - { - if (activity is null) - { - return; - } - - activity.SetTag(Message.Id, messageId); - - if (!string.IsNullOrEmpty(messageType)) - { - activity.SetTag(Message.Type, messageType); - } - - if (!string.IsNullOrEmpty(method)) - { - activity.SetTag(Message.Method, method); - } - } - - /// - /// Sets common request attributes on an activity. - /// - /// The activity to set attributes on. - /// The request ID. - /// Optional request method. - /// Optional timeout value. - public static void SetRequestAttributes(this System.Diagnostics.Activity? activity, string requestId, string? method = null, System.TimeSpan? timeout = null) - { - if (activity is null) - { - return; - } - - activity.SetTag(Request.Id, requestId); - - if (!string.IsNullOrEmpty(method)) - { - activity.SetTag(Request.Method, method); - } - - if (timeout.HasValue) - { - activity.SetTag(Request.Timeout, timeout.Value.TotalMilliseconds); - } - } - - /// - /// Sets common state operation attributes on an activity. - /// - /// The activity to set attributes on. - /// The type of state operation. - /// Optional count of operations. - /// Optional ETag value. - public static void SetStateAttributes(this System.Diagnostics.Activity? activity, string operationType, int? operationCount = null, string? etag = null) - { - if (activity is null) - { - return; - } - - activity.SetTag(State.OperationType, operationType); - - if (operationCount.HasValue) - { - activity.SetTag(State.OperationCount, operationCount.Value); - } - - if (!string.IsNullOrEmpty(etag)) - { - activity.SetTag(State.ETag, etag); - } - } - - /// - /// Sets success/failure status on an activity. - /// - /// The activity to set status on. - /// Whether the operation was successful. - /// Optional error message for failures. - public static void SetOperationStatus(this System.Diagnostics.Activity? activity, bool success, string? errorMessage = null) - { - if (activity is null) - { - return; - } - - if (success) - { - activity.SetStatus(System.Diagnostics.ActivityStatusCode.Ok); - } - else - { - activity.SetStatus(System.Diagnostics.ActivityStatusCode.Error, errorMessage); - } - } - - /// - /// Sets error attributes on an activity. - /// - /// The activity to set error attributes on. - /// The exception that occurred. - /// Optional custom error type. - public static void SetErrorAttributes(this System.Diagnostics.Activity? activity, System.Exception exception, string? errorType = null) => - activity? - .SetTag(ErrorInfo.Type, errorType ?? exception.GetType().Name) - .SetTag(ErrorInfo.Message, exception.Message) - .SetStatus(System.Diagnostics.ActivityStatusCode.Error, exception.Message) - .AddEvent(new System.Diagnostics.ActivityEvent("exception", System.DateTimeOffset.UtcNow, new System.Diagnostics.ActivityTagsCollection - { - [ErrorInfo.Type] = errorType ?? exception.GetType().Name, - [ErrorInfo.Message] = exception.Message, - [ErrorInfo.StackTrace] = exception.StackTrace - })); - - /// - /// Sets RPC-style attributes for actor operations. - /// - /// The activity to set attributes on. - /// The RPC service name. - /// The RPC method name. - public static void SetRpcAttributes(this System.Diagnostics.Activity? activity, string service, string method) => - activity? - .SetTag(Actor.RpcSystem, Actor.SystemName) - .SetTag(Actor.RpcService, service) - .SetTag(Actor.RpcMethod, method); - - /// - /// Sets up complete telemetry for actor retrieval/creation operations. - /// - /// The activity to set attributes on. - /// The actor ID. - /// Whether the actor already exists. - /// Whether the actor was started. - public static void SetupActorOperation(this System.Diagnostics.Activity? activity, ActorId actorId, bool? exists = null, bool? started = null) - { - if (activity is null) - { - return; - } - - SetActorAttributes(activity, actorId); - SetRpcAttributes(activity, "ActorRuntime", "GetOrCreateActor"); - - if (exists.HasValue) - { - activity.SetTag(Actor.Exists, exists.Value); - } - - if (started.HasValue) - { - activity.SetTag(Actor.Started, started.Value); - } - } - - /// - /// Sets up complete telemetry for message operations. - /// - /// The activity to set attributes on. - /// The actor ID. - /// The message ID. - /// Optional message type. - /// Optional message method. - /// Optional message status. - public static void SetupMessageOperation(this System.Diagnostics.Activity? activity, ActorId actorId, string messageId, string? messageType = null, string? method = null, string? status = null) - { - if (activity is null) - { - return; - } - - SetActorAttributes(activity, actorId); - SetMessageAttributes(activity, messageId, messageType, method); - - if (!string.IsNullOrEmpty(status)) - { - activity.SetTag(Message.Status, status); - } - } - - /// - /// Sets up complete telemetry for request operations. - /// - /// The activity to set attributes on. - /// The actor ID. - /// The request ID. - /// Optional request method. - /// The RPC service name. - /// The RPC method name. - /// Optional timeout value. - public static void SetupRequestOperation(this System.Diagnostics.Activity? activity, ActorId actorId, string requestId, string? method = null, string service = "ActorClient", string rpcMethod = "SendRequest", System.TimeSpan? timeout = null) - { - if (activity is null) - { - return; - } - - SetActorAttributes(activity, actorId); - SetRequestAttributes(activity, requestId, method, timeout); - SetRpcAttributes(activity, service, rpcMethod); - } - - /// - /// Sets up complete telemetry for state operations. - /// - /// The activity to set attributes on. - /// The actor ID. - /// The type of state operation. - /// Optional count of operations. - /// Optional ETag value. - public static void SetupStateOperation(this System.Diagnostics.Activity? activity, ActorId actorId, string operationType, int? operationCount = null, string? etag = null) - { - if (activity is null) - { - return; - } - - SetActorAttributes(activity, actorId); - SetStateAttributes(activity, operationType, operationCount, etag); - } - - /// - /// Records successful completion of an operation with optional additional attributes. - /// - /// The activity to update. - /// Optional additional tags to set. - public static void RecordSuccess(this System.Diagnostics.Activity? activity, params (string key, object? value)[] additionalTags) - { - if (activity is null) - { - return; - } - - SetOperationStatus(activity, true); - - foreach (var (key, value) in additionalTags) - { - activity.SetTag(key, value); - } - } - - /// - /// Records failure of an operation with error details. - /// - /// The activity to update. - /// The exception that occurred. - /// Optional custom error type. - /// Optional additional tags to set. - public static void RecordFailure(this System.Diagnostics.Activity? activity, System.Exception exception, string? errorType = null, params (string key, object? value)[] additionalTags) - { - if (activity is null) - { - return; - } - - SetErrorAttributes(activity, exception, errorType); - - foreach (var (key, value) in additionalTags) - { - activity.SetTag(key, value); - } - } - - /// - /// Adds an event with common actor context. - /// - /// The activity to add the event to. - /// The name of the event. - /// The actor ID. - /// Optional additional event data. - public static void AddActorEvent(this System.Diagnostics.Activity? activity, string eventName, ActorId actorId, params (string key, object? value)[] additionalData) - { - if (activity is null) - { - return; - } - - var tags = new System.Diagnostics.ActivityTagsCollection - { - [Actor.Id] = actorId.ToString(), - [Actor.Type] = actorId.Type.Name - }; - - foreach (var (key, value) in additionalData) - { - tags[key] = value; - } - - activity.AddEvent(new System.Diagnostics.ActivityEvent(eventName, System.DateTimeOffset.UtcNow, tags)); - } - - /// - /// Records successful completion and adds an event in a single terse call. - /// - /// The activity to update. - /// The name of the event to add. - /// The actor ID for the event. - /// Status tags to set on the activity. - /// Additional event data. - public static void CompleteWithEvent(this System.Diagnostics.Activity? activity, string eventName, ActorId actorId, (string key, object? value)[] statusTags, params (string key, object? value)[] eventData) - { - if (activity is null) - { - return; - } - - RecordSuccess(activity, statusTags); - AddActorEvent(activity, eventName, actorId, eventData); - } - - /// - /// Complete with event - ultra-terse single-line calls. - /// - public static void Complete(this System.Diagnostics.Activity? activity, string @event, ActorId actor, string status, params (string, object?)[] data) => - CompleteWithEvent(activity, @event, actor, [(Request.Status, status)], data); - - /// - /// Complete with multiple status tags and event. - /// - public static void Complete(this System.Diagnostics.Activity? activity, string @event, ActorId actor, (string, object?)[] status, params (string, object?)[] data) => - CompleteWithEvent(activity, @event, actor, status, data); - - /// - /// Record success with single status. - /// - public static void Success(this System.Diagnostics.Activity? activity, string status) => - RecordSuccess(activity, (Request.Status, status)); - - /// - /// Add actor event. - /// - public static void Event(this System.Diagnostics.Activity? activity, string @event, ActorId actor, params (string, object?)[] data) => - AddActorEvent(activity, @event, actor, data); - - /// - /// Record failure. - /// - public static void Fail(this System.Diagnostics.Activity? activity, System.Exception exception, string? status = null) => - RecordFailure(activity, exception, null, status is not null ? (Request.Status, status) : default); -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime/ActorRuntimeBuilder.cs b/dotnet/src/Microsoft.Agents.AI.Runtime/ActorRuntimeBuilder.cs deleted file mode 100644 index a7414bbc9f..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime/ActorRuntimeBuilder.cs +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Internal implementation of that manages actor type registrations -/// and their associated factory methods for the actor runtime system. -/// -internal sealed class ActorRuntimeBuilder : IActorRuntimeBuilder -{ - /// - /// Gets the collection of registered actor types and their corresponding factory methods. - /// - /// - /// A dictionary where keys are instances and values are factory functions - /// that create instances given an and . - /// - public Dictionary> ActorFactories { get; } = []; - - /// - /// Gets or creates an instance for the specified host application builder. - /// If an instance already exists in the service collection, it returns the existing instance. - /// Otherwise, it creates a new instance and registers it as a singleton service. - /// - /// The host application builder to associate with the actor runtime builder. - /// - /// An instance that can be used to configure actor types. - /// - /// Thrown when is null. - public static ActorRuntimeBuilder GetOrAdd(IHostApplicationBuilder builder) - { - Shared.Diagnostics.Throw.IfNull(builder); - var services = builder.Services; - var descriptor = services.FirstOrDefault(s => s.ImplementationInstance is ActorRuntimeBuilder); - if (descriptor?.ImplementationInstance is not ActorRuntimeBuilder instance) - { - instance = new ActorRuntimeBuilder(); - services.Add(ServiceDescriptor.Singleton(instance)); - instance.ConfigureServices(services); - } - - return instance; - } - - /// - /// Initializes a new instance of the class. - /// - private ActorRuntimeBuilder() - { - } - - /// - /// Registers an actor type with its factory method in the actor runtime. - /// - /// The actor type to register. - /// - /// The factory method that creates instances of the actor. This function receives an - /// for dependency injection and an - /// for the actor's runtime context, and returns an instance. - /// - /// - /// Thrown when an actor type with the same name is already registered. - /// - /// - /// Each actor type can only be registered once. Attempting to register the same actor type - /// multiple times will result in an exception being thrown by the underlying dictionary. - /// - public void AddActorType(ActorType type, Func activator) => - this.ActorFactories.Add(type, activator); - - private void ConfigureServices(IServiceCollection services) - { - services.AddSingleton(this); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(sp => - { - var actorStateStorage = sp.GetRequiredService(); - return new InProcessActorRuntime(sp, this.ActorFactories, actorStateStorage); - }); - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime/ActorRuntimeHostingExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Runtime/ActorRuntimeHostingExtensions.cs deleted file mode 100644 index 0fdc4c36b3..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime/ActorRuntimeHostingExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using Microsoft.Extensions.Hosting; - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Provides extension methods for configuring actor runtime services in a host application. -/// -public static class ActorRuntimeHostingExtensions -{ - /// - /// Adds actor runtime services to the specified host application builder. - /// - /// The to configure. - /// An that can be used to further configure the actor runtime. - /// Thrown when is null. - public static IActorRuntimeBuilder AddActorRuntime(this IHostApplicationBuilder builder) => - ActorRuntimeBuilder.GetOrAdd(builder); -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime/ActorRuntimeOpenTelemetryConsts.cs b/dotnet/src/Microsoft.Agents.AI.Runtime/ActorRuntimeOpenTelemetryConsts.cs deleted file mode 100644 index 434a56c19e..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime/ActorRuntimeOpenTelemetryConsts.cs +++ /dev/null @@ -1,782 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// Provides constants used by actor runtime telemetry services following OpenTelemetry semantic conventions. -/// Extends the base agent telemetry with runtime-specific attributes and operations. -/// -internal static class ActorRuntimeOpenTelemetryConsts -{ - /// - /// The default source name for actor runtime telemetry. - /// - public const string DefaultSourceName = "Microsoft.Agents.AI.Runtime"; - - /// - /// The default source name for in-process actor runtime telemetry. - /// - public const string InProcessSourceName = "Microsoft.Agents.AI.Runtime.InProcess"; - - /// - /// The unit for count measurements. - /// - public const string CountUnit = "count"; - - /// - /// The unit for byte measurements. - /// - public const string ByteUnit = "byte"; - - /// - /// Constants for runtime operation names following OpenTelemetry semantic conventions. - /// These operations align with RPC and GenAI conventions where applicable. - /// - public static class Operations - { - /// - /// Actor creation operation. - /// - public const string CreateActor = "create_actor"; - - /// - /// Actor retrieval operation. - /// - public const string GetActor = "get_actor"; - - /// - /// Actor invocation operation (aligns with GenAI agent invoke conventions). - /// - public const string InvokeActor = "invoke_actor"; - - /// - /// Actor start operation. - /// - public const string StartActor = "start_actor"; - - /// - /// Actor stop operation. - /// - public const string StopActor = "stop_actor"; - - /// - /// Actor dispose operation. - /// - public const string DisposeActor = "dispose_actor"; - - /// - /// Message send operation. - /// - public const string SendMessage = "send_message"; - - /// - /// Message receive operation. - /// - public const string ReceiveMessage = "receive_message"; - - /// - /// Message process operation. - /// - public const string ProcessMessage = "process_message"; - - /// - /// Request send operation (follows RPC client pattern). - /// - public const string SendRequest = "send_request"; - - /// - /// Request receive operation (follows RPC server pattern). - /// - public const string ReceiveRequest = "receive_request"; - - /// - /// Request process operation. - /// - public const string ProcessRequest = "process_request"; - - /// - /// Response send operation. - /// - public const string SendResponse = "send_response"; - - /// - /// Response receive operation. - /// - public const string ReceiveResponse = "receive_response"; - - /// - /// Progress update operation. - /// - public const string ProgressUpdate = "progress_update"; - - /// - /// State read operation. - /// - public const string StateRead = "state_read"; - - /// - /// State write operation. - /// - public const string StateWrite = "state_write"; - - /// - /// Actor runtime initialization operation. - /// - public const string InitializeRuntime = "initialize_runtime"; - - /// - /// Actor runtime shutdown operation. - /// - public const string ShutdownRuntime = "shutdown_runtime"; - } - - /// - /// Constants for span naming patterns following OpenTelemetry semantic conventions. - /// Span names should be low-cardinality and follow the pattern: {namespace} {operation_name} [{target}] - /// - public static class SpanNames - { - /// - /// Base pattern for actor operations: "actor {operation}" - /// - public const string ActorOperationPattern = "actor {0}"; - - /// - /// Pattern for actor operations with specific actor type: "actor {operation} {actor_type}" - /// - public const string ActorOperationWithTypePattern = "actor {0} {1}"; - - /// - /// Pattern for message operations: "actor.message {operation}" - /// - public const string MessageOperationPattern = "actor.message {0}"; - - /// - /// Pattern for request operations: "actor.request {operation}" - /// - public const string RequestOperationPattern = "actor.request {0}"; - - /// - /// Pattern for state operations: "actor.state {operation}" - /// - public const string StateOperationPattern = "actor.state {0}"; - - /// - /// Pattern for runtime operations: "actor.runtime {operation}" - /// - public const string RuntimeOperationPattern = "actor.runtime {0}"; - - /// - /// Formats a span name for actor operations. - /// - /// The operation name - /// Formatted span name - public static string FormatActorOperation(string operation) => $"actor {operation}"; - - /// - /// Formats a span name for actor operations with actor type. - /// - /// The operation name - /// The actor type - /// Formatted span name - public static string FormatActorOperationWithType(string operation, string actorType) => $"actor {operation} {actorType}"; - - /// - /// Formats a span name for message operations. - /// - /// The operation name - /// Formatted span name - public static string FormatMessageOperation(string operation) => $"actor.message {operation}"; - - /// - /// Formats a span name for request operations. - /// - /// The operation name - /// Formatted span name - public static string FormatRequestOperation(string operation) => $"actor.request {operation}"; - - /// - /// Formats a span name for state operations. - /// - /// The operation name - /// Formatted span name - public static string FormatStateOperation(string operation) => $"actor.state {operation}"; - - /// - /// Formats a span name for runtime operations. - /// - /// The operation name - /// Formatted span name - public static string FormatRuntimeOperation(string operation) => $"actor.runtime {operation}"; - } - - /// - /// Constants for actor-related telemetry attributes. - /// - public static class Actor - { - /// - /// The attribute name for the actor ID. - /// - public const string Id = "actor.id"; - - /// - /// The attribute name for the actor type. - /// - public const string Type = "actor.type"; - - /// - /// The attribute name for the actor key. - /// - public const string Key = "actor.key"; - - /// - /// The attribute name for the actor operation. - /// - public const string Operation = "actor.operation"; - - /// - /// The attribute name for whether the actor exists. - /// - public const string Exists = "actor.exists"; - - /// - /// The attribute name for whether the actor was started. - /// - public const string Started = "actor.started"; - - /// - /// The attribute name for the actor runtime type. - /// - public const string RuntimeType = "actor.runtime.type"; - - /// - /// The attribute name for the actor state. - /// - public const string State = "actor.state"; - - /// - /// RPC system identifier for actor runtime (follows RPC semantic conventions). - /// - public const string RpcSystem = "rpc.system"; - - /// - /// RPC service name for actor runtime (follows RPC semantic conventions). - /// - public const string RpcService = "rpc.service"; - - /// - /// RPC method name for actor runtime (follows RPC semantic conventions). - /// - public const string RpcMethod = "rpc.method"; - - /// - /// The system name for actor runtime operations. - /// - public const string SystemName = "actor_runtime"; - - /// - /// Constants for actor lifecycle attributes. - /// - public static class Lifecycle - { - /// - /// The attribute name for the actor creation time. - /// - public const string CreatedAt = "actor.lifecycle.created_at"; - - /// - /// The attribute name for the actor start time. - /// - public const string StartedAt = "actor.lifecycle.started_at"; - - /// - /// The attribute name for the actor stop time. - /// - public const string StoppedAt = "actor.lifecycle.stopped_at"; - - /// - /// The attribute name for the actor uptime. - /// - public const string Uptime = "actor.lifecycle.uptime"; - } - - /// - /// Constants for actor context attributes. - /// - public static class Context - { - /// - /// The attribute name for the actor context type. - /// - public const string Type = "actor.context.type"; - - /// - /// The attribute name for the actor context status. - /// - public const string Status = "actor.context.status"; - - /// - /// The attribute name for the actor context error. - /// - public const string Error = "actor.context.error"; - } - - /// - /// Constants for actor performance metrics. - /// - public static class Performance - { - /// - /// The attribute name for messages processed count. - /// - public const string MessagesProcessed = "actor.performance.messages_processed"; - - /// - /// The attribute name for requests processed count. - /// - public const string RequestsProcessed = "actor.performance.requests_processed"; - - /// - /// The attribute name for processing time. - /// - public const string ProcessingTime = "actor.performance.processing_time"; - - /// - /// The attribute name for queue size. - /// - public const string QueueSize = "actor.performance.queue_size"; - } - } - - /// - /// Constants for message-related telemetry attributes. - /// - public static class Message - { - /// - /// The attribute name for the message ID. - /// - public const string Id = "message.id"; - - /// - /// The attribute name for the message type. - /// - public const string Type = "message.type"; - - /// - /// The attribute name for the message method. - /// - public const string Method = "message.method"; - - /// - /// The attribute name for the message size in bytes. - /// - public const string Size = "message.size"; - - /// - /// The attribute name for the message timestamp. - /// - public const string Timestamp = "message.timestamp"; - - /// - /// The attribute name for the message sender. - /// - public const string Sender = "message.sender"; - - /// - /// The attribute name for the message recipient. - /// - public const string Recipient = "message.recipient"; - - /// - /// The attribute name for the message status. - /// - public const string Status = "message.status"; - - /// - /// The attribute name for the message sequence number. - /// - public const string SequenceNumber = "message.sequence_number"; - - /// - /// Constants for message processing attributes. - /// - public static class Processing - { - /// - /// The attribute name for processing start time. - /// - public const string StartTime = "message.processing.start_time"; - - /// - /// The attribute name for processing end time. - /// - public const string EndTime = "message.processing.end_time"; - - /// - /// The attribute name for processing duration. - /// - public const string Duration = "message.processing.duration"; - - /// - /// The attribute name for processing status. - /// - public const string Status = "message.processing.status"; - - /// - /// The attribute name for processing error. - /// - public const string Error = "message.processing.error"; - } - } - - /// - /// Constants for request-related telemetry attributes. - /// - public static class Request - { - /// - /// The attribute name for the request ID. - /// - public const string Id = "request.id"; - - /// - /// The attribute name for the request method. - /// - public const string Method = "request.method"; - - /// - /// The attribute name for the request status. - /// - public const string Status = "request.status"; - - /// - /// The attribute name for the request timeout. - /// - public const string Timeout = "request.timeout"; - - /// - /// The attribute name for whether the request was cancelled. - /// - public const string Cancelled = "request.cancelled"; - - /// - /// The attribute name for the request retry count. - /// - public const string RetryCount = "request.retry_count"; - } - - /// - /// Constants for response-related telemetry attributes. - /// - public static class Response - { - /// - /// The attribute name for the response ID. - /// - public const string Id = "response.id"; - - /// - /// The attribute name for the response status. - /// - public const string Status = "response.status"; - - /// - /// The attribute name for the response size. - /// - public const string Size = "response.size"; - - /// - /// The attribute name for the response type. - /// - public const string Type = "response.type"; - } - - /// - /// Constants for state-related telemetry attributes. - /// - public static class State - { - /// - /// The attribute name for the state operation type. - /// - public const string OperationType = "state.operation.type"; - - /// - /// The attribute name for the state operation count. - /// - public const string OperationCount = "state.operation.count"; - - /// - /// The attribute name for the state result count. - /// - public const string ResultCount = "state.result.count"; - - /// - /// The attribute name for the state operation success. - /// - public const string Success = "state.success"; - - /// - /// The attribute name for the state ETag. - /// - public const string ETag = "state.etag"; - - /// - /// The attribute name for the state size. - /// - public const string Size = "state.size"; - - /// - /// The attribute name for the state key. - /// - public const string Key = "state.key"; - } - - /// - /// Constants for runtime client metrics. - /// - public static class Client - { - /// - /// Constants for operation duration metrics. - /// - public static class OperationDuration - { - /// - /// The description for the operation duration metric. - /// - public const string Description = "Measures the duration of actor runtime operations"; - - /// - /// The name for the operation duration metric. - /// - public const string Name = "actor.runtime.client.operation.duration"; - - /// - /// The explicit bucket boundaries for the operation duration histogram. - /// - public static readonly double[] ExplicitBucketBoundaries = [0.001, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0, 20.0, 50.0]; - } - - /// - /// Constants for message count metrics. - /// - public static class MessageCount - { - /// - /// The description for the message count metric. - /// - public const string Description = "Measures the number of messages processed by actors"; - - /// - /// The name for the message count metric. - /// - public const string Name = "actor.runtime.client.message.count"; - } - - /// - /// Constants for request count metrics. - /// - public static class RequestCount - { - /// - /// The description for the request count metric. - /// - public const string Description = "Measures the number of requests processed by actors"; - - /// - /// The name for the request count metric. - /// - public const string Name = "actor.runtime.client.request.count"; - } - - /// - /// Constants for actor count metrics. - /// - public static class ActorCount - { - /// - /// The description for the actor count metric. - /// - public const string Description = "Measures the number of active actors"; - - /// - /// The name for the actor count metric. - /// - public const string Name = "actor.runtime.client.actor.count"; - } - - /// - /// Constants for queue size metrics. - /// - public static class QueueSize - { - /// - /// The description for the queue size metric. - /// - public const string Description = "Measures the size of actor message queues"; - - /// - /// The name for the queue size metric. - /// - public const string Name = "actor.runtime.client.queue.size"; - - /// - /// The explicit bucket boundaries for the queue size histogram. - /// - public static readonly int[] ExplicitBucketBoundaries = [0, 1, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000]; - } - - /// - /// Constants for state operation metrics. - /// - public static class StateOperations - { - /// - /// The description for the state operations metric. - /// - public const string Description = "Measures the number of state operations"; - - /// - /// The name for the state operations metric. - /// - public const string Name = "actor.runtime.client.state.operations"; - } - } - - /// - /// Constants for error attributes. - /// - public static class ErrorInfo - { - /// - /// The attribute name for the error type (follows OpenTelemetry error conventions). - /// - public const string Type = "error.type"; - - /// - /// The attribute name for the error message. - /// - public const string Message = "error.message"; - - /// - /// The attribute name for the error stack trace. - /// - public const string StackTrace = "error.stack_trace"; - - /// - /// Well-known error type for unknown errors. - /// - public const string TypeOther = "_OTHER"; - - /// - /// Well-known error types for actor runtime operations. - /// - public static class Types - { - /// - /// Actor not found error. - /// - public const string ActorNotFound = "actor_not_found"; - - /// - /// Actor already exists error. - /// - public const string ActorAlreadyExists = "actor_already_exists"; - - /// - /// Message delivery failure. - /// - public const string MessageDeliveryFailure = "message_delivery_failure"; - - /// - /// Request timeout error. - /// - public const string RequestTimeout = "request_timeout"; - - /// - /// State operation failure. - /// - public const string StateOperationFailure = "state_operation_failure"; - - /// - /// Runtime initialization failure. - /// - public const string RuntimeInitializationFailure = "runtime_initialization_failure"; - } - } - - /// - /// Constants for event attributes and well-known event names. - /// - public static class EventInfo - { - /// - /// The attribute name for the event name. - /// - public const string Name = "event.name"; - - /// - /// The attribute name for the event data. - /// - public const string Data = "event.data"; - - /// - /// The attribute name for the event timestamp. - /// - public const string Timestamp = "event.timestamp"; - - /// - /// Well-known event names for actor runtime operations. - /// - public static class Names - { - /// - /// Actor created event. - /// - public const string ActorCreated = "actor.created"; - - /// - /// Actor started event. - /// - public const string ActorStarted = "actor.started"; - - /// - /// Actor stopped event. - /// - public const string ActorStopped = "actor.stopped"; - - /// - /// Message sent event. - /// - public const string MessageSent = "actor.message.sent"; - - /// - /// Message received event. - /// - public const string MessageReceived = "actor.message.received"; - - /// - /// Request completed event. - /// - public const string RequestCompleted = "actor.request.completed"; - - /// - /// State updated event. - /// - public const string StateUpdated = "actor.state.updated"; - - /// - /// Runtime initialized event. - /// - public const string RuntimeInitialized = "actor.runtime.initialized"; - - /// - /// Runtime shutdown event. - /// - public const string RuntimeShutdown = "actor.runtime.shutdown"; - } - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime/AgentRuntimeJsonUtilities.cs b/dotnet/src/Microsoft.Agents.AI.Runtime/AgentRuntimeJsonUtilities.cs deleted file mode 100644 index be055dac55..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime/AgentRuntimeJsonUtilities.cs +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Diagnostics.CodeAnalysis; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Microsoft.Agents.AI.Runtime; - -/// Provides a collection of utility methods for working with JSON data in the context of agent runtime. -public static partial class AgentRuntimeJsonUtilities -{ - /// - /// Gets the singleton used as the default in JSON serialization operations. - /// - /// - /// - /// For Native AOT or applications disabling , this instance - /// includes source generated contracts for all common exchange types contained in this library. - /// - /// - /// It additionally turns on the following settings: - /// - /// Enables defaults. - /// Enables as the default ignore condition for properties. - /// Enables as the default number handling for number types. - /// Enables for enum serialization. - /// - /// - /// - public static JsonSerializerOptions DefaultOptions { get; } = CreateDefaultOptions(); - - /// - /// Creates default options to use for agent runtime-related serialization. - /// - /// The configured options. - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050:RequiresDynamicCode", Justification = "Converter is guarded by IsReflectionEnabledByDefault check.")] - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access", Justification = "Converter is guarded by IsReflectionEnabledByDefault check.")] - private static JsonSerializerOptions CreateDefaultOptions() - { - // Copy the configuration from the source generated context. - JsonSerializerOptions options = new(JsonContext.Default.Options); - - // Chain with all supported types from Microsoft.Agents.AI.Runtime.Abstractions. - options.TypeInfoResolverChain.Add(AgentRuntimeAbstractionsJsonUtilities.DefaultOptions.TypeInfoResolver!); - - options.MakeReadOnly(); - return options; - } - - [JsonSourceGenerationOptions( - JsonSerializerDefaults.Web, - UseStringEnumConverter = true, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - WriteIndented = false)] - [JsonSerializable(typeof(string))] - internal sealed partial class JsonContext : JsonSerializerContext; -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime/InProcessActorContext.Log.cs b/dotnet/src/Microsoft.Agents.AI.Runtime/InProcessActorContext.Log.cs deleted file mode 100644 index c3af454f0d..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime/InProcessActorContext.Log.cs +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.Extensions.Logging; - -namespace Microsoft.Agents.AI.Runtime; - -/// -/// High-performance logging messages using LoggerMessage source generator for InProcessActorContext. -/// -internal static partial class Log -{ - // Actor context lifecycle logging - [LoggerMessage( - Level = LogLevel.Information, - Message = "Actor context created: ActorId={ActorId}")] - public static partial void ActorContextCreated(ILogger logger, string actorId); - - [LoggerMessage( - Level = LogLevel.Information, - Message = "Actor context starting: ActorId={ActorId}")] - public static partial void ActorContextStarting(ILogger logger, string actorId); - - [LoggerMessage( - Level = LogLevel.Information, - Message = "Actor context started: ActorId={ActorId}")] - public static partial void ActorContextStarted(ILogger logger, string actorId); - - [LoggerMessage( - Level = LogLevel.Information, - Message = "Actor context disposing: ActorId={ActorId}")] - public static partial void ActorContextDisposing(ILogger logger, string actorId); - - [LoggerMessage( - Level = LogLevel.Information, - Message = "Actor context disposed: ActorId={ActorId}")] - public static partial void ActorContextDisposed(ILogger logger, string actorId); - - // Message handling logging - [LoggerMessage( - Level = LogLevel.Debug, - Message = "Message enqueued: ActorId={ActorId}, MessageId={MessageId}, Type={MessageType}")] - public static partial void MessageEnqueued(ILogger logger, string actorId, string messageId, string messageType); - - [LoggerMessage( - Level = LogLevel.Debug, - Message = "Message yielded: ActorId={ActorId}, MessageId={MessageId}, Type={MessageType}, Count={MessageCount}")] - public static partial void MessageYielded(ILogger logger, string actorId, string messageId, string messageType, int messageCount); - - [LoggerMessage( - Level = LogLevel.Debug, - Message = "Watch messages started: ActorId={ActorId}")] - public static partial void WatchMessagesStarted(ILogger logger, string actorId); - - [LoggerMessage( - Level = LogLevel.Debug, - Message = "Watch messages completed: ActorId={ActorId}, TotalMessages={MessageCount}")] - public static partial void WatchMessagesCompleted(ILogger logger, string actorId, int messageCount); - - // Request handling logging - [LoggerMessage( - Level = LogLevel.Debug, - Message = "Send request started: ActorId={ActorId}, MessageId={MessageId}")] - public static partial void SendRequestStarted(ILogger logger, string actorId, string messageId); - - [LoggerMessage( - Level = LogLevel.Debug, - Message = "Request message created: ActorId={ActorId}, MessageId={MessageId}, Method={Method}")] - public static partial void RequestMessageCreated(ILogger logger, string actorId, string messageId, string method); - - [LoggerMessage( - Level = LogLevel.Debug, - Message = "Request message found in inbox: ActorId={ActorId}, MessageId={MessageId}")] - public static partial void RequestMessageFound(ILogger logger, string actorId, string messageId); - - [LoggerMessage( - Level = LogLevel.Debug, - Message = "Response handle created: ActorId={ActorId}, MessageId={MessageId}")] - public static partial void ResponseHandleCreated(ILogger logger, string actorId, string messageId); - - // Progress update logging - [LoggerMessage( - Level = LogLevel.Debug, - Message = "Progress update received: ActorId={ActorId}, MessageId={MessageId}, SequenceNumber={SequenceNumber}")] - public static partial void ProgressUpdateReceived(ILogger logger, string actorId, string messageId, int sequenceNumber); - - [LoggerMessage( - Level = LogLevel.Debug, - Message = "Progress update published: ActorId={ActorId}, MessageId={MessageId}")] - public static partial void ProgressUpdatePublished(ILogger logger, string actorId, string messageId); - - [LoggerMessage( - Level = LogLevel.Error, - Message = "Progress update failed: ActorId={ActorId}, MessageId={MessageId}, Reason={Reason}")] - public static partial void ProgressUpdateFailed(ILogger logger, string actorId, string messageId, string reason); - - // Storage operation logging - [LoggerMessage( - Level = LogLevel.Debug, - Message = "Read operation started: ActorId={ActorId}, OperationCount={OperationCount}")] - public static partial void ReadOperationStarted(ILogger logger, string actorId, int operationCount); - - [LoggerMessage( - Level = LogLevel.Debug, - Message = "Read operation completed: ActorId={ActorId}, ResultCount={ResultCount}")] - public static partial void ReadOperationCompleted(ILogger logger, string actorId, int resultCount); - - [LoggerMessage( - Level = LogLevel.Debug, - Message = "Write operation started: ActorId={ActorId}, OperationCount={OperationCount}")] - public static partial void WriteOperationStarted(ILogger logger, string actorId, int operationCount); - - [LoggerMessage( - Level = LogLevel.Debug, - Message = "Write operation completed: ActorId={ActorId}, Success={Success}")] - public static partial void WriteOperationCompleted(ILogger logger, string actorId, bool success); - - [LoggerMessage( - Level = LogLevel.Debug, - Message = "Send request operation encountered: ActorId={ActorId}")] - public static partial void SendRequestOperationEncountered(ILogger logger, string actorId); - - [LoggerMessage( - Level = LogLevel.Debug, - Message = "Update request operation processing: ActorId={ActorId}, MessageId={MessageId}")] - public static partial void UpdateRequestOperationProcessing(ILogger logger, string actorId, string messageId); - - [LoggerMessage( - Level = LogLevel.Debug, - Message = "Operation processing completed: ActorId={ActorId}, ProcessedCount={ProcessedCount}")] - public static partial void OperationProcessingCompleted(ILogger logger, string actorId, int processedCount); -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime/InProcessActorContext.cs b/dotnet/src/Microsoft.Agents.AI.Runtime/InProcessActorContext.cs deleted file mode 100644 index 831fc1ca42..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime/InProcessActorContext.cs +++ /dev/null @@ -1,445 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text.Json; -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using static Microsoft.Agents.AI.Runtime.ActivityExtensions; -using Tel = Microsoft.Agents.AI.Runtime.ActorRuntimeOpenTelemetryConsts; - -namespace Microsoft.Agents.AI.Runtime; - -internal sealed class InProcessActorContext : IActorRuntimeContext, IAsyncDisposable, IDisposable -{ - private static readonly ActivitySource ActivitySource = new(Tel.InProcessSourceName); - - private readonly CancellationTokenSource _cts = new(); - private readonly Channel _pendingMessages = Channel.CreateUnbounded(); - private readonly object _lock = new(); - private readonly Dictionary _inbox = []; - private readonly InProcessActorRuntime _runtime; - private readonly IActor _actorInstance; - private readonly ILogger _logger; - private Task? _actorRunTask; - - public InProcessActorContext( - ActorId ActorId, - InProcessActorRuntime runtime, - Func actorFactory) - { - this._runtime = runtime; - this.ActorId = ActorId; - this._logger = runtime.Services.GetRequiredService>(); - this._actorInstance = actorFactory(runtime.Services, this); - - Log.ActorContextCreated(this._logger, this.ActorId.ToString()); - } - - public ActorId ActorId { get; } - - private IActorStateStorage Storage => this._runtime.Storage; - - public void Start() - { - using var activity = ActivitySource.StartActivity( - Tel.SpanNames.FormatActorOperation(Tel.Operations.StartActor)); - - activity.SetActorAttributes(this.ActorId, "start"); - - try - { - Log.ActorContextStarting(this._logger, this.ActorId.ToString()); - this._actorRunTask = this._actorInstance.RunAsync(this._cts.Token).AsTask(); - Log.ActorContextStarted(this._logger, this.ActorId.ToString()); - - activity.Complete(ActorStarted, this.ActorId, [(Tel.Actor.Started, true)]); - } - catch (Exception ex) - { - activity.Fail(ex); - throw; - } - } - - public void EnqueueMessage(ActorMessage message) - { - using var activity = ActivitySource.StartActivity( - Tel.SpanNames.FormatMessageOperation(Tel.Operations.ReceiveMessage)); - - var messageId = message switch - { - ActorRequestMessage requestMessage => requestMessage.MessageId, - ActorResponseMessage responseMessage => responseMessage.MessageId, - _ => "unknown" - }; - - // Set message tracing attributes - activity.SetActorAttributes(this.ActorId); - activity.SetMessageAttributes(messageId, message.Type.ToString()); - - try - { - Log.MessageEnqueued(this._logger, this.ActorId.ToString(), messageId, message.Type.ToString()); - this._pendingMessages.Writer.TryWrite(message); - - activity.Complete(MessageReceived, this.ActorId, Enqueued, - (Tel.Message.Id, messageId), (Tel.Message.Type, message.Type.ToString())); - } - catch (Exception ex) - { - activity.RecordFailure(ex, null, (Tel.Message.Status, "failed")); - throw; - } - } - - public bool TryGetResponseHandle(string messageId, [NotNullWhen(true)] out ActorResponseHandle? handle) - { - lock (this._lock) - { - if (!this._inbox.TryGetValue(messageId, out var entry)) - { - handle = null; - return false; - } - - handle = new InProcessActorResponseHandle(this, entry); - return true; - } - } - - public ActorResponseHandle SendRequest(ActorRequest request) - { - using var activity = ActivitySource.StartActivity( - Tel.SpanNames.FormatRequestOperation(Tel.Operations.ProcessRequest)); - - activity.SetupRequestOperation(this.ActorId, request.MessageId, request.Method, "ActorContext", "SendRequest"); - - Log.SendRequestStarted(this._logger, this.ActorId.ToString(), request.MessageId); - - try - { - lock (this._lock) - { - string requestStatus; - if (!this._inbox.TryGetValue(request.MessageId, out var entry)) - { - var requestMessage = new ActorRequestMessage(request.MessageId) - { - Method = request.Method, - Params = request.Params - }; - - entry = this._inbox[request.MessageId] = new(requestMessage); - this._pendingMessages.Writer.TryWrite(requestMessage); - Log.RequestMessageCreated(this._logger, this.ActorId.ToString(), request.MessageId, request.Method); - requestStatus = "created"; - } - else - { - Log.RequestMessageFound(this._logger, this.ActorId.ToString(), request.MessageId); - requestStatus = "found"; - } - - var handle = new InProcessActorResponseHandle(this, entry); - Log.ResponseHandleCreated(this._logger, this.ActorId.ToString(), request.MessageId); - - activity.Complete(RequestCompleted, this.ActorId, [(Tel.Request.Status, requestStatus), (Tel.Response.Status, HandleCreated)], - (Tel.Message.Id, request.MessageId), (Tel.Message.Method, request.Method)); - - return handle; - } - } - catch (Exception ex) - { - activity.RecordFailure(ex, null, (Tel.Request.Status, "failed")); - throw; - } - } - - public void OnProgressUpdate(string messageId, int sequenceNumber, JsonElement data) - { - using var activity = ActivitySource.StartActivity( - Tel.SpanNames.FormatActorOperation(Tel.Operations.ProgressUpdate)); - - activity.SetActorAttributes(this.ActorId); - activity.SetMessageAttributes(messageId); - activity?.SetTag(Tel.Message.SequenceNumber, sequenceNumber); - - try - { - Log.ProgressUpdateReceived(this._logger, this.ActorId.ToString(), messageId, sequenceNumber); - var update = new UpdateRequestOperation(messageId, RequestStatus.Pending, data); - this.PostRequestUpdate(update); - - activity.RecordSuccess((Tel.Message.Status, "processed")); - } - catch (Exception ex) - { - activity.RecordFailure(ex); - throw; - } - } - - private void PostRequestUpdate(UpdateRequestOperation update) - { - lock (this._lock) - { - if (!this._inbox.TryGetValue(update.MessageId, out var entry)) - { - Log.ProgressUpdateFailed(this._logger, this.ActorId.ToString(), update.MessageId, "Message not found in inbox"); - throw new InvalidOperationException($"Message with id '{update.MessageId}' not found while publishing update."); - } - - entry.PostUpdate(update); - if (update.Status is RequestStatus.Completed or RequestStatus.Failed) - { - entry.SetResponse(new ActorResponseMessage(update.MessageId) - { - SenderId = this.ActorId, - Status = update.Status, - Data = update.Data - }); - } - - Log.ProgressUpdatePublished(this._logger, this.ActorId.ToString(), update.MessageId); - } - } - - public async ValueTask ReadAsync(ActorReadOperationBatch operations, CancellationToken cancellationToken = default) - { - Log.ReadOperationStarted(this._logger, this.ActorId.ToString(), operations.Operations.Count); - var result = await this.Storage.ReadStateAsync( - this.ActorId, - [.. operations.Operations.OfType()], - cancellationToken).ConfigureAwait(false); - Log.ReadOperationCompleted(this._logger, this.ActorId.ToString(), result.Results.Count); - return result; - } - - public async IAsyncEnumerable WatchMessagesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) - { - Log.WatchMessagesStarted(this._logger, this.ActorId.ToString()); - - // TODO: Yield all pending requests - this likely requires reading the inbox from storage. - // TODO: Yield all responses - // TODO: Yield all updates - var messageCount = 0; - await foreach (var message in this._pendingMessages.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) - { - messageCount++; - var messageId = message switch - { - ActorRequestMessage requestMessage => requestMessage.MessageId, - ActorResponseMessage responseMessage => responseMessage.MessageId, - _ => "unknown" - }; - Log.MessageYielded(this._logger, this.ActorId.ToString(), messageId, message.Type.ToString(), messageCount); - yield return message; - } - - Log.WatchMessagesCompleted(this._logger, this.ActorId.ToString(), messageCount); - } - - public async ValueTask WriteAsync(ActorWriteOperationBatch operations, CancellationToken cancellationToken = default) - { - Log.WriteOperationStarted(this._logger, this.ActorId.ToString(), operations.Operations.Count); - - // TODO: Turn send & update message operations into storage writes to outbox - - IReadOnlyCollection writeOps = - [.. operations.Operations.OfType()]; - - WriteResponse result = await this.Storage.WriteStateAsync( - this.ActorId, - writeOps, - operations.ETag, - cancellationToken).ConfigureAwait(false); - - Log.WriteOperationCompleted(this._logger, this.ActorId.ToString(), result.Success); - - // Check if result success and schedule durable task to pump outbox if needed. - if (result.Success) - { - var processedOperations = 0; - foreach (var operation in operations.Operations) - { - if (operation is SendRequestOperation sendRequestOperation) - { - Log.SendRequestOperationEncountered(this._logger, this.ActorId.ToString()); - // Get the target actor from the runtime. - // Enqueue the request on the actor's inbox. - throw new NotImplementedException(); - } - else if (operation is UpdateRequestOperation updateRequestOperation) - { - Log.UpdateRequestOperationProcessing(this._logger, this.ActorId.ToString(), updateRequestOperation.MessageId); - // Find the request in this actor's inbox. - // Get the SenderId from the request. - // Get the sending actor from the runtime. - // Enqueue the request on the actor's inbox. - this.PostRequestUpdate(updateRequestOperation); - processedOperations++; - } - } - Log.OperationProcessingCompleted(this._logger, this.ActorId.ToString(), processedOperations); - } - - return result; - } - - public async ValueTask DisposeAsync() - { - Log.ActorContextDisposing(this._logger, this.ActorId.ToString()); - - this._cts.Dispose(); - await this._actorInstance.DisposeAsync().ConfigureAwait(false); - if (this._actorRunTask is { } actorRunTask) - { - await actorRunTask.ConfigureAwait(false); - } - - Log.ActorContextDisposed(this._logger, this.ActorId.ToString()); - } - - public void Dispose() - { - Log.ActorContextDisposing(this._logger, this.ActorId.ToString()); - - this._cts.Dispose(); -#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits - if (this._actorInstance is IDisposable actorInstanceDisposable) - { - actorInstanceDisposable.Dispose(); - } - else - { - this._actorInstance.DisposeAsync().AsTask().GetAwaiter().GetResult(); - } - - this._actorRunTask?.GetAwaiter().GetResult(); -#pragma warning restore VSTHRD002 - - Log.ActorContextDisposed(this._logger, this.ActorId.ToString()); - } - - private sealed class ActorInboxEntry(ActorRequestMessage Request) - { - private readonly TaskCompletionSource _responseTcs = new(TaskCreationOptions.RunContinuationsAsynchronously); - private readonly Channel _updates = Channel.CreateUnbounded(); - public CancellationTokenSource Cts { get; } = new(); - public ActorRequestMessage Request { get; } = Request; - public Task Response => this._responseTcs.Task; - - public IAsyncEnumerable WatchUpdatesAsync(CancellationToken cancellationToken) - => this._updates.Reader.ReadAllAsync(cancellationToken); - - public void PostUpdate(UpdateRequestOperation update) - { - if (!this._updates.Writer.TryWrite(update)) - { - throw new InvalidOperationException("Failed to write update to the channel."); - } - } - - public void SetResponse(ActorResponseMessage response) - { - if (!this._responseTcs.TrySetResult(response)) - { - throw new InvalidOperationException("Response has already been set."); - } - - this._updates.Writer.TryComplete(); - } - } - - private sealed class InProcessActorResponseHandle(InProcessActorContext context, ActorInboxEntry entry) : ActorResponseHandle - { -#if NET8_0_OR_GREATER - public override async ValueTask CancelAsync(CancellationToken cancellationToken) => - await entry.Cts.CancelAsync().ConfigureAwait(false); -#else - public override ValueTask CancelAsync(CancellationToken cancellationToken) - { - entry.Cts.Cancel(); - return default; - } -#endif - - public override async ValueTask GetResponseAsync(CancellationToken cancellationToken) - { - ActorResponse response; - try - { - var responseMessage = await entry.Response -#if NET8_0_OR_GREATER - .WaitAsync(cancellationToken) -#endif - .ConfigureAwait(false); - response = new ActorResponse - { - ActorId = context.ActorId, - MessageId = entry.Request.MessageId, - Data = responseMessage.Data, - Status = responseMessage.Status, - }; - } - catch (Exception exception) - { - response = new ActorResponse - { - ActorId = context.ActorId, - MessageId = entry.Request.MessageId, - Data = JsonSerializer.SerializeToElement($"Error: {exception.Message}", AgentRuntimeJsonUtilities.JsonContext.Default.String), - Status = RequestStatus.Failed, - }; - } - - return response; - } - - public override bool TryGetResponse([NotNullWhen(true)] out ActorResponse? response) - { - if (entry.Response.Status is TaskStatus.RanToCompletion) - { -#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits - var responseMessage = entry.Response.Result; -#pragma warning restore VSTHRD002 - response = new ActorResponse - { - ActorId = context.ActorId, - MessageId = entry.Request.MessageId, - Data = responseMessage.Data, - Status = responseMessage.Status, - }; - - return true; - } - - response = null; - return false; - } - - public override async IAsyncEnumerable WatchUpdatesAsync([EnumeratorCancellation] CancellationToken cancellationToken) - { - await foreach (var update in entry.WatchUpdatesAsync(cancellationToken).ConfigureAwait(false)) - { - yield return new ActorRequestUpdate(update.Status, update.Data); - } - - var response = await entry.Response -#if NET8_0_OR_GREATER - .WaitAsync(cancellationToken) -#endif - .ConfigureAwait(false); - yield return new ActorRequestUpdate(response.Status, response.Data); - } - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime/InProcessActorRuntime.cs b/dotnet/src/Microsoft.Agents.AI.Runtime/InProcessActorRuntime.cs deleted file mode 100644 index 5adf4ba4fe..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime/InProcessActorRuntime.cs +++ /dev/null @@ -1,214 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.Metrics; -using System.Threading; -using System.Threading.Tasks; -using static Microsoft.Agents.AI.Runtime.ActivityExtensions; -using Tel = Microsoft.Agents.AI.Runtime.ActorRuntimeOpenTelemetryConsts; - -namespace Microsoft.Agents.AI.Runtime; - -internal sealed class InProcessActorRuntime( - IServiceProvider serviceProvider, - IReadOnlyDictionary> actorFactories, - IActorStateStorage storage) -{ - private static readonly ActivitySource ActivitySource = new(Tel.InProcessSourceName); - private static readonly Meter Meter = new(Tel.InProcessSourceName); - - // Metrics following OpenTelemetry semantic conventions - private static readonly Counter ActorCreatedCounter = Meter.CreateCounter( - Tel.Client.ActorCount.Name, - Tel.CountUnit, - Tel.Client.ActorCount.Description); - - private static readonly Histogram OperationDurationHistogram = Meter.CreateHistogram( - Tel.Client.OperationDuration.Name, - "s", - Tel.Client.OperationDuration.Description); - - private readonly object _createActorLock = new(); - private readonly IReadOnlyDictionary> _actorFactories = actorFactories; - private readonly ConcurrentDictionary _actors = []; - - public IActorStateStorage Storage { get; } = storage; - public IServiceProvider Services { get; } = serviceProvider; - - internal InProcessActorContext GetOrCreateActor(ActorId actorId) - { - var stopwatch = Stopwatch.StartNew(); - - // Create span following OpenTelemetry conventions for RPC operations - using var activity = ActivitySource.StartActivity( - Tel.SpanNames.FormatActorOperation(Tel.Operations.GetActor)); - - try - { - if (this._actors.TryGetValue(actorId, out var context)) - { - activity.SetupActorOperation(actorId, exists: true); - activity.Event(ActorStarted, actorId); - return context; - } - - if (!this._actorFactories.TryGetValue(actorId.Type, out var factory)) - { - var errorMessage = $"No factory registered for actor type '{actorId.Type}'"; - var exception = new InvalidOperationException(errorMessage); - - activity.SetupActorOperation(actorId, exists: false); - activity.RecordFailure(exception, Tel.ErrorInfo.Types.ActorNotFound); - throw exception; - } - - if (!this._actors.TryGetValue(actorId, out var actorContext)) - { -#if NETSTANDARD - InProcessActorContext ValueFactory(ActorId actorId) - { - var self = this; - - return CreateActorInstance(actorId, self, factory); - } - - actorContext = this._actors.GetOrAdd(actorId, ValueFactory); -#else - static InProcessActorContext ValueFactory( - ActorId actorId, - (InProcessActorRuntime, Func) state) - { - var (self, factory) = state; - return CreateActorInstance(actorId, self, factory); - } - - actorContext = this._actors.GetOrAdd(actorId, ValueFactory, (this, factory)); -#endif - } - - activity.SetupActorOperation(actorId, exists: false); - activity.RecordSuccess(); - return actorContext; - } - catch (Exception ex) - { - activity.RecordFailure(ex); - throw; - } - finally - { - // Record operation duration metric - var duration = stopwatch.Elapsed.TotalSeconds; - OperationDurationHistogram.Record(duration, - new KeyValuePair(Tel.Actor.Operation, Tel.Operations.GetActor), - new KeyValuePair(Tel.Actor.Type, actorId.Type.Name)); - } - } - - private static InProcessActorContext CreateActorInstance(ActorId actorId, InProcessActorRuntime self, Func factory) - { - lock (self._createActorLock) - { - // Create nested span for actor creation - var createActivity = ActivitySource.StartActivity( - Tel.SpanNames.FormatActorOperation(Tel.Operations.CreateActor)); - InProcessActorContext? instance = null; - try - { - createActivity.SetupActorOperation(actorId); - - instance = new InProcessActorContext(actorId, self, factory); - instance.Start(); - - createActivity.Complete(ActorCreated, actorId, [(Tel.Actor.Started, true)]); - - // Record metrics for successful actor creation - ActorCreatedCounter.Add(1, new KeyValuePair(Tel.Actor.Type, actorId.Type.Name)); - return instance; - } - catch (Exception ex) - { - instance?.Dispose(); - createActivity.RecordFailure(ex); - throw; - } - } - } -} - -internal sealed class InProcessActorClient(InProcessActorRuntime runtime) : IActorClient -{ - private static readonly ActivitySource ActivitySource = new(Tel.InProcessSourceName); - private static readonly Meter ClientMeter = new(Tel.InProcessSourceName); - private static readonly Counter RequestCounter = ClientMeter.CreateCounter( - Tel.Client.RequestCount.Name, - Tel.CountUnit, - Tel.Client.RequestCount.Description); - private static readonly Histogram ClientOperationDurationHistogram = ClientMeter.CreateHistogram( - Tel.Client.OperationDuration.Name, - "s", - Tel.Client.OperationDuration.Description); - - private readonly InProcessActorRuntime _runtime = runtime; - - public ValueTask GetResponseAsync(ActorId actorId, string messageId, CancellationToken cancellationToken) - { - // Create span for get response operation - using var activity = ActivitySource.StartActivity( - Tel.SpanNames.FormatRequestOperation(Tel.Operations.ReceiveResponse)); - - activity.SetupRequestOperation(actorId, messageId, service: "ActorClient", rpcMethod: "GetResponse"); - - var actorContext = this._runtime.GetOrCreateActor(actorId); - if (actorContext.TryGetResponseHandle(messageId, out var handle)) - { - return new(handle); - } - - return new(new NotFoundActorResponseHandle(actorId, messageId)); - } - - public ValueTask SendRequestAsync(ActorRequest request, CancellationToken cancellationToken) - { - var stopwatch = Stopwatch.StartNew(); - - // Create span for send request operation following RPC client conventions - using var activity = ActivitySource.StartActivity( - Tel.SpanNames.FormatRequestOperation(Tel.Operations.SendRequest)); - - try - { - activity.SetupRequestOperation(request.ActorId, request.MessageId, request.Method); - - // Ensure the message is enqueued on the actor's inbox, getting a response handle for it. - var actorId = request.ActorId; - var actorContext = this._runtime.GetOrCreateActor(actorId); - var response = actorContext.SendRequest(request); - - activity.Complete(MessageSent, actorId, Sent, (Tel.Message.Id, request.MessageId)); - - // Record request metric - RequestCounter.Add(1, - new KeyValuePair(Tel.Actor.Type, actorId.Type.Name), - new KeyValuePair(Tel.Message.Method, request.Method)); - - return new(response); - } - catch (Exception ex) - { - activity.RecordFailure(ex, null, (Tel.Request.Status, "failed")); - throw; - } - finally - { - // Record operation duration - var duration = stopwatch.Elapsed.TotalSeconds; - ClientOperationDurationHistogram.Record(duration, - new KeyValuePair(Tel.Actor.Operation, Tel.Operations.SendRequest), - new KeyValuePair(Tel.Actor.Type, request.ActorId.Type.Name)); - } - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime/Microsoft.Agents.AI.Runtime.csproj b/dotnet/src/Microsoft.Agents.AI.Runtime/Microsoft.Agents.AI.Runtime.csproj deleted file mode 100644 index b6e7518124..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime/Microsoft.Agents.AI.Runtime.csproj +++ /dev/null @@ -1,40 +0,0 @@ - - - - $(ProjectsTargetFrameworks) - $(ProjectsDebugTargetFrameworks) - $(NoWarn);IDE1006;IDE0130 - preview - - - - true - true - true - true - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/dotnet/src/Microsoft.Agents.AI.Runtime/NotFoundActorResponseHandle.cs b/dotnet/src/Microsoft.Agents.AI.Runtime/NotFoundActorResponseHandle.cs deleted file mode 100644 index 636d65ba45..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Runtime/NotFoundActorResponseHandle.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Agents.AI.Runtime; - -internal sealed class NotFoundActorResponseHandle : ActorResponseHandle -{ - private readonly ActorResponse _response; - - public NotFoundActorResponseHandle(ActorId actorId, string messageId) - { - this._response = new ActorResponse() - { - Status = RequestStatus.NotFound, - ActorId = actorId, - MessageId = messageId, - }; - } - - public override ValueTask CancelAsync(CancellationToken cancellationToken) => - throw new InvalidOperationException( - $"Failed to cancel request for actor '{this._response.ActorId}' with message ID '{this._response.MessageId}'. The request was not found."); - - public override ValueTask GetResponseAsync(CancellationToken cancellationToken) => - new(this._response); - - public override bool TryGetResponse([NotNullWhen(true)] out ActorResponse? response) - { - response = this._response; - return true; - } - - public override async IAsyncEnumerable WatchUpdatesAsync([EnumeratorCancellation] CancellationToken cancellationToken) - { - yield break; - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Microsoft.Agents.AI.Workflows.Declarative.csproj b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Microsoft.Agents.AI.Workflows.Declarative.csproj index 432bdbe499..b81e21d185 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Microsoft.Agents.AI.Workflows.Declarative.csproj +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Microsoft.Agents.AI.Workflows.Declarative.csproj @@ -28,6 +28,8 @@ + + diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Microsoft.Agents.AI.Workflows.csproj b/dotnet/src/Microsoft.Agents.AI.Workflows/Microsoft.Agents.AI.Workflows.csproj index 7940ae8b3d..9d03856427 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/Microsoft.Agents.AI.Workflows.csproj +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Microsoft.Agents.AI.Workflows.csproj @@ -10,7 +10,6 @@ true true true - @@ -24,12 +23,22 @@ - - + + + + + + + + + + + + diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowHostingExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowHostingExtensions.cs index 08e6b6df07..b41034b317 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowHostingExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowHostingExtensions.cs @@ -41,7 +41,7 @@ public static class WorkflowHostingExtensions throw new InvalidOperationException("Cannot host a workflow that does not accept List as an input"); } - return maybeTyped.AsAgent(id, name); + return maybeTyped.AsAgent(id: id, name: name); } internal static FunctionCallContent ToFunctionCall(this ExternalRequest request) diff --git a/dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/AppHost.cs b/dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/AppHost.cs deleted file mode 100644 index 6f5a47c6c2..0000000000 --- a/dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/AppHost.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using CosmosDB.Testing.AppHost; - -var builder = DistributedApplication.CreateBuilder(args); -var cosmosDb = builder.AddAzureCosmosDB(CosmosDBTestConstants.TestCosmosDbName); - -if (CosmosDBTestConstants.UseEmulatorInCICD) -{ - // Emulator created in the CI/CD pipeline gives more control over some settings and port-configuration today. - // It probably should be configured here to use 8081 port + setup the partition count and throughput, but it's not supported in Aspire yet, so leaving as a placeholder. - // Once Aspire's emulator is suported, the emulator in CI/CD can be removed. - cosmosDb.RunAsEmulator(emulator => emulator.WithLifetime(ContainerLifetime.Persistent)); -} -else if (CosmosDBTestConstants.UseAspireEmulatorForTesting) -{ - cosmosDb.RunAsEmulator(emulator => emulator.WithLifetime(ContainerLifetime.Persistent)); -} -else -{ - var cosmosDbResource = builder.AddParameterFromConfiguration("CosmosDbName", "CosmosDb:Name"); - var cosmosDbResourceGroup = builder.AddParameterFromConfiguration("CosmosDbResourceGroup", "CosmosDb:ResourceGroup"); - cosmosDb.RunAsExisting(cosmosDbResource, cosmosDbResourceGroup); -} - -cosmosDb.AddCosmosDatabase(CosmosDBTestConstants.TestCosmosDbDatabaseName); - -builder.Build().Run(); diff --git a/dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/CosmosDB.Testing.AppHost.csproj b/dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/CosmosDB.Testing.AppHost.csproj deleted file mode 100644 index feb032f7d8..0000000000 --- a/dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/CosmosDB.Testing.AppHost.csproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - Exe - net9.0 - enable - enable - cb8630a8-ec5e-4676-a2b0-4497965c809d - false - - - - - - - - diff --git a/dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/CosmosDBTestConstants.cs b/dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/CosmosDBTestConstants.cs deleted file mode 100644 index 8b958ac80f..0000000000 --- a/dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/CosmosDBTestConstants.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -//using System.Linq.Expressions; - -namespace CosmosDB.Testing.AppHost; - -public static class CosmosDBTestConstants -{ - public const string TestCosmosDbName = "ActorStateStorageTests"; - public const string TestCosmosDbDatabaseName = "state-database"; - - //Set to use the CosmosDB emulator for testing via environment variable. - //Example: set COSMOSDB_TESTS_USE_EMULATOR = true in your environment. - //Warning: Using the emulator may cause test flakiness. - public static bool UseAspireEmulatorForTesting => string.Equals( - Environment.GetEnvironmentVariable("COSMOSDB_TESTS_USE_EMULATOR"), - "true", - StringComparison.OrdinalIgnoreCase); - - public static bool UseEmulatorInCICD => string.Equals( - Environment.GetEnvironmentVariable("COSMOSDB_TESTS_USE_EMULATOR_CICD"), - "true", - StringComparison.OrdinalIgnoreCase); -} diff --git a/dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/Properties/launchSettings.json b/dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/Properties/launchSettings.json deleted file mode 100644 index 3b06925f1c..0000000000 --- a/dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/Properties/launchSettings.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/launchsettings.json", - "profiles": { - "https": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "applicationUrl": "https://localhost:17163;http://localhost:15113", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "DOTNET_ENVIRONMENT": "Development", - "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21207", - "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22258" - } - }, - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "applicationUrl": "http://localhost:15113", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "DOTNET_ENVIRONMENT": "Development", - "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true", - "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19080", - "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20201" - } - } - } -} diff --git a/dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/appsettings.Development.json b/dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/appsettings.Development.json deleted file mode 100644 index 0c208ae918..0000000000 --- a/dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/appsettings.Development.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - } -} diff --git a/dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/appsettings.json b/dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/appsettings.json deleted file mode 100644 index 31c092aa45..0000000000 --- a/dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/appsettings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning", - "Aspire.Hosting.Dcp": "Warning" - } - } -} diff --git a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Agents.AI.Runtime.Storage.CosmosDB.Tests/CosmosActorStateStorageConcurrencyTests.cs b/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Agents.AI.Runtime.Storage.CosmosDB.Tests/CosmosActorStateStorageConcurrencyTests.cs deleted file mode 100644 index 7bd8b678fd..0000000000 --- a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Agents.AI.Runtime.Storage.CosmosDB.Tests/CosmosActorStateStorageConcurrencyTests.cs +++ /dev/null @@ -1,335 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json; - -namespace Microsoft.Agents.AI.Runtime.Storage.CosmosDB.Tests; - -/// -/// Integration tests for CosmosActorStateStorage focusing on concurrency control and ETag progression. -/// -[Collection("Cosmos Test Collection")] -public class CosmosActorStateStorageConcurrencyTests -{ - private readonly CosmosTestFixture _fixture; - - public CosmosActorStateStorageConcurrencyTests(CosmosTestFixture fixture) - { - this._fixture = fixture; - } - - private static readonly TimeSpan s_defaultTimeout = TimeSpan.FromSeconds(300); - - [Fact] - public async Task ETagProgression_ShouldChangeWithEachWriteAsync() - { - // CosmosDB ETags are not guaranteed to be numeric or monotonically increasing - // They are opaque strings that change with each update, which is sufficient for optimistic concurrency - - // Arrange - using var cts = new CancellationTokenSource(s_defaultTimeout); - var cancellationToken = cts.Token; - - await using var storage = new CosmosActorStateStorage(this._fixture.Container); - var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N")); - - const string Key = "testKey"; - var value1 = JsonSerializer.SerializeToElement("value1"); - var value2 = JsonSerializer.SerializeToElement("value2"); - - // Act - First write - var operations1 = new List { new SetValueOperation(Key, value1) }; - var result1 = await storage.WriteStateAsync(testActorId, operations1, "0", cancellationToken); - - // Act - Second write - var operations2 = new List { new SetValueOperation(Key, value2) }; - var result2 = await storage.WriteStateAsync(testActorId, operations2, result1.ETag, cancellationToken); - - // Act - Third write - var operations3 = new List { new RemoveKeyOperation(Key) }; - var result3 = await storage.WriteStateAsync(testActorId, operations3, result2.ETag, cancellationToken); - - // Assert - Assert.True(result1.Success); - Assert.True(result2.Success); - Assert.True(result3.Success); - Assert.NotEqual("0", result1.ETag); - Assert.NotEqual(result1.ETag, result2.ETag); - Assert.NotEqual(result2.ETag, result3.ETag); - - // Verify ETags are all different and represent progression - string[] etags = [result1.ETag, result2.ETag, result3.ETag]; - Assert.Equal(3, etags.Distinct().Count()); - } - - [Fact] - public async Task ConcurrentWrites_ShouldHandleOptimisticConcurrencyCorrectlyAsync() - { - // Arrange - using var cts = new CancellationTokenSource(s_defaultTimeout); - var cancellationToken = cts.Token; - - await using var storage = new CosmosActorStateStorage(this._fixture.Container); - var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N")); - - // Setup initial state - var initialOperations = new List - { - new SetValueOperation("counter", JsonSerializer.SerializeToElement(0)) - }; - var initialResult = await storage.WriteStateAsync(testActorId, initialOperations, "0", cancellationToken); - Assert.True(initialResult.Success); - - const int ConcurrentOperations = 10; - var tasks = new List>(); - - // Act - Simulate concurrent writes with retry logic - for (int i = 0; i < ConcurrentOperations; i++) - { - var operationNumber = i; - tasks.Add(Task.Run(async () => - { - var success = false; - var retryCount = 0; - const int MaxRetries = 20; - string? finalETag = null; - - while (!success && retryCount < MaxRetries) - { - try - { - // Read current state to get latest ETag - var readOps = new List - { - new GetValueOperation("counter") - }; - var readResult = await storage.ReadStateAsync(testActorId, readOps, cancellationToken); - var currentETag = readResult.ETag; - - var currentValue = readResult.Results[0] as GetValueResult; - var currentCounter = currentValue?.Value?.GetInt32() ?? 0; - - // Try to increment the counter - var writeOps = new List - { - new SetValueOperation("counter", JsonSerializer.SerializeToElement(currentCounter + 1)), - new SetValueOperation($"operation_{operationNumber}", JsonSerializer.SerializeToElement($"completed_attempt_{retryCount}")) - }; - - var writeResult = await storage.WriteStateAsync(testActorId, writeOps, currentETag, cancellationToken); - - if (writeResult.Success) - { - success = true; - finalETag = writeResult.ETag; - } - else - { - retryCount++; - // Small delay to reduce contention - await Task.Delay(Random.Shared.Next(1, 10), cancellationToken); - } - } - catch (Exception) - { - retryCount++; - await Task.Delay(Random.Shared.Next(1, 10), cancellationToken); - } - } - - return (success, finalETag, retryCount); - })); - } - - // Wait for all operations to complete - var results = await Task.WhenAll(tasks); - - // Assert - All operations should eventually succeed - Assert.All(results, result => Assert.True(result.Success, $"Operation failed after {result.AttemptNumber} attempts")); - - // Act - Verify final state - var finalReadOps = new List - { - new GetValueOperation("counter"), - new ListKeysOperation(continuationToken: null) - }; - var finalResult = await storage.ReadStateAsync(testActorId, finalReadOps, cancellationToken); - - var finalCounter = finalResult.Results[0] as GetValueResult; - var finalKeys = finalResult.Results[1] as ListKeysResult; - - // Assert final state is consistent - Assert.NotNull(finalCounter); - Assert.NotNull(finalKeys); - Assert.Equal(ConcurrentOperations, finalCounter.Value?.GetInt32()); // Counter should equal number of operations - Assert.Equal(ConcurrentOperations + 1, finalKeys.Keys.Count); // counter + operation_N keys - - // Verify all operation keys are present - Assert.Contains("counter", finalKeys.Keys); - for (int i = 0; i < ConcurrentOperations; i++) - { - Assert.Contains($"operation_{i}", finalKeys.Keys); - } - - // Log retry statistics for debugging - var totalRetries = results.Sum(r => r.AttemptNumber); - var maxRetries = results.Max(r => r.AttemptNumber); - Console.WriteLine($"Concurrent operations completed. Total retries: {totalRetries}, Max retries for single operation: {maxRetries}"); - } - - [Fact] - public async Task WriteStateAsync_InitialETagHandling_ShouldWorkCorrectlyAsync() - { - // Arrange - using var cts = new CancellationTokenSource(s_defaultTimeout); - var cancellationToken = cts.Token; - - await using var storage = new CosmosActorStateStorage(this._fixture.Container); - var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N")); - - const string Key = "testKey"; - var value = JsonSerializer.SerializeToElement("testValue"); - var operations = new List - { - new SetValueOperation(Key, value) - }; - - // Act & Assert - Test null eTag (should create new document) - var resultWithNullETag = await storage.WriteStateAsync(testActorId, operations, null!, cancellationToken); - Assert.True(resultWithNullETag.Success); - Assert.NotNull(resultWithNullETag.ETag); - Assert.NotEmpty(resultWithNullETag.ETag); - - // Clean up for next test - var uniqueActorId1 = new ActorId("TestActor", Guid.NewGuid().ToString("N")); - - // Act & Assert - Test empty eTag (should create new document) - var resultWithEmptyETag = await storage.WriteStateAsync(uniqueActorId1, operations, string.Empty, cancellationToken); - Assert.True(resultWithEmptyETag.Success); - Assert.NotNull(resultWithEmptyETag.ETag); - Assert.NotEmpty(resultWithEmptyETag.ETag); - - // Clean up for next test - var uniqueActorId2 = new ActorId("TestActor", Guid.NewGuid().ToString("N")); - - // Act & Assert - Test "0" initial eTag (should create new document) - var resultWithInitialETag = await storage.WriteStateAsync(uniqueActorId2, operations, "0", cancellationToken); - Assert.True(resultWithInitialETag.Success); - Assert.NotNull(resultWithInitialETag.ETag); - Assert.NotEmpty(resultWithInitialETag.ETag); - Assert.NotEqual("0", resultWithInitialETag.ETag); - - // Act & Assert - Test writing again with "0" should fail (document already exists) - var secondWriteWithInitialETag = await storage.WriteStateAsync(uniqueActorId2, operations, "0", cancellationToken); - Assert.False(secondWriteWithInitialETag.Success); - Assert.Empty(secondWriteWithInitialETag.ETag); - - // Act & Assert - Test writing with correct eTag should succeed - var updateOperations = new List - { - new SetValueOperation(Key, JsonSerializer.SerializeToElement("updatedValue")) - }; - var resultWithCorrectETag = await storage.WriteStateAsync(uniqueActorId2, updateOperations, resultWithInitialETag.ETag, cancellationToken); - Assert.True(resultWithCorrectETag.Success); - Assert.NotNull(resultWithCorrectETag.ETag); - Assert.NotEqual(resultWithInitialETag.ETag, resultWithCorrectETag.ETag); - - // Verify the value was actually updated - var readOperations = new List - { - new GetValueOperation(Key) - }; - var readResult = await storage.ReadStateAsync(uniqueActorId2, readOperations, cancellationToken); - var getValue = readResult.Results[0] as GetValueResult; - Assert.NotNull(getValue); - Assert.Equal("updatedValue", getValue.Value?.GetString()); - } - - [Fact] - public async Task ReadThenWrite_OnNonExistentActor_ShouldWorkCorrectlyAsync() - { - // 1. Read state from a non-existent actor (gets initial ETag) - // 2. Write with that ETag (should succeed) - - // Arrange - using var cts = new CancellationTokenSource(s_defaultTimeout); - var cancellationToken = cts.Token; - - await using var storage = new CosmosActorStateStorage(this._fixture.Container); - var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N")); // Fresh actor - - const string Key = "testKey"; - var value = JsonSerializer.SerializeToElement("testValue"); - - // Act - Read state from non-existent actor (this calls GetActorETagAsync internally) - var readOperations = new List - { - new GetValueOperation(Key) - }; - var readResult = await storage.ReadStateAsync(testActorId, readOperations, cancellationToken); - - // Assert - Read should succeed but return null value and initial ETag - Assert.Single(readResult.Results); - var getValue = readResult.Results[0] as GetValueResult; - Assert.NotNull(getValue); - Assert.Null(getValue.Value); // No value exists yet - Assert.Equal("0", readResult.ETag); // Should return initial ETag for non-existent actor - - // Act - Write using the ETag from the read operation - var writeOperations = new List - { - new SetValueOperation(Key, value) - }; - var writeResult = await storage.WriteStateAsync(testActorId, writeOperations, readResult.ETag, cancellationToken); - - // Assert - Write should succeed - Assert.True(writeResult.Success); - Assert.NotNull(writeResult.ETag); - Assert.NotEqual("0", writeResult.ETag); // Should get a real ETag after write - - // Act - Verify the value was written - var verifyReadResult = await storage.ReadStateAsync(testActorId, readOperations, cancellationToken); - var verifyGetValue = verifyReadResult.Results[0] as GetValueResult; - - // Assert - Value should now exist - Assert.NotNull(verifyGetValue); - Assert.NotNull(verifyGetValue.Value); - Assert.Equal("testValue", verifyGetValue.Value?.GetString()); - Assert.Equal(writeResult.ETag, verifyReadResult.ETag); // ETags should match - } - - [Fact] - public async Task WriteStateAsync_WithInvalidETag_ShouldFailAsync() - { - // Arrange - using var cts = new CancellationTokenSource(s_defaultTimeout); - var cancellationToken = cts.Token; - - await using var storage = new CosmosActorStateStorage(this._fixture.Container); - var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N")); // Non-existent actor - - const string Key = "testKey"; - var value = JsonSerializer.SerializeToElement("testValue"); - var operations = new List - { - new SetValueOperation(Key, value) - }; - - // Act - Try to write with a completely fabricated/invalid ETag (no document exists) - const string FabricatedETag = "\"fabricated-etag-12345\""; // Made-up ETag for non-existent document - var resultWithFabricatedETag = await storage.WriteStateAsync(testActorId, operations, FabricatedETag, cancellationToken); - - // Assert - The write should fail due to ETag mismatch (document doesn't exist) - Assert.False(resultWithFabricatedETag.Success); - Assert.Empty(resultWithFabricatedETag.ETag); - - // Verify no document was created - var readOperations = new List - { - new GetValueOperation(Key) - }; - var readResult = await storage.ReadStateAsync(testActorId, readOperations, cancellationToken); - var getValue = readResult.Results[0] as GetValueResult; - Assert.NotNull(getValue); - Assert.Null(getValue.Value); // Should be null since no document exists - } -} diff --git a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Agents.AI.Runtime.Storage.CosmosDB.Tests/CosmosActorStateStorageListKeysTests.cs b/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Agents.AI.Runtime.Storage.CosmosDB.Tests/CosmosActorStateStorageListKeysTests.cs deleted file mode 100644 index 7a47855bdd..0000000000 --- a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Agents.AI.Runtime.Storage.CosmosDB.Tests/CosmosActorStateStorageListKeysTests.cs +++ /dev/null @@ -1,288 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json; - -namespace Microsoft.Agents.AI.Runtime.Storage.CosmosDB.Tests; - -/// -/// Integration tests for CosmosActorStateStorage focusing on ListKeys functionality. -/// -[Collection("Cosmos Test Collection")] -public class CosmosActorStateStorageListKeysTests -{ - private readonly CosmosTestFixture _fixture; - - public CosmosActorStateStorageListKeysTests(CosmosTestFixture fixture) - { - this._fixture = fixture; - } - - private static readonly TimeSpan s_defaultTimeout = TimeSpan.FromSeconds(300); - - [Fact] - public async Task ReadStateAsync_WithListKeysAndKeyPrefix_ShouldReturnFilteredKeysAsync() - { - // Arrange - using var cts = new CancellationTokenSource(s_defaultTimeout); - var cancellationToken = cts.Token; - - await using var storage = new CosmosActorStateStorage(this._fixture.Container); - var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N")); - - const string PrefixKey1 = "prefix_key1"; - const string PrefixKey2 = "prefix_key2"; - const string OtherKey = "other_key"; - var value1 = JsonSerializer.SerializeToElement("value1"); - var value2 = JsonSerializer.SerializeToElement("value2"); - var value3 = JsonSerializer.SerializeToElement("value3"); - - var writeOperations = new List - { - new SetValueOperation(PrefixKey1, value1), - new SetValueOperation(PrefixKey2, value2), - new SetValueOperation(OtherKey, value3) - }; - - await storage.WriteStateAsync(testActorId, writeOperations, "0", cancellationToken); - - // Act - List keys with prefix filter - var readOperations = new List - { - new ListKeysOperation(continuationToken: null, keyPrefix: "prefix_") - }; - var result = await storage.ReadStateAsync(testActorId, readOperations, cancellationToken); - - // Assert - Assert.Single(result.Results); - var listKeys = result.Results[0] as ListKeysResult; - Assert.NotNull(listKeys); - Assert.Equal(2, listKeys.Keys.Count); - Assert.Contains(PrefixKey1, listKeys.Keys); - Assert.Contains(PrefixKey2, listKeys.Keys); - Assert.DoesNotContain(OtherKey, listKeys.Keys); - } - - [Fact] - public async Task ReadStateAsync_WithListKeysAndNonMatchingPrefix_ShouldReturnEmptyListAsync() - { - // Arrange - using var cts = new CancellationTokenSource(s_defaultTimeout); - var cancellationToken = cts.Token; - - await using var storage = new CosmosActorStateStorage(this._fixture.Container); - var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N")); - - const string Key1 = "key1"; - const string Key2 = "key2"; - var value1 = JsonSerializer.SerializeToElement("value1"); - var value2 = JsonSerializer.SerializeToElement("value2"); - - var writeOperations = new List - { - new SetValueOperation(Key1, value1), - new SetValueOperation(Key2, value2) - }; - - await storage.WriteStateAsync(testActorId, writeOperations, "0", cancellationToken); - - // Act - List keys with non-matching prefix - var readOperations = new List - { - new ListKeysOperation(continuationToken: null, keyPrefix: "nonexistent_") - }; - var result = await storage.ReadStateAsync(testActorId, readOperations, cancellationToken); - - // Assert - Assert.Single(result.Results); - var listKeys = result.Results[0] as ListKeysResult; - Assert.NotNull(listKeys); - Assert.Empty(listKeys.Keys); - Assert.Null(listKeys.ContinuationToken); - } - - [Fact] - public async Task ReadStateAsync_WithListKeysForEmptyActor_ShouldReturnEmptyListAsync() - { - // Arrange - using var cts = new CancellationTokenSource(s_defaultTimeout); - var cancellationToken = cts.Token; - - await using var storage = new CosmosActorStateStorage(this._fixture.Container); - var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N")); - - // Act - List keys for actor with no state - var readOperations = new List - { - new ListKeysOperation(continuationToken: null) - }; - var result = await storage.ReadStateAsync(testActorId, readOperations, cancellationToken); - - // Assert - Assert.Single(result.Results); - var listKeys = result.Results[0] as ListKeysResult; - Assert.NotNull(listKeys); - Assert.Empty(listKeys.Keys); - Assert.Null(listKeys.ContinuationToken); - } - - [Fact] - public async Task ReadStateAsync_WithListKeysOperation_ShouldReturnAllKeysAsync() - { - // Arrange - using var cts = new CancellationTokenSource(s_defaultTimeout); - var cancellationToken = cts.Token; - - await using var storage = new CosmosActorStateStorage(this._fixture.Container); - var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N")); - - const string Key1 = "key1"; - const string Key2 = "key2"; - var value1 = JsonSerializer.SerializeToElement("value1"); - var value2 = JsonSerializer.SerializeToElement("value2"); - - var writeOperations = new List - { - new SetValueOperation(Key1, value1), - new SetValueOperation(Key2, value2) - }; - - // First write some data - var writeResult = await storage.WriteStateAsync(testActorId, writeOperations, "0", cancellationToken); - Assert.True(writeResult.Success); - - // Act - List keys - var readOperations = new List - { - new ListKeysOperation(continuationToken: null) - }; - var readResult = await storage.ReadStateAsync(testActorId, readOperations, cancellationToken); - - // Assert - Assert.Single(readResult.Results); - var listKeys = readResult.Results[0] as ListKeysResult; - Assert.NotNull(listKeys); - Assert.Equal(2, listKeys.Keys.Count); - Assert.Contains(Key1, listKeys.Keys); - Assert.Contains(Key2, listKeys.Keys); - } - - [Fact] - public async Task ReadStateAsync_WithListKeysAfterKeyRemoval_ShouldNotIncludeRemovedKeysAsync() - { - // Arrange - using var cts = new CancellationTokenSource(s_defaultTimeout); - var cancellationToken = cts.Token; - - await using var storage = new CosmosActorStateStorage(this._fixture.Container); - var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N")); - - const string Key1 = "key1"; - const string Key2 = "key2"; - const string Key3 = "key3"; - var value1 = JsonSerializer.SerializeToElement("value1"); - var value2 = JsonSerializer.SerializeToElement("value2"); - var value3 = JsonSerializer.SerializeToElement("value3"); - - // Setup initial state with 3 keys - var writeOperations = new List - { - new SetValueOperation(Key1, value1), - new SetValueOperation(Key2, value2), - new SetValueOperation(Key3, value3) - }; - var writeResult = await storage.WriteStateAsync(testActorId, writeOperations, "0", cancellationToken); - Assert.True(writeResult.Success); - - // Remove one key - var removeOperations = new List - { - new RemoveKeyOperation(Key2) - }; - var removeResult = await storage.WriteStateAsync(testActorId, removeOperations, writeResult.ETag, cancellationToken); - Assert.True(removeResult.Success); - - // Act - List keys after removal - var readOperations = new List - { - new ListKeysOperation(continuationToken: null) - }; - var readResult = await storage.ReadStateAsync(testActorId, readOperations, cancellationToken); - - // Assert - Only remaining keys should be listed - Assert.Single(readResult.Results); - var listKeys = readResult.Results[0] as ListKeysResult; - Assert.NotNull(listKeys); - Assert.Equal(2, listKeys.Keys.Count); - Assert.Contains(Key1, listKeys.Keys); - Assert.Contains(Key3, listKeys.Keys); - Assert.DoesNotContain(Key2, listKeys.Keys); - } - - [Fact] - public async Task ReadStateAsync_WithListKeysAndMultiplePrefixes_ShouldFilterCorrectlyAsync() - { - // Arrange - using var cts = new CancellationTokenSource(s_defaultTimeout); - var cancellationToken = cts.Token; - - await using var storage = new CosmosActorStateStorage(this._fixture.Container); - var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N")); - - // Create keys with different prefixes - string[] userKeys = ["user_profile", "user_settings", "user_preferences"]; - string[] sessionKeys = ["session_token", "session_data"]; - string[] cacheKeys = ["cache_item1", "cache_item2", "cache_item3"]; - string[] miscKeys = ["config", "metadata"]; - - var writeOperations = new List(); - foreach (var key in userKeys.Concat(sessionKeys).Concat(cacheKeys).Concat(miscKeys)) - { - writeOperations.Add(new SetValueOperation(key, JsonSerializer.SerializeToElement($"value_for_{key}"))); - } - - await storage.WriteStateAsync(testActorId, writeOperations, "0", cancellationToken); - - // Test user_ prefix - var userReadOps = new List - { - new ListKeysOperation(continuationToken: null, keyPrefix: "user_") - }; - var userResult = await storage.ReadStateAsync(testActorId, userReadOps, cancellationToken); - var userListKeys = userResult.Results[0] as ListKeysResult; - Assert.NotNull(userListKeys); - Assert.Equal(3, userListKeys.Keys.Count); - Assert.All(userKeys, key => Assert.Contains(key, userListKeys.Keys)); - - // Test session_ prefix - var sessionReadOps = new List - { - new ListKeysOperation(continuationToken: null, keyPrefix: "session_") - }; - var sessionResult = await storage.ReadStateAsync(testActorId, sessionReadOps, cancellationToken); - var sessionListKeys = sessionResult.Results[0] as ListKeysResult; - Assert.NotNull(sessionListKeys); - Assert.Equal(2, sessionListKeys.Keys.Count); - Assert.All(sessionKeys, key => Assert.Contains(key, sessionListKeys.Keys)); - - // Test cache_ prefix - var cacheReadOps = new List - { - new ListKeysOperation(continuationToken: null, keyPrefix: "cache_") - }; - var cacheResult = await storage.ReadStateAsync(testActorId, cacheReadOps, cancellationToken); - var cacheListKeys = cacheResult.Results[0] as ListKeysResult; - Assert.NotNull(cacheListKeys); - Assert.Equal(3, cacheListKeys.Keys.Count); - Assert.All(cacheKeys, key => Assert.Contains(key, cacheListKeys.Keys)); - - // Test no prefix (should return all keys) - var allReadOps = new List - { - new ListKeysOperation(continuationToken: null) - }; - var allResult = await storage.ReadStateAsync(testActorId, allReadOps, cancellationToken); - var allListKeys = allResult.Results[0] as ListKeysResult; - Assert.NotNull(allListKeys); - Assert.Equal(10, allListKeys.Keys.Count); // 3 + 2 + 3 + 2 = 10 total keys - } -} diff --git a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Agents.AI.Runtime.Storage.CosmosDB.Tests/CosmosActorStateStorageTests.cs b/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Agents.AI.Runtime.Storage.CosmosDB.Tests/CosmosActorStateStorageTests.cs deleted file mode 100644 index 9d864e85d7..0000000000 --- a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Agents.AI.Runtime.Storage.CosmosDB.Tests/CosmosActorStateStorageTests.cs +++ /dev/null @@ -1,487 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json; - -namespace Microsoft.Agents.AI.Runtime.Storage.CosmosDB.Tests; - -/// -/// Integration tests for CosmosActorStateStorage covering basic CRUD operations and advanced scenarios. -/// -[Collection("Cosmos Test Collection")] -public class CosmosActorStateStorageTests -{ - private readonly CosmosTestFixture _fixture; - - public CosmosActorStateStorageTests(CosmosTestFixture fixture) - { - this._fixture = fixture; - } - - private static readonly TimeSpan s_defaultTimeout = TimeSpan.FromSeconds(300); - - [Fact] - public async Task WriteStateAsync_WithSetValueOperation_ShouldStoreValueAsync() - { - // Arrange - using var cts = new CancellationTokenSource(s_defaultTimeout); - var cancellationToken = cts.Token; - - await using var storage = new CosmosActorStateStorage(this._fixture.Container); - var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N")); - - const string Key = "testKey"; - var value = JsonSerializer.SerializeToElement("testValue"); - - var operations = new List - { - new SetValueOperation(Key, value) - }; - - // Act - var result = await storage.WriteStateAsync(testActorId, operations, "0", cancellationToken); - - // Assert - Assert.True(result.Success); - Assert.NotEqual("0", result.ETag); - } - - [Fact] - public async Task WriteAndReadState_WithMultipleOperations_ShouldMaintainConsistencyAsync() - { - // Arrange - using var cts = new CancellationTokenSource(s_defaultTimeout); - var cancellationToken = cts.Token; - - await using var storage = new CosmosActorStateStorage(this._fixture.Container); - var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N")); - - const string Key1 = "key1"; - const string Key2 = "key2"; - var value1 = JsonSerializer.SerializeToElement("value1"); - var value2 = JsonSerializer.SerializeToElement(42); - - var writeOperations = new List - { - new SetValueOperation(Key1, value1), - new SetValueOperation(Key2, value2) - }; - - // Act - Write state - var writeResult = await storage.WriteStateAsync(testActorId, writeOperations, "0", cancellationToken); - - // Assert write succeeded - Assert.True(writeResult.Success); - Assert.NotNull(writeResult.ETag); - Assert.NotEqual("0", writeResult.ETag); - - // Act - Read individual values - var readOperations = new List - { - new GetValueOperation(Key1), - new GetValueOperation(Key2) - }; - var readResult = await storage.ReadStateAsync(testActorId, readOperations, cancellationToken); - - // Assert read succeeded and values match - Assert.Equal(2, readResult.Results.Count); - - var getValue1 = readResult.Results[0] as GetValueResult; - var getValue2 = readResult.Results[1] as GetValueResult; - - Assert.NotNull(getValue1); - Assert.NotNull(getValue2); - Assert.Equal("value1", getValue1.Value?.GetString()); - Assert.Equal(42, getValue2.Value?.GetInt32()); - - // Act - List keys - var listKeysOperations = new List - { - new ListKeysOperation(continuationToken: null) - }; - var listResult = await storage.ReadStateAsync(testActorId, listKeysOperations, cancellationToken); - - // Assert keys are listed correctly - Assert.Single(listResult.Results); - var listKeys = listResult.Results[0] as ListKeysResult; - Assert.NotNull(listKeys); - Assert.Equal(2, listKeys.Keys.Count); - Assert.Contains(Key1, listKeys.Keys); - Assert.Contains(Key2, listKeys.Keys); - - // Act - Update with correct ETag - var updateOperations = new List - { - new SetValueOperation(Key1, JsonSerializer.SerializeToElement("updated_value1")), - new RemoveKeyOperation(Key2) - }; - var updateResult = await storage.WriteStateAsync(testActorId, updateOperations, writeResult.ETag, cancellationToken); - - // Assert update succeeded - Assert.True(updateResult.Success); - Assert.NotEqual(writeResult.ETag, updateResult.ETag); - - // Act - Verify final state - var finalReadOperations = new List - { - new GetValueOperation(Key1), - new GetValueOperation(Key2), - new ListKeysOperation(continuationToken: null) - }; - var finalResult = await storage.ReadStateAsync(testActorId, finalReadOperations, cancellationToken); - - // Assert final state is correct - Assert.Equal(3, finalResult.Results.Count); - - var finalValue1 = finalResult.Results[0] as GetValueResult; - var finalValue2 = finalResult.Results[1] as GetValueResult; - var finalKeys = finalResult.Results[2] as ListKeysResult; - - Assert.NotNull(finalValue1); - Assert.NotNull(finalValue2); - Assert.NotNull(finalKeys); - - Assert.Equal("updated_value1", finalValue1.Value?.GetString()); - Assert.Null(finalValue2.Value); // key2 was removed - Assert.Single(finalKeys.Keys); - Assert.Contains(Key1, finalKeys.Keys); - Assert.DoesNotContain(Key2, finalKeys.Keys); - } - - [Fact] - public async Task WriteStateAsync_WithIncorrectETag_ShouldReturnFailureAsync() - { - // Arrange - using var cts = new CancellationTokenSource(s_defaultTimeout); - var cancellationToken = cts.Token; - - await using var storage = new CosmosActorStateStorage(this._fixture.Container); - var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N")); - - const string Key = "testKey"; - var value = JsonSerializer.SerializeToElement("testValue"); - var operations = new List - { - new SetValueOperation(Key, value) - }; - - // First write to establish state - var firstResult = await storage.WriteStateAsync(testActorId, operations, "0", cancellationToken); - Assert.True(firstResult.Success); - - // Act - Try to write with incorrect ETag - var incorrectOperations = new List - { - new SetValueOperation(Key, JsonSerializer.SerializeToElement("newValue")) - }; - var result = await storage.WriteStateAsync(testActorId, incorrectOperations, "incorrect-etag", cancellationToken); - - // Assert - Assert.False(result.Success); - Assert.Empty(result.ETag); - - // Verify original value is unchanged - var readOperations = new List - { - new GetValueOperation(Key) - }; - var readResult = await storage.ReadStateAsync(testActorId, readOperations, cancellationToken); - var getValue = readResult.Results[0] as GetValueResult; - Assert.Equal("testValue", getValue?.Value?.GetString()); - } - - [Fact] - public async Task DifferentActors_ShouldHaveIsolatedStateAsync() - { - // Arrange - using var cts = new CancellationTokenSource(s_defaultTimeout); - var cancellationToken = cts.Token; - - await using var storage = new CosmosActorStateStorage(this._fixture.Container); - var testActorId1 = new ActorId("TestActor1", Guid.NewGuid().ToString("N")); - var testActorId2 = new ActorId("TestActor2", Guid.NewGuid().ToString("N")); - - const string Key = "sharedKey"; - var value1 = JsonSerializer.SerializeToElement("value1"); - var value2 = JsonSerializer.SerializeToElement("value2"); - - var operations1 = new List - { - new SetValueOperation(Key, value1) - }; - var operations2 = new List - { - new SetValueOperation(Key, value2) - }; - - // Act - Write to both actors - await storage.WriteStateAsync(testActorId1, operations1, "0", cancellationToken); - await storage.WriteStateAsync(testActorId2, operations2, "0", cancellationToken); - - // Assert - Verify values are different - var readOperations = new List - { - new GetValueOperation(Key) - }; - - var result1 = await storage.ReadStateAsync(testActorId1, readOperations, cancellationToken); - var result2 = await storage.ReadStateAsync(testActorId2, readOperations, cancellationToken); - - var getValue1 = result1.Results[0] as GetValueResult; - var getValue2 = result2.Results[0] as GetValueResult; - - Assert.NotNull(getValue1); - Assert.NotNull(getValue2); - Assert.Equal("value1", getValue1.Value?.GetString()); - Assert.Equal("value2", getValue2.Value?.GetString()); - Assert.NotEqual(result1.ETag, result2.ETag); - } - - [Fact] - public async Task WriteStateAsync_WithEmptyOperations_ShouldThrowExceptionAsync() - { - // Arrange - using var cts = new CancellationTokenSource(s_defaultTimeout); - var cancellationToken = cts.Token; - await using var storage = new CosmosActorStateStorage(this._fixture.Container); - var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N")); - var emptyOperations = new List(); - // Act & Assert - await Assert.ThrowsAsync(async () => await storage.WriteStateAsync(testActorId, emptyOperations, "0", cancellationToken)); - } - - [Fact] - public async Task ReadStateAsync_WithGetValueForNonExistentKey_ShouldReturnNullAsync() - { - // Arrange - using var cts = new CancellationTokenSource(s_defaultTimeout); - var cancellationToken = cts.Token; - - await using var storage = new CosmosActorStateStorage(this._fixture.Container); - var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N")); - - var readOperations = new List - { - new GetValueOperation("nonExistentKey") - }; - - // Act - var result = await storage.ReadStateAsync(testActorId, readOperations, cancellationToken); - - // Assert - Assert.Single(result.Results); - var getValue = result.Results[0] as GetValueResult; - Assert.NotNull(getValue); - Assert.Null(getValue.Value); - } - - [Fact] - public async Task WriteStateAsync_WithComplexJsonValue_ShouldSerializeCorrectlyAsync() - { - // Arrange - using var cts = new CancellationTokenSource(s_defaultTimeout); - var cancellationToken = cts.Token; - - await using var storage = new CosmosActorStateStorage(this._fixture.Container); - var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N")); - - // Create a complex object with various types - var complexObject = new - { - Id = 123, - Name = "Test Object", - Properties = new Dictionary - { - { "StringProp", "value" }, - { "NumberProp", 42.5 }, - { "BoolProp", true }, - { "ArrayProp", (int[])[1, 2, 3] }, - { "NestedProp", new { Inner = "nested value" } } - }, - Tags = new[] { "tag1", "tag2", "tag3" }, - Metadata = new Dictionary - { - { "version", "1.0" }, - { "author", "test" } - } - }; - - const string Key = "complexObject"; - var value = JsonSerializer.SerializeToElement(complexObject); - - var operations = new List - { - new SetValueOperation(Key, value) - }; - - // Act - Write complex object - var writeResult = await storage.WriteStateAsync(testActorId, operations, "0", cancellationToken); - Assert.True(writeResult.Success); - - // Act - Read back complex object - var readOperations = new List - { - new GetValueOperation(Key) - }; - var readResult = await storage.ReadStateAsync(testActorId, readOperations, cancellationToken); - - // Assert - Verify complex object was stored and retrieved correctly - Assert.Single(readResult.Results); - var getValue = readResult.Results[0] as GetValueResult; - Assert.NotNull(getValue); - Assert.NotNull(getValue.Value); - - // Deserialize and verify structure - var retrievedObject = JsonSerializer.Deserialize(getValue.Value!.Value.GetRawText()); - Assert.Equal(123, retrievedObject.GetProperty("Id").GetInt32()); - Assert.Equal("Test Object", retrievedObject.GetProperty("Name").GetString()); - - var properties = retrievedObject.GetProperty("Properties"); - Assert.Equal("value", properties.GetProperty("StringProp").GetString()); - Assert.Equal(42.5, properties.GetProperty("NumberProp").GetDouble()); - Assert.True(properties.GetProperty("BoolProp").GetBoolean()); - - var tags = retrievedObject.GetProperty("Tags"); - Assert.Equal(3, tags.GetArrayLength()); - Assert.Equal("tag1", tags[0].GetString()); - } - - [Fact] - public async Task MultipleOperationsInSequence_ShouldBeProcessedInOrderAsync() - { - // Arrange - using var cts = new CancellationTokenSource(s_defaultTimeout); - var cancellationToken = cts.Token; - - await using var storage = new CosmosActorStateStorage(this._fixture.Container); - var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N")); - - const string Key1 = "key1"; - const string Key2 = "key2"; - const string Key3 = "key3"; - var value1 = JsonSerializer.SerializeToElement("value1"); - var value2 = JsonSerializer.SerializeToElement("value2"); - var value3 = JsonSerializer.SerializeToElement("value3"); - - // Act - Perform multiple operations in a single batch - var operations = new List - { - new SetValueOperation(Key1, value1), // Set key1 - new SetValueOperation(Key2, value2), // Set key2 - new SetValueOperation(Key3, value3), // Set key3 - new RemoveKeyOperation(Key1), // Remove key1 - new SetValueOperation(Key1, JsonSerializer.SerializeToElement("new_value1")) // Re-add key1 with new value - }; - - var result = await storage.WriteStateAsync(testActorId, operations, "0", cancellationToken); - - // Assert write succeeded - Assert.True(result.Success); - - // Act - Verify final state - var readOperations = new List - { - new GetValueOperation(Key1), - new GetValueOperation(Key2), - new GetValueOperation(Key3), - new ListKeysOperation(continuationToken: null) - }; - var readResult = await storage.ReadStateAsync(testActorId, readOperations, cancellationToken); - - // Assert final state is correct - Assert.Equal(4, readResult.Results.Count); - - var getValue1 = readResult.Results[0] as GetValueResult; - var getValue2 = readResult.Results[1] as GetValueResult; - var getValue3 = readResult.Results[2] as GetValueResult; - var listKeys = readResult.Results[3] as ListKeysResult; - - Assert.NotNull(getValue1); - Assert.NotNull(getValue2); - Assert.NotNull(getValue3); - Assert.NotNull(listKeys); - - // key1 should have the final value from the last operation - Assert.Equal("new_value1", getValue1.Value?.GetString()); - Assert.Equal("value2", getValue2.Value?.GetString()); - Assert.Equal("value3", getValue3.Value?.GetString()); - - // All three keys should be present - Assert.Equal(3, listKeys.Keys.Count); - Assert.Contains(Key1, listKeys.Keys); - Assert.Contains(Key2, listKeys.Keys); - Assert.Contains(Key3, listKeys.Keys); - } - - [SkipOnEmulatorFact] - public async Task WriteAndReadState_WithSpecialCharactersInKeys_ShouldHandleSanitizationAsync() - { - // Arrange - using var cts = new CancellationTokenSource(s_defaultTimeout); - var cancellationToken = cts.Token; - - await using var storage = new CosmosActorStateStorage(this._fixture.Container); - var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N")); - - // Test keys with special characters that need sanitization - var specialKeys = new[] - { - "key/with/slashes", - "key with spaces", - "key:with:colons", - "key@with@symbols", - "key%with%percent", - "key#with#hash", - "key?with?query", - "key&with&ersand" - }; - - var writeOperations = new List(); - for (int i = 0; i < specialKeys.Length; i++) - { - var value = JsonSerializer.SerializeToElement($"value{i}"); - writeOperations.Add(new SetValueOperation(specialKeys[i], value)); - } - - // Act - Write keys with special characters - var writeResult = await storage.WriteStateAsync(testActorId, writeOperations, "0", cancellationToken); - - // Assert write succeeded - Assert.True(writeResult.Success); - Assert.NotNull(writeResult.ETag); - - // Act - Read back each key individually - for (int i = 0; i < specialKeys.Length; i++) - { - var readOperations = new List - { - new GetValueOperation(specialKeys[i]) - }; - var readResult = await storage.ReadStateAsync(testActorId, readOperations, cancellationToken); - - // Assert each key can be read back correctly - Assert.Single(readResult.Results); - var getValue = readResult.Results[0] as GetValueResult; - Assert.NotNull(getValue); - Assert.NotNull(getValue.Value); - Assert.Equal($"value{i}", getValue.Value?.GetString()); - } - - // Act - List all keys - var listOperations = new List - { - new ListKeysOperation(continuationToken: null) - }; - var listResult = await storage.ReadStateAsync(testActorId, listOperations, cancellationToken); - - // Assert all keys are present in the list - Assert.Single(listResult.Results); - var listKeys = listResult.Results[0] as ListKeysResult; - Assert.NotNull(listKeys); - Assert.Equal(specialKeys.Length, listKeys.Keys.Count); - - foreach (var specialKey in specialKeys) - { - Assert.Contains(specialKey, listKeys.Keys); - } - } -} diff --git a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Agents.AI.Runtime.Storage.CosmosDB.Tests/CosmosIdSanitizerTests.cs b/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Agents.AI.Runtime.Storage.CosmosDB.Tests/CosmosIdSanitizerTests.cs deleted file mode 100644 index 57cba36184..0000000000 --- a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Agents.AI.Runtime.Storage.CosmosDB.Tests/CosmosIdSanitizerTests.cs +++ /dev/null @@ -1,266 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -namespace Microsoft.Agents.AI.Runtime.Storage.CosmosDB.Tests; - -public class CosmosIdSanitizerTests -{ - [Fact] - public void Sanitize_WithValidInputNoSpecialChars_ReturnsOriginalString() - { - // Arrange - const string Input = "ValidId123"; - - // Act - string result = CosmosIdSanitizer.Sanitize(Input); - - // Assert - Assert.Equal(Input, result); - } - - [Fact] - public void Sanitize_WithEmptyString_ReturnsEmptyString() - { - // Arrange - const string Input = ""; - - // Act - string result = CosmosIdSanitizer.Sanitize(Input); - - // Assert - Assert.Equal(Input, result); - } - - [Theory] - [InlineData("/", "~0")] - [InlineData("\\", "~1")] - [InlineData("?", "~2")] - [InlineData("#", "~3")] - [InlineData("_", "~4")] - [InlineData("~", "~5")] - public void Sanitize_WithSingleSpecialChar_ReturnsCorrectEscapeSequence(string input, string expected) - { - // Act - string result = CosmosIdSanitizer.Sanitize(input); - - // Assert - Assert.Equal(expected, result); - } - - [Fact] - public void Sanitize_WithMultipleSpecialChars_ReturnsCorrectEscapeSequences() - { - // Arrange - const string Input = "test/path\\file?query#fragment_underscore~tilde"; - const string Expected = "test~0path~1file~2query~3fragment~4underscore~5tilde"; - - // Act - string result = CosmosIdSanitizer.Sanitize(Input); - - // Assert - Assert.Equal(Expected, result); - } - - [Fact] - public void Sanitize_WithMixedValidAndSpecialChars_ReturnsCorrectResult() - { - // Arrange - const string Input = "user/123"; - const string Expected = "user~0123"; - - // Act - string result = CosmosIdSanitizer.Sanitize(Input); - - // Assert - Assert.Equal(Expected, result); - } - - [Fact] - public void Unsanitize_WithValidInputNoEscapeChars_ReturnsOriginalString() - { - // Arrange - const string Input = "ValidId123"; - - // Act - string result = CosmosIdSanitizer.Unsanitize(Input); - - // Assert - Assert.Equal(Input, result); - } - - [Fact] - public void Unsanitize_WithEmptyString_ReturnsEmptyString() - { - // Arrange - const string Input = ""; - - // Act - string result = CosmosIdSanitizer.Unsanitize(Input); - - // Assert - Assert.Equal(Input, result); - } - - [Theory] - [InlineData("~0", "/")] - [InlineData("~1", "\\")] - [InlineData("~2", "?")] - [InlineData("~3", "#")] - [InlineData("~4", "_")] - [InlineData("~5", "~")] - public void Unsanitize_WithSingleEscapeSequence_ReturnsCorrectChar(string input, string expected) - { - // Act - string result = CosmosIdSanitizer.Unsanitize(input); - - // Assert - Assert.Equal(expected, result); - } - - [Fact] - public void Unsanitize_WithMultipleEscapeSequences_ReturnsCorrectResult() - { - // Arrange - const string Input = "test~0path~1file~2query~3fragment~4underscore~5tilde"; - const string Expected = "test/path\\file?query#fragment_underscore~tilde"; - - // Act - string result = CosmosIdSanitizer.Unsanitize(Input); - - // Assert - Assert.Equal(Expected, result); - } - - [Fact] - public void Unsanitize_WithMixedValidAndEscapeChars_ReturnsCorrectResult() - { - // Arrange - const string Input = "user~0123"; - const string Expected = "user/123"; - - // Act - string result = CosmosIdSanitizer.Unsanitize(Input); - - // Assert - Assert.Equal(Expected, result); - } - - [Theory] - [InlineData("~6")] - [InlineData("~A")] - [InlineData("~z")] - public void Unsanitize_WithInvalidEscapeSequence_ThrowsArgumentException(string input) - { - // Act & Assert - var exception = Assert.Throws(() => CosmosIdSanitizer.Unsanitize(input)); - Assert.Contains("Input is not in a valid format: Encountered unsupported escape sequence", exception.Message); - } - - [Fact] - public void SanitizeUnsanitize_RoundTrip_ReturnsOriginalString() - { - // Arrange - const string Original = "user/path\\to?file#with_underscore~and~tildes"; - - // Act - string sanitized = CosmosIdSanitizer.Sanitize(Original); - string unsanitized = CosmosIdSanitizer.Unsanitize(sanitized); - - // Assert - Assert.Equal(Original, unsanitized); - } - - [Theory] - [InlineData("")] - [InlineData("a")] - [InlineData("abc")] - [InlineData("simple-text")] - [InlineData("user123")] - [InlineData("user/path")] - [InlineData("/\\?#_~")] - [InlineData("complex/path\\with?query#fragment_underscore~tilde")] - public void SanitizeUnsanitize_RoundTripProperty_AlwaysReturnsOriginal(string original) - { - // Act - string sanitized = CosmosIdSanitizer.Sanitize(original); - string unsanitized = CosmosIdSanitizer.Unsanitize(sanitized); - - // Assert - Assert.Equal(original, unsanitized); - } - - [Fact] - public void Sanitize_WithLongString_HandlesCorrectly() - { - // Arrange - var input = new string('a', 1000) + "/" + new string('b', 1000) + "\\" + new string('c', 1000); - - // Act - string result = CosmosIdSanitizer.Sanitize(input); - - // Assert - Assert.Contains("~0", result); - Assert.Contains("~1", result); - Assert.Equal(3004, result.Length); // Original 3002 chars + 2 escape chars - } - - [Fact] - public void Unsanitize_WithLongString_HandlesCorrectly() - { - // Arrange - var input = new string('a', 1000) + "~0" + new string('b', 1000) + "~1" + new string('c', 1000); - - // Act - string result = CosmosIdSanitizer.Unsanitize(input); - - // Assert - Assert.Contains("/", result); - Assert.Contains("\\", result); - Assert.Equal(3002, result.Length); // 3004 chars - 2 escape chars - } - - [Fact] - public void SeparatorChar_HasCorrectValue() => - // Assert - Assert.Equal('_', CosmosIdSanitizer.SeparatorChar); - - [Fact] - public void Sanitize_WithOnlySeparatorChar_EscapesCorrectly() - { - // Arrange - const string Input = "_"; - - // Act - string result = CosmosIdSanitizer.Sanitize(Input); - - // Assert - Assert.Equal("~4", result); - } - - [Fact] - public void Sanitize_WithConsecutiveSpecialChars_HandlesCorrectly() - { - // Arrange - const string Input = "//\\\\??##__~~"; - const string Expected = "~0~0~1~1~2~2~3~3~4~4~5~5"; - - // Act - string result = CosmosIdSanitizer.Sanitize(Input); - - // Assert - Assert.Equal(Expected, result); - } - - [Fact] - public void Unsanitize_WithConsecutiveEscapeSequences_HandlesCorrectly() - { - // Arrange - const string Input = "~0~0~1~1~2~2~3~3~4~4~5~5"; - const string Expected = "//\\\\??##__~~"; - - // Act - string result = CosmosIdSanitizer.Unsanitize(Input); - - // Assert - Assert.Equal(Expected, result); - } -} diff --git a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Agents.AI.Runtime.Storage.CosmosDB.Tests/CosmosTestFixture.cs b/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Agents.AI.Runtime.Storage.CosmosDB.Tests/CosmosTestFixture.cs deleted file mode 100644 index f94438d377..0000000000 --- a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Agents.AI.Runtime.Storage.CosmosDB.Tests/CosmosTestFixture.cs +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json; -using Aspire.Hosting; -using Azure.Identity; -using CosmosDB.Testing.AppHost; -using Microsoft.Azure.Cosmos; -using Microsoft.Extensions.Logging; - -#pragma warning disable CA2007, VSTHRD111, CS1591 - -namespace Microsoft.Agents.AI.Runtime.Storage.CosmosDB.Tests; - -[CollectionDefinition("Cosmos Test Collection")] -public class CosmosTests : ICollectionFixture; - -/// -/// Shared test fixture for CosmosDB integration tests. -/// Sets up and manages the CosmosDB container for all tests. -/// -public class CosmosTestFixture : IAsyncLifetime -{ - public DistributedApplication App { get; private set; } = default!; - public CosmosClient CosmosClient { get; private set; } = default!; - public Container Container { get; private set; } = default!; - - /// - public async Task InitializeAsync() - { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(300)); - var cancellationToken = cts.Token; - - var appHost = await DistributedApplicationTestingBuilder - .CreateAsync(cancellationToken); - - appHost.Services.AddLogging(logging => - { - logging.SetMinimumLevel(LogLevel.Debug); - logging.AddFilter(appHost.Environment.ApplicationName, LogLevel.Debug); - logging.AddFilter("Aspire.", LogLevel.Debug); - }); - - appHost.Services.ConfigureHttpClientDefaults(clientBuilder => - clientBuilder.AddStandardResilienceHandler()); - - this.App = await appHost.BuildAsync(cancellationToken).WaitAsync(cancellationToken); - await this.App.StartAsync(cancellationToken).WaitAsync(cancellationToken); - - var connectionString = await this.App.GetConnectionStringAsync(CosmosDBTestConstants.TestCosmosDbName, cancellationToken); - if (CosmosDBTestConstants.UseEmulatorInCICD) - { - // Emulator is setup in the CI/CD pipeline, so we will not use one produced by Aspire. - // For simplicity, we override the connection string here with the well-known emulator connection string. - // https://learn.microsoft.com/en-us/azure/cosmos-db/emulator - - connectionString = "AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==;"; - } - - CosmosClientOptions ccoptions = new() - { - UseSystemTextJsonSerializerWithOptions = new JsonSerializerOptions() - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - TypeInfoResolver = CosmosActorStateJsonContext.Default - } - }; - - if (CosmosDBTestConstants.UseAspireEmulatorForTesting || CosmosDBTestConstants.UseEmulatorInCICD) - { - ccoptions.ConnectionMode = ConnectionMode.Gateway; - ccoptions.LimitToEndpoint = true; - this.CosmosClient = new CosmosClient(connectionString, ccoptions); - } - else - { - this.CosmosClient = new CosmosClient(connectionString, new DefaultAzureCredential(), ccoptions); - } - - var database = this.CosmosClient.GetDatabase(CosmosDBTestConstants.TestCosmosDbDatabaseName); - - // raise throughput to avoid parallel test execution failures - var throughputProperties = ThroughputProperties.CreateAutoscaleThroughput(100000); - - // Ensure database exists. It will be a no-op if it was already created before. - _ = await this.CosmosClient.CreateDatabaseIfNotExistsAsync(CosmosDBTestConstants.TestCosmosDbDatabaseName, throughputProperties); - - var containerProperties = new ContainerProperties() - { - Id = "CosmosActorStateStorageTests", - PartitionKeyPaths = LazyCosmosContainer.CosmosPartitionKeyPaths - }; - - this.Container = await database.CreateContainerIfNotExistsAsync(containerProperties); - } - - public async Task DisposeAsync() - { - await this.App.DisposeAsync(); - this.CosmosClient.Dispose(); - } -} diff --git a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Agents.AI.Runtime.Storage.CosmosDB.Tests/LazyCosmosContainerTests.cs b/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Agents.AI.Runtime.Storage.CosmosDB.Tests/LazyCosmosContainerTests.cs deleted file mode 100644 index e4ab805640..0000000000 --- a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Agents.AI.Runtime.Storage.CosmosDB.Tests/LazyCosmosContainerTests.cs +++ /dev/null @@ -1,278 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json; -using CosmosDB.Testing.AppHost; -using Microsoft.Azure.Cosmos; - -namespace Microsoft.Agents.AI.Runtime.Storage.CosmosDB.Tests; - -/// -/// Integration tests for LazyCosmosContainer to verify lazy initialization behavior. -/// -[Collection("Cosmos Test Collection")] -public class LazyCosmosContainerTests -{ - private readonly CosmosTestFixture _fixture; - - public LazyCosmosContainerTests(CosmosTestFixture fixture) - { - this._fixture = fixture; - } - - private static readonly TimeSpan s_defaultTimeout = TimeSpan.FromSeconds(300); - - [Fact] - public async Task GetContainerAsync_WithExistingContainer_ShouldReturnImmediatelyAsync() - { - // Arrange - using var cts = new CancellationTokenSource(s_defaultTimeout); - await using var lazyContainer = new LazyCosmosContainer(this._fixture.Container); - - // Act - var result = await lazyContainer.GetContainerAsync(); - - // Assert - Assert.Same(this._fixture.Container, result); - } - - [Fact] - public async Task GetContainerAsync_WithExistingContainer_MultipleCalls_ShouldReturnSameInstanceAsync() - { - // Arrange - using var cts = new CancellationTokenSource(s_defaultTimeout); - await using var lazyContainer = new LazyCosmosContainer(this._fixture.Container); - - // Act - var result1 = await lazyContainer.GetContainerAsync(); - var result2 = await lazyContainer.GetContainerAsync(); - var result3 = await lazyContainer.GetContainerAsync(); - - // Assert - Assert.Same(result1, result2); - Assert.Same(result2, result3); - Assert.Same(this._fixture.Container, result1); - } - - [SkipOnEmulatorFact] - public async Task GetContainerAsync_WithCosmosClient_ShouldInitializeAndWorkCorrectlyAsync() - { - // Arrange - using var cts = new CancellationTokenSource(s_defaultTimeout); - var cancellationToken = cts.Token; - - // Create a unique container name for this test - var testContainerName = $"LazyContainerTest_{Guid.NewGuid():N}"; - await using var lazyContainer = new LazyCosmosContainer(this._fixture.CosmosClient, CosmosDBTestConstants.TestCosmosDbDatabaseName, testContainerName); - - try - { - // Act - var container = await lazyContainer.GetContainerAsync(); - - // Assert - Container should be usable for actual operations - Assert.NotNull(container); - Assert.Equal(testContainerName, container.Id); - - // Verify the container can perform basic operations - var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N")); - await using var storage = new CosmosActorStateStorage(lazyContainer); - - const string Key = "testKey"; - var value = JsonSerializer.SerializeToElement("testValue"); - var operations = new List - { - new SetValueOperation(Key, value) - }; - - // This should work if the container was properly initialized - var writeResult = await storage.WriteStateAsync(testActorId, operations, "0", cancellationToken); - Assert.True(writeResult.Success); - Assert.NotEqual("0", writeResult.ETag); - } - finally - { - // Cleanup - delete the test container - try - { - var container = await lazyContainer.GetContainerAsync(); - await container.DeleteContainerAsync(); - } - catch - { - // Ignore cleanup errors - } - } - } - - [Fact] - public async Task GetContainerAsync_WithCosmosClient_MultipleCalls_ShouldReturnSameInstanceAsync() - { - // Arrange - using var cts = new CancellationTokenSource(s_defaultTimeout); - - // Create a unique container name for this test - var testContainerName = $"LazyContainerTest_{Guid.NewGuid():N}"; - await using var lazyContainer = new LazyCosmosContainer(this._fixture.CosmosClient, CosmosDBTestConstants.TestCosmosDbDatabaseName, testContainerName); - - try - { - // Act - var result1 = await lazyContainer.GetContainerAsync(); - var result2 = await lazyContainer.GetContainerAsync(); - var result3 = await lazyContainer.GetContainerAsync(); - - // Assert - Assert.Same(result1, result2); - Assert.Same(result2, result3); - Assert.Equal(testContainerName, result1.Id); - } - finally - { - // Cleanup - try - { - var container = await lazyContainer.GetContainerAsync(); - await container.DeleteContainerAsync(); - } - catch - { - // Ignore cleanup errors - } - } - } - - [Fact] - public async Task GetContainerAsync_WithCosmosClient_ConcurrentAccess_ShouldInitializeOnlyOnceAsync() - { - // Arrange - using var cts = new CancellationTokenSource(s_defaultTimeout); - - // Create a unique container name for this test - var testContainerName = $"LazyContainerTest_{Guid.NewGuid():N}"; - await using var lazyContainer = new LazyCosmosContainer(this._fixture.CosmosClient, CosmosDBTestConstants.TestCosmosDbDatabaseName, testContainerName); - - try - { - // Act - Execute multiple concurrent calls - var tasks = new List>(); - for (int i = 0; i < 10; i++) - { - tasks.Add(lazyContainer.GetContainerAsync()); - } - var results = await Task.WhenAll(tasks); - - // Assert - // All results should be the same instance - for (int i = 1; i < results.Length; i++) - { - Assert.Same(results[0], results[i]); - } - Assert.Equal(testContainerName, results[0].Id); - } - finally - { - // Cleanup - try - { - var container = await lazyContainer.GetContainerAsync(); - await container.DeleteContainerAsync(); - } - catch - { - // Ignore cleanup errors - } - } - } - - [Fact] - public void Constructor_WithNullContainer_ShouldThrowArgumentNullException() => - // Act & Assert - Assert.Throws(() => new LazyCosmosContainer(null!)); - - [Fact] - public void Constructor_WithNullCosmosClient_ShouldThrowArgumentNullException() => - // Act & Assert - Assert.Throws(() => new LazyCosmosContainer(null!, "test-db", "test-container")); - - [Fact] - public void Constructor_WithNullDatabaseName_ShouldThrowArgumentNullException() => - // Act & Assert - Assert.Throws(() => new LazyCosmosContainer(this._fixture.CosmosClient, null!, "test-container")); - - [Fact] - public void Constructor_WithNullContainerName_ShouldThrowArgumentNullException() => - // Act & Assert - Assert.Throws(() => new LazyCosmosContainer(this._fixture.CosmosClient, "test-db", null!)); - - [SkipOnEmulatorFact] - public async Task LazyCosmosContainer_WithInternalConstructor_ShouldWorkWithCosmosActorStateStorageAsync() - { - // Arrange - using var cts = new CancellationTokenSource(s_defaultTimeout); - var cancellationToken = cts.Token; - - // Create a unique container name for this test - var testContainerName = $"LazyContainerTest_{Guid.NewGuid():N}"; - await using var lazyContainer = new LazyCosmosContainer(this._fixture.CosmosClient, CosmosDBTestConstants.TestCosmosDbDatabaseName, testContainerName); - - try - { - // Act - Create storage using the internal constructor (like DI would) - await using var storage = new CosmosActorStateStorage(lazyContainer); - var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N")); - - const string Key = "testKey"; - var value = JsonSerializer.SerializeToElement("testValue"); - var operations = new List - { - new SetValueOperation(Key, value) - }; - - // This should work - container should be initialized on first storage operation - var writeResult = await storage.WriteStateAsync(testActorId, operations, "0", cancellationToken); - - // Assert - Assert.True(writeResult.Success); - Assert.NotEqual("0", writeResult.ETag); - - // Verify we can read back the value - var readOperations = new List - { - new GetValueOperation(Key) - }; - var readResult = await storage.ReadStateAsync(testActorId, readOperations, cancellationToken); - - Assert.Single(readResult.Results); - var getValue = readResult.Results[0] as GetValueResult; - Assert.NotNull(getValue); - Assert.Equal("testValue", getValue.Value?.GetString()); - } - finally - { - // Cleanup - try - { - var container = await lazyContainer.GetContainerAsync(); - await container.DeleteContainerAsync(); - } - catch - { - // Ignore cleanup errors - } - } - } - - [Fact] - public async Task GetContainerAsync_WithInvalidDatabaseName_ShouldThrowCosmosExceptionAsync() - { - // Arrange - using var cts = new CancellationTokenSource(s_defaultTimeout); - - // Use an invalid database name that should cause Cosmos to reject it - var invalidDatabaseName = new string('a', 256); // Database names have limits - await using var lazyContainer = new LazyCosmosContainer(this._fixture.CosmosClient, invalidDatabaseName, "test-container"); - - // Act & Assert - await Assert.ThrowsAsync(lazyContainer.GetContainerAsync); - } -} diff --git a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Agents.AI.Runtime.Storage.CosmosDB.Tests/Microsoft.Agents.AI.Runtime.Storage.CosmosDB.Tests.csproj b/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Agents.AI.Runtime.Storage.CosmosDB.Tests/Microsoft.Agents.AI.Runtime.Storage.CosmosDB.Tests.csproj deleted file mode 100644 index 9481804f67..0000000000 --- a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Agents.AI.Runtime.Storage.CosmosDB.Tests/Microsoft.Agents.AI.Runtime.Storage.CosmosDB.Tests.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - - net9.0 - enable - enable - false - true - - - - - - - - - - - - - - - - - - - - diff --git a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Agents.AI.Runtime.Storage.CosmosDB.Tests/SkipOnEmulatorFactAttribute.cs b/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Agents.AI.Runtime.Storage.CosmosDB.Tests/SkipOnEmulatorFactAttribute.cs deleted file mode 100644 index af8fefe340..0000000000 --- a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Agents.AI.Runtime.Storage.CosmosDB.Tests/SkipOnEmulatorFactAttribute.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using CosmosDB.Testing.AppHost; - -namespace Microsoft.Agents.AI.Runtime.Storage.CosmosDB.Tests; - -/// -/// Skip test if running on CosmosDB emulator. -/// -[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)] -public sealed class SkipOnEmulatorFactAttribute : FactAttribute -{ - /// - /// Initializes a new instance of the class. - /// - public SkipOnEmulatorFactAttribute() - { - if (CosmosDBTestConstants.UseAspireEmulatorForTesting) - { - this.Skip = "Skipping test on Aspire-configured CosmosDB emulator."; - } - - if (CosmosDBTestConstants.UseEmulatorInCICD) - { - this.Skip = "Skipping test on CICD-configured CosmosDB emulator."; - } - } -} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/AgentActorTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/AgentActorTests.cs deleted file mode 100644 index 645a52e22c..0000000000 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/AgentActorTests.cs +++ /dev/null @@ -1,215 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Agents.AI.Runtime; -using Microsoft.Extensions.AI; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; - -namespace Microsoft.Agents.AI.Hosting.UnitTests; - -/// -/// Unit tests for . -/// -public class AgentActorTests -{ - /// - /// Verifies that calling DisposeAsync completes successfully without throwing an exception. - /// - [Fact] - public async Task DisposeAsync_NoException_CompletesSuccessfullyAsync() - { - var mockAgent = new Mock(); - var mockContext = new Mock(); - var mockLogger = NullLoggerFactory.Instance.CreateLogger(); - var actor = new AgentActor(mockAgent.Object, mockContext.Object, mockLogger); - - var valueTask = actor.DisposeAsync(); - - Assert.True(valueTask.IsCompleted, "DisposeAsync should return a completed ValueTask."); - await valueTask; - } - - /// - /// Verifies that when no thread state exists, GetNewThread is called. - /// - [Fact] - public async Task RunAsync_WithNoExistingThreadState_CallsGetNewThreadAsync() - { - var mockExpectedThread = new Mock(); - - var mockAgent = new Mock(); - mockAgent.Setup(a => a.GetNewThread()).Returns(mockExpectedThread.Object); - - var mockContext = new Mock(); - var actorId = new ActorId("TestAgent", "test-instance"); - mockContext.Setup(c => c.ActorId).Returns(actorId); - - // Setup ReadAsync to return no existing thread state - var readResponse = new ReadResponse("test-etag", [new GetValueResult(null)]); - mockContext.Setup(c => c.ReadAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(readResponse); - - // Setup WatchMessagesAsync to return empty sequence to prevent infinite loop - mockContext.Setup(c => c.WatchMessagesAsync(It.IsAny())) - .Returns(CreateEmptyAsyncEnumerableAsync()); - - var mockLogger = NullLoggerFactory.Instance.CreateLogger(); - await using var actor = new AgentActor(mockAgent.Object, mockContext.Object, mockLogger); - - using var cts = new CancellationTokenSource(); - cts.CancelAfter(TimeSpan.FromMilliseconds(100)); // Cancel quickly to exit the loop - - await actor.RunAsync(cts.Token); - - mockAgent.Verify(a => a.GetNewThread(), Times.Once); - } - - /// - /// Verifies that when ReadAsync throws an exception, the actor handles it gracefully. - /// - [Fact] - public async Task RunAsync_WhenReadAsyncThrows_HandlesExceptionGracefullyAsync() - { - var mockAgent = new Mock(); - var mockContext = new Mock(); - var actorId = new ActorId("TestAgent", "test-instance"); - mockContext.Setup(c => c.ActorId).Returns(actorId); - - mockContext.Setup(c => c.ReadAsync(It.IsAny(), It.IsAny())) - .ThrowsAsync(new InvalidOperationException("Read failed")); - - var mockLogger = NullLoggerFactory.Instance.CreateLogger(); - await using var actor = new AgentActor(mockAgent.Object, mockContext.Object, mockLogger); - - using var cts = new CancellationTokenSource(); - - await Assert.ThrowsAsync(async () => - await actor.RunAsync(cts.Token)); - - mockAgent.Verify(a => a.GetNewThread(), Times.Never); - } - - /// - /// Verifies that the thread assignment works correctly when processing an agent request. - /// This test checks that the thread used in the agent request is properly assigned. - /// - [Fact] - public async Task HandleAgentRequest_UsesCorrectThreadAsync() - { - var threadJson = JsonSerializer.SerializeToElement(new { conversationId = "expected-thread-id" }); - var mockThread = new Mock(); - - TestAgent testAgent = new() - { - ThreadForCreate = mockThread.Object - }; - - var mockContext = new Mock(); - var actorId = new ActorId("TestAgent", "test-instance"); - mockContext.Setup(c => c.ActorId).Returns(actorId); - - var readResponse = new ReadResponse("test-etag", [new GetValueResult(threadJson)]); - mockContext.Setup(c => c.ReadAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(readResponse); - - // Create a request message - var requestMessage = new ActorRequestMessage("test-message-id") - { - SenderId = actorId, - Method = AgentActorConstants.RunMethodName, - Params = JsonSerializer.SerializeToElement(new AgentRunRequest - { - Messages = [new ChatMessage(ChatRole.User, "Test message")] - }) - }; - - var messageSequence = CreateAsyncEnumerableAsync(new List { requestMessage }); - mockContext.Setup(c => c.WatchMessagesAsync(It.IsAny())) - .Returns(messageSequence); - - mockContext.Setup(c => c.WriteAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(new WriteResponse("new-etag", true)); - - var mockLogger = NullLoggerFactory.Instance.CreateLogger(); - await using var actor = new AgentActor(testAgent, mockContext.Object, mockLogger); - - using var cts = new CancellationTokenSource(); - - cts.CancelAfter(TimeSpan.FromSeconds(1)); - - await actor.RunAsync(cts.Token); - - Assert.True(testAgent.RunStreamingAsyncCalled, "RunStreamingAsync should have been called"); - - // Verify the thread was used in RunStreamingAsync and has the expected ID - Assert.NotNull(testAgent.ThreadUsedInRunStreamingAsync); - Assert.Same(mockThread.Object, testAgent.ThreadUsedInRunStreamingAsync); - Assert.Equal(threadJson, testAgent.ElementUsedInDeserializeThread); - } - - /// - /// Helper method to create an empty async enumerable. - /// - private static async IAsyncEnumerable CreateEmptyAsyncEnumerableAsync() - { - await Task.CompletedTask; - yield break; - } - - /// - /// Helper method to create an async enumerable from a list. - /// - private static async IAsyncEnumerable CreateAsyncEnumerableAsync(IEnumerable items) - { - foreach (var item in items) - { - yield return item; - } - } - - /// - /// Test agent implementation to track method calls. - /// - private sealed class TestAgent : AIAgent - { - public AgentThread? ThreadForCreate { get; set; } - public JsonElement? ElementUsedInDeserializeThread { get; set; } - public bool RunStreamingAsyncCalled { get; private set; } - public AgentThread? ThreadUsedInRunStreamingAsync { get; private set; } - - public override AgentThread GetNewThread() - { - return this.ThreadForCreate!; - } - - public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null) - { - this.ElementUsedInDeserializeThread = serializedThread; - return this.ThreadForCreate!; - } - - public override Task RunAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) - { - this.ThreadUsedInRunStreamingAsync = thread; - return Task.FromResult(new AgentRunResponse - { - Messages = [new ChatMessage(ChatRole.Assistant, "Test response")] - }); - } - - public override async IAsyncEnumerable RunStreamingAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default) - { - this.RunStreamingAsyncCalled = true; - this.ThreadUsedInRunStreamingAsync = thread; - - yield return new AgentRunResponseUpdate(ChatRole.Assistant, "Test response"); - await Task.CompletedTask; - } - } -} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/AgentProxyTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/AgentProxyTests.cs deleted file mode 100644 index 882577df93..0000000000 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/AgentProxyTests.cs +++ /dev/null @@ -1,836 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Agents.AI.Runtime; -using Microsoft.Extensions.AI; -using Moq; - -namespace Microsoft.Agents.AI.Hosting.UnitTests; - -/// -/// Tests for the constructor. -/// -public class AgentProxyTests -{ - /// - /// Verifies that the constructor assigns the Name property correctly for various valid agent names. - /// - [Theory] - [InlineData("agent")] - [InlineData(" ")] - [InlineData("特殊字符")] - [InlineData(" a")] - public void Constructor_ValidName_SetsNameProperty(string name) - { - // Arrange - var mockClient = new Mock(); - - // Act - var proxy = new AgentProxy(name, mockClient.Object); - - // Assert - Assert.Equal(name, proxy.Name); - } - - /// - /// Verifies that GetNewThread returns a non-null instance. - /// - [Fact] - public void GetNewThread_WhenCalled_ReturnsNewAgentProxyThreadInstance() - { - // Arrange - var mockClient = new Mock(); - var proxy = new AgentProxy("agentName", mockClient.Object); - - // Act - AgentThread result = proxy.GetNewThread(); - - // Assert - Assert.NotNull(result); - Assert.IsType(result); - } - - /// - /// Verifies that consecutive calls to GetNewThread return distinct instances. - /// - [Fact] - public void GetNewThread_MultipleCalls_ReturnsDistinctInstances() - { - // Arrange - var mockClient = new Mock(); - var proxy = new AgentProxy("agentName", mockClient.Object); - - // Act - AgentThread first = proxy.GetNewThread(); - AgentThread second = proxy.GetNewThread(); - - // Assert - Assert.NotNull(first); - Assert.NotNull(second); - Assert.NotSame(first, second); - } - private const string AgentName = "agentName"; - private const string ThreadId = "thread1"; - private static readonly IReadOnlyCollection s_emptyMessages = []; - - private static bool IsValidGuid(string value) => - Guid.TryParse(value, out _); - - /// - /// Verifies that RunAsync returns a deserialized AgentRunResponse when the actor response status is Completed. - /// Input: empty messages, threadId, Completed status with empty JSON object. - /// Expected: AgentRunResponse with no messages. - /// - [Fact] - public async Task RunAsync_WhenStatusIsCompleted_ReturnsDeserializedResponseAsync() - { - // Arrange - var mockClient = new Mock(); - var mockHandle = new Mock(); - var jsonElement = JsonDocument.Parse("{}").RootElement; - var actorResponse = new ActorResponse - { - ActorId = new ActorId(AgentName, ThreadId), - MessageId = "msg1", - Data = jsonElement, - Status = RequestStatus.Completed - }; - mockHandle - .Setup(h => h.GetResponseAsync(It.IsAny())) - .Returns(new ValueTask(actorResponse)); - mockClient - .Setup(c => c.SendRequestAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(mockHandle.Object); - - var proxy = new AgentProxy(AgentName, mockClient.Object); - var thread = proxy.GetNewThread(ThreadId); - - // Act - var result = await proxy.RunAsync(s_emptyMessages, thread); - - // Assert - Assert.NotNull(result); - Assert.Empty(result.Messages); - } - - /// - /// Verifies that RunAsync throws an InvalidOperationException when the actor response status is Failed. - /// Input: empty messages, threadId, Failed status. - /// Expected: InvalidOperationException with message containing the response data. - /// - [Fact] - public async Task RunAsync_WhenStatusIsFailed_ThrowsInvalidOperationExceptionAsync() - { - // Arrange - var mockClient = new Mock(); - var mockHandle = new Mock(); - var jsonElement = JsonDocument.Parse("{}").RootElement; - var actorResponse = new ActorResponse - { - ActorId = new ActorId(AgentName, ThreadId), - MessageId = "msg1", - Data = jsonElement, - Status = RequestStatus.Failed - }; - mockHandle - .Setup(h => h.GetResponseAsync(It.IsAny())) - .Returns(new ValueTask(actorResponse)); - mockClient - .Setup(c => c.SendRequestAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(mockHandle.Object); - - var proxy = new AgentProxy(AgentName, mockClient.Object); - var thread = proxy.GetNewThread(ThreadId); - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => - proxy.RunAsync(s_emptyMessages, thread)); - Assert.Equal("The agent run request failed: {}", exception.Message); - } - - /// - /// Verifies that RunAsync throws an InvalidOperationException when the actor response status is Pending. - /// Input: empty messages, threadId, Pending status. - /// Expected: InvalidOperationException with pending message. - /// - [Fact] - public async Task RunAsync_WhenStatusIsPending_ThrowsInvalidOperationExceptionAsync() - { - // Arrange - var mockClient = new Mock(); - var mockHandle = new Mock(); - var jsonElement = JsonDocument.Parse("{}").RootElement; - var actorResponse = new ActorResponse - { - ActorId = new ActorId(AgentName, ThreadId), - MessageId = "msg1", - Data = jsonElement, - Status = RequestStatus.Pending - }; - mockHandle - .Setup(h => h.GetResponseAsync(It.IsAny())) - .Returns(new ValueTask(actorResponse)); - mockClient - .Setup(c => c.SendRequestAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(mockHandle.Object); - - var proxy = new AgentProxy(AgentName, mockClient.Object); - var thread = proxy.GetNewThread(ThreadId); - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => - proxy.RunAsync(s_emptyMessages, thread)); - Assert.Equal("The agent run request is still pending.", exception.Message); - } - - /// - /// Verifies that RunAsync throws a NotSupportedException when the actor response status is unsupported. - /// Input: empty messages, threadId, NotFound status. - /// Expected: NotSupportedException with unsupported status message. - /// - [Fact] - public async Task RunAsync_WhenStatusIsUnsupported_ThrowsNotSupportedExceptionAsync() - { - // Arrange - var mockClient = new Mock(); - var mockHandle = new Mock(); - var jsonElement = JsonDocument.Parse("{}").RootElement; - var actorResponse = new ActorResponse - { - ActorId = new ActorId(AgentName, ThreadId), - MessageId = "msg1", - Data = jsonElement, - Status = RequestStatus.NotFound - }; - mockHandle - .Setup(h => h.GetResponseAsync(It.IsAny())) - .Returns(new ValueTask(actorResponse)); - mockClient - .Setup(c => c.SendRequestAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(mockHandle.Object); - - var proxy = new AgentProxy(AgentName, mockClient.Object); - var thread = proxy.GetNewThread(ThreadId); - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => - proxy.RunAsync(s_emptyMessages, thread)); - Assert.Equal($"The agent run request returned an unsupported status: {RequestStatus.NotFound}.", exception.Message); - } - - /// - /// Verifies that passing an AgentThread that is not an AgentProxyThread to RunStreamingAsync throws an ArgumentException. - /// - [Fact] - public async Task RunStreamingAsync_InvalidThread_ThrowsArgumentExceptionAsync() - { - // Arrange - var mockClient = new Mock(); - var proxy = new AgentProxy("testAgent", mockClient.Object); - AgentThread invalidThread = new Mock().Object; - - // Act & Assert - await Assert.ThrowsAsync(async () => - { - await foreach (var _ in proxy.RunStreamingAsync([], invalidThread, cancellationToken: CancellationToken.None)) - { - } - }); - } - - /// - /// This test verifies that RunStreamingAsync completes without throwing when a valid AgentProxyThread is used. - /// TODO: Mock IActorClient.SendRequestAsync to return an ActorResponseHandle whose WatchUpdatesAsync yields no updates. - /// - [Fact(Skip = "Mocking of ActorResponseHandle.WatchUpdatesAsync with IActorClient is required")] - public async Task RunStreamingAsync_ValidProxyThread_CompletesSuccessfullyAsync() - { - // Arrange - var mockClient = new Mock(); - var proxy = new AgentProxy("testAgent", mockClient.Object); - var proxyThread = new AgentProxyThread(); - - // Act & Assert - await foreach (var _ in proxy.RunStreamingAsync([], proxyThread, cancellationToken: CancellationToken.None)) - { - // No items expected - } - } - - /// - /// Verifies that RunStreamingAsync yields AgentRunResponseUpdate for pending status. - /// - [Fact] - public async Task RunStreamingAsync_PendingStatus_YieldsAgentRunResponseUpdateAsync() - { - // Arrange - var messages = Array.Empty(); - const string ThreadId = "thread1"; - var expectedUpdate = new AgentRunResponseUpdate(ChatRole.Assistant, "response"); - - var updateTypeInfo = AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(AgentRunResponseUpdate)); - var jsonElement = JsonSerializer.SerializeToElement(expectedUpdate, updateTypeInfo); - - var actorUpdate = new ActorRequestUpdate(RequestStatus.Pending, jsonElement); - var mockHandle = new Mock(); - mockHandle - .Setup(h => h.WatchUpdatesAsync(It.IsAny())) - .Returns(GetAsyncEnumerableAsync(actorUpdate)); - var mockClient = new Mock(); - mockClient - .Setup(c => c.SendRequestAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(mockHandle.Object); - - var proxy = new AgentProxy("agentName", mockClient.Object); - var thread = proxy.GetNewThread(ThreadId); - - // Act - var results = new List(); - await foreach (var update in proxy.RunStreamingAsync(messages, thread)) - { - results.Add(update); - } - - // Assert - Assert.Single(results); - Assert.Equal(expectedUpdate.Text, results[0].Text); - Assert.Equal(expectedUpdate.Role, results[0].Role); - } - - /// - /// Verifies that RunStreamingAsync completes without yielding any updates when receiving only a completed status. - /// - [Fact] - public async Task RunStreamingAsync_CompletedStatus_YieldsNoUpdatesAsync() - { - // Arrange - var messages = Array.Empty(); - const string ThreadId = "thread1"; - - var agentRunResponse = new AgentRunResponse - { - Messages = [new(ChatRole.Assistant, "response")] - }; - var responseTypeInfo = AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(AgentRunResponse)); - var jsonElement = JsonSerializer.SerializeToElement(agentRunResponse, responseTypeInfo); - - var actorUpdate = new ActorRequestUpdate(RequestStatus.Completed, jsonElement); - var mockHandle = new Mock(); - mockHandle - .Setup(h => h.WatchUpdatesAsync(It.IsAny())) - .Returns(GetAsyncEnumerableAsync(actorUpdate)); - var mockClient = new Mock(); - mockClient - .Setup(c => c.SendRequestAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(mockHandle.Object); - - var proxy = new AgentProxy("agentName", mockClient.Object); - var thread = proxy.GetNewThread(ThreadId); - - // Act - var results = new List(); - await foreach (var update in proxy.RunStreamingAsync(messages, thread)) - { - results.Add(update); - } - - // Assert - Assert.Empty(results); // Completed status should not yield any updates - } - - /// - /// Verifies that RunStreamingAsync does not yield duplicate content when receiving both - /// streaming updates and a completed message containing the same content. - /// - [Fact] - public async Task RunStreamingAsync_CompletedAfterUpdates_DoesNotYieldDuplicateContentAsync() - { - // Arrange: Create a scenario with streaming updates followed by completion - var messages = Array.Empty(); - const string ThreadId = "thread1"; - - var pendingUpdate = new AgentRunResponseUpdate(ChatRole.Assistant, "streaming response"); - var completedResponse = new AgentRunResponse - { - Messages = [new(ChatRole.Assistant, "streaming response")] - }; - - var updates = new List - { - new(RequestStatus.Pending, JsonSerializer.SerializeToElement(pendingUpdate, - AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(AgentRunResponseUpdate)))), - new(RequestStatus.Completed, JsonSerializer.SerializeToElement(completedResponse, - AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(AgentRunResponse)))) - }; - - var mockHandle = new Mock(); - mockHandle - .Setup(h => h.WatchUpdatesAsync(It.IsAny())) - .Returns(GetAsyncEnumerableAsync(updates)); - var mockClient = new Mock(); - mockClient - .Setup(c => c.SendRequestAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(mockHandle.Object); - - var proxy = new AgentProxy("agentName", mockClient.Object); - var thread = proxy.GetNewThread(ThreadId); - - // Act - var results = new List(); - await foreach (var update in proxy.RunStreamingAsync(messages, thread)) - { - results.Add(update); - } - - // Assert: Should only get the pending update, not duplicate content from completion - Assert.Single(results); - Assert.Equal("streaming response", results[0].Text); - Assert.Equal(ChatRole.Assistant, results[0].Role); - } - - private static async IAsyncEnumerable GetAsyncEnumerableAsync(ActorRequestUpdate update) - { - yield return update; - await Task.CompletedTask; - } - - private static async IAsyncEnumerable GetAsyncEnumerableAsync(List updates) - { - foreach (var update in updates) - { - yield return update; - await Task.CompletedTask; - } - } - - /// - /// Verifies that RunStreamingAsync throws InvalidOperationException when an update status is Failed. - /// Uses a mock IActorClient to return a Failed update. Expected: InvalidOperationException is thrown. - /// - [Fact] - public async Task RunStreamingAsync_FailedStatus_ThrowsInvalidOperationExceptionAsync() - { - // Arrange - var messages = Array.Empty(); - const string ThreadId = "thread1"; - var expectedUpdate = new AgentRunResponseUpdate(ChatRole.Assistant, "response"); - var updateTypeInfo = AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(AgentRunResponseUpdate)); - var jsonElement = JsonSerializer.SerializeToElement(expectedUpdate, updateTypeInfo); - - var actorUpdate = new ActorRequestUpdate(RequestStatus.Failed, jsonElement); - var mockHandle = new Mock(); - mockHandle - .Setup(h => h.WatchUpdatesAsync(It.IsAny())) - .Returns(GetAsyncEnumerableAsync(actorUpdate)); - var mockClient = new Mock(); - mockClient - .Setup(c => c.SendRequestAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(mockHandle.Object); - - var proxy = new AgentProxy("agentName", mockClient.Object); - var thread = proxy.GetNewThread(ThreadId); - - // Act & Assert - var exception = await Assert.ThrowsAsync(async () => - { - await foreach (var update in proxy.RunStreamingAsync(messages, thread)) - { - // force enumeration - } - }); - Assert.Contains("The agent run request failed", exception.Message); - } - - /// - /// Verifies that constructor throws ArgumentNullException when client is null. - /// - [Fact] - public void Constructor_NullClient_ThrowsArgumentNullException() => - // Act & Assert - Assert.Throws(() => new AgentProxy("agentName", null!)); - - /// - /// Verifies that constructor throws ArgumentNullException when name is null. - /// - [Fact] - public void Constructor_NullName_ThrowsArgumentNullException() - { - // Arrange - var mockClient = new Mock(); - - // Act & Assert - Assert.Throws(() => new AgentProxy(null!, mockClient.Object)); - } - - /// - /// Verifies that constructor throws ArgumentException when name is empty. - /// - [Fact] - public void Constructor_EmptyName_ThrowsArgumentException() - { - // Arrange - var mockClient = new Mock(); - - // Act & Assert - Assert.Throws(() => new AgentProxy("", mockClient.Object)); - } - - /// - /// Verifies that RunAsync with thread overload validates null messages. - /// - [Fact] - public async Task RunAsync_WithThread_NullMessages_ThrowsArgumentNullExceptionAsync() - { - // Arrange - var mockClient = new Mock(); - var proxy = new AgentProxy("agentName", mockClient.Object); - var thread = new AgentProxyThread(); - - // Act & Assert - await Assert.ThrowsAsync(() => - proxy.RunAsync(messages: null!, thread, null, CancellationToken.None)); - } - - /// - /// Verifies that RunAsync with thread overload throws for invalid thread type. - /// - [Fact] - public async Task RunAsync_WithInvalidThreadType_ThrowsArgumentExceptionAsync() - { - // Arrange - var mockClient = new Mock(); - var proxy = new AgentProxy("agentName", mockClient.Object); - var invalidThread = new Mock().Object; - var messages = new List { new(ChatRole.User, "test") }; - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => - proxy.RunAsync(messages, invalidThread, null, CancellationToken.None)); - Assert.Contains("thread must be an instance of AgentProxyThread", exception.Message); - } - - /// - /// Verifies that RunAsync with thread overload creates new thread ID when thread is null. - /// - [Fact] - public async Task RunAsync_WithNullThread_CreatesNewThreadIdAsync() - { - // Arrange - var mockClient = new Mock(); - var mockHandle = new Mock(); - var response = new AgentRunResponse { Messages = [] }; - var jsonElement = JsonSerializer.SerializeToElement(response, - AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(AgentRunResponse))); - var actorResponse = new ActorResponse - { - ActorId = new ActorId(AgentName, ThreadId), - MessageId = "msg1", - Data = jsonElement, - Status = RequestStatus.Completed - }; - - mockHandle.Setup(h => h.GetResponseAsync(It.IsAny())) - .Returns(new ValueTask(actorResponse)); - - mockClient.Setup(c => c.SendRequestAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(mockHandle.Object); - - var proxy = new AgentProxy("agentName", mockClient.Object); - var messages = new List { new(ChatRole.User, "test") }; - - // Act - var result = await proxy.RunAsync(messages, thread: null, options: null, CancellationToken.None); - - // Assert - Assert.NotNull(result); - mockClient.Verify(c => c.SendRequestAsync( - It.Is(r => !string.IsNullOrEmpty(r.ActorId.Key)), - It.IsAny()), Times.Once); - } - - /// - /// Verifies that RunAsync handles cancellation properly. - /// - [Fact] - public async Task RunAsync_CancellationRequested_ThrowsOperationCanceledExceptionAsync() - { - // Arrange - var mockClient = new Mock(); - using var cts = new CancellationTokenSource(); - cts.Cancel(); - - mockClient.Setup(c => c.SendRequestAsync(It.IsAny(), It.IsAny())) - .ThrowsAsync(new OperationCanceledException()); - - var proxy = new AgentProxy("agentName", mockClient.Object); - var messages = new List { new(ChatRole.User, "test") }; - var thread = proxy.GetNewThread(ThreadId); - - // Act & Assert - await Assert.ThrowsAsync(() => - proxy.RunAsync(messages, thread, cancellationToken: cts.Token)); - } - - /// - /// Verifies that RunStreamingAsync with thread overload validates null messages. - /// - [Fact] - public async Task RunStreamingAsync_WithThread_NullMessages_ThrowsArgumentNullExceptionAsync() - { - // Arrange - var mockClient = new Mock(); - var proxy = new AgentProxy("agentName", mockClient.Object); - var thread = new AgentProxyThread(); - - // Act & Assert - await Assert.ThrowsAsync(async () => - { - await foreach (var _ in proxy.RunStreamingAsync(messages: null!, thread, null, CancellationToken.None)) - { - // force enumeration - } - }); - } - - /// - /// Verifies that RunStreamingAsync with thread overload throws for invalid thread type. - /// - [Fact] - public async Task RunStreamingAsync_WithInvalidThreadType_ThrowsArgumentExceptionAsync() - { - // Arrange - var mockClient = new Mock(); - var proxy = new AgentProxy("agentName", mockClient.Object); - var invalidThread = new Mock().Object; - var messages = new List { new(ChatRole.User, "test") }; - - // Act & Assert - var exception = await Assert.ThrowsAsync(async () => - { - await foreach (var _ in proxy.RunStreamingAsync(messages, invalidThread, null, CancellationToken.None)) - { - // force enumeration - } - }); - Assert.Contains("thread must be an instance of AgentProxyThread", exception.Message); - } - - /// - /// Verifies that RunStreamingAsync handles cancellation during enumeration. - /// - [Fact] - public async Task RunStreamingAsync_CancellationDuringEnumeration_StopsEnumerationAsync() - { - // Arrange - var mockClient = new Mock(); - using var cts = new CancellationTokenSource(); - - var updates = new List - { - new(RequestStatus.Pending, JsonSerializer.SerializeToElement( - new AgentRunResponseUpdate(ChatRole.Assistant, "1"), - AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(AgentRunResponseUpdate)))), - new(RequestStatus.Pending, JsonSerializer.SerializeToElement( - new AgentRunResponseUpdate(ChatRole.Assistant, "2"), - AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(AgentRunResponseUpdate)))) - }; - - using var fakeHandle = new FakeActorResponseHandle(updates, cts); - - mockClient.Setup(c => c.SendRequestAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(fakeHandle); - - var proxy = new AgentProxy("agentName", mockClient.Object); - var thread = proxy.GetNewThread(ThreadId); - var messages = new List { new(ChatRole.User, "test") }; - - // Act - var receivedUpdates = new List(); - await Assert.ThrowsAnyAsync(async () => - { - await foreach (var update in proxy.RunStreamingAsync(messages, thread, cancellationToken: cts.Token)) - { - receivedUpdates.Add(update); - } - }); - - // Assert - Assert.Single(receivedUpdates); // Only first update should be received - } - - /// - /// Verifies that RunAsync correctly uses message ID from last message if available. - /// - [Fact] - public async Task RunAsync_UsesLastMessageId_WhenAvailableAsync() - { - // Arrange - var mockClient = new Mock(); - var mockHandle = new Mock(); - const string ExpectedMessageId = "custom-message-id"; - var response = new AgentRunResponse { Messages = [] }; - var jsonElement = JsonSerializer.SerializeToElement(response, - AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(AgentRunResponse))); - var actorResponse = new ActorResponse - { - ActorId = new ActorId(AgentName, ThreadId), - MessageId = ExpectedMessageId, - Data = jsonElement, - Status = RequestStatus.Completed - }; - - mockHandle.Setup(h => h.GetResponseAsync(It.IsAny())) - .Returns(new ValueTask(actorResponse)); - - mockClient.Setup(c => c.SendRequestAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(mockHandle.Object); - - var proxy = new AgentProxy("agentName", mockClient.Object); - var thread = proxy.GetNewThread(ThreadId); - var messages = new List - { - new(ChatRole.User, "first"), - new(ChatRole.User, "last") { MessageId = ExpectedMessageId } - }; - - // Act - await proxy.RunAsync(messages, thread); - - // Assert - mockClient.Verify(c => c.SendRequestAsync( - It.Is(r => r.MessageId == ExpectedMessageId), - It.IsAny()), Times.Once); - } - - /// - /// Verifies that RunAsync generates new message ID when last message has no ID. - /// - [Fact] - public async Task RunAsync_GeneratesMessageId_WhenLastMessageHasNoIdAsync() - { - // Arrange - var mockClient = new Mock(); - var mockHandle = new Mock(); - var response = new AgentRunResponse { Messages = [] }; - var jsonElement = JsonSerializer.SerializeToElement(response, - AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(AgentRunResponse))); - var actorResponse = new ActorResponse - { - ActorId = new ActorId(AgentName, ThreadId), - MessageId = "generated-id", - Data = jsonElement, - Status = RequestStatus.Completed - }; - - mockHandle.Setup(h => h.GetResponseAsync(It.IsAny())) - .Returns(new ValueTask(actorResponse)); - - mockClient.Setup(c => c.SendRequestAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(mockHandle.Object); - - var proxy = new AgentProxy("agentName", mockClient.Object); - var thread = proxy.GetNewThread(ThreadId); - var messages = new List { new(ChatRole.User, "test") }; - - // Act - await proxy.RunAsync(messages, thread); - - // Assert - mockClient.Verify(c => c.SendRequestAsync( - It.Is(r => !string.IsNullOrEmpty(r.MessageId) && IsValidGuid(r.MessageId)), - It.IsAny()), Times.Once); - } - - /// - /// Verifies that GetNewThread returns unique instances with unique IDs. - /// - [Fact] - public void GetNewThread_MultipleCalls_ReturnsUniqueThreadsWithUniqueIds() - { - // Arrange - var mockClient = new Mock(); - var proxy = new AgentProxy("agentName", mockClient.Object); - var threads = new List(); - - // Act - for (int i = 0; i < 10; i++) - { - threads.Add(proxy.GetNewThread()); - } - - // Assert - var threadIds = threads.Cast().Select(t => t.ConversationId).ToList(); - Assert.Equal(10, threadIds.Count); - Assert.Equal(10, threadIds.Distinct().Count()); // All IDs should be unique - } - - /// - /// Fake implementation of ActorResponseHandle for testing purposes. - /// - private sealed class FakeActorResponseHandle : ActorResponseHandle - { - private readonly List _updates; - private readonly CancellationTokenSource _cancellationTokenSource; - private readonly ActorResponse? _response; - private readonly int _delayBetweenUpdates; - - public FakeActorResponseHandle( - List updates, - CancellationTokenSource cancellationTokenSource, - ActorResponse? response = null, - int delayBetweenUpdates = 10) - { - this._updates = updates; - this._cancellationTokenSource = cancellationTokenSource; - this._response = response; - this._delayBetweenUpdates = delayBetweenUpdates; - } - - public override bool TryGetResponse([System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out ActorResponse? response) - { - response = this._response; - return this._response is not null; - } - - public override ValueTask GetResponseAsync(CancellationToken cancellationToken) - { - if (this._response is null) - { - throw new InvalidOperationException("No response configured"); - } - return new ValueTask(this._response); - } - - public override ValueTask CancelAsync(CancellationToken cancellationToken) - { - this._cancellationTokenSource.Cancel(); - return default; - } - - public override async IAsyncEnumerable WatchUpdatesAsync([System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) - { - for (int i = 0; i < this._updates.Count; i++) - { - cancellationToken.ThrowIfCancellationRequested(); - - yield return this._updates[i]; - - // Cancel after the first update - if (i == 0) - { - this._cancellationTokenSource.Cancel(); - } - - if (i < this._updates.Count - 1) // Don't delay after the last update - { - await Task.Delay(this._delayBetweenUpdates, cancellationToken); - } - } - } - } -} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/AgentProxyThreadTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/AgentProxyThreadTests.cs deleted file mode 100644 index 239400ac52..0000000000 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/AgentProxyThreadTests.cs +++ /dev/null @@ -1,298 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Microsoft.Agents.AI.Hosting.UnitTests; - -public class AgentProxyThreadTests -{ - /// - /// Provides valid identifier values that conform to RFC 3986 unreserved characters. - /// - public static IEnumerable ValidIds { get; } = - [ - ["normal"], - ["test-id"], - ["test_id"], - ["test.id"], - ["test~id"], - ["ABC123"], - ["a"], - ["123"], - ["test-id_with.various~chars"], - [new string('a', 100)] // Long but valid ID - ]; - - /// - /// Provides invalid identifier values that violate the RFC 3986 unreserved character rules. - /// - public static IEnumerable InvalidIds { get; } = - [ - [" "], // Space not allowed - ["!@#$%^&*()"], // Special characters not allowed - ["test id"], // Space not allowed - ["test/id"], // Forward slash not allowed - ["test?id"], // Question mark not allowed - ["test#id"], // Hash not allowed - ["test@id"], // At symbol not allowed - ["test id with spaces"], // Multiple spaces not allowed - ["test\tid"], // Tab not allowed - ["test\nid"], // Newline not allowed - ]; - - /// - /// Verifies that providing valid id to constructor sets the Id property correctly. - /// - /// The valid identifier to test. - [Theory] - [MemberData(nameof(ValidIds))] - public void Constructor_ValidId_SetsIdProperty(string id) - { - // Act - var thread = new AgentProxyThread(id); - - // Assert - Assert.Equal(id, thread.ConversationId); - } - - /// - /// Verifies that providing invalid id to constructor throws an . - /// - /// The invalid identifier to test. - [Theory] - [MemberData(nameof(InvalidIds))] - public void Constructor_InvalidId_ThrowsArgumentException(string id) - { - // Act & Assert - var exception = Assert.Throws(() => new AgentProxyThread(id)); - Assert.Contains("Thread ID", exception.Message); - Assert.Contains("alphanumeric characters, hyphens, underscores, dots, and tildes", exception.Message); - } - - /// - /// Verifies that providing a null id to constructor throws an . - /// - [Fact] - public void Constructor_NullId_ThrowsArgumentNullException() => - // Act & Assert - Assert.Throws(() => new AgentProxyThread(null!)); - - /// - /// Verifies that providing an empty id to constructor throws an . - /// - [Fact] - public void Constructor_EmptyId_ThrowsArgumentException() => - // Act & Assert - Assert.Throws(() => new AgentProxyThread("")); - - /// - /// Verifies that the default constructor initializes the Id property with a valid non-empty GUID string in "N" format. - /// - [Fact] - public void Constructor_Default_AssignsValidGuidStringAsId() - { - // Arrange & Act - var thread = new AgentProxyThread(); - - // Assert - Assert.False(string.IsNullOrEmpty(thread.ConversationId)); - Assert.True(Guid.TryParseExact(thread.ConversationId, "N", out _), $"Id '{thread.ConversationId}' is not a valid GUID in 'N' format."); - } - - /// - /// Verifies that successive default constructors produce unique Id values. - /// - [Fact] - public void Constructor_Default_CreatesUniqueIds() - { - // Arrange & Act - var thread1 = new AgentProxyThread(); - var thread2 = new AgentProxyThread(); - - // Assert - Assert.NotEqual(thread1.ConversationId, thread2.ConversationId); - } - - /// - /// Verifies that CreateId returns a non-null, non-empty 32-character hexadecimal string without dashes. - /// - [Fact] - public void CreateId_ReturnsValidHexString() - { - // Arrange & Act - string id = AgentProxyThread.CreateId(); - - // Assert - Assert.False(string.IsNullOrEmpty(id)); - Assert.Equal(32, id.Length); - Assert.Matches("^[0-9a-f]{32}$", id); - } - - /// - /// Verifies that multiple calls to CreateId produce unique identifiers. - /// - [Fact] - public void CreateId_MultipleCalls_ReturnUniqueValues() - { - // Arrange & Act - string id1 = AgentProxyThread.CreateId(); - string id2 = AgentProxyThread.CreateId(); - - // Assert - Assert.NotEqual(id1, id2); - } - - /// - /// Verifies that ManyCallsInParallel produces unique values across many calls. - /// - [Fact] - public void CreateId_ManyCallsInParallel_AllUnique() - { - // Arrange - const int NumberOfIds = 1000; - var ids = new string[NumberOfIds]; - - // Act - Create IDs in parallel to test thread safety - Parallel.For(0, NumberOfIds, i => ids[i] = AgentProxyThread.CreateId()); - - // Assert - var uniqueIds = ids.Distinct().Count(); - Assert.Equal(NumberOfIds, uniqueIds); - } - - /// - /// Verifies that CreateId generates IDs that pass validation. - /// - [Fact] - public void CreateId_GeneratesValidIds() - { - // Arrange & Act - for (int i = 0; i < 100; i++) - { - string id = AgentProxyThread.CreateId(); - - // Assert - Should not throw exception - var thread = new AgentProxyThread(id); - Assert.Equal(id, thread.ConversationId); - } - } - - /// - /// Verifies specific edge cases for valid IDs. - /// - [Theory] - [InlineData("a")] - [InlineData("1")] - [InlineData("_")] - [InlineData("-")] - [InlineData(".")] - [InlineData("~")] - [InlineData("a1")] - [InlineData("test-123")] - [InlineData("my_thread.id~1")] - public void Constructor_ValidIdEdgeCases_SetsIdProperty(string id) - { - // Act - var thread = new AgentProxyThread(id); - - // Assert - Assert.Equal(id, thread.ConversationId); - } - - /// - /// Verifies specific edge cases for invalid IDs. - /// - [Theory] - [InlineData(" leading-space")] - [InlineData("trailing-space ")] - [InlineData("with spaces")] - [InlineData("with\ttab")] - [InlineData("with\nnewline")] - [InlineData("with/slash")] - [InlineData("with\\backslash")] - [InlineData("with%percent")] - [InlineData("with+plus")] - [InlineData("with=equals")] - [InlineData("with?question")] - [InlineData("with#hash")] - [InlineData("with@at")] - [InlineData("with[bracket")] - [InlineData("with]bracket")] - [InlineData("with{brace")] - [InlineData("with}brace")] - [InlineData("with(paren")] - [InlineData("with)paren")] - [InlineData("with!exclamation")] - [InlineData("with*asterisk")] - [InlineData("with:colon")] - [InlineData("with;semicolon")] - [InlineData("with,comma")] - [InlineData("with\"quote")] - [InlineData("with'apostrophe")] - public void Constructor_InvalidIdEdgeCases_ThrowsArgumentException(string id) - { - // Act & Assert - var exception = Assert.Throws(() => new AgentProxyThread(id)); - Assert.Contains("Thread ID", exception.Message); - } - - /// - /// Verifies that AgentProxyThread inherits from AgentThread. - /// - [Fact] - public void AgentProxyThread_InheritsFromAgentThread() - { - // Arrange & Act - var thread = new AgentProxyThread(); - - // Assert - Assert.IsType(thread, exactMatch: false); - } - - /// - /// Verifies that Id property is accessible. - /// - [Fact] - public void Id_IsAccessible() - { - // Arrange & Act - var thread = new AgentProxyThread("test-id"); - - // Assert - Assert.NotNull(thread.ConversationId); - Assert.Equal("test-id", thread.ConversationId); - } - - /// - /// Verifies that thread ID remains immutable after construction. - /// - [Fact] - public void Id_IsImmutable() - { - // Arrange - const string OriginalId = "immutable-id"; - var thread = new AgentProxyThread(OriginalId); - - // Act & Assert - Assert.Equal(OriginalId, thread.ConversationId); - } - - /// - /// Verifies that default constructor creates thread with valid GUID format. - /// - [Fact] - public void Constructor_Default_AlwaysCreatesValidGuid() - { - // Arrange & Act - var thread = new AgentProxyThread(); - - // Assert - Assert.NotNull(thread.ConversationId); - Assert.Equal(32, thread.ConversationId.Length); - Assert.True(Guid.TryParseExact(thread.ConversationId, "N", out var guid)); - Assert.NotEqual(Guid.Empty, guid); - } -} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/HostApplicationBuilderAgentExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/HostApplicationBuilderAgentExtensionsTests.cs index 677f8a0492..c9a70cc189 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/HostApplicationBuilderAgentExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/HostApplicationBuilderAgentExtensionsTests.cs @@ -236,25 +236,6 @@ public class HostApplicationBuilderAgentExtensionsTests // Assert Assert.Same(builder, result); } - - /// - /// Verifies that AddAIAgent with whitespace name throws ArgumentException. - /// - [Theory] - [InlineData(" ")] - [InlineData("\t")] - [InlineData(" agent ")] - public void AddAIAgent_WhitespaceName_ThrowsArgumentException(string name) - { - // Arrange - var builder = new HostApplicationBuilder(); - - // Act & Assert - var exception = Assert.Throws(() => - builder.AddAIAgent(name, "instructions")); - Assert.Contains("Invalid type", exception.Message); - } - /// /// Verifies that AddAIAgent without chat client key calls the overload with null key. /// @@ -302,27 +283,4 @@ public class HostApplicationBuilderAgentExtensionsTests d.ServiceType == typeof(AIAgent)); Assert.NotNull(descriptor); } - - /// - /// Verifies that AddAIAgent with invalid special characters throws ArgumentException. - /// - [Theory] - [InlineData("特殊字符")] // non-ASCII not allowed - [InlineData("123agent")] // cannot start with number - [InlineData("agent@name")] // @ not allowed - [InlineData("agent/name")] // / not allowed - [InlineData("agent name")] // space not allowed - [InlineData(".agent")] // cannot start with period - [InlineData("-agent")] // cannot start with dash - [InlineData(":agent")] // cannot start with colon - public void AddAIAgent_InvalidSpecialCharactersInName_ThrowsArgumentException(string name) - { - // Arrange - var builder = new HostApplicationBuilder(); - - // Act & Assert - var exception = Assert.Throws(() => - builder.AddAIAgent(name, "instructions")); - Assert.Contains("Invalid type", exception.Message); - } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Runtime.Abstractions.UnitTests/ActorTypeTests.cs b/dotnet/tests/Microsoft.Agents.AI.Runtime.Abstractions.UnitTests/ActorTypeTests.cs deleted file mode 100644 index 71e19afa54..0000000000 --- a/dotnet/tests/Microsoft.Agents.AI.Runtime.Abstractions.UnitTests/ActorTypeTests.cs +++ /dev/null @@ -1,285 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; - -namespace Microsoft.Agents.AI.Runtime.UnitTests; - -public class ActorTypeTests -{ - /// - /// Provides valid ActorType names that conform to the regex pattern ^[a-zA-Z_][a-zA-Z._:\-0-9]*$. - /// - public static IEnumerable ValidActorTypeNames { get; } = - [ - ["a"], // Single letter - ["A"], // Single uppercase letter - ["_"], // Single underscore - ["agent"], // Simple name - ["Agent"], // Capitalized name - ["AGENT"], // All caps name - ["my_agent"], // With underscore - ["MyAgent"], // Camel case - ["agent1"], // With number - ["agent_1"], // With underscore and number - ["agent:type"], // With colon - ["agent-type"], // With hyphen - ["my_agent:type-1"], // Complex valid name - ["A1_test:complex-name"], // Very complex valid name - ["_private_agent"], // Starting with underscore - ["agent_with_many_underscores"], // Multiple underscores - ["agent:with:colons"], // Multiple colons - ["agent-with-hyphens"], // Multiple hyphens - ["agent123456789"], // With many numbers - ["agent.type"], // With dot - ["agent.sub.type"], // With multiple dots - ["my.agent_1:type-name"], // Complex with dots - ]; - - /// - /// Provides invalid ActorType names that violate the regex pattern ^[a-zA-Z_][a-zA-Z._:\-0-9]*$. - /// - public static IEnumerable InvalidActorTypeNames { get; } = - [ - ["1agent"], // Starting with number - ["9test"], // Starting with number - ["-agent"], // Starting with hyphen - [":agent"], // Starting with colon - [" agent"], // Starting with space - ["agent "], // Trailing space - ["agent agent"], // Space in middle - ["agent@type"], // Invalid character @ - ["agent#type"], // Invalid character # - ["agent$type"], // Invalid character $ - ["agent%type"], // Invalid character % - ["agent^type"], // Invalid character ^ - ["agent&type"], // Invalid character & - ["agent*type"], // Invalid character * - ["agent(type)"], // Invalid characters ( ) - ["agent[type]"], // Invalid characters [ ] - ["agent{type}"], // Invalid characters { } - ["agent+type"], // Invalid character + - ["agent=type"], // Invalid character = - ["agent\\type"], // Invalid character \ - ["agent/type"], // Invalid character / - ["agent?type"], // Invalid character ? - ["agent,type"], // Invalid character , - ["agent;type"], // Invalid character ; - ["agent\"type"], // Invalid character " - ["agent'type"], // Invalid character ' - ["agent`type"], // Invalid character ` - ["agent~type"], // Invalid character ~ - ["agent!type"], // Invalid character ! - ["agent\ttype"], // Tab character - ["agent\ntype"], // Newline character - ]; - - /// - /// Verifies that providing valid actor type name to constructor sets the Name property correctly. - /// - /// The valid type name to test. - [Theory] - [MemberData(nameof(ValidActorTypeNames))] - public void Constructor_ValidTypeName_SetsNameProperty(string typeName) - { - // Act - var actorType = new ActorType(typeName); - - // Assert - Assert.Equal(typeName, actorType.Name); - } - - /// - /// Verifies that providing invalid actor type name to constructor throws an . - /// - /// The invalid type name to test. - [Theory] - [MemberData(nameof(InvalidActorTypeNames))] - public void Constructor_InvalidTypeName_ThrowsArgumentException(string typeName) - { - // Act & Assert - var exception = Assert.Throws(() => new ActorType(typeName)); - Assert.Contains("Invalid type", exception.Message); - Assert.Contains("Must start with a letter or underscore, and can only contain letters, dots, underscores, colons, hyphens, and numbers", exception.Message); - } - - /// - /// Verifies that providing a null type name to constructor throws an . - /// - [Fact] - public void Constructor_NullTypeName_ThrowsArgumentNullException() => - // Act & Assert - Assert.Throws(() => new ActorType(null!)); - - /// - /// Verifies that providing an empty type name to constructor throws an . - /// - [Fact] - public void Constructor_EmptyTypeName_ThrowsArgumentException() => - // Act & Assert - Assert.Throws(() => new ActorType("")); - - /// - /// Verifies specific edge cases for valid type names. - /// - [Theory] - [InlineData("a")] - [InlineData("Z")] - [InlineData("_")] - [InlineData("a1")] - [InlineData("_1")] - [InlineData("agent_123")] - [InlineData("MyAgent:SubType")] - [InlineData("my-agent")] - [InlineData("agent_type:sub-type_123")] - [InlineData("agent.type")] - [InlineData("my.agent.name")] - [InlineData("complex.name_1:type-sub")] - public void Constructor_ValidTypeNameEdgeCases_SetsNameProperty(string typeName) - { - // Act - var actorType = new ActorType(typeName); - - // Assert - Assert.Equal(typeName, actorType.Name); - } - - /// - /// Verifies specific edge cases for invalid type names. - /// - [Theory] - [InlineData("1")] - [InlineData("9")] - [InlineData("-")] - [InlineData(":")] - [InlineData("1agent")] - [InlineData("-agent")] - [InlineData(":agent")] - [InlineData(" ")] - [InlineData("agent ")] - [InlineData(" agent")] - [InlineData("a b")] - [InlineData("agent@type")] - [InlineData("agent/type")] - public void Constructor_InvalidTypeNameEdgeCases_ThrowsArgumentException(string typeName) - { - // Act & Assert - var exception = Assert.Throws(() => new ActorType(typeName)); - Assert.Contains("Invalid type", exception.Message); - Assert.Contains("Must start with a letter or underscore", exception.Message); - } - - /// - /// Verifies that ToString returns the type name. - /// - [Fact] - public void ToString_ReturnsTypeName() - { - // Arrange - const string TypeName = "test_agent"; - var actorType = new ActorType(TypeName); - - // Act - string result = actorType.ToString(); - - // Assert - Assert.Equal(TypeName, result); - } - - /// - /// Verifies equality comparison between ActorType instances. - /// - [Fact] - public void Equals_SameTypeName_ReturnsTrue() - { - // Arrange - var actorType1 = new ActorType("test_agent"); - var actorType2 = new ActorType("test_agent"); - - // Act & Assert - Assert.True(actorType1.Equals(actorType2)); - Assert.True(actorType1 == actorType2); - Assert.False(actorType1 != actorType2); - } - - /// - /// Verifies inequality comparison between ActorType instances. - /// - [Fact] - public void Equals_DifferentTypeName_ReturnsFalse() - { - // Arrange - var actorType1 = new ActorType("test_agent1"); - var actorType2 = new ActorType("test_agent2"); - - // Act & Assert - Assert.False(actorType1.Equals(actorType2)); - Assert.False(actorType1 == actorType2); - Assert.True(actorType1 != actorType2); - } - - /// - /// Verifies that GetHashCode returns same value for equal instances. - /// - [Fact] - public void GetHashCode_SameTypeName_ReturnsSameHashCode() - { - // Arrange - var actorType1 = new ActorType("test_agent"); - var actorType2 = new ActorType("test_agent"); - - // Act & Assert - Assert.Equal(actorType1.GetHashCode(), actorType2.GetHashCode()); - } - - /// - /// Verifies that ActorType is case sensitive. - /// - [Fact] - public void Equality_IsCaseSensitive() - { - // Arrange - var actorType1 = new ActorType("TestAgent"); - var actorType2 = new ActorType("testagent"); - - // Act & Assert - Assert.False(actorType1.Equals(actorType2)); - Assert.False(actorType1 == actorType2); - Assert.True(actorType1 != actorType2); - Assert.NotEqual(actorType1.GetHashCode(), actorType2.GetHashCode()); - } - - /// - /// Verifies that IsValidType static method works correctly for valid names. - /// - [Theory] - [MemberData(nameof(ValidActorTypeNames))] - public void IsValidType_ValidTypeName_ReturnsTrue(string typeName) => - // Act & Assert - Assert.True(ActorType.IsValidType(typeName)); - - /// - /// Verifies that IsValidType static method works correctly for invalid names. - /// - [Theory] - [MemberData(nameof(InvalidActorTypeNames))] - public void IsValidType_InvalidTypeName_ReturnsFalse(string typeName) => - // Act & Assert - Assert.False(ActorType.IsValidType(typeName)); - - /// - /// Verifies that IsValidType throws for null. - /// - [Fact] - public void IsValidType_NullTypeName_ThrowsArgumentNullException() => - // Act & Assert - Assert.Throws(() => ActorType.IsValidType(null!)); - - /// - /// Verifies that IsValidType throws for empty string. - /// - [Fact] - public void IsValidType_EmptyTypeName_ThrowsArgumentException() => - // Act & Assert - Assert.Throws(() => ActorType.IsValidType("")); -} diff --git a/dotnet/tests/Microsoft.Agents.AI.Runtime.Abstractions.UnitTests/AgentIdTests.cs b/dotnet/tests/Microsoft.Agents.AI.Runtime.Abstractions.UnitTests/AgentIdTests.cs deleted file mode 100644 index 32db6d0fba..0000000000 --- a/dotnet/tests/Microsoft.Agents.AI.Runtime.Abstractions.UnitTests/AgentIdTests.cs +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; - -namespace Microsoft.Agents.AI.Runtime.Abstractions.Tests; - -public class AgentIdTests() -{ - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - [InlineData("invalid\u007Fkey")] // DEL character (127) is outside ASCII 32-126 range - [InlineData("invalid\u0000key")] // NULL character is outside ASCII 32-126 range - [InlineData("invalid\u0010key")] // Control character is outside ASCII 32-126 range - [InlineData("InvalidKey💀")] // Control character is outside ASCII 32-126 range - public void AgentIdShouldThrowArgumentExceptionWithInvalidKey(string? invalidKey) - { - // Act & Assert - ArgumentException exception = Assert.Throws(() => new ActorId("validType", invalidKey!)); - Assert.Contains("Invalid ActorId key", exception.Message); - } - - [Fact] - public void AgentIdShouldInitializeCorrectlyTest() - { - ActorId agentId = new("TestType", "TestKey"); - - Assert.Equal("TestType", agentId.Type.Name); - Assert.Equal("TestKey", agentId.Key); - } - - [Fact] - public void AgentIdShouldParseFromStringTest() - { - ActorId agentId = ActorId.Parse("ParsedType/ParsedKey"); - - Assert.Equal("ParsedType", agentId.Type.Name); - Assert.Equal("ParsedKey", agentId.Key); - } - - [Fact] - public void AgentIdShouldCompareEqualityCorrectlyTest() - { - ActorId agentId1 = new("SameType", "SameKey"); - ActorId agentId2 = new("SameType", "SameKey"); - ActorId agentId3 = new("DifferentType", "DifferentKey"); - - Assert.Equal(agentId2, agentId1); - Assert.NotEqual(agentId3, agentId1); - Assert.True(agentId1 == agentId2); - Assert.True(agentId1 != agentId3); - } - - [Fact] - public void AgentIdShouldGenerateCorrectHashCodeTest() - { - 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 AgentIdShouldReturnCorrectToStringTest() - { - ActorId agentId = new("ToStringType", "ToStringKey"); - - Assert.Equal("ToStringType/ToStringKey", agentId.ToString()); - } - - [Fact] - public void AgentIdShouldCompareInequalityForWrongTypeTest() - { - ActorId agentId1 = new("Type1", "Key1"); - - Assert.False(agentId1.Equals(Guid.NewGuid())); - } - - [Fact] - public void AgentIdShouldCompareInequalityCorrectlyTest() - { - ActorId agentId1 = new("Type1", "Key1"); - ActorId agentId2 = new("Type2", "Key2"); - - Assert.True(agentId1 != agentId2); - } -} diff --git a/dotnet/tests/Microsoft.Agents.AI.Runtime.Abstractions.UnitTests/InMemoryActorStateStorageTests.cs b/dotnet/tests/Microsoft.Agents.AI.Runtime.Abstractions.UnitTests/InMemoryActorStateStorageTests.cs deleted file mode 100644 index f6895e9b08..0000000000 --- a/dotnet/tests/Microsoft.Agents.AI.Runtime.Abstractions.UnitTests/InMemoryActorStateStorageTests.cs +++ /dev/null @@ -1,510 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Agents.AI.Runtime.Abstractions.UnitTests; - -/// -/// Unit tests for the class. -/// -[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "Test naming convention")] -public sealed class InMemoryActorStateStorageTests -{ - private readonly InMemoryActorStateStorage _storage = new(); - private readonly ActorId _testActorId = new("TestActor", "test-instance"); - private readonly ActorId _anotherActorId = new("AnotherActor", "another-instance"); - - [Fact] - public async Task WriteStateAsync_WithSetValueOperation_ShouldStoreValueAsync() - { - // Arrange - const string Key = "testKey"; - var value = JsonSerializer.SerializeToElement("testValue"); - var operations = new List - { - new SetValueOperation(Key, value) - }; - - // Act - var result = await this._storage.WriteStateAsync(this._testActorId, operations, "0", CancellationToken.None); - - // Assert - Assert.True(result.Success); - Assert.NotEqual("0", result.ETag); - Assert.Equal(1, this._storage.GetKeyCount(this._testActorId)); - } - - [Fact] - public async Task WriteStateAsync_WithRemoveKeyOperation_ShouldRemoveValueAsync() - { - // Arrange - const string Key = "testKey"; - var value = JsonSerializer.SerializeToElement("testValue"); - - // First set a value - var setOperations = new List - { - new SetValueOperation(Key, value) - }; - var setResult = await this._storage.WriteStateAsync(this._testActorId, setOperations, "0", CancellationToken.None); - - // Now remove the value - var removeOperations = new List - { - new RemoveKeyOperation(Key) - }; - - // Act - var result = await this._storage.WriteStateAsync(this._testActorId, removeOperations, setResult.ETag, CancellationToken.None); - - // Assert - Assert.True(result.Success); - Assert.NotEqual(setResult.ETag, result.ETag); - Assert.Equal(0, this._storage.GetKeyCount(this._testActorId)); - } - - [Fact] - public async Task WriteStateAsync_WithIncorrectETag_ShouldReturnFailureAsync() - { - // Arrange - const string Key = "testKey"; - var value = JsonSerializer.SerializeToElement("testValue"); - var operations = new List - { - new SetValueOperation(Key, value) - }; - - // Act - var result = await this._storage.WriteStateAsync(this._testActorId, operations, "incorrect-etag", CancellationToken.None); - - // Assert - Assert.False(result.Success); - Assert.Equal("0", result.ETag); // Should return current ETag - Assert.Equal(0, this._storage.GetKeyCount(this._testActorId)); - } - - [Fact] - public async Task ReadStateAsync_WithGetValueOperation_ShouldReturnValueAsync() - { - // Arrange - const string Key = "testKey"; - var value = JsonSerializer.SerializeToElement("testValue"); - var writeOperations = new List - { - new SetValueOperation(Key, value) - }; - await this._storage.WriteStateAsync(this._testActorId, writeOperations, "0", CancellationToken.None); - - var readOperations = new List - { - new GetValueOperation(Key) - }; - - // Act - var result = await this._storage.ReadStateAsync(this._testActorId, readOperations, CancellationToken.None); - - // Assert - Assert.Single(result.Results); - var getValue = result.Results[0] as GetValueResult; - Assert.NotNull(getValue); - Assert.NotNull(getValue.Value); - Assert.Equal("testValue", getValue.Value?.GetString()); - } - - [Fact] - public async Task ReadStateAsync_WithGetValueOperationForNonExistentKey_ShouldReturnNullAsync() - { - // Arrange - var readOperations = new List - { - new GetValueOperation("nonExistentKey") - }; - - // Act - var result = await this._storage.ReadStateAsync(this._testActorId, readOperations, CancellationToken.None); - - // Assert - Assert.Single(result.Results); - var getValue = result.Results[0] as GetValueResult; - Assert.NotNull(getValue); - Assert.Null(getValue.Value); - } - - [Fact] - public async Task ReadStateAsync_WithListKeysOperation_ShouldReturnAllKeysAsync() - { - // Arrange - const string Key1 = "key1"; - const string Key2 = "key2"; - var value1 = JsonSerializer.SerializeToElement("value1"); - var value2 = JsonSerializer.SerializeToElement("value2"); - - var writeOperations = new List - { - new SetValueOperation(Key1, value1), - new SetValueOperation(Key2, value2) - }; - await this._storage.WriteStateAsync(this._testActorId, writeOperations, "0", CancellationToken.None); - - var readOperations = new List - { - new ListKeysOperation(continuationToken: null) - }; - - // Act - var result = await this._storage.ReadStateAsync(this._testActorId, readOperations, CancellationToken.None); - - // Assert - Assert.Single(result.Results); - var listKeys = result.Results[0] as ListKeysResult; - Assert.NotNull(listKeys); - Assert.Equal(2, listKeys.Keys.Count); - Assert.Contains(Key1, listKeys.Keys); - Assert.Contains(Key2, listKeys.Keys); - Assert.Null(listKeys.ContinuationToken); - } - - [Fact] - public async Task ReadStateAsync_WithListKeysOperationForEmptyActor_ShouldReturnEmptyListAsync() - { - // Arrange - var readOperations = new List - { - new ListKeysOperation(continuationToken: null) - }; - - // Act - var result = await this._storage.ReadStateAsync(this._testActorId, readOperations, CancellationToken.None); - - // Assert - Assert.Single(result.Results); - var listKeys = result.Results[0] as ListKeysResult; - Assert.NotNull(listKeys); - Assert.Empty(listKeys.Keys); - Assert.Null(listKeys.ContinuationToken); - } - - [Fact] - public async Task ReadStateAsync_WithListKeysOperationAndKeyPrefix_ShouldReturnFilteredKeysAsync() - { - // Arrange - const string PrefixKey1 = "prefix_key1"; - const string PrefixKey2 = "prefix_key2"; - const string OtherKey = "other_key"; - var value1 = JsonSerializer.SerializeToElement("value1"); - var value2 = JsonSerializer.SerializeToElement("value2"); - var value3 = JsonSerializer.SerializeToElement("value3"); - - var writeOperations = new List - { - new SetValueOperation(PrefixKey1, value1), - new SetValueOperation(PrefixKey2, value2), - new SetValueOperation(OtherKey, value3) - }; - await this._storage.WriteStateAsync(this._testActorId, writeOperations, "0", CancellationToken.None); - - var readOperations = new List - { - new ListKeysOperation(continuationToken: null, keyPrefix: "prefix_") - }; - - // Act - var result = await this._storage.ReadStateAsync(this._testActorId, readOperations, CancellationToken.None); - - // Assert - Assert.Single(result.Results); - var listKeys = result.Results[0] as ListKeysResult; - Assert.NotNull(listKeys); - Assert.Equal(2, listKeys.Keys.Count); - Assert.Contains(PrefixKey1, listKeys.Keys); - Assert.Contains(PrefixKey2, listKeys.Keys); - Assert.DoesNotContain(OtherKey, listKeys.Keys); - Assert.Null(listKeys.ContinuationToken); - } - - [Fact] - public async Task ReadStateAsync_WithListKeysOperationAndNonMatchingKeyPrefix_ShouldReturnEmptyListAsync() - { - // Arrange - const string Key1 = "key1"; - const string Key2 = "key2"; - var value1 = JsonSerializer.SerializeToElement("value1"); - var value2 = JsonSerializer.SerializeToElement("value2"); - - var writeOperations = new List - { - new SetValueOperation(Key1, value1), - new SetValueOperation(Key2, value2) - }; - await this._storage.WriteStateAsync(this._testActorId, writeOperations, "0", CancellationToken.None); - - var readOperations = new List - { - new ListKeysOperation(continuationToken: null, keyPrefix: "prefix_") - }; - - // Act - var result = await this._storage.ReadStateAsync(this._testActorId, readOperations, CancellationToken.None); - - // Assert - Assert.Single(result.Results); - var listKeys = result.Results[0] as ListKeysResult; - Assert.NotNull(listKeys); - Assert.Empty(listKeys.Keys); - Assert.Null(listKeys.ContinuationToken); - } - - [Fact] - public async Task MultipleOperations_ShouldBeProcessedInOrderAsync() - { - // Arrange - const string Key1 = "key1"; - const string Key2 = "key2"; - var value1 = JsonSerializer.SerializeToElement("value1"); - var value2 = JsonSerializer.SerializeToElement("value2"); - - var operations = new List - { - new SetValueOperation(Key1, value1), - new SetValueOperation(Key2, value2), - new RemoveKeyOperation(Key1) - }; - - // Act - var result = await this._storage.WriteStateAsync(this._testActorId, operations, "0", CancellationToken.None); - - // Assert - Assert.True(result.Success); - Assert.Equal(1, this._storage.GetKeyCount(this._testActorId)); - - // Verify remaining key - var readOperations = new List - { - new GetValueOperation(Key2) - }; - var readResult = await this._storage.ReadStateAsync(this._testActorId, readOperations, CancellationToken.None); - var getValue = readResult.Results[0] as GetValueResult; - Assert.NotNull(getValue); - Assert.Equal("value2", getValue.Value?.GetString()); - } - - [Fact] - public async Task DifferentActors_ShouldHaveIsolatedStateAsync() - { - // Arrange - const string Key = "sharedKey"; - var value1 = JsonSerializer.SerializeToElement("value1"); - var value2 = JsonSerializer.SerializeToElement("value2"); - - var operations1 = new List - { - new SetValueOperation(Key, value1) - }; - var operations2 = new List - { - new SetValueOperation(Key, value2) - }; - - // Act - await this._storage.WriteStateAsync(this._testActorId, operations1, "0", CancellationToken.None); - await this._storage.WriteStateAsync(this._anotherActorId, operations2, "0", CancellationToken.None); - - // Assert - Assert.Equal(2, this._storage.ActorCount); - Assert.Equal(1, this._storage.GetKeyCount(this._testActorId)); - Assert.Equal(1, this._storage.GetKeyCount(this._anotherActorId)); - - // Verify values are different - var readOperations = new List - { - new GetValueOperation(Key) - }; - - var result1 = await this._storage.ReadStateAsync(this._testActorId, readOperations, CancellationToken.None); - var result2 = await this._storage.ReadStateAsync(this._anotherActorId, readOperations, CancellationToken.None); - - var getValue1 = result1.Results[0] as GetValueResult; - var getValue2 = result2.Results[0] as GetValueResult; - - Assert.NotNull(getValue1); - Assert.NotNull(getValue2); - Assert.Equal("value1", getValue1.Value?.GetString()); - Assert.Equal("value2", getValue2.Value?.GetString()); - } - - [Fact] - public async Task ConcurrentOperations_ShouldBeThreadSafeAsync() - { - // Arrange - const int OperationCount = 100; - var tasks = new List(); - - // Act - for (int i = 0; i < OperationCount; i++) - { - var key = $"key{i}"; - var value = JsonSerializer.SerializeToElement($"value{i}"); - var actorId = new ActorId("TestActor", $"instance{i % 10}"); // 10 different actors - var operations = new List - { - new SetValueOperation(key, value) - }; - - tasks.Add(Task.Run(async () => - { - // Retry logic to handle concurrent updates - var success = false; - var retryCount = 0; - const int MaxRetries = 10; - - while (!success && retryCount < MaxRetries) - { - var currentETag = this._storage.GetETag(actorId); - var result = await this._storage.WriteStateAsync(actorId, operations, currentETag, CancellationToken.None); - success = result.Success; - retryCount++; - } - })); - } - - await Task.WhenAll(tasks); - - // Assert - Assert.Equal(10, this._storage.ActorCount); // 10 different actors - - // Verify each actor has the expected number of keys - for (int i = 0; i < 10; i++) - { - var actorId = new ActorId("TestActor", $"instance{i}"); - Assert.Equal(10, this._storage.GetKeyCount(actorId)); // Each actor should have 10 keys - } - } - - [Fact] - public async Task Clear_ShouldRemoveAllStateAsync() - { - // Arrange - const string Key = "testKey"; - var value = JsonSerializer.SerializeToElement("testValue"); - var operations = new List - { - new SetValueOperation(Key, value) - }; - - await this._storage.WriteStateAsync(this._testActorId, operations, "0", CancellationToken.None); - Assert.Equal(1, this._storage.ActorCount); - - // Act - this._storage.Clear(); - - // Assert - Assert.Equal(0, this._storage.ActorCount); - Assert.Equal(0, this._storage.GetKeyCount(this._testActorId)); - Assert.Equal("0", this._storage.GetETag(this._testActorId)); - } - - [Fact] - public void GetETag_ForNewActor_ShouldReturnZero() - { - // Act - var etag = this._storage.GetETag(this._testActorId); - - // Assert - Assert.Equal("0", etag); - } - - [Fact] - public async Task WriteStateAsync_WithNullOperations_ShouldThrowArgumentNullExceptionAsync() => - // Act & Assert - await Assert.ThrowsAsync(() => - this._storage.WriteStateAsync(this._testActorId, null!, "0", CancellationToken.None).AsTask()); - - [Fact] - public async Task WriteStateAsync_WithNullETag_ShouldThrowArgumentNullExceptionAsync() - { - // Arrange - var operations = new List(); - - // Act & Assert - await Assert.ThrowsAsync(() => - this._storage.WriteStateAsync(this._testActorId, operations, null!, CancellationToken.None).AsTask()); - } - - [Fact] - public async Task ReadStateAsync_WithNullOperations_ShouldThrowArgumentNullExceptionAsync() => - // Act & Assert - await Assert.ThrowsAsync(() => - this._storage.ReadStateAsync(this._testActorId, null!, CancellationToken.None).AsTask()); - - [Fact] - public async Task WriteStateAsync_WithCancelledToken_ShouldThrowOperationCanceledExceptionAsync() - { - // Arrange - var operations = new List(); - var cancellationToken = new CancellationToken(canceled: true); - - // Act & Assert - await Assert.ThrowsAsync(() => - this._storage.WriteStateAsync(this._testActorId, operations, "0", cancellationToken).AsTask()); - } - - [Fact] - public async Task ReadStateAsync_WithCancelledToken_ShouldThrowOperationCanceledExceptionAsync() - { - // Arrange - var operations = new List(); - var cancellationToken = new CancellationToken(canceled: true); - - // Act & Assert - await Assert.ThrowsAsync(() => - this._storage.ReadStateAsync(this._testActorId, operations, cancellationToken).AsTask()); - } - - [Fact] - public async Task ETagProgression_ShouldIncrementMonotonicallyAsync() - { - // Arrange - const string Key = "testKey"; - var value1 = JsonSerializer.SerializeToElement("value1"); - var value2 = JsonSerializer.SerializeToElement("value2"); - - // Act - var operations1 = new List { new SetValueOperation(Key, value1) }; - var result1 = await this._storage.WriteStateAsync(this._testActorId, operations1, "0", CancellationToken.None); - - var operations2 = new List { new SetValueOperation(Key, value2) }; - var result2 = await this._storage.WriteStateAsync(this._testActorId, operations2, result1.ETag, CancellationToken.None); - - // Assert - Assert.True(result1.Success); - Assert.True(result2.Success); - Assert.NotEqual("0", result1.ETag); - Assert.NotEqual(result1.ETag, result2.ETag); - - // ETags should be numeric and increasing - Assert.True(long.Parse(result1.ETag) < long.Parse(result2.ETag)); - } - - [Fact] - public void ListKeysOperation_JsonSerialization_ShouldWorkCorrectly() - { - // Arrange - var operation = new ListKeysOperation(continuationToken: "token123", keyPrefix: "prefix_"); - - // Act - Serialize to JSON - var json = JsonSerializer.Serialize(operation); - - // Deserialize back to object - var deserializedOperation = JsonSerializer.Deserialize(json); - - // Assert - Assert.NotNull(deserializedOperation); - Assert.Equal("token123", deserializedOperation.ContinuationToken); - Assert.Equal("prefix_", deserializedOperation.KeyPrefix); - Assert.Equal(ActorReadOperationType.ListKeys, deserializedOperation.Type); - } -} diff --git a/dotnet/tests/Microsoft.Agents.AI.Runtime.Abstractions.UnitTests/JsonSerializationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Runtime.Abstractions.UnitTests/JsonSerializationTests.cs deleted file mode 100644 index 5f9395c460..0000000000 --- a/dotnet/tests/Microsoft.Agents.AI.Runtime.Abstractions.UnitTests/JsonSerializationTests.cs +++ /dev/null @@ -1,533 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Microsoft.Agents.AI.Runtime.Abstractions.UnitTests; - -/// -/// Tests for JSON serialization and deserialization of all JSON-serializable types. -/// -public class JsonSerializationTests -{ - private readonly JsonSerializerOptions _options; - - public JsonSerializationTests() - { - this._options = new JsonSerializerOptions - { - WriteIndented = false, // Use compact JSON for easier testing - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - Converters = { new JsonStringEnumConverter() }, - TypeInfoResolver = AgentRuntimeAbstractionsJsonUtilities.JsonContext.Default - }; - } - - #region ActorMessage Tests - - [Fact] - public void ActorRequestMessage_SerializesAndDeserializes() - { - // Arrange - var originalMessage = new ActorRequestMessage("msg123") - { - SenderId = new ActorId("TestActor", "instance1"), - Method = "TestMethod", - Params = JsonSerializer.SerializeToElement(new { param = "value" }) - }; - - // Act - Serialize to JSON - string json = JsonSerializer.Serialize(originalMessage, this._options); - - // Assert - JSON structure - Assert.Contains("\"type\":\"request\"", json); - Assert.Contains("\"messageId\":\"msg123\"", json); - Assert.Contains("\"method\":\"TestMethod\"", json); - - // Act - Deserialize back - var deserializedMessage = JsonSerializer.Deserialize(json, this._options) as ActorRequestMessage; - - // Assert - Verify deserialization - Assert.NotNull(deserializedMessage); - Assert.Equal(originalMessage.MessageId, deserializedMessage.MessageId); - Assert.Equal(originalMessage.Method, deserializedMessage.Method); - Assert.Equal(originalMessage.SenderId?.Type.Name, deserializedMessage.SenderId?.Type.Name); - Assert.Equal(originalMessage.SenderId?.Key, deserializedMessage.SenderId?.Key); - Assert.Equal(originalMessage.Params.GetRawText(), deserializedMessage.Params.GetRawText()); - } - - #endregion - - #region ActorWriteOperation Tests - - [Fact] - public void SetValueOperation_SerializesAndDeserializes() - { - // Arrange - var originalOperation = new SetValueOperation("testKey", JsonSerializer.SerializeToElement(new { value = "testValue" })); - - // Act - Serialize to JSON - string json = JsonSerializer.Serialize(originalOperation, this._options); - - // Assert - JSON structure (uses snake_case) - Assert.Contains("\"type\":\"set_value\"", json); - Assert.Contains("\"key\":\"testKey\"", json); - Assert.Contains("\"value\":", json); - - // Act - Deserialize back - var deserializedOperation = JsonSerializer.Deserialize(json, this._options) as SetValueOperation; - - // Assert - Verify deserialization - Assert.NotNull(deserializedOperation); - Assert.Equal(originalOperation.Key, deserializedOperation.Key); - Assert.Equal(originalOperation.Value.GetRawText(), deserializedOperation.Value.GetRawText()); - } - - [Fact] - public void RemoveKeyOperation_SerializesAndDeserializes() - { - // Arrange - var originalOperation = new RemoveKeyOperation("keyToRemove"); - - // Act - Serialize to JSON - string json = JsonSerializer.Serialize(originalOperation, this._options); - - // Assert - JSON structure (uses snake_case) - Assert.Contains("\"type\":\"remove_key\"", json); - Assert.Contains("\"key\":\"keyToRemove\"", json); - - // Act - Deserialize back - var deserializedOperation = JsonSerializer.Deserialize(json, this._options) as RemoveKeyOperation; - - // Assert - Verify deserialization - Assert.NotNull(deserializedOperation); - Assert.Equal(originalOperation.Key, deserializedOperation.Key); - } - - [Fact] - public void ActorSendRequestOperation_SerializesAndDeserializes() - { - // Arrange - var request = new ActorRequestMessage("msg456") - { - SenderId = new ActorId("TargetActor", "instance1"), - Method = "ProcessData", - Params = JsonSerializer.SerializeToElement(new { data = "payload" }) - }; - var originalOperation = new SendRequestOperation(request); - - // Act - Serialize to JSON - string json = JsonSerializer.Serialize(originalOperation, this._options); - - // Assert - JSON structure (uses snake_case) - Assert.Contains("\"type\":\"send_request\"", json); - Assert.Contains("\"message\":", json); - Assert.Contains("\"messageId\":\"msg456\"", json); - - // Act - Deserialize back - var deserializedOperation = JsonSerializer.Deserialize(json, this._options) as SendRequestOperation; - - // Assert - Verify deserialization - Assert.NotNull(deserializedOperation); - Assert.Equal(originalOperation.Message.MessageId, deserializedOperation.Message.MessageId); - Assert.Equal(originalOperation.Message.Method, deserializedOperation.Message.Method); - } - - [Fact] - public void ActorUpdateRequestOperation_SerializesAndDeserializes() - { - // Arrange - var originalOperation = new UpdateRequestOperation("msg789", RequestStatus.Failed, JsonSerializer.SerializeToElement(new { error = "timeout" })); - - // Act - Serialize to JSON - string json = JsonSerializer.Serialize(originalOperation, this._options); - - // Assert - JSON structure (uses snake_case) - Assert.Contains("\"type\":\"update_request\"", json); - Assert.Contains("\"messageId\":\"msg789\"", json); - Assert.Contains("\"status\":\"failed\"", json); - - // Act - Deserialize back - var deserializedOperation = JsonSerializer.Deserialize(json, this._options) as UpdateRequestOperation; - - // Assert - Verify deserialization - Assert.NotNull(deserializedOperation); - Assert.Equal(originalOperation.MessageId, deserializedOperation.MessageId); - Assert.Equal(originalOperation.Status, deserializedOperation.Status); - Assert.Equal(originalOperation.Data.GetRawText(), deserializedOperation.Data.GetRawText()); - } - - #endregion - - #region ActorReadOperation Tests - - [Fact] - public void ListKeysOperation_SerializesAndDeserializes() - { - // Arrange - var originalOperation = new ListKeysOperation("continuationToken123", "prefix_"); - - // Act - Serialize to JSON - string json = JsonSerializer.Serialize(originalOperation, this._options); - - // Assert - JSON structure (uses snake_case) - Assert.Contains("\"type\":\"list_keys\"", json); - Assert.Contains("\"continuationToken\":\"continuationToken123\"", json); - Assert.Contains("\"keyPrefix\":\"prefix_\"", json); - - // Act - Deserialize back - var deserializedOperation = JsonSerializer.Deserialize(json, this._options) as ListKeysOperation; - - // Assert - Verify deserialization - Assert.NotNull(deserializedOperation); - Assert.Equal(originalOperation.ContinuationToken, deserializedOperation.ContinuationToken); - Assert.Equal(originalOperation.KeyPrefix, deserializedOperation.KeyPrefix); - } - - [Fact] - public void GetValueOperation_SerializesAndDeserializes() - { - // Arrange - var originalOperation = new GetValueOperation("myKey"); - - // Act - Serialize to JSON - string json = JsonSerializer.Serialize(originalOperation, this._options); - - // Assert - JSON structure (uses snake_case) - Assert.Contains("\"type\":\"get_value\"", json); - Assert.Contains("\"key\":\"myKey\"", json); - - // Act - Deserialize back - var deserializedOperation = JsonSerializer.Deserialize(json, this._options) as GetValueOperation; - - // Assert - Verify deserialization - Assert.NotNull(deserializedOperation); - Assert.Equal(originalOperation.Key, deserializedOperation.Key); - } - - #endregion - - #region Enum Serialization Tests - - [Fact] - public void ActorMessageType_SerializesAsString() - { - // Test that enums serialize as strings - Assert.Equal("\"request\"", JsonSerializer.Serialize(ActorMessageType.Request, this._options)); - Assert.Equal("\"response\"", JsonSerializer.Serialize(ActorMessageType.Response, this._options)); - } - - [Fact] - public void RequestStatus_SerializesAsString() - { - // Test that enums serialize as strings - Assert.Equal("\"pending\"", JsonSerializer.Serialize(RequestStatus.Pending, this._options)); - Assert.Equal("\"completed\"", JsonSerializer.Serialize(RequestStatus.Completed, this._options)); - Assert.Equal("\"failed\"", JsonSerializer.Serialize(RequestStatus.Failed, this._options)); - Assert.Equal("\"not_found\"", JsonSerializer.Serialize(RequestStatus.NotFound, this._options)); - } - - [Fact] - public void ActorWriteOperationType_SerializesAsString() - { - // Test that enums serialize as strings (snake_case) - Assert.Equal("\"set_value\"", JsonSerializer.Serialize(ActorWriteOperationType.SetValue, this._options)); - Assert.Equal("\"remove_key\"", JsonSerializer.Serialize(ActorWriteOperationType.RemoveKey, this._options)); - Assert.Equal("\"send_request\"", JsonSerializer.Serialize(ActorWriteOperationType.SendRequest, this._options)); - Assert.Equal("\"update_request\"", JsonSerializer.Serialize(ActorWriteOperationType.UpdateRequest, this._options)); - } - - [Fact] - public void ActorReadOperationType_SerializesAsString() - { - // Test that enums serialize as strings (snake_case) - Assert.Equal("\"list_keys\"", JsonSerializer.Serialize(ActorReadOperationType.ListKeys, this._options)); - Assert.Equal("\"get_value\"", JsonSerializer.Serialize(ActorReadOperationType.GetValue, this._options)); - } - - [Fact] - public void ActorReadResultType_SerializesAsString() - { - // Test that enums serialize as strings (snake_case) - Assert.Equal("\"list_keys\"", JsonSerializer.Serialize(ActorReadResultType.ListKeys, this._options)); - Assert.Equal("\"get_value\"", JsonSerializer.Serialize(ActorReadResultType.GetValue, this._options)); - } - - #endregion - - #region ActorId Tests - - [Fact] - public void ActorId_SerializesAndDeserializes() - { - // Arrange - var actorId = new ActorId("UserActor", "user123"); - - // Act - Serialize to JSON - string json = JsonSerializer.Serialize(actorId, this._options); - - // Assert - JSON structure - Assert.Contains("UserActor/user123", json); - - // Act - Deserialize back - var deserializedActorId = JsonSerializer.Deserialize(json, this._options); - - // Assert - Verify deserialization - Assert.Equal(actorId.Type.Name, deserializedActorId.Type.Name); - Assert.Equal(actorId.Key, deserializedActorId.Key); - } - - #endregion - - #region Non-message types Tests - - [Fact] - public void ActorRequest_SerializesAndDeserializes() - { - // Arrange - var originalRequest = new ActorRequest( - new ActorId("TestActor", "instance1"), - "msg123", - "TestMethod", - JsonSerializer.SerializeToElement(new { param = "value" })); - - // Act - Serialize to JSON - string json = JsonSerializer.Serialize(originalRequest, this._options); - - // Assert - JSON structure - Assert.Contains("\"messageId\":\"msg123\"", json); - Assert.Contains("\"method\":\"TestMethod\"", json); - - // Act - Deserialize back - var deserializedRequest = JsonSerializer.Deserialize(json, this._options); - - // Assert - Verify deserialization - Assert.NotNull(deserializedRequest); - Assert.Equal(originalRequest.MessageId, deserializedRequest.MessageId); - Assert.Equal(originalRequest.Method, deserializedRequest.Method); - Assert.Equal(originalRequest.ActorId.Type.Name, deserializedRequest.ActorId.Type.Name); - Assert.Equal(originalRequest.ActorId.Key, deserializedRequest.ActorId.Key); - Assert.Equal(originalRequest.Params.GetRawText(), deserializedRequest.Params.GetRawText()); - } - - [Fact] - public void ActorRequestUpdate_SerializesAndDeserializes() - { - // Arrange - var originalUpdate = new ActorRequestUpdate(RequestStatus.Completed, JsonSerializer.SerializeToElement(new { result = "done" })); - - // Act - Serialize to JSON - string json = JsonSerializer.Serialize(originalUpdate, this._options); - - // Assert - JSON structure - Assert.Contains("\"status\":\"completed\"", json); - - // Act - Deserialize back - var deserializedUpdate = JsonSerializer.Deserialize(json, this._options); - - // Assert - Verify deserialization - Assert.NotNull(deserializedUpdate); - Assert.Equal(originalUpdate.Status, deserializedUpdate.Status); - Assert.Equal(originalUpdate.Data.GetRawText(), deserializedUpdate.Data.GetRawText()); - } - - #endregion - - #region ActorResponse Tests - - [Fact] - public void ActorResponse_SerializesAndDeserializes() - { - // Arrange - var originalResponse = new ActorResponse - { - ActorId = new ActorId("TestActor", "instance1"), - MessageId = "msg123", - Status = RequestStatus.Completed, - Data = JsonSerializer.SerializeToElement(new { result = "success" }) - }; - - // Act - Serialize to JSON - string json = JsonSerializer.Serialize(originalResponse, this._options); - - // Assert - JSON structure - Assert.Contains("\"messageId\":\"msg123\"", json); - Assert.Contains("\"status\":\"completed\"", json); - - // Act - Deserialize back - var deserializedResponse = JsonSerializer.Deserialize(json, this._options); - - // Assert - Verify deserialization - Assert.NotNull(deserializedResponse); - Assert.Equal(originalResponse.MessageId, deserializedResponse.MessageId); - Assert.Equal(originalResponse.Status, deserializedResponse.Status); - Assert.Equal(originalResponse.ActorId.Type.Name, deserializedResponse.ActorId.Type.Name); - Assert.Equal(originalResponse.ActorId.Key, deserializedResponse.ActorId.Key); - Assert.Equal(originalResponse.Data.GetRawText(), deserializedResponse.Data.GetRawText()); - } - - [Fact] - public void ActorResponse_ToString_OutputsExpectedFormat() - { - // Arrange - var testData = JsonSerializer.SerializeToElement(new { result = "success" }); - var response = new ActorResponse - { - ActorId = new ActorId("TestActor", "instance1"), - MessageId = "msg123", - Status = RequestStatus.Completed, - Data = testData - }; - - // Act - string result = response.ToString(); - - // Assert - Assert.Equal($"ActorResponse(ActorId: TestActor/instance1, Status: Completed, MessageId: msg123, Data: {testData.GetRawText()})", result); - } - - [Fact] - public void ActorResponse_ToString_WithNullMessageId_OutputsExpectedFormat() - { - // Arrange - var testData = JsonSerializer.SerializeToElement(new { error = "timeout" }); - var response = new ActorResponse - { - ActorId = new ActorId("TestActor", "instance1"), - MessageId = null, - Status = RequestStatus.Pending, - Data = testData - }; - - // Act - string result = response.ToString(); - - // Assert - Assert.Equal($"ActorResponse(ActorId: TestActor/instance1, Status: Pending, MessageId: null, Data: {testData.GetRawText()})", result); - } - - [Fact] - public void ActorResponse_ToString_WithEmptyData_OutputsExpectedFormat() - { - // Arrange - var emptyData = new JsonElement(); // Default JsonElement (empty) - var response = new ActorResponse - { - ActorId = new ActorId("TestActor", "instance1"), - MessageId = "msg456", - Status = RequestStatus.Failed, - Data = emptyData - }; - - // Act - string result = response.ToString(); - - // Assert - Assert.Equal("ActorResponse(ActorId: TestActor/instance1, Status: Failed, MessageId: msg456, Data: undefined)", result); - } - - [Fact] - public void ActorResponse_ToString_WithLargeData_TruncatesAfter250Characters() - { - // Arrange - // Create a large object that will serialize to more than 250 characters - var largeArray = new List(); - for (int i = 0; i < 20; i++) - { - largeArray.Add(new - { - id = $"item-{i:000}", - name = $"This is item number {i} with a long description to make the JSON larger", - properties = new - { - prop1 = $"value1-{i}", - prop2 = $"value2-{i}", - prop3 = $"value3-{i}", - prop4 = $"value4-{i}", - prop5 = $"value5-{i}" - } - }); - } - var largeData = JsonSerializer.SerializeToElement(largeArray); - var response = new ActorResponse - { - ActorId = new ActorId("TestActor", "instance1"), - MessageId = "msg789", - Status = RequestStatus.Completed, - Data = largeData - }; - - // Act - string result = response.ToString(); - var rawText = largeData.GetRawText(); - - // Assert - // Verify that the raw JSON is indeed larger than 250 characters - Assert.True(rawText.Length > 250, $"Test data should be larger than 250 characters, but was {rawText.Length}"); - - // The ToString should truncate the data and add "..." - Assert.EndsWith("...)", result); - - // Extract the data portion from the result - var dataStartIndex = result.IndexOf("Data: ", System.StringComparison.Ordinal) + 6; - var dataEndIndex = result.Length - 1; // Exclude the closing parenthesis - var dataInResult = result.Substring(dataStartIndex, dataEndIndex - dataStartIndex); - - // Verify truncation: data should be 253 characters (250 + "...") - Assert.Equal(253, dataInResult.Length); - - // Verify that the truncated data matches the first 250 characters of the original - Assert.Equal(rawText.Substring(0, 250), dataInResult.Substring(0, 250)); - } - - [Fact] - public void ActorResponse_ToString_WithSmallData_DoesNotTruncate() - { - // Arrange - var smallObject = new - { - id = "test-id-123", - name = "Small Test Object", - value = 42 - }; - var smallData = JsonSerializer.SerializeToElement(smallObject); - var response = new ActorResponse - { - ActorId = new ActorId("TestActor", "instance1"), - MessageId = "msg789", - Status = RequestStatus.Completed, - Data = smallData - }; - - // Act - string result = response.ToString(); - - // Assert - // The ToString should include the full JSON data without truncation - Assert.Equal($"ActorResponse(ActorId: TestActor/instance1, Status: Completed, MessageId: msg789, Data: {smallData.GetRawText()})", result); - // Verify no truncation occurred - Assert.DoesNotContain("...", result); - } - - [Fact] - public void ActorResponse_ToString_WithNullData_OutputsExpectedFormat() - { - // Arrange - var response = new ActorResponse - { - ActorId = new ActorId("TestActor", "instance1"), - MessageId = "msg999", - Status = RequestStatus.Completed, - Data = JsonSerializer.SerializeToElement((object?)null) - }; - - // Act - string result = response.ToString(); - - // Assert - Assert.Equal("ActorResponse(ActorId: TestActor/instance1, Status: Completed, MessageId: msg999, Data: null)", result); - } - - #endregion -} diff --git a/dotnet/tests/Microsoft.Agents.AI.Runtime.Abstractions.UnitTests/Microsoft.Agents.AI.Runtime.Abstractions.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.Runtime.Abstractions.UnitTests/Microsoft.Agents.AI.Runtime.Abstractions.UnitTests.csproj deleted file mode 100644 index 17eb809ce3..0000000000 --- a/dotnet/tests/Microsoft.Agents.AI.Runtime.Abstractions.UnitTests/Microsoft.Agents.AI.Runtime.Abstractions.UnitTests.csproj +++ /dev/null @@ -1,11 +0,0 @@ - - - - $(ProjectsTargetFrameworks) - - - - - - - diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.csproj b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.csproj index 7660a9d98a..bb72598c33 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.csproj +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.csproj @@ -16,6 +16,9 @@ + + +