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