))]
+[JsonSerializable(typeof(EventData))]
+[JsonSerializable(typeof(AgentRunResponseUpdate))]
+[JsonSerializable(typeof(AgentResponse))]
+[JsonSerializable(typeof(JsonElement))]
+internal sealed partial class AgentClientJsonContext : JsonSerializerContext;
diff --git a/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Components/App.razor b/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Components/App.razor
new file mode 100644
index 0000000000..2a9bdedfe7
--- /dev/null
+++ b/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Components/App.razor
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Components/Layout/MainLayout.razor b/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Components/Layout/MainLayout.razor
new file mode 100644
index 0000000000..5a24bb1371
--- /dev/null
+++ b/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Components/Layout/MainLayout.razor
@@ -0,0 +1,23 @@
+@inherits LayoutComponentBase
+
+
+
+
+
+
+
+
+ @Body
+
+
+
+
+
+ An unhandled error has occurred.
+
Reload
+
🗙
+
diff --git a/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Components/Layout/MainLayout.razor.css b/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Components/Layout/MainLayout.razor.css
new file mode 100644
index 0000000000..038baf178b
--- /dev/null
+++ b/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Components/Layout/MainLayout.razor.css
@@ -0,0 +1,96 @@
+.page {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+}
+
+main {
+ flex: 1;
+}
+
+.sidebar {
+ background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
+}
+
+.top-row {
+ background-color: #f7f7f7;
+ border-bottom: 1px solid #d6d5d5;
+ justify-content: flex-end;
+ height: 3.5rem;
+ display: flex;
+ align-items: center;
+}
+
+ .top-row ::deep a, .top-row ::deep .btn-link {
+ white-space: nowrap;
+ margin-left: 1.5rem;
+ text-decoration: none;
+ }
+
+ .top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
+ text-decoration: underline;
+ }
+
+ .top-row ::deep a:first-child {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+@media (max-width: 640.98px) {
+ .top-row {
+ justify-content: space-between;
+ }
+
+ .top-row ::deep a, .top-row ::deep .btn-link {
+ margin-left: 0;
+ }
+}
+
+@media (min-width: 641px) {
+ .page {
+ flex-direction: row;
+ }
+
+ .sidebar {
+ width: 250px;
+ height: 100vh;
+ position: sticky;
+ top: 0;
+ }
+
+ .top-row {
+ position: sticky;
+ top: 0;
+ z-index: 1;
+ }
+
+ .top-row.auth ::deep a:first-child {
+ flex: 1;
+ text-align: right;
+ width: 0;
+ }
+
+ .top-row, article {
+ padding-left: 2rem !important;
+ padding-right: 1.5rem !important;
+ }
+}
+
+#blazor-error-ui {
+ background: lightyellow;
+ bottom: 0;
+ box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
+ display: none;
+ left: 0;
+ padding: 0.6rem 1.25rem 0.7rem 1.25rem;
+ position: fixed;
+ width: 100%;
+ z-index: 1000;
+}
+
+ #blazor-error-ui .dismiss {
+ cursor: pointer;
+ position: absolute;
+ right: 0.75rem;
+ top: 0.5rem;
+ }
diff --git a/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Components/Layout/NavMenu.razor b/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Components/Layout/NavMenu.razor
new file mode 100644
index 0000000000..8cc90687e7
--- /dev/null
+++ b/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Components/Layout/NavMenu.razor
@@ -0,0 +1,29 @@
+
+
+
+
+
diff --git a/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Components/Layout/NavMenu.razor.css b/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Components/Layout/NavMenu.razor.css
new file mode 100644
index 0000000000..1338edb61e
--- /dev/null
+++ b/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Components/Layout/NavMenu.razor.css
@@ -0,0 +1,102 @@
+.navbar-toggler {
+ appearance: none;
+ cursor: pointer;
+ width: 3.5rem;
+ height: 2.5rem;
+ color: white;
+ position: absolute;
+ top: 0.5rem;
+ right: 1rem;
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
+}
+
+.navbar-toggler:checked {
+ background-color: rgba(255, 255, 255, 0.5);
+}
+
+.top-row {
+ min-height: 3.5rem;
+ background-color: rgba(0,0,0,0.4);
+}
+
+.navbar-brand {
+ font-size: 1.1rem;
+}
+
+.bi {
+ display: inline-block;
+ position: relative;
+ width: 1.25rem;
+ height: 1.25rem;
+ margin-right: 0.75rem;
+ top: -1px;
+ background-size: cover;
+}
+
+.bi-house-door-fill {
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
+}
+
+.bi-plus-square-fill {
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
+}
+
+.bi-list-nested {
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
+}
+
+.nav-item {
+ font-size: 0.9rem;
+ padding-bottom: 0.5rem;
+}
+
+ .nav-item:first-of-type {
+ padding-top: 1rem;
+ }
+
+ .nav-item:last-of-type {
+ padding-bottom: 1rem;
+ }
+
+ .nav-item ::deep a {
+ color: #d7d7d7;
+ border-radius: 4px;
+ height: 3rem;
+ display: flex;
+ align-items: center;
+ line-height: 3rem;
+ }
+
+.nav-item ::deep a.active {
+ background-color: rgba(255,255,255,0.37);
+ color: white;
+}
+
+.nav-item ::deep a:hover {
+ background-color: rgba(255,255,255,0.1);
+ color: white;
+}
+
+.nav-scrollable {
+ display: none;
+}
+
+.navbar-toggler:checked ~ .nav-scrollable {
+ display: block;
+}
+
+@media (min-width: 641px) {
+ .navbar-toggler {
+ display: none;
+ }
+
+ .nav-scrollable {
+ /* Never collapse the sidebar for wide screens */
+ display: block;
+
+ /* Allow sidebar to scroll for tall menus */
+ height: calc(100vh - 3.5rem);
+ overflow-y: auto;
+ }
+}
diff --git a/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Components/Pages/Counter.razor b/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Components/Pages/Counter.razor
new file mode 100644
index 0000000000..1a4f8e7553
--- /dev/null
+++ b/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Components/Pages/Counter.razor
@@ -0,0 +1,19 @@
+@page "/counter"
+@rendermode InteractiveServer
+
+Counter
+
+Counter
+
+Current count: @currentCount
+
+Click me
+
+@code {
+ private int currentCount = 0;
+
+ private void IncrementCount()
+ {
+ currentCount++;
+ }
+}
diff --git a/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Components/Pages/Error.razor b/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Components/Pages/Error.razor
new file mode 100644
index 0000000000..fcaa7c6ef6
--- /dev/null
+++ b/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Components/Pages/Error.razor
@@ -0,0 +1,38 @@
+@page "/Error"
+@using System.Diagnostics
+
+Error
+
+Error.
+An error occurred while processing your request.
+
+@if (ShowRequestId)
+{
+
+ Request ID: @requestId
+
+}
+
+Development Mode
+
+ Swapping to Development environment will display more detailed information about the error that occurred.
+
+
+ The Development environment shouldn't be enabled for deployed applications.
+ It can result in displaying sensitive information from exceptions to end users.
+ For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development
+ and restarting the app.
+
+
+@code{
+ [CascadingParameter]
+ public HttpContext? HttpContext { get; set; }
+
+ private string? requestId;
+ private bool ShowRequestId => !string.IsNullOrEmpty(requestId);
+
+ protected override void OnInitialized()
+ {
+ requestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
+ }
+}
diff --git a/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Components/Pages/Home.razor b/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Components/Pages/Home.razor
new file mode 100644
index 0000000000..9001e0bd27
--- /dev/null
+++ b/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Components/Pages/Home.razor
@@ -0,0 +1,7 @@
+@page "/"
+
+Home
+
+Hello, world!
+
+Welcome to your new app.
diff --git a/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Components/Pages/PirateTalk.razor b/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Components/Pages/PirateTalk.razor
new file mode 100644
index 0000000000..cba45d10a7
--- /dev/null
+++ b/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Components/Pages/PirateTalk.razor
@@ -0,0 +1,208 @@
+@page "/pirate-talk"
+@attribute [StreamRendering(true)]
+@inject AgentClient AgentClient
+@inject IJSRuntime JSRuntime
+@inject ILogger Logger
+@rendermode InteractiveServer
+@using System.Text
+@using System.Text.Json
+@using Microsoft.Extensions.AI
+@using Microsoft.Extensions.AI.Agents
+
+Pirate Talk
+
+🏴☠️ Pirate Talk
+
+Chat with a pirate agent! Send a message and get a response in pirate speak.
+
+
+
+ @foreach (var message in chatMessages)
+ {
+
+
@(message.IsUser ? "You" : "🏴☠️ Pirate"):
+
@message.Content
+
+ }
+
+ @if (isStreaming && currentStreamedMessage.Length > 0)
+ {
+
+
🏴☠️ Pirate:
+
@currentStreamedMessage▋
+
+ }
+
+
+
+
+
+ @if (isStreaming)
+ {
+
+ Sending...
+ }
+ else
+ {
+ Send
+ }
+
+
+
+
+
+
+@code {
+ private string currentMessage = "";
+ private bool isStreaming = false;
+ private string currentStreamedMessage = "";
+ private List chatMessages = new();
+ private string sessionId = Guid.NewGuid().ToString();
+ private const string AgentName = "agent:pirate";
+
+ protected override void OnInitialized()
+ {
+ Logger.LogDebug("Initializing PirateTalk component with session ID: {SessionId}", sessionId);
+ }
+
+ private async Task SendMessage()
+ {
+ if (string.IsNullOrWhiteSpace(currentMessage) || isStreaming)
+ return;
+
+ var userMessage = currentMessage.Trim();
+ currentMessage = "";
+
+ Logger.LogInformation("User sending message: '{UserMessage}' in session {SessionId}", userMessage, sessionId);
+
+ // Add user message to chat
+ chatMessages.Add(new ChatMessage { Content = userMessage, IsUser = true });
+ Logger.LogDebug("Chat message added to collection. Total messages: {MessageCount}, Is user message: {IsUserMessage}", chatMessages.Count, true);
+ Logger.LogTrace("StateHasChanged called during streaming for session {SessionId}", sessionId);
+ StateHasChanged();
+ await ScrollToBottom();
+
+ // Start streaming response
+ isStreaming = true;
+ currentStreamedMessage = "";
+ Logger.LogDebug("Starting streaming response for session {SessionId}", sessionId);
+ Logger.LogTrace("StateHasChanged called during streaming for session {SessionId}", sessionId);
+ StateHasChanged();
+
+ try
+ {
+ var responseContent = new StringBuilder();
+
+ await foreach (var update in AgentClient.SendMessageStreamAsync(AgentName, userMessage, sessionId))
+ {
+ Logger.LogTrace("Received streaming update with text length: {TextLength} for session {SessionId}", update.Text?.Length ?? 0, sessionId);
+
+ // Extract text content from the AgentRunResponseUpdate
+ var content = update.Text ?? "";
+ if (!string.IsNullOrEmpty(content))
+ {
+ Logger.LogDebug("Extracted content from update: '{ExtractedContent}' for session {SessionId}", content, sessionId);
+ responseContent.Append(content);
+ currentStreamedMessage = responseContent.ToString();
+ Logger.LogTrace("StateHasChanged called during streaming for session {SessionId}", sessionId);
+ StateHasChanged();
+ await ScrollToBottom();
+ }
+ }
+
+ // Add the complete pirate response to chat messages
+ if (responseContent.Length > 0)
+ {
+ Logger.LogInformation("Streaming completed with total response length: {ResponseLength} for session {SessionId}", responseContent.Length, sessionId);
+ chatMessages.Add(new ChatMessage { Content = responseContent.ToString(), IsUser = false });
+ Logger.LogDebug("Chat message added to collection. Total messages: {MessageCount}, Is user message: {IsUserMessage}", chatMessages.Count, false);
+ }
+ else
+ {
+ Logger.LogWarning("Empty response received from agent for session {SessionId}", sessionId);
+ chatMessages.Add(new ChatMessage { Content = "Arrr, something went wrong with me response, matey!", IsUser = false });
+ Logger.LogDebug("Chat message added to collection. Total messages: {MessageCount}, Is user message: {IsUserMessage}", chatMessages.Count, false);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, "Error occurred while processing message in session {SessionId}: {ErrorMessage}", sessionId, ex.Message);
+ chatMessages.Add(new ChatMessage { Content = $"Arrr, encountered rough seas: {ex.Message}", IsUser = false });
+ Logger.LogDebug("Chat message added to collection. Total messages: {MessageCount}, Is user message: {IsUserMessage}", chatMessages.Count, false);
+ }
+ finally
+ {
+ isStreaming = false;
+ currentStreamedMessage = "";
+ Logger.LogTrace("StateHasChanged called during streaming for session {SessionId}", sessionId);
+ StateHasChanged();
+ await ScrollToBottom();
+ }
+ }
+
+ private async Task HandleKeyPress(KeyboardEventArgs e)
+ {
+ Logger.LogDebug("Handling key press event: {Key} for session {SessionId}", e.Key, sessionId);
+ if (e.Key == "Enter" && !e.ShiftKey)
+ {
+ await SendMessage();
+ }
+ }
+
+ private async Task ScrollToBottom()
+ {
+ try
+ {
+ Logger.LogTrace("Scrolling chat to bottom for session {SessionId}", sessionId);
+ await JSRuntime.InvokeVoidAsync("scrollToBottom", "chat-messages");
+ }
+ catch (Exception ex)
+ {
+ Logger.LogWarning(ex, "Failed to scroll to bottom due to JavaScript error for session {SessionId}", sessionId);
+ // Ignore JS errors
+ }
+ }
+
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ if (firstRender)
+ {
+ Logger.LogDebug("Component first render completed, JavaScript functions initialized for session {SessionId}", sessionId);
+ await JSRuntime.InvokeVoidAsync("eval", @"
+ window.scrollToBottom = function(elementId) {
+ const element = document.getElementById(elementId);
+ if (element) {
+ element.scrollTop = element.scrollHeight;
+ }
+ };
+ ");
+ }
+ }
+
+ private class ChatMessage
+ {
+ public string Content { get; set; } = "";
+ public bool IsUser { get; set; }
+ }
+}
diff --git a/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Components/Routes.razor b/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Components/Routes.razor
new file mode 100644
index 0000000000..f756e19dfb
--- /dev/null
+++ b/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Components/Routes.razor
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Components/_Imports.razor b/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Components/_Imports.razor
new file mode 100644
index 0000000000..4c2793f6db
--- /dev/null
+++ b/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Components/_Imports.razor
@@ -0,0 +1,11 @@
+@using System.Net.Http
+@using System.Net.Http.Json
+@using Microsoft.AspNetCore.Components.Forms
+@using Microsoft.AspNetCore.Components.Routing
+@using Microsoft.AspNetCore.Components.Web
+@using static Microsoft.AspNetCore.Components.Web.RenderMode
+@using Microsoft.AspNetCore.Components.Web.Virtualization
+@using Microsoft.AspNetCore.OutputCaching
+@using Microsoft.JSInterop
+@using HelloHttpApi.Web
+@using HelloHttpApi.Web.Components
diff --git a/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/HelloHttpApi.Web.csproj b/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/HelloHttpApi.Web.csproj
new file mode 100644
index 0000000000..e0aec4096f
--- /dev/null
+++ b/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/HelloHttpApi.Web.csproj
@@ -0,0 +1,14 @@
+
+
+
+ net9.0
+ enable
+ enable
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Program.cs b/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Program.cs
new file mode 100644
index 0000000000..68c8f8be5c
--- /dev/null
+++ b/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Program.cs
@@ -0,0 +1,45 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using HelloHttpApi.Web;
+using HelloHttpApi.Web.Components;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Add service defaults & Aspire client integrations.
+builder.AddServiceDefaults();
+
+// Add services to the container.
+builder.Services.AddRazorComponents()
+ .AddInteractiveServerComponents();
+
+builder.Services.AddOutputCache();
+
+builder.Services.AddHttpClient(client =>
+ {
+ // This URL uses "https+http://" to indicate HTTPS is preferred over HTTP.
+ // Learn more about service discovery scheme resolution at https://aka.ms/dotnet/sdschemes.
+ client.BaseAddress = new("https+http://apiservice");
+ });
+
+var app = builder.Build();
+
+if (!app.Environment.IsDevelopment())
+{
+ app.UseExceptionHandler("/Error", createScopeForErrors: true);
+ // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
+ app.UseHsts();
+}
+
+app.UseHttpsRedirection();
+
+app.UseStaticFiles();
+app.UseAntiforgery();
+
+app.UseOutputCache();
+
+app.MapRazorComponents()
+ .AddInteractiveServerRenderMode();
+
+app.MapDefaultEndpoints();
+
+app.Run();
diff --git a/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Properties/launchSettings.json b/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Properties/launchSettings.json
new file mode 100644
index 0000000000..f0fb11469c
--- /dev/null
+++ b/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/Properties/launchSettings.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:5154",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:7020;http://localhost:5154",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/appsettings.Development.json b/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/appsettings.Development.json
new file mode 100644
index 0000000000..527a02bb3c
--- /dev/null
+++ b/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Trace",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/appsettings.json b/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/appsettings.json
new file mode 100644
index 0000000000..f41d5acb6b
--- /dev/null
+++ b/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Trace",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/wwwroot/app.css b/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/wwwroot/app.css
new file mode 100644
index 0000000000..2a2a313024
--- /dev/null
+++ b/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/wwwroot/app.css
@@ -0,0 +1,56 @@
+html, body {
+ font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
+}
+
+a, .btn-link {
+ color: #006bb7;
+}
+
+.btn-primary {
+ color: #fff;
+ background-color: #1b6ec2;
+ border-color: #1861ac;
+}
+
+.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
+ box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
+}
+
+.content {
+ padding-top: 1.1rem;
+}
+
+h1:focus {
+ outline: none;
+}
+
+.valid.modified:not([type=checkbox]) {
+ outline: 1px solid #26b050;
+}
+
+.invalid {
+ outline: 1px solid #e51540;
+}
+
+.validation-message {
+ color: #e51540;
+}
+
+.blazor-error-boundary {
+ background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
+ padding: 1rem 1rem 1rem 3.7rem;
+ color: white;
+}
+
+ .blazor-error-boundary::after {
+ content: "An error has occurred."
+ }
+
+.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
+ color: var(--bs-secondary-color);
+ text-align: end;
+}
+
+.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
+ text-align: start;
+}
diff --git a/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/wwwroot/favicon.png b/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/wwwroot/favicon.png
new file mode 100644
index 0000000000..8422b59695
Binary files /dev/null and b/dotnet/samples/HelloHttpApi/HelloHttpApi.Web/wwwroot/favicon.png differ
diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorId.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorId.cs
index 8a4c433fc8..fd70faaf83 100644
--- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorId.cs
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorId.cs
@@ -2,6 +2,8 @@
using System;
using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
+using System.Text.Json.Serialization;
namespace Microsoft.Extensions.AI.Agents.Runtime;
@@ -9,6 +11,7 @@ namespace Microsoft.Extensions.AI.Agents.Runtime;
/// Provides a unique identifier for an actor instance within an agent runtime,
/// serving as the "address" of the actor instance for receiving messages.
///
+[JsonConverter(typeof(Converter))]
public readonly struct ActorId : IEquatable
{
///
@@ -113,4 +116,28 @@ public readonly struct ActorId : IEquatable
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 ActorId.Parse(actorIdString);
+ }
+
+ ///
+ public override void Write(Utf8JsonWriter writer, ActorId value, JsonSerializerOptions options)
+ {
+ writer.WriteStringValue(value.ToString());
+ }
+ }
}
diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorJsonContext.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorJsonContext.cs
new file mode 100644
index 0000000000..df87360eb4
--- /dev/null
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorJsonContext.cs
@@ -0,0 +1,41 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Extensions.AI.Agents.Runtime;
+
+///
+/// Source-generated JSON type information for use by all Actor abstractions.
+///
+[JsonSourceGenerationOptions(
+ JsonSerializerDefaults.Web,
+ UseStringEnumConverter = true,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ WriteIndented = false)]
+[JsonSerializable(typeof(ActorMessage))]
+[JsonSerializable(typeof(ActorRequestMessage))]
+[JsonSerializable(typeof(ActorResponseMessage))]
+[JsonSerializable(typeof(ActorWriteOperation))]
+[JsonSerializable(typeof(SetValueOperation))]
+[JsonSerializable(typeof(RemoveKeyOperation))]
+[JsonSerializable(typeof(SendRequestOperation))]
+[JsonSerializable(typeof(UpdateRequestOperation))]
+[JsonSerializable(typeof(ActorReadOperation))]
+[JsonSerializable(typeof(ListKeysOperation))]
+[JsonSerializable(typeof(GetValueOperation))]
+[JsonSerializable(typeof(ActorReadResult))]
+[JsonSerializable(typeof(ListKeysResult))]
+[JsonSerializable(typeof(GetValueResult))]
+[JsonSerializable(typeof(ActorRequest))]
+[JsonSerializable(typeof(ActorRequestUpdate))]
+[JsonSerializable(typeof(ActorResponse))]
+[JsonSerializable(typeof(ActorId))]
+[JsonSerializable(typeof(RequestStatus))]
+[JsonSerializable(typeof(ActorWriteOperationBatch))]
+[JsonSerializable(typeof(ActorReadOperationBatch))]
+[JsonSerializable(typeof(ReadResponse))]
+[JsonSerializable(typeof(WriteResponse))]
+[JsonSerializable(typeof(ActorType))]
+[JsonSerializable(typeof(JsonElement))]
+internal sealed partial class ActorJsonContext : JsonSerializerContext;
diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorMessage.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorMessage.cs
new file mode 100644
index 0000000000..c81a9ff244
--- /dev/null
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorMessage.cs
@@ -0,0 +1,39 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Extensions.AI.Agents.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.Extensions.AI.Agents.Runtime.Abstractions/ActorMessageType.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorMessageType.cs
new file mode 100644
index 0000000000..46b3c689a5
--- /dev/null
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorMessageType.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Extensions.AI.Agents.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.Extensions.AI.Agents.Runtime.Abstractions/ActorMessageWriteOperation.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorMessageWriteOperation.cs
new file mode 100644
index 0000000000..5e365927eb
--- /dev/null
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorMessageWriteOperation.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Microsoft.Extensions.AI.Agents.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.Extensions.AI.Agents.Runtime.Abstractions/ActorReadOperation.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorReadOperation.cs
new file mode 100644
index 0000000000..1375fe639c
--- /dev/null
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorReadOperation.cs
@@ -0,0 +1,31 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
+
+namespace Microsoft.Extensions.AI.Agents.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.Extensions.AI.Agents.Runtime.Abstractions/ActorReadOperationBatch.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorReadOperationBatch.cs
new file mode 100644
index 0000000000..6a44e35095
--- /dev/null
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorReadOperationBatch.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Extensions.AI.Agents.Runtime;
+
+///
+/// Represents a batch of read operations to be performed on an actor.
+///
+/// The collection of read operations to perform.
+public 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.Extensions.AI.Agents.Runtime.Abstractions/ActorReadOperationType.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorReadOperationType.cs
new file mode 100644
index 0000000000..17e2a45ebe
--- /dev/null
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorReadOperationType.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Extensions.AI.Agents.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.Extensions.AI.Agents.Runtime.Abstractions/ActorReadResult.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorReadResult.cs
new file mode 100644
index 0000000000..e83ce179f4
--- /dev/null
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorReadResult.cs
@@ -0,0 +1,31 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
+
+namespace Microsoft.Extensions.AI.Agents.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.Extensions.AI.Agents.Runtime.Abstractions/ActorReadResultType.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorReadResultType.cs
new file mode 100644
index 0000000000..1fb79a7dba
--- /dev/null
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorReadResultType.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Extensions.AI.Agents.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.Extensions.AI.Agents.Runtime.Abstractions/ActorRequest.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorRequest.cs
new file mode 100644
index 0000000000..fa019e1cdd
--- /dev/null
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorRequest.cs
@@ -0,0 +1,36 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Extensions.AI.Agents.Runtime;
+
+///
+/// Represents a request to be sent to an actor.
+///
+public 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.Extensions.AI.Agents.Runtime.Abstractions/ActorRequestMessage.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorRequestMessage.cs
new file mode 100644
index 0000000000..fb05194c8c
--- /dev/null
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorRequestMessage.cs
@@ -0,0 +1,39 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Extensions.AI.Agents.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.Extensions.AI.Agents.Runtime.Abstractions/ActorRequestUpdate.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorRequestUpdate.cs
new file mode 100644
index 0000000000..77e8c301c6
--- /dev/null
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorRequestUpdate.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Extensions.AI.Agents.Runtime;
+
+// External (client) interface.
+
+///
+/// Represents an update to an actor request's status and data.
+///
+public 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.Extensions.AI.Agents.Runtime.Abstractions/ActorResponse.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorResponse.cs
new file mode 100644
index 0000000000..7a97807faa
--- /dev/null
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorResponse.cs
@@ -0,0 +1,36 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Extensions.AI.Agents.Runtime;
+
+///
+/// Represents a response handle for an actor request, providing access to the result and status updates.
+///
+public 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; }
+}
diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorResponseHandle.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorResponseHandle.cs
new file mode 100644
index 0000000000..2508bd28eb
--- /dev/null
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorResponseHandle.cs
@@ -0,0 +1,46 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.Extensions.AI.Agents.Runtime;
+
+///
+/// Represents a handle to an actor response, allowing retrieval of the response data and status updates.
+///
+public abstract class ActorResponseHandle
+{
+ ///
+ /// 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.
+ ///
+ /// A token to cancel the wait operation.
+ /// 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.
+ ///
+ /// A token to cancel the watch operation.
+ /// An asynchronous enumerable of request updates.
+ public abstract IAsyncEnumerable WatchUpdatesAsync(CancellationToken cancellationToken);
+}
diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorResponseMessage.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorResponseMessage.cs
new file mode 100644
index 0000000000..68510d38e5
--- /dev/null
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorResponseMessage.cs
@@ -0,0 +1,39 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Extensions.AI.Agents.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.Extensions.AI.Agents.Runtime.Abstractions/ActorStateReadOperation.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorStateReadOperation.cs
new file mode 100644
index 0000000000..43e830631a
--- /dev/null
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorStateReadOperation.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Microsoft.Extensions.AI.Agents.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.Extensions.AI.Agents.Runtime.Abstractions/ActorStateWriteOperation.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorStateWriteOperation.cs
new file mode 100644
index 0000000000..f0ec2dcbd8
--- /dev/null
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorStateWriteOperation.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Microsoft.Extensions.AI.Agents.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.Extensions.AI.Agents.Runtime.Abstractions/ActorType.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorType.cs
index 6f101b46f4..619e018083 100644
--- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorType.cs
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorType.cs
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
namespace Microsoft.Extensions.AI.Agents.Runtime;
@@ -8,6 +10,7 @@ namespace Microsoft.Extensions.AI.Agents.Runtime;
///
/// Represents the type of an actor.
///
+[JsonConverter(typeof(Converter))]
public readonly partial struct ActorType : IEquatable
{
///
@@ -60,9 +63,33 @@ public readonly partial struct ActorType : IEquatable
type is not null && TypeRegex().IsMatch(type);
#if NET
- [GeneratedRegex("^[a-zA-Z_][a-zA-Z_0-9]*$")]
+ [GeneratedRegex("^[a-zA-Z_][a-zA-Z_:0-9]*$")]
private static partial Regex TypeRegex();
#else
- private static Regex TypeRegex() => new("^[a-zA-Z_][a-zA-Z_0-9]*$", RegexOptions.Compiled);
+ private static Regex TypeRegex() => new("^[a-zA-Z_][a-zA-Z_:0-9:]*$", RegexOptions.Compiled);
#endif
+
+ ///
+ /// 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.Extensions.AI.Agents.Runtime.Abstractions/ActorWriteOperation.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorWriteOperation.cs
new file mode 100644
index 0000000000..101b3e3c30
--- /dev/null
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorWriteOperation.cs
@@ -0,0 +1,32 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Extensions.AI.Agents.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.Extensions.AI.Agents.Runtime.Abstractions/ActorWriteOperationBatch.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorWriteOperationBatch.cs
new file mode 100644
index 0000000000..7f56e958ad
--- /dev/null
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorWriteOperationBatch.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Extensions.AI.Agents.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 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.Extensions.AI.Agents.Runtime.Abstractions/ActorWriteOperationType.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorWriteOperationType.cs
new file mode 100644
index 0000000000..05e5240500
--- /dev/null
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ActorWriteOperationType.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Extensions.AI.Agents.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.Extensions.AI.Agents.Runtime.Abstractions/GetValueOperation.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/GetValueOperation.cs
new file mode 100644
index 0000000000..66b4f09e64
--- /dev/null
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/GetValueOperation.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Extensions.AI.Agents.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 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.Extensions.AI.Agents.Runtime.Abstractions/GetValueResult.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/GetValueResult.cs
new file mode 100644
index 0000000000..024a45f441
--- /dev/null
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/GetValueResult.cs
@@ -0,0 +1,24 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Extensions.AI.Agents.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 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.Extensions.AI.Agents.Runtime.Abstractions/IActor.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IActor.cs
new file mode 100644
index 0000000000..9ba0babf10
--- /dev/null
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IActor.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.Extensions.AI.Agents.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.
+ ///
+ /// A token to cancel the start operation.
+ /// A task representing the start operation.
+ ValueTask RunAsync(CancellationToken cancellationToken);
+}
diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IActorClient.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IActorClient.cs
new file mode 100644
index 0000000000..aa51782dc0
--- /dev/null
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IActorClient.cs
@@ -0,0 +1,30 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.Extensions.AI.Agents.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.
+ /// A token to cancel the operation.
+ /// 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.
+ /// A token to cancel the operation.
+ /// A task representing the actor response handle.
+ ValueTask GetResponseAsync(ActorId actorId, string messageId, CancellationToken cancellationToken);
+}
diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IActorRuntimeBuilder.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IActorRuntimeBuilder.cs
new file mode 100644
index 0000000000..cc1a310854
--- /dev/null
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IActorRuntimeBuilder.cs
@@ -0,0 +1,18 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+
+namespace Microsoft.Extensions.AI.Agents.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.Extensions.AI.Agents.Runtime.Abstractions/IActorRuntimeContext.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IActorRuntimeContext.cs
new file mode 100644
index 0000000000..43b87cdcba
--- /dev/null
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IActorRuntimeContext.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.Extensions.AI.Agents.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.
+ ///
+ /// A token to cancel the watch operation.
+ /// 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.
+ /// A token to cancel the operation.
+ /// 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.
+ /// A token to cancel the operation.
+ /// 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.Extensions.AI.Agents.Runtime.Abstractions/IActorStateStorage.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IActorStateStorage.cs
new file mode 100644
index 0000000000..1cd51cf074
--- /dev/null
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IActorStateStorage.cs
@@ -0,0 +1,32 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.Extensions.AI.Agents.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.
+ /// A token to cancel the operation.
+ /// 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.
+ /// A token to cancel the operation.
+ /// 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.Extensions.AI.Agents.Runtime.Abstractions/InMemoryActorStateStorage.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/InMemoryActorStateStorage.cs
new file mode 100644
index 0000000000..2063420ab3
--- /dev/null
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/InMemoryActorStateStorage.cs
@@ -0,0 +1,384 @@
+// 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.Extensions.AI.Agents.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.Extensions.AI.Agents.Runtime.Abstractions.InMemoryActorStateStorage");
+
+ private readonly ConcurrentDictionary _actorStates = new();
+ private readonly object _lockObject = new();
+ private long _globalETagCounter = 0;
+
+ ///
+ /// Represents the internal state of an actor including its key-value pairs and ETag.
+ ///
+ private sealed class ActorState
+ {
+ public ConcurrentDictionary Data { get; } = new();
+ 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);
+ activity?.SetTag("error.type", "etag_mismatch");
+ activity?.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);
+ activity?.SetTag("state.new_etag", newETag);
+ activity?.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);
+ activity?.SetTag("state.operations", string.Join(",", operationTypes));
+ activity?.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)
+ {
+ return 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)
+ {
+ return 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)
+ {
+ if (activity == null)
+ {
+ return;
+ }
+
+ activity.SetTag("actor.id", actorId.ToString());
+ activity.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 == 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 == 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)
+ {
+ if (activity == null)
+ {
+ return;
+ }
+
+ activity.SetTag("error.type", exception.GetType().Name);
+ activity.SetTag("error.message", exception.Message);
+ activity.SetStatus(ActivityStatusCode.Error, exception.Message);
+
+ // Add exception event
+ activity.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.Extensions.AI.Agents.Runtime.Abstractions/JsonSerializerExtensions.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/JsonSerializerExtensions.cs
new file mode 100644
index 0000000000..5f01bcc57d
--- /dev/null
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/JsonSerializerExtensions.cs
@@ -0,0 +1,33 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
+
+namespace Microsoft.Extensions.AI.Agents.Runtime;
+
+///
+/// Provides extension methods for JSON serialization with source generation support.
+///
+internal static class JsonSerializerExtensions
+{
+ ///
+ /// Gets the JsonTypeInfo for a type, preferring the one from options if available,
+ /// otherwise falling back to the source-generated context.
+ ///
+ /// The type to get JsonTypeInfo for.
+ /// The JsonSerializerOptions to check first.
+ /// The fallback JsonSerializerContext to use if not found in options.
+ /// The JsonTypeInfo for the requested type.
+ public static JsonTypeInfo GetTypeInfo(this JsonSerializerOptions options, JsonSerializerContext fallbackContext)
+ {
+ // Try to get from the options first (if a context is configured)
+ if (options.TypeInfoResolver?.GetTypeInfo(typeof(T), options) is JsonTypeInfo typeInfo)
+ {
+ return typeInfo;
+ }
+
+ // Fall back to the provided source-generated context
+ return (JsonTypeInfo)fallbackContext.GetTypeInfo(typeof(T))!;
+ }
+}
diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ListKeysOperation.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ListKeysOperation.cs
new file mode 100644
index 0000000000..981a66501d
--- /dev/null
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ListKeysOperation.cs
@@ -0,0 +1,30 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Extensions.AI.Agents.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 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.Extensions.AI.Agents.Runtime.Abstractions/ListKeysResult.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ListKeysResult.cs
new file mode 100644
index 0000000000..a177aeec59
--- /dev/null
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ListKeysResult.cs
@@ -0,0 +1,31 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Extensions.AI.Agents.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 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.Extensions.AI.Agents.Runtime.Abstractions/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.csproj b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.csproj
index 6941c59277..da893dbb4c 100644
--- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.csproj
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.csproj
@@ -5,12 +5,14 @@
$(ProjectsDebugTargetFrameworks)
$(NoWarn);IDE1006;IDE0130
alpha
+ Microsoft.Extensions.AI.Agents.Runtime
true
true
true
+ true
@@ -23,6 +25,10 @@