diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 5866f1f895..2c57bd5071 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -12,6 +12,7 @@ When contributing to this repository, please follow these guidelines:
Here are some general guidelines that apply to all code.
+- All new files must be saved with UTF-8 encoding with BOM (Byte Order Mark). This is required for `dotnet format` to work correctly.
- The top of all *.cs files should have a copyright notice: `// Copyright (c) Microsoft. All rights reserved.`
- All public methods and classes should have XML documentation comments.
- After adding, modifying or deleting code, run `dotnet build`, and then fix any reported build errors.
diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 630afbd6a5..26a4eba95b 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -137,6 +137,7 @@
+
@@ -411,6 +412,7 @@
+
@@ -432,6 +434,7 @@
+
@@ -454,6 +457,7 @@
+
diff --git a/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/AgentWithMemory_Step04_MemoryUsingFoundry.csproj b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/AgentWithMemory_Step04_MemoryUsingFoundry.csproj
new file mode 100644
index 0000000000..0b6c06a5a8
--- /dev/null
+++ b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/AgentWithMemory_Step04_MemoryUsingFoundry.csproj
@@ -0,0 +1,21 @@
+
+
+
+ Exe
+ net10.0
+
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/Program.cs b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/Program.cs
new file mode 100644
index 0000000000..8125ffcb0b
--- /dev/null
+++ b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/Program.cs
@@ -0,0 +1,123 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample shows how to use the FoundryMemoryProvider to persist and recall memories for an agent.
+// The sample stores conversation messages in an Azure AI Foundry memory store and retrieves relevant
+// memories for subsequent invocations, even across new sessions.
+//
+// Note: Memory extraction in Azure AI Foundry is asynchronous and takes time. This sample demonstrates
+// a simple polling approach to wait for memory updates to complete before querying.
+
+using System.ClientModel.Primitives;
+using System.Text.Json;
+using Azure.AI.Projects;
+using Azure.Identity;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.FoundryMemory;
+
+string foundryEndpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+string memoryStoreName = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_MEMORY_STORE_NAME") ?? "sample-memory-store-name";
+string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_MODEL") ?? "gpt-4o-mini";
+string embeddingModelName = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_EMBEDDING_MODEL") ?? "text-embedding-ada-002";
+
+// Create an AIProjectClient for Foundry with Azure Identity authentication.
+AzureCliCredential credential = new();
+
+// Add a debug handler to log all HTTP requests
+DebugHttpClientHandler debugHandler = new() { CheckCertificateRevocationList = true };
+HttpClient httpClient = new(debugHandler);
+AIProjectClientOptions clientOptions = new()
+{
+ Transport = new HttpClientPipelineTransport(httpClient)
+};
+AIProjectClient projectClient = new(new Uri(foundryEndpoint), credential, clientOptions);
+
+// Get the ChatClient from the AIProjectClient's OpenAI property using the deployment name.
+AIAgent agent = await projectClient.CreateAIAgentAsync(deploymentName,
+ options: new ChatClientAgentOptions()
+ {
+ Name = "TravelAssistantWithFoundryMemory",
+ ChatOptions = new() { Instructions = "You are a friendly travel assistant. Use known memories about the user when responding, and do not invent details." },
+ AIContextProviderFactory = (ctx, ct) => new ValueTask(ctx.SerializedState.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined
+ // If each session should have its own scope, you can create a new id per session here:
+ // ? new FoundryMemoryProvider(projectClient, new FoundryMemoryProviderScope() { Scope = Guid.NewGuid().ToString() }, new FoundryMemoryProviderOptions() { MemoryStoreName = memoryStoreName })
+ // In this case we are storing memories scoped by user so that memories are retained across sessions.
+ ? new FoundryMemoryProvider(projectClient, new FoundryMemoryProviderScope() { Scope = "sample-user-123" }, new FoundryMemoryProviderOptions() { MemoryStoreName = memoryStoreName })
+ // For cases where we are restoring from serialized state:
+ : new FoundryMemoryProvider(projectClient, ctx.SerializedState, ctx.JsonSerializerOptions, new FoundryMemoryProviderOptions() { MemoryStoreName = memoryStoreName }))
+ });
+
+AgentSession session = await agent.CreateSessionAsync();
+
+FoundryMemoryProvider memoryProvider = session.GetService()!;
+
+Console.WriteLine("\n>> Setting up Foundry Memory Store\n");
+
+// Ensure the memory store exists (creates it with the specified models if needed).
+await memoryProvider.EnsureMemoryStoreCreatedAsync(deploymentName, embeddingModelName, "Sample memory store for travel assistant");
+
+// Clear any existing memories for this scope to demonstrate fresh behavior.
+await memoryProvider.EnsureStoredMemoriesDeletedAsync();
+
+Console.WriteLine(await agent.RunAsync("Hi there! My name is Taylor and I'm planning a hiking trip to Patagonia in November.", session));
+Console.WriteLine(await agent.RunAsync("I'm travelling with my sister and we love finding scenic viewpoints.", session));
+
+// Memory extraction in Azure AI Foundry is asynchronous and takes time to process.
+// WhenUpdatesCompletedAsync polls all pending updates and waits for them to complete.
+Console.WriteLine("\nWaiting for Foundry Memory to process updates...");
+await memoryProvider.WhenUpdatesCompletedAsync();
+
+Console.WriteLine("Updates completed.\n");
+
+Console.WriteLine(await agent.RunAsync("What do you already know about my upcoming trip?", session));
+
+Console.WriteLine("\n>> Serialize and deserialize the session to demonstrate persisted state\n");
+JsonElement serializedSession = session.Serialize();
+AgentSession restoredSession = await agent.DeserializeSessionAsync(serializedSession);
+Console.WriteLine(await agent.RunAsync("Can you recap the personal details you remember?", restoredSession));
+
+Console.WriteLine("\n>> Start a new session that shares the same Foundry Memory scope\n");
+
+Console.WriteLine("\nWaiting for Foundry Memory to process updates...");
+await memoryProvider.WhenUpdatesCompletedAsync();
+
+AgentSession newSession = await agent.CreateSessionAsync();
+Console.WriteLine(await agent.RunAsync("Summarize what you already know about me.", newSession));
+
+// Debug HTTP handler to log all requests (commented out by default)
+internal sealed class DebugHttpClientHandler : HttpClientHandler
+{
+ protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ if (!request.RequestUri?.PathAndQuery.Contains("sample-memory-store-name") ?? false)
+ {
+ return await base.SendAsync(request, cancellationToken);
+ }
+
+ Console.WriteLine("\n=== HTTP REQUEST ===");
+ Console.WriteLine($"Method: {request.Method}");
+ Console.WriteLine($"URI: {request.RequestUri}");
+ Console.WriteLine("Headers:");
+ foreach (var header in request.Headers)
+ {
+ Console.WriteLine($" {header.Key}: {string.Join(", ", header.Value)}");
+ }
+
+ if (request.Content != null)
+ {
+ string body = await request.Content.ReadAsStringAsync(cancellationToken);
+ Console.WriteLine("Body: " + body);
+ }
+
+ HttpResponseMessage response = await base.SendAsync(request, cancellationToken);
+
+ Console.WriteLine("====================\n");
+
+ Console.WriteLine("\n=== HTTP RESPONSE ===");
+ Console.WriteLine($"Status: {(int)response.StatusCode} {response.StatusCode}");
+ string responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
+ Console.WriteLine("Body: " + responseBody);
+ Console.WriteLine("=====================\n");
+
+ return response;
+ }
+}
diff --git a/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/README.md b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/README.md
new file mode 100644
index 0000000000..dfea386d82
--- /dev/null
+++ b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/README.md
@@ -0,0 +1,57 @@
+# Agent with Memory Using Azure AI Foundry
+
+This sample demonstrates how to create and run an agent that uses Azure AI Foundry's managed memory service to extract and retrieve individual memories across sessions.
+
+## Features Demonstrated
+
+- Creating a `FoundryMemoryProvider` with Azure Identity authentication
+- Automatic memory store creation if it doesn't exist
+- Multi-turn conversations with automatic memory extraction
+- Memory retrieval to inform agent responses
+- Session serialization and deserialization
+- Memory persistence across completely new sessions
+
+## Prerequisites
+
+1. Azure subscription with Azure AI Foundry project
+2. Azure OpenAI resource with a chat model deployment (e.g., gpt-4o-mini) and an embedding model deployment (e.g., text-embedding-ada-002)
+3. .NET 10.0 SDK
+4. Azure CLI logged in (`az login`)
+
+## Environment Variables
+
+```bash
+# Azure AI Foundry project endpoint and memory store name
+export FOUNDRY_PROJECT_ENDPOINT="https://your-account.services.ai.azure.com/api/projects/your-project"
+export FOUNDRY_PROJECT_MEMORY_STORE_NAME="my_memory_store"
+
+# Model deployment names (models deployed in your Foundry project)
+export FOUNDRY_PROJECT_MODEL="gpt-4o-mini"
+export FOUNDRY_PROJECT_EMBEDDING_MODEL="text-embedding-ada-002"
+```
+
+## Run the Sample
+
+```bash
+dotnet run
+```
+
+## Expected Output
+
+The agent will:
+1. Create the memory store if it doesn't exist (using the specified chat and embedding models)
+2. Learn your name (Taylor), travel destination (Patagonia), timing (November), companions (sister), and interests (scenic viewpoints)
+3. Wait for Foundry Memory to index the memories
+4. Recall those details when asked about the trip
+5. Demonstrate memory persistence across session serialization/deserialization
+6. Show that a brand new session can still access the same memories
+
+## Key Differences from Mem0
+
+| Aspect | Mem0 | Azure AI Foundry Memory |
+|--------|------|------------------------|
+| Authentication | API Key | Azure Identity (DefaultAzureCredential) |
+| Scope | ApplicationId, UserId, AgentId, ThreadId | Single `Scope` string |
+| Memory Types | Single memory store | User Profile + Chat Summary |
+| Hosting | Mem0 cloud or self-hosted | Azure AI Foundry managed service |
+| Store Creation | N/A (automatic) | Explicit via `EnsureMemoryStoreCreatedAsync` |
diff --git a/dotnet/samples/GettingStarted/AgentWithMemory/README.md b/dotnet/samples/GettingStarted/AgentWithMemory/README.md
index 903fcf1b78..6e36ba0511 100644
--- a/dotnet/samples/GettingStarted/AgentWithMemory/README.md
+++ b/dotnet/samples/GettingStarted/AgentWithMemory/README.md
@@ -7,3 +7,4 @@ These samples show how to create an agent with the Agent Framework that uses Mem
|[Chat History memory](./AgentWithMemory_Step01_ChatHistoryMemory/)|This sample demonstrates how to enable an agent to remember messages from previous conversations.|
|[Memory with MemoryStore](./AgentWithMemory_Step02_MemoryUsingMem0/)|This sample demonstrates how to create and run an agent that uses the Mem0 service to extract and retrieve individual memories.|
|[Custom Memory Implementation](./AgentWithMemory_Step03_CustomMemory/)|This sample demonstrates how to create a custom memory component and attach it to an agent.|
+|[Memory with Azure AI Foundry](./AgentWithMemory_Step04_MemoryUsingFoundry/)|This sample demonstrates how to create and run an agent that uses Azure AI Foundry's managed memory service to extract and retrieve individual memories.|
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/AIProjectClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/AIProjectClientExtensions.cs
new file mode 100644
index 0000000000..6107a01cc7
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/AIProjectClientExtensions.cs
@@ -0,0 +1,157 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.ClientModel;
+using System.ClientModel.Primitives;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Azure.AI.Projects;
+using Microsoft.Agents.AI.FoundryMemory.Core.Models;
+
+namespace Microsoft.Agents.AI.FoundryMemory;
+
+///
+/// Internal extension methods for to provide MemoryStores operations
+/// using the SDK's HTTP pipeline until the SDK releases convenience methods.
+///
+internal static class AIProjectClientExtensions
+{
+ ///
+ /// Creates a memory store if it doesn't already exist.
+ ///
+ internal static async Task CreateMemoryStoreIfNotExistsAsync(
+ this AIProjectClient client,
+ string memoryStoreName,
+ string? description,
+ string chatModel,
+ string embeddingModel,
+ CancellationToken cancellationToken)
+ {
+ // First try to get the store to see if it exists
+ try
+ {
+ RequestOptions requestOptions = new() { CancellationToken = cancellationToken };
+ await client.MemoryStores.GetMemoryStoreAsync(memoryStoreName, requestOptions).ConfigureAwait(false);
+ return false; // Store already exists
+ }
+ catch (ClientResultException ex) when (ex.Status == 404)
+ {
+ // Store doesn't exist, create it
+ }
+
+ CreateMemoryStoreRequest request = new()
+ {
+ Name = memoryStoreName,
+ Description = description,
+ Definition = new MemoryStoreDefinitionRequest
+ {
+ Kind = "default",
+ ChatModel = chatModel,
+ EmbeddingModel = embeddingModel
+ }
+ };
+
+ string json = JsonSerializer.Serialize(request, FoundryMemoryJsonContext.Default.CreateMemoryStoreRequest);
+ BinaryContent content = BinaryContent.Create(BinaryData.FromString(json));
+
+ RequestOptions createOptions = new() { CancellationToken = cancellationToken };
+ await client.MemoryStores.CreateMemoryStoreAsync(content, createOptions).ConfigureAwait(false);
+ return true;
+ }
+
+ ///
+ /// Searches for relevant memories from a memory store based on conversation context.
+ ///
+ internal static async Task SearchMemoriesAsync(
+ this AIProjectClient client,
+ string memoryStoreName,
+ string scope,
+ IEnumerable messages,
+ int maxMemories,
+ CancellationToken cancellationToken)
+ {
+ SearchMemoriesRequest request = new()
+ {
+ Scope = scope,
+ Items = messages.ToArray(),
+ Options = new SearchMemoriesOptions { MaxMemories = maxMemories }
+ };
+
+ string json = JsonSerializer.Serialize(request, FoundryMemoryJsonContext.Default.SearchMemoriesRequest);
+ BinaryContent content = BinaryContent.Create(BinaryData.FromString(json));
+
+ RequestOptions requestOptions = new() { CancellationToken = cancellationToken };
+ ClientResult result = await client.MemoryStores.SearchMemoriesAsync(memoryStoreName, content, requestOptions).ConfigureAwait(false);
+
+ return JsonSerializer.Deserialize(
+ result.GetRawResponse().Content.ToString(),
+ FoundryMemoryJsonContext.Default.SearchMemoriesResponse);
+ }
+
+ ///
+ /// Updates memory store with conversation memories.
+ ///
+ internal static async Task UpdateMemoriesAsync(
+ this AIProjectClient client,
+ string memoryStoreName,
+ string scope,
+ IEnumerable messages,
+ int updateDelay,
+ CancellationToken cancellationToken)
+ {
+ UpdateMemoriesRequest request = new()
+ {
+ Scope = scope,
+ Items = messages.ToArray(),
+ UpdateDelay = updateDelay
+ };
+
+ string json = JsonSerializer.Serialize(request, FoundryMemoryJsonContext.Default.UpdateMemoriesRequest);
+ BinaryContent content = BinaryContent.Create(BinaryData.FromString(json));
+
+ RequestOptions requestOptions = new() { CancellationToken = cancellationToken };
+ ClientResult result = await client.MemoryStores.UpdateMemoriesAsync(memoryStoreName, content, requestOptions).ConfigureAwait(false);
+
+ return JsonSerializer.Deserialize(
+ result.GetRawResponse().Content.ToString(),
+ FoundryMemoryJsonContext.Default.UpdateMemoriesResponse);
+ }
+
+ ///
+ /// Gets the status of a memory update operation.
+ ///
+ internal static async Task GetUpdateStatusAsync(
+ this AIProjectClient client,
+ string memoryStoreName,
+ string updateId,
+ CancellationToken cancellationToken)
+ {
+ RequestOptions requestOptions = new() { CancellationToken = cancellationToken };
+ ClientResult result = await client.MemoryStores.GetUpdateResultAsync(memoryStoreName, updateId, requestOptions).ConfigureAwait(false);
+
+ return JsonSerializer.Deserialize(
+ result.GetRawResponse().Content.ToString(),
+ FoundryMemoryJsonContext.Default.UpdateMemoriesResponse);
+ }
+
+ ///
+ /// Deletes all memories associated with a specific scope from a memory store.
+ ///
+ internal static async Task DeleteScopeAsync(
+ this AIProjectClient client,
+ string memoryStoreName,
+ string scope,
+ CancellationToken cancellationToken)
+ {
+ DeleteScopeRequest request = new() { Scope = scope };
+
+ string json = JsonSerializer.Serialize(request, FoundryMemoryJsonContext.Default.DeleteScopeRequest);
+ BinaryContent content = BinaryContent.Create(BinaryData.FromString(json));
+
+ RequestOptions requestOptions = new() { CancellationToken = cancellationToken };
+ await client.MemoryStores.DeleteScopeAsync(memoryStoreName, content, requestOptions).ConfigureAwait(false);
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/CreateMemoryStoreRequest.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/CreateMemoryStoreRequest.cs
new file mode 100644
index 0000000000..76264e7a06
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/CreateMemoryStoreRequest.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.FoundryMemory.Core.Models;
+
+///
+/// Request body for creating a memory store.
+///
+internal sealed class CreateMemoryStoreRequest
+{
+ ///
+ /// Gets or sets the name of the memory store.
+ ///
+ [JsonPropertyName("name")]
+ public string? Name { get; set; }
+
+ ///
+ /// Gets or sets an optional description for the memory store.
+ ///
+ [JsonPropertyName("description")]
+ public string? Description { get; set; }
+
+ ///
+ /// Gets or sets the definition for the memory store.
+ ///
+ [JsonPropertyName("definition")]
+ public MemoryStoreDefinitionRequest? Definition { get; set; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/DeleteScopeRequest.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/DeleteScopeRequest.cs
new file mode 100644
index 0000000000..48e1363efe
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/DeleteScopeRequest.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.FoundryMemory.Core.Models;
+
+///
+/// Request body for the delete scope API.
+///
+internal sealed class DeleteScopeRequest
+{
+ ///
+ /// Gets or sets the scope to delete.
+ ///
+ [JsonPropertyName("scope")]
+ public string Scope { get; set; } = string.Empty;
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/MemoryInputMessage.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/MemoryInputMessage.cs
new file mode 100644
index 0000000000..e34a3d8f74
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/MemoryInputMessage.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.FoundryMemory.Core.Models;
+
+///
+/// Represents an input message for the memory API.
+///
+internal sealed class MemoryInputMessage
+{
+ ///
+ /// Gets or sets the role of the message (e.g., "user", "assistant", "system").
+ ///
+ [JsonPropertyName("role")]
+ public string Role { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the content of the message.
+ ///
+ [JsonPropertyName("content")]
+ public string Content { get; set; } = string.Empty;
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/MemoryItem.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/MemoryItem.cs
new file mode 100644
index 0000000000..04e2c413f6
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/MemoryItem.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.FoundryMemory.Core.Models;
+
+///
+/// Represents a memory item.
+///
+internal sealed class MemoryItem
+{
+ ///
+ /// Gets or sets the unique identifier of the memory.
+ ///
+ [JsonPropertyName("memory_id")]
+ public string? MemoryId { get; set; }
+
+ ///
+ /// Gets or sets the content of the memory.
+ ///
+ [JsonPropertyName("content")]
+ public string? Content { get; set; }
+
+ ///
+ /// Gets or sets the type of the memory.
+ ///
+ [JsonPropertyName("memory_type")]
+ public string? MemoryType { get; set; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/MemorySearchResult.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/MemorySearchResult.cs
new file mode 100644
index 0000000000..0e841060ba
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/MemorySearchResult.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.FoundryMemory.Core.Models;
+
+///
+/// Represents a memory search result.
+///
+internal sealed class MemorySearchResult
+{
+ ///
+ /// Gets or sets the memory item.
+ ///
+ [JsonPropertyName("memory_item")]
+ public MemoryItem? MemoryItem { get; set; }
+
+ ///
+ /// Gets or sets the relevance score.
+ ///
+ [JsonPropertyName("score")]
+ public double Score { get; set; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/MemoryStoreDefinition.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/MemoryStoreDefinition.cs
new file mode 100644
index 0000000000..8b4328b796
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/MemoryStoreDefinition.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.FoundryMemory.Core.Models;
+
+///
+/// Definition for a memory store specifying the models to use.
+///
+internal sealed class MemoryStoreDefinitionRequest
+{
+ ///
+ /// Gets or sets the kind of memory store definition.
+ ///
+ [JsonPropertyName("kind")]
+ public string Kind { get; set; } = "default";
+
+ ///
+ /// Gets or sets the deployment name of the chat model for memory processing.
+ ///
+ [JsonPropertyName("chat_model")]
+ public string? ChatModel { get; set; }
+
+ ///
+ /// Gets or sets the deployment name of the embedding model for memory search.
+ ///
+ [JsonPropertyName("embedding_model")]
+ public string? EmbeddingModel { get; set; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/MemoryStoreResponse.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/MemoryStoreResponse.cs
new file mode 100644
index 0000000000..6df9c31d2a
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/MemoryStoreResponse.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.FoundryMemory.Core.Models;
+
+///
+/// Response from creating or getting a memory store.
+///
+internal sealed class MemoryStoreResponse
+{
+ ///
+ /// Gets or sets the unique identifier of the memory store.
+ ///
+ [JsonPropertyName("id")]
+ public string? Id { get; set; }
+
+ ///
+ /// Gets or sets the name of the memory store.
+ ///
+ [JsonPropertyName("name")]
+ public string? Name { get; set; }
+
+ ///
+ /// Gets or sets the description of the memory store.
+ ///
+ [JsonPropertyName("description")]
+ public string? Description { get; set; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/SearchMemoriesOptions.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/SearchMemoriesOptions.cs
new file mode 100644
index 0000000000..cf12335083
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/SearchMemoriesOptions.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.FoundryMemory.Core.Models;
+
+///
+/// Options for searching memories.
+///
+internal sealed class SearchMemoriesOptions
+{
+ ///
+ /// Gets or sets the maximum number of memories to return.
+ ///
+ [JsonPropertyName("max_memories")]
+ public int MaxMemories { get; set; } = 5;
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/SearchMemoriesRequest.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/SearchMemoriesRequest.cs
new file mode 100644
index 0000000000..74ee62d9bc
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/SearchMemoriesRequest.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.FoundryMemory.Core.Models;
+
+///
+/// Request body for the search memories API.
+///
+internal sealed class SearchMemoriesRequest
+{
+ ///
+ /// Gets or sets the namespace that logically groups and isolates memories, such as a user ID.
+ ///
+ [JsonPropertyName("scope")]
+ public string Scope { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the conversation messages to use for the search query.
+ ///
+ [JsonPropertyName("items")]
+ public MemoryInputMessage[] Items { get; set; } = [];
+
+ ///
+ /// Gets or sets the search options.
+ ///
+ [JsonPropertyName("options")]
+ public SearchMemoriesOptions? Options { get; set; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/SearchMemoriesResponse.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/SearchMemoriesResponse.cs
new file mode 100644
index 0000000000..f3811c1461
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/SearchMemoriesResponse.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.FoundryMemory.Core.Models;
+
+///
+/// Response from the search memories API.
+///
+internal sealed class SearchMemoriesResponse
+{
+ ///
+ /// Gets or sets the unique identifier for the search operation.
+ ///
+ [JsonPropertyName("search_id")]
+ public string? SearchId { get; set; }
+
+ ///
+ /// Gets or sets the list of retrieved memories.
+ ///
+ [JsonPropertyName("memories")]
+ public MemorySearchResult[]? Memories { get; set; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/UpdateMemoriesError.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/UpdateMemoriesError.cs
new file mode 100644
index 0000000000..6d62469da4
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/UpdateMemoriesError.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.FoundryMemory.Core.Models;
+
+///
+/// Error information for a failed update operation.
+///
+internal sealed class UpdateMemoriesError
+{
+ ///
+ /// Gets or sets the error code.
+ ///
+ [JsonPropertyName("code")]
+ public string? Code { get; set; }
+
+ ///
+ /// Gets or sets the error message.
+ ///
+ [JsonPropertyName("message")]
+ public string? Message { get; set; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/UpdateMemoriesRequest.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/UpdateMemoriesRequest.cs
new file mode 100644
index 0000000000..a356059e19
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/UpdateMemoriesRequest.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.FoundryMemory.Core.Models;
+
+///
+/// Request body for the update memories API.
+///
+internal sealed class UpdateMemoriesRequest
+{
+ ///
+ /// Gets or sets the namespace that logically groups and isolates memories, such as a user ID.
+ ///
+ [JsonPropertyName("scope")]
+ public string Scope { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the conversation messages to extract memories from.
+ ///
+ [JsonPropertyName("items")]
+ public MemoryInputMessage[] Items { get; set; } = [];
+
+ ///
+ /// Gets or sets the delay in seconds before processing the update.
+ ///
+ [JsonPropertyName("update_delay")]
+ public int UpdateDelay { get; set; }
+
+ ///
+ /// Gets or sets the ID of a previous update operation to chain with.
+ ///
+ [JsonPropertyName("previous_update_id")]
+ public string? PreviousUpdateId { get; set; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/UpdateMemoriesResponse.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/UpdateMemoriesResponse.cs
new file mode 100644
index 0000000000..9f6cd9651b
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/UpdateMemoriesResponse.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.FoundryMemory.Core.Models;
+
+///
+/// Response from the update memories API.
+///
+internal sealed class UpdateMemoriesResponse
+{
+ /// Status indicating the update is waiting to be processed.
+ internal const string StatusQueued = "queued";
+
+ /// Status indicating the update is currently being processed.
+ internal const string StatusInProgress = "in_progress";
+
+ /// Status indicating the update completed successfully.
+ internal const string StatusCompleted = "completed";
+
+ /// Status indicating the update failed.
+ internal const string StatusFailed = "failed";
+
+ /// Status indicating the update was superseded by a newer update.
+ internal const string StatusSuperseded = "superseded";
+
+ ///
+ /// Gets or sets the unique identifier of the update operation.
+ ///
+ [JsonPropertyName("update_id")]
+ public string? UpdateId { get; set; }
+
+ ///
+ /// Gets or sets the status of the update operation.
+ /// Known values are: "queued", "in_progress", "completed", "failed", "superseded".
+ ///
+ [JsonPropertyName("status")]
+ public string? Status { get; set; }
+
+ ///
+ /// Gets or sets the update_id that superseded this operation when status is "superseded".
+ ///
+ [JsonPropertyName("superseded_by")]
+ public string? SupersededBy { get; set; }
+
+ ///
+ /// Gets or sets the error information when status is "failed".
+ ///
+ [JsonPropertyName("error")]
+ public UpdateMemoriesError? Error { get; set; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryJsonUtilities.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryJsonUtilities.cs
new file mode 100644
index 0000000000..f63d4a52e8
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryJsonUtilities.cs
@@ -0,0 +1,52 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Agents.AI.FoundryMemory.Core.Models;
+
+namespace Microsoft.Agents.AI.FoundryMemory;
+
+///
+/// Provides JSON serialization utilities for the Foundry Memory provider.
+///
+internal static class FoundryMemoryJsonUtilities
+{
+ ///
+ /// Gets the default JSON serializer options for Foundry Memory operations.
+ ///
+ public static JsonSerializerOptions DefaultOptions { get; } = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ WriteIndented = false,
+ TypeInfoResolver = FoundryMemoryJsonContext.Default
+ };
+}
+
+///
+/// Source-generated JSON serialization context for Foundry Memory types.
+///
+[JsonSourceGenerationOptions(
+ JsonSerializerDefaults.General,
+ UseStringEnumConverter = false,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
+ WriteIndented = false)]
+[JsonSerializable(typeof(FoundryMemoryProviderScope))]
+[JsonSerializable(typeof(FoundryMemoryProvider.FoundryMemoryState))]
+[JsonSerializable(typeof(SearchMemoriesRequest))]
+[JsonSerializable(typeof(SearchMemoriesResponse))]
+[JsonSerializable(typeof(SearchMemoriesOptions))]
+[JsonSerializable(typeof(UpdateMemoriesRequest))]
+[JsonSerializable(typeof(UpdateMemoriesResponse))]
+[JsonSerializable(typeof(UpdateMemoriesError))]
+[JsonSerializable(typeof(DeleteScopeRequest))]
+[JsonSerializable(typeof(CreateMemoryStoreRequest))]
+[JsonSerializable(typeof(MemoryStoreDefinitionRequest))]
+[JsonSerializable(typeof(MemoryStoreResponse))]
+[JsonSerializable(typeof(MemoryInputMessage))]
+[JsonSerializable(typeof(MemoryInputMessage[]))]
+[JsonSerializable(typeof(MemorySearchResult))]
+[JsonSerializable(typeof(MemorySearchResult[]))]
+[JsonSerializable(typeof(MemoryItem))]
+internal partial class FoundryMemoryJsonContext : JsonSerializerContext;
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProvider.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProvider.cs
new file mode 100644
index 0000000000..24c604ce8e
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProvider.cs
@@ -0,0 +1,429 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.ClientModel;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+using Azure.AI.Projects;
+using Microsoft.Agents.AI.FoundryMemory.Core.Models;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Logging;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Agents.AI.FoundryMemory;
+
+///
+/// Provides an Azure AI Foundry Memory backed that persists conversation messages as memories
+/// and retrieves related memories to augment the agent invocation context.
+///
+///
+/// The provider stores user, assistant and system messages as Foundry memories and retrieves relevant memories
+/// for new invocations using the memory search endpoint. Retrieved memories are injected as user messages
+/// to the model, prefixed by a configurable context prompt.
+///
+public sealed class FoundryMemoryProvider : AIContextProvider
+{
+ private const string DefaultContextPrompt = "## Memories\nConsider the following memories when answering user questions:";
+
+ private readonly string _contextPrompt;
+ private readonly string _memoryStoreName;
+ private readonly int _maxMemories;
+ private readonly int _updateDelay;
+ private readonly bool _enableSensitiveTelemetryData;
+
+ private readonly AIProjectClient _client;
+ private readonly ILogger? _logger;
+
+ private readonly FoundryMemoryProviderScope _scope;
+ private readonly ConcurrentQueue _pendingUpdateIds = new();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Azure AI Project client configured for your Foundry project.
+ /// The scope configuration for memory storage and retrieval.
+ /// Provider options including memory store name.
+ /// Optional logger factory.
+ public FoundryMemoryProvider(
+ AIProjectClient client,
+ FoundryMemoryProviderScope scope,
+ FoundryMemoryProviderOptions? options = null,
+ ILoggerFactory? loggerFactory = null)
+ {
+ Throw.IfNull(client);
+ Throw.IfNull(scope);
+
+ if (string.IsNullOrWhiteSpace(scope.Scope))
+ {
+ throw new ArgumentException("The Scope property must be provided.", nameof(scope));
+ }
+
+ FoundryMemoryProviderOptions effectiveOptions = options ?? new FoundryMemoryProviderOptions();
+
+ if (string.IsNullOrWhiteSpace(effectiveOptions.MemoryStoreName))
+ {
+ throw new ArgumentException("The MemoryStoreName option must be provided.", nameof(options));
+ }
+
+ this._logger = loggerFactory?.CreateLogger();
+ this._client = client;
+
+ this._contextPrompt = effectiveOptions.ContextPrompt ?? DefaultContextPrompt;
+ this._memoryStoreName = effectiveOptions.MemoryStoreName;
+ this._maxMemories = effectiveOptions.MaxMemories;
+ this._updateDelay = effectiveOptions.UpdateDelay;
+ this._enableSensitiveTelemetryData = effectiveOptions.EnableSensitiveTelemetryData;
+ this._scope = new FoundryMemoryProviderScope(scope);
+ }
+
+ ///
+ /// Initializes a new instance of the class, with existing state from a serialized JSON element.
+ ///
+ /// The Azure AI Project client configured for your Foundry project.
+ /// A representing the serialized state of the provider.
+ /// Optional settings for customizing the JSON deserialization process.
+ /// Provider options including memory store name.
+ /// Optional logger factory.
+ public FoundryMemoryProvider(
+ AIProjectClient client,
+ JsonElement serializedState,
+ JsonSerializerOptions? jsonSerializerOptions = null,
+ FoundryMemoryProviderOptions? options = null,
+ ILoggerFactory? loggerFactory = null)
+ {
+ Throw.IfNull(client);
+
+ FoundryMemoryProviderOptions effectiveOptions = options ?? new FoundryMemoryProviderOptions();
+
+ if (string.IsNullOrWhiteSpace(effectiveOptions.MemoryStoreName))
+ {
+ throw new ArgumentException("The MemoryStoreName option must be provided.", nameof(options));
+ }
+
+ this._logger = loggerFactory?.CreateLogger();
+ this._client = client;
+
+ this._contextPrompt = effectiveOptions.ContextPrompt ?? DefaultContextPrompt;
+ this._memoryStoreName = effectiveOptions.MemoryStoreName;
+ this._maxMemories = effectiveOptions.MaxMemories;
+ this._updateDelay = effectiveOptions.UpdateDelay;
+ this._enableSensitiveTelemetryData = effectiveOptions.EnableSensitiveTelemetryData;
+
+ JsonSerializerOptions jso = jsonSerializerOptions ?? FoundryMemoryJsonUtilities.DefaultOptions;
+ FoundryMemoryState? state = serializedState.Deserialize(jso.GetTypeInfo(typeof(FoundryMemoryState))) as FoundryMemoryState;
+
+ if (state?.Scope == null || string.IsNullOrWhiteSpace(state.Scope.Scope))
+ {
+ throw new InvalidOperationException("The FoundryMemoryProvider state did not contain the required scope property.");
+ }
+
+ this._scope = state.Scope;
+ }
+
+ ///
+ public override async ValueTask InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
+ {
+ Throw.IfNull(context);
+
+#pragma warning disable CA1308 // Lowercase required by service
+ MemoryInputMessage[] messageItems = context.RequestMessages
+ .Where(m => !string.IsNullOrWhiteSpace(m.Text))
+ .Select(m => new MemoryInputMessage
+ {
+ Role = m.Role.Value.ToLowerInvariant(),
+ Content = m.Text!
+ })
+ .ToArray();
+#pragma warning restore CA1308
+
+ if (messageItems.Length == 0)
+ {
+ return new AIContext();
+ }
+
+ try
+ {
+ SearchMemoriesResponse? response = await this._client.SearchMemoriesAsync(
+ this._memoryStoreName,
+ this._scope.Scope!,
+ messageItems,
+ this._maxMemories,
+ cancellationToken).ConfigureAwait(false);
+
+ var memories = response?.Memories?
+ .Select(m => m.MemoryItem?.Content ?? string.Empty)
+ .Where(c => !string.IsNullOrWhiteSpace(c))
+ .ToList() ?? [];
+
+ string? outputMessageText = memories.Count == 0
+ ? null
+ : $"{this._contextPrompt}\n{string.Join(Environment.NewLine, memories)}";
+
+ if (this._logger?.IsEnabled(LogLevel.Information) is true)
+ {
+ this._logger.LogInformation(
+ "FoundryMemoryProvider: Retrieved {Count} memories. MemoryStore: '{MemoryStoreName}', Scope: '{Scope}'.",
+ memories.Count,
+ this._memoryStoreName,
+ this.SanitizeLogData(this._scope.Scope));
+
+ if (outputMessageText is not null && this._logger.IsEnabled(LogLevel.Trace))
+ {
+ this._logger.LogTrace(
+ "FoundryMemoryProvider: Search Results\nOutput:{MessageText}\nMemoryStore: '{MemoryStoreName}', Scope: '{Scope}'.",
+ this.SanitizeLogData(outputMessageText),
+ this._memoryStoreName,
+ this.SanitizeLogData(this._scope.Scope));
+ }
+ }
+
+ return new AIContext
+ {
+ Messages = [new ChatMessage(ChatRole.User, outputMessageText)]
+ };
+ }
+ catch (ArgumentException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ if (this._logger?.IsEnabled(LogLevel.Error) is true)
+ {
+ this._logger.LogError(
+ ex,
+ "FoundryMemoryProvider: Failed to search for memories due to error. MemoryStore: '{MemoryStoreName}', Scope: '{Scope}'.",
+ this._memoryStoreName,
+ this.SanitizeLogData(this._scope.Scope));
+ }
+
+ return new AIContext();
+ }
+ }
+
+ ///
+ public override async ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
+ {
+ if (context.InvokeException is not null)
+ {
+ return; // Do not update memory on failed invocations.
+ }
+
+ try
+ {
+#pragma warning disable CA1308 // Lowercase required by service
+ MemoryInputMessage[] messageItems = context.RequestMessages
+ .Concat(context.ResponseMessages ?? [])
+ .Where(m => IsAllowedRole(m.Role) && !string.IsNullOrWhiteSpace(m.Text))
+ .Select(m => new MemoryInputMessage
+ {
+ Role = m.Role.Value.ToLowerInvariant(),
+ Content = m.Text!
+ })
+ .ToArray();
+#pragma warning restore CA1308
+
+ if (messageItems.Length == 0)
+ {
+ return;
+ }
+
+ UpdateMemoriesResponse? response = await this._client.UpdateMemoriesAsync(
+ this._memoryStoreName,
+ this._scope.Scope!,
+ messageItems,
+ this._updateDelay,
+ cancellationToken).ConfigureAwait(false);
+
+ if (response?.UpdateId is not null)
+ {
+ this._pendingUpdateIds.Enqueue(response.UpdateId);
+ }
+
+ if (this._logger?.IsEnabled(LogLevel.Information) is true)
+ {
+ this._logger.LogInformation(
+ "FoundryMemoryProvider: Sent {Count} messages to update memories. MemoryStore: '{MemoryStoreName}', Scope: '{Scope}', UpdateId: '{UpdateId}'.",
+ messageItems.Length,
+ this._memoryStoreName,
+ this.SanitizeLogData(this._scope.Scope),
+ response?.UpdateId);
+ }
+ }
+ catch (Exception ex)
+ {
+ if (this._logger?.IsEnabled(LogLevel.Error) is true)
+ {
+ this._logger.LogError(
+ ex,
+ "FoundryMemoryProvider: Failed to send messages to update memories due to error. MemoryStore: '{MemoryStoreName}', Scope: '{Scope}'.",
+ this._memoryStoreName,
+ this.SanitizeLogData(this._scope.Scope));
+ }
+ }
+ }
+
+ ///
+ /// Ensures all stored memories for the configured scope are deleted.
+ /// This method handles cases where the scope doesn't exist (no memories stored yet).
+ ///
+ /// Cancellation token.
+ public async Task EnsureStoredMemoriesDeletedAsync(CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ await this._client.DeleteScopeAsync(this._memoryStoreName, this._scope.Scope!, cancellationToken).ConfigureAwait(false);
+ }
+ catch (ClientResultException ex) when (ex.Status == 404)
+ {
+ // Scope doesn't exist (no memories stored yet), nothing to delete
+ if (this._logger?.IsEnabled(LogLevel.Debug) is true)
+ {
+ this._logger.LogDebug(
+ "FoundryMemoryProvider: No memories to delete for scope. MemoryStore: '{MemoryStoreName}', Scope: '{Scope}'.",
+ this._memoryStoreName,
+ this.SanitizeLogData(this._scope.Scope));
+ }
+ }
+ }
+
+ ///
+ /// Ensures the memory store exists, creating it if necessary.
+ ///
+ /// The deployment name of the chat model for memory processing.
+ /// The deployment name of the embedding model for memory search.
+ /// Optional description for the memory store.
+ /// Cancellation token.
+ public async Task EnsureMemoryStoreCreatedAsync(
+ string chatModel,
+ string embeddingModel,
+ string? description = null,
+ CancellationToken cancellationToken = default)
+ {
+ bool created = await this._client.CreateMemoryStoreIfNotExistsAsync(
+ this._memoryStoreName,
+ description,
+ chatModel,
+ embeddingModel,
+ cancellationToken).ConfigureAwait(false);
+
+ if (created)
+ {
+ if (this._logger?.IsEnabled(LogLevel.Information) is true)
+ {
+ this._logger.LogInformation(
+ "FoundryMemoryProvider: Created memory store '{MemoryStoreName}'.",
+ this._memoryStoreName);
+ }
+ }
+ else
+ {
+ if (this._logger?.IsEnabled(LogLevel.Debug) is true)
+ {
+ this._logger.LogDebug(
+ "FoundryMemoryProvider: Memory store '{MemoryStoreName}' already exists.",
+ this._memoryStoreName);
+ }
+ }
+ }
+
+ ///
+ /// Waits for all pending memory update operations to complete.
+ ///
+ ///
+ /// Memory extraction in Azure AI Foundry is asynchronous. This method polls all pending updates
+ /// in parallel and returns when all have completed, failed, or been superseded.
+ ///
+ /// The interval between status checks. Defaults to 5 seconds.
+ /// Cancellation token.
+ /// Thrown if any update operation failed, containing all failures.
+ public async Task WhenUpdatesCompletedAsync(
+ TimeSpan? pollingInterval = null,
+ CancellationToken cancellationToken = default)
+ {
+ TimeSpan interval = pollingInterval ?? TimeSpan.FromSeconds(5);
+
+ // Collect all pending update IDs
+ List updateIds = [];
+ while (this._pendingUpdateIds.TryDequeue(out string? updateId))
+ {
+ updateIds.Add(updateId);
+ }
+
+ if (updateIds.Count == 0)
+ {
+ return;
+ }
+
+ // Poll all updates in parallel
+ await Task.WhenAll(updateIds.Select(updateId => this.WaitForUpdateAsync(updateId, interval, cancellationToken))).ConfigureAwait(false);
+ }
+
+ private async Task WaitForUpdateAsync(string updateId, TimeSpan interval, CancellationToken cancellationToken)
+ {
+ while (true)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ UpdateMemoriesResponse? response = await this._client.GetUpdateStatusAsync(
+ this._memoryStoreName,
+ updateId,
+ cancellationToken).ConfigureAwait(false);
+
+ string status = response?.Status ?? "unknown";
+
+ if (this._logger?.IsEnabled(LogLevel.Debug) is true)
+ {
+ this._logger.LogDebug(
+ "FoundryMemoryProvider: Update status for '{UpdateId}': {Status}",
+ updateId,
+ status);
+ }
+
+ switch (status)
+ {
+ case UpdateMemoriesResponse.StatusCompleted:
+ case UpdateMemoriesResponse.StatusSuperseded:
+ return;
+ case UpdateMemoriesResponse.StatusFailed:
+ throw new InvalidOperationException($"Memory update operation '{updateId}' failed: {response?.Error?.Message}");
+ case UpdateMemoriesResponse.StatusQueued:
+ case UpdateMemoriesResponse.StatusInProgress:
+ await Task.Delay(interval, cancellationToken).ConfigureAwait(false);
+ break;
+ default:
+ throw new InvalidOperationException($"Unknown update status '{status}' for update '{updateId}'.");
+ }
+ }
+ }
+
+ ///
+ public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
+ {
+ FoundryMemoryState state = new(this._scope);
+
+ JsonSerializerOptions jso = jsonSerializerOptions ?? FoundryMemoryJsonUtilities.DefaultOptions;
+ return JsonSerializer.SerializeToElement(state, jso.GetTypeInfo(typeof(FoundryMemoryState)));
+ }
+
+ private static bool IsAllowedRole(ChatRole role) =>
+ role == ChatRole.User || role == ChatRole.Assistant || role == ChatRole.System;
+
+ private string? SanitizeLogData(string? data) => this._enableSensitiveTelemetryData ? data : "";
+
+ internal sealed class FoundryMemoryState
+ {
+ [JsonConstructor]
+ public FoundryMemoryState(FoundryMemoryProviderScope scope)
+ {
+ this.Scope = scope;
+ }
+
+ public FoundryMemoryProviderScope Scope { get; set; }
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProviderOptions.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProviderOptions.cs
new file mode 100644
index 0000000000..b6b157431d
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProviderOptions.cs
@@ -0,0 +1,45 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Microsoft.Agents.AI.FoundryMemory;
+
+///
+/// Options for configuring the .
+///
+public sealed class FoundryMemoryProviderOptions
+{
+ ///
+ /// Gets or sets the name of the pre-existing memory store in Azure AI Foundry.
+ ///
+ ///
+ /// The memory store must be created in your Azure AI Foundry project before using this provider.
+ ///
+ public string? MemoryStoreName { get; set; }
+
+ ///
+ /// When providing memories to the model, this string is prefixed to the retrieved memories to supply context.
+ ///
+ /// Defaults to "## Memories\nConsider the following memories when answering user questions:".
+ public string? ContextPrompt { get; set; }
+
+ ///
+ /// Gets or sets the maximum number of memories to retrieve during search.
+ ///
+ /// Defaults to 5.
+ public int MaxMemories { get; set; } = 5;
+
+ ///
+ /// Gets or sets the delay in seconds before memory updates are processed.
+ ///
+ ///
+ /// Setting to 0 triggers updates immediately without waiting for inactivity.
+ /// Higher values allow the service to batch multiple updates together.
+ ///
+ /// Defaults to 0 (immediate).
+ public int UpdateDelay { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether sensitive data such as user ids and user messages may appear in logs.
+ ///
+ /// Defaults to .
+ public bool EnableSensitiveTelemetryData { get; set; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProviderScope.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProviderScope.cs
new file mode 100644
index 0000000000..542e5bf995
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProviderScope.cs
@@ -0,0 +1,41 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Agents.AI.FoundryMemory;
+
+///
+/// Allows scoping of memories for the .
+///
+///
+/// Azure AI Foundry memories are scoped by a single string identifier that you control.
+/// Common patterns include using a user ID, team ID, or other unique identifier
+/// to partition memories across different contexts.
+///
+public sealed class FoundryMemoryProviderScope
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public FoundryMemoryProviderScope() { }
+
+ ///
+ /// Initializes a new instance of the class by cloning an existing scope.
+ ///
+ /// The scope to clone.
+ public FoundryMemoryProviderScope(FoundryMemoryProviderScope sourceScope)
+ {
+ Throw.IfNull(sourceScope);
+ this.Scope = sourceScope.Scope;
+ }
+
+ ///
+ /// Gets or sets the scope identifier used to partition memories.
+ ///
+ ///
+ /// This value controls how memory is partitioned in the memory store.
+ /// Each unique scope maintains its own isolated collection of memory items.
+ /// For example, use a user ID to ensure each user has their own individual memory.
+ ///
+ public string? Scope { get; set; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Microsoft.Agents.AI.FoundryMemory.csproj b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Microsoft.Agents.AI.FoundryMemory.csproj
new file mode 100644
index 0000000000..bc3baac152
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Microsoft.Agents.AI.FoundryMemory.csproj
@@ -0,0 +1,37 @@
+
+
+
+ preview
+
+
+
+ true
+ true
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+ Microsoft Agent Framework - Azure AI Foundry Memory integration
+ Provides Azure AI Foundry Memory integration for Microsoft Agent Framework.
+
+
+
+
+
+
+
+
diff --git a/dotnet/src/Shared/IntegrationTests/FoundryMemoryConfiguration.cs b/dotnet/src/Shared/IntegrationTests/FoundryMemoryConfiguration.cs
new file mode 100644
index 0000000000..b752754e59
--- /dev/null
+++ b/dotnet/src/Shared/IntegrationTests/FoundryMemoryConfiguration.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Shared.IntegrationTests;
+
+#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
+#pragma warning disable CA1812 // Internal class that is apparently never instantiated.
+
+internal sealed class FoundryMemoryConfiguration
+{
+ public string Endpoint { get; set; }
+ public string MemoryStoreName { get; set; }
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/FoundryMemoryProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/FoundryMemoryProviderTests.cs
new file mode 100644
index 0000000000..2a5e154cbc
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/FoundryMemoryProviderTests.cs
@@ -0,0 +1,190 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Azure.AI.Projects;
+using Azure.Identity;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Configuration;
+using Shared.IntegrationTests;
+
+namespace Microsoft.Agents.AI.FoundryMemory.IntegrationTests;
+
+///
+/// Integration tests for against a configured Azure AI Foundry Memory service.
+///
+public sealed class FoundryMemoryProviderTests : IDisposable
+{
+ private const string SkipReason = "Requires an Azure AI Foundry Memory service configured"; // Set to null to enable.
+
+ private readonly AIProjectClient? _client;
+ private readonly string? _memoryStoreName;
+ private bool _disposed;
+
+ public FoundryMemoryProviderTests()
+ {
+ IConfigurationRoot configuration = new ConfigurationBuilder()
+ .AddJsonFile(path: "testsettings.json", optional: true, reloadOnChange: true)
+ .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true)
+ .AddEnvironmentVariables()
+ .AddUserSecrets(optional: true)
+ .Build();
+
+ var foundrySettings = configuration.GetSection("FoundryMemory").Get();
+
+ if (foundrySettings is not null &&
+ !string.IsNullOrWhiteSpace(foundrySettings.Endpoint) &&
+ !string.IsNullOrWhiteSpace(foundrySettings.MemoryStoreName))
+ {
+ this._client = new AIProjectClient(new Uri(foundrySettings.Endpoint), new AzureCliCredential());
+ this._memoryStoreName = foundrySettings.MemoryStoreName;
+ }
+ }
+
+ [Fact(Skip = SkipReason)]
+ public async Task CanAddAndRetrieveUserMemoriesAsync()
+ {
+ // Arrange
+ var question = new ChatMessage(ChatRole.User, "What is my name?");
+ var input = new ChatMessage(ChatRole.User, "Hello, my name is Caoimhe.");
+ var storageScope = new FoundryMemoryProviderScope { Scope = "it-user-1" };
+ var options = new FoundryMemoryProviderOptions { MemoryStoreName = this._memoryStoreName! };
+ var sut = new FoundryMemoryProvider(this._client!, storageScope, options);
+
+ await sut.EnsureStoredMemoriesDeletedAsync();
+ var ctxBefore = await sut.InvokingAsync(new AIContextProvider.InvokingContext([question]));
+ Assert.DoesNotContain("Caoimhe", ctxBefore.Messages?[0].Text ?? string.Empty);
+
+ // Act
+ await sut.InvokedAsync(new AIContextProvider.InvokedContext([input], aiContextProviderMessages: null));
+ var ctxAfterAdding = await GetContextWithRetryAsync(sut, question);
+ await sut.EnsureStoredMemoriesDeletedAsync();
+ var ctxAfterClearing = await sut.InvokingAsync(new AIContextProvider.InvokingContext([question]));
+
+ // Assert
+ Assert.Contains("Caoimhe", ctxAfterAdding.Messages?[0].Text ?? string.Empty);
+ Assert.DoesNotContain("Caoimhe", ctxAfterClearing.Messages?[0].Text ?? string.Empty);
+ }
+
+ [Fact(Skip = SkipReason)]
+ public async Task CanAddAndRetrieveAssistantMemoriesAsync()
+ {
+ // Arrange
+ var question = new ChatMessage(ChatRole.User, "What is your name?");
+ var assistantIntro = new ChatMessage(ChatRole.Assistant, "Hello, I'm a friendly assistant and my name is Caoimhe.");
+ var storageScope = new FoundryMemoryProviderScope { Scope = "it-agent-1" };
+ var options = new FoundryMemoryProviderOptions { MemoryStoreName = this._memoryStoreName! };
+ var sut = new FoundryMemoryProvider(this._client!, storageScope, options);
+
+ await sut.EnsureStoredMemoriesDeletedAsync();
+ var ctxBefore = await sut.InvokingAsync(new AIContextProvider.InvokingContext([question]));
+ Assert.DoesNotContain("Caoimhe", ctxBefore.Messages?[0].Text ?? string.Empty);
+
+ // Act
+ await sut.InvokedAsync(new AIContextProvider.InvokedContext([assistantIntro], aiContextProviderMessages: null));
+ var ctxAfterAdding = await GetContextWithRetryAsync(sut, question);
+ await sut.EnsureStoredMemoriesDeletedAsync();
+ var ctxAfterClearing = await sut.InvokingAsync(new AIContextProvider.InvokingContext([question]));
+
+ // Assert
+ Assert.Contains("Caoimhe", ctxAfterAdding.Messages?[0].Text ?? string.Empty);
+ Assert.DoesNotContain("Caoimhe", ctxAfterClearing.Messages?[0].Text ?? string.Empty);
+ }
+
+ [Fact(Skip = SkipReason)]
+ public async Task DoesNotLeakMemoriesAcrossScopesAsync()
+ {
+ // Arrange
+ var question = new ChatMessage(ChatRole.User, "What is your name?");
+ var assistantIntro = new ChatMessage(ChatRole.Assistant, "I'm an AI tutor and my name is Caoimhe.");
+ var options = new FoundryMemoryProviderOptions { MemoryStoreName = this._memoryStoreName! };
+ var sut1 = new FoundryMemoryProvider(this._client!, new FoundryMemoryProviderScope { Scope = "it-scope-a" }, options);
+ var sut2 = new FoundryMemoryProvider(this._client!, new FoundryMemoryProviderScope { Scope = "it-scope-b" }, options);
+
+ await sut1.EnsureStoredMemoriesDeletedAsync();
+ await sut2.EnsureStoredMemoriesDeletedAsync();
+
+ var ctxBefore1 = await sut1.InvokingAsync(new AIContextProvider.InvokingContext([question]));
+ var ctxBefore2 = await sut2.InvokingAsync(new AIContextProvider.InvokingContext([question]));
+ Assert.DoesNotContain("Caoimhe", ctxBefore1.Messages?[0].Text ?? string.Empty);
+ Assert.DoesNotContain("Caoimhe", ctxBefore2.Messages?[0].Text ?? string.Empty);
+
+ // Act
+ await sut1.InvokedAsync(new AIContextProvider.InvokedContext([assistantIntro], aiContextProviderMessages: null));
+ var ctxAfterAdding1 = await GetContextWithRetryAsync(sut1, question);
+ var ctxAfterAdding2 = await GetContextWithRetryAsync(sut2, question);
+
+ // Assert
+ Assert.Contains("Caoimhe", ctxAfterAdding1.Messages?[0].Text ?? string.Empty);
+ Assert.DoesNotContain("Caoimhe", ctxAfterAdding2.Messages?[0].Text ?? string.Empty);
+
+ // Cleanup
+ await sut1.EnsureStoredMemoriesDeletedAsync();
+ await sut2.EnsureStoredMemoriesDeletedAsync();
+ }
+
+ [Fact(Skip = SkipReason)]
+ public async Task ClearStoredMemoriesRemovesAllMemoriesAsync()
+ {
+ // Arrange
+ var input1 = new ChatMessage(ChatRole.User, "My favorite color is blue.");
+ var input2 = new ChatMessage(ChatRole.User, "My favorite food is pizza.");
+ var question = new ChatMessage(ChatRole.User, "What do you know about my preferences?");
+ var storageScope = new FoundryMemoryProviderScope { Scope = "it-clear-test" };
+ var options = new FoundryMemoryProviderOptions { MemoryStoreName = this._memoryStoreName! };
+ var sut = new FoundryMemoryProvider(this._client!, storageScope, options);
+
+ await sut.EnsureStoredMemoriesDeletedAsync();
+
+ // Act - Add multiple memories
+ await sut.InvokedAsync(new AIContextProvider.InvokedContext([input1], aiContextProviderMessages: null));
+ await sut.InvokedAsync(new AIContextProvider.InvokedContext([input2], aiContextProviderMessages: null));
+ var ctxBeforeClear = await GetContextWithRetryAsync(sut, question, searchTerms: ["blue", "pizza"]);
+
+ await sut.EnsureStoredMemoriesDeletedAsync();
+ var ctxAfterClear = await sut.InvokingAsync(new AIContextProvider.InvokingContext([question]));
+
+ // Assert
+ var textBefore = ctxBeforeClear.Messages?[0].Text ?? string.Empty;
+ var textAfter = ctxAfterClear.Messages?[0].Text ?? string.Empty;
+
+ Assert.True(textBefore.Contains("blue") || textBefore.Contains("pizza"), "Should contain at least one preference before clear");
+ Assert.DoesNotContain("blue", textAfter);
+ Assert.DoesNotContain("pizza", textAfter);
+ }
+
+ private static async Task GetContextWithRetryAsync(
+ FoundryMemoryProvider provider,
+ ChatMessage question,
+ string[]? searchTerms = null,
+ int attempts = 5,
+ int delayMs = 2000)
+ {
+ searchTerms ??= ["Caoimhe"];
+ AIContext? ctx = null;
+
+ for (int i = 0; i < attempts; i++)
+ {
+ ctx = await provider.InvokingAsync(new AIContextProvider.InvokingContext([question]), CancellationToken.None);
+ var text = ctx.Messages?[0].Text ?? string.Empty;
+
+ if (Array.Exists(searchTerms, term => text.Contains(term, StringComparison.OrdinalIgnoreCase)))
+ {
+ break;
+ }
+
+ await Task.Delay(delayMs);
+ }
+
+ return ctx!;
+ }
+
+ public void Dispose()
+ {
+ if (!this._disposed)
+ {
+ this._disposed = true;
+ }
+ }
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests.csproj b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests.csproj
new file mode 100644
index 0000000000..652178aef8
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests.csproj
@@ -0,0 +1,20 @@
+
+
+
+ True
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/FoundryMemoryProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/FoundryMemoryProviderTests.cs
new file mode 100644
index 0000000000..12307c24ca
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/FoundryMemoryProviderTests.cs
@@ -0,0 +1,169 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Text.Json;
+
+namespace Microsoft.Agents.AI.FoundryMemory.UnitTests;
+
+///
+/// Tests for constructor validation and serialization.
+///
+///
+/// Since directly uses ,
+/// integration tests are used to verify the memory operations. These unit tests focus on:
+/// - Constructor parameter validation
+/// - Serialization and deserialization of provider state
+///
+public sealed class FoundryMemoryProviderTests
+{
+ [Fact]
+ public void Constructor_Throws_WhenClientIsNull()
+ {
+ // Act & Assert
+ ArgumentNullException ex = Assert.Throws(() => new FoundryMemoryProvider(
+ null!,
+ new FoundryMemoryProviderScope { Scope = "test" },
+ new FoundryMemoryProviderOptions { MemoryStoreName = "store" }));
+ Assert.Equal("client", ex.ParamName);
+ }
+
+ [Fact]
+ public void Constructor_Throws_WhenScopeIsNull()
+ {
+ // Arrange
+ using TestableAIProjectClient testClient = new();
+
+ // Act & Assert
+ ArgumentNullException ex = Assert.Throws(() => new FoundryMemoryProvider(
+ testClient.Client,
+ null!,
+ new FoundryMemoryProviderOptions { MemoryStoreName = "store" }));
+ Assert.Equal("scope", ex.ParamName);
+ }
+
+ [Fact]
+ public void Constructor_Throws_WhenScopeValueIsEmpty()
+ {
+ // Arrange
+ using TestableAIProjectClient testClient = new();
+
+ // Act & Assert
+ ArgumentException ex = Assert.Throws(() => new FoundryMemoryProvider(
+ testClient.Client,
+ new FoundryMemoryProviderScope(),
+ new FoundryMemoryProviderOptions { MemoryStoreName = "store" }));
+ Assert.StartsWith("The Scope property must be provided.", ex.Message);
+ }
+
+ [Fact]
+ public void Constructor_Throws_WhenMemoryStoreNameIsMissing()
+ {
+ // Arrange
+ using TestableAIProjectClient testClient = new();
+
+ // Act & Assert
+ ArgumentException ex = Assert.Throws(() => new FoundryMemoryProvider(
+ testClient.Client,
+ new FoundryMemoryProviderScope { Scope = "test" },
+ new FoundryMemoryProviderOptions()));
+ Assert.StartsWith("The MemoryStoreName option must be provided.", ex.Message);
+ }
+
+ [Fact]
+ public void Constructor_Throws_WhenMemoryStoreNameIsNull()
+ {
+ // Arrange
+ using TestableAIProjectClient testClient = new();
+
+ // Act & Assert
+ ArgumentException ex = Assert.Throws(() => new FoundryMemoryProvider(
+ testClient.Client,
+ new FoundryMemoryProviderScope { Scope = "test" },
+ null));
+ Assert.StartsWith("The MemoryStoreName option must be provided.", ex.Message);
+ }
+
+ [Fact]
+ public void DeserializingConstructor_Throws_WhenClientIsNull()
+ {
+ // Arrange - use source-generated JSON context
+ JsonElement jsonElement = JsonSerializer.SerializeToElement(
+ new TestState { Scope = new TestScope { Scope = "test" } },
+ TestJsonContext.Default.TestState);
+
+ // Act & Assert
+ ArgumentNullException ex = Assert.Throws(() => new FoundryMemoryProvider(
+ null!,
+ jsonElement,
+ options: new FoundryMemoryProviderOptions { MemoryStoreName = "store" }));
+ Assert.Equal("client", ex.ParamName);
+ }
+
+ [Fact]
+ public void DeserializingConstructor_Throws_WithEmptyJsonElement()
+ {
+ // Arrange
+ using TestableAIProjectClient testClient = new();
+ JsonElement jsonElement = JsonDocument.Parse("{}").RootElement;
+
+ // Act & Assert
+ InvalidOperationException ex = Assert.Throws(() => new FoundryMemoryProvider(
+ testClient.Client,
+ jsonElement,
+ options: new FoundryMemoryProviderOptions { MemoryStoreName = "store" }));
+ Assert.StartsWith("The FoundryMemoryProvider state did not contain the required scope property.", ex.Message);
+ }
+
+ [Fact]
+ public void DeserializingConstructor_Throws_WithMissingScopeValue()
+ {
+ // Arrange
+ using TestableAIProjectClient testClient = new();
+ JsonElement jsonElement = JsonSerializer.SerializeToElement(
+ new TestState { Scope = new TestScope() },
+ TestJsonContext.Default.TestState);
+
+ // Act & Assert
+ InvalidOperationException ex = Assert.Throws(() => new FoundryMemoryProvider(
+ testClient.Client,
+ jsonElement,
+ options: new FoundryMemoryProviderOptions { MemoryStoreName = "store" }));
+ Assert.StartsWith("The FoundryMemoryProvider state did not contain the required scope property.", ex.Message);
+ }
+
+ [Fact]
+ public void Serialize_RoundTripsScope()
+ {
+ // Arrange
+ using TestableAIProjectClient testClient = new();
+ FoundryMemoryProviderScope scope = new() { Scope = "user-456" };
+ FoundryMemoryProvider sut = new(testClient.Client, scope, new FoundryMemoryProviderOptions { MemoryStoreName = "my-store" });
+
+ // Act
+ JsonElement stateElement = sut.Serialize();
+ using JsonDocument doc = JsonDocument.Parse(stateElement.GetRawText());
+
+ // Assert (JSON uses camelCase naming policy)
+ Assert.True(doc.RootElement.TryGetProperty("scope", out JsonElement scopeElement));
+ Assert.Equal("user-456", scopeElement.GetProperty("scope").GetString());
+ }
+
+ [Fact]
+ public void DeserializingConstructor_RestoresScope()
+ {
+ // Arrange
+ using TestableAIProjectClient testClient = new();
+ FoundryMemoryProviderScope originalScope = new() { Scope = "restored-user-789" };
+ FoundryMemoryProvider original = new(testClient.Client, originalScope, new FoundryMemoryProviderOptions { MemoryStoreName = "my-store" });
+
+ // Act
+ JsonElement serializedState = original.Serialize();
+ FoundryMemoryProvider restored = new(testClient.Client, serializedState, options: new FoundryMemoryProviderOptions { MemoryStoreName = "my-store" });
+
+ // Assert - serialize again to verify scope was restored
+ JsonElement restoredState = restored.Serialize();
+ using JsonDocument doc = JsonDocument.Parse(restoredState.GetRawText());
+ Assert.True(doc.RootElement.TryGetProperty("scope", out JsonElement scopeElement));
+ Assert.Equal("restored-user-789", scopeElement.GetProperty("scope").GetString());
+ }
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/Microsoft.Agents.AI.FoundryMemory.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/Microsoft.Agents.AI.FoundryMemory.UnitTests.csproj
new file mode 100644
index 0000000000..1fe8dc57bd
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/Microsoft.Agents.AI.FoundryMemory.UnitTests.csproj
@@ -0,0 +1,16 @@
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/TestableAIProjectClient.cs b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/TestableAIProjectClient.cs
new file mode 100644
index 0000000000..25c041f754
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/TestableAIProjectClient.cs
@@ -0,0 +1,196 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.ClientModel.Primitives;
+using System.Net;
+using System.Net.Http;
+using System.Text;
+using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+using Azure.AI.Projects;
+using Azure.Core;
+
+namespace Microsoft.Agents.AI.FoundryMemory.UnitTests;
+
+///
+/// Creates a testable AIProjectClient with a mock HTTP handler.
+///
+internal sealed class TestableAIProjectClient : IDisposable
+{
+ private readonly HttpClient _httpClient;
+
+ public TestableAIProjectClient(
+ string? searchMemoriesResponse = null,
+ string? updateMemoriesResponse = null,
+ HttpStatusCode? searchStatusCode = null,
+ HttpStatusCode? updateStatusCode = null,
+ HttpStatusCode? deleteStatusCode = null,
+ HttpStatusCode? createStoreStatusCode = null,
+ HttpStatusCode? getStoreStatusCode = null)
+ {
+ this.Handler = new MockHttpMessageHandler(
+ searchMemoriesResponse,
+ updateMemoriesResponse,
+ searchStatusCode,
+ updateStatusCode,
+ deleteStatusCode,
+ createStoreStatusCode,
+ getStoreStatusCode);
+
+ this._httpClient = new HttpClient(this.Handler);
+
+ AIProjectClientOptions options = new()
+ {
+ Transport = new HttpClientPipelineTransport(this._httpClient)
+ };
+
+ // Using a valid format endpoint
+ this.Client = new AIProjectClient(
+ new Uri("https://test.services.ai.azure.com/api/projects/test-project"),
+ new MockTokenCredential(),
+ options);
+ }
+
+ public AIProjectClient Client { get; }
+
+ public MockHttpMessageHandler Handler { get; }
+
+ public void Dispose()
+ {
+ this._httpClient.Dispose();
+ this.Handler.Dispose();
+ }
+}
+
+///
+/// Mock HTTP message handler for testing.
+///
+internal sealed class MockHttpMessageHandler : HttpMessageHandler
+{
+ private readonly string? _searchMemoriesResponse;
+ private readonly string? _updateMemoriesResponse;
+ private readonly HttpStatusCode _searchStatusCode;
+ private readonly HttpStatusCode _updateStatusCode;
+ private readonly HttpStatusCode _deleteStatusCode;
+ private readonly HttpStatusCode _createStoreStatusCode;
+ private readonly HttpStatusCode _getStoreStatusCode;
+
+ public MockHttpMessageHandler(
+ string? searchMemoriesResponse = null,
+ string? updateMemoriesResponse = null,
+ HttpStatusCode? searchStatusCode = null,
+ HttpStatusCode? updateStatusCode = null,
+ HttpStatusCode? deleteStatusCode = null,
+ HttpStatusCode? createStoreStatusCode = null,
+ HttpStatusCode? getStoreStatusCode = null)
+ {
+ this._searchMemoriesResponse = searchMemoriesResponse ?? """{"memories":[]}""";
+ this._updateMemoriesResponse = updateMemoriesResponse ?? """{"update_id":"test-update-id","status":"queued"}""";
+ this._searchStatusCode = searchStatusCode ?? HttpStatusCode.OK;
+ this._updateStatusCode = updateStatusCode ?? HttpStatusCode.OK;
+ this._deleteStatusCode = deleteStatusCode ?? HttpStatusCode.NoContent;
+ this._createStoreStatusCode = createStoreStatusCode ?? HttpStatusCode.Created;
+ this._getStoreStatusCode = getStoreStatusCode ?? HttpStatusCode.NotFound;
+ }
+
+ public string? LastRequestUri { get; private set; }
+ public string? LastRequestBody { get; private set; }
+ public HttpMethod? LastRequestMethod { get; private set; }
+
+ protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ this.LastRequestUri = request.RequestUri?.ToString();
+ this.LastRequestMethod = request.Method;
+
+ if (request.Content != null)
+ {
+#if NET472
+ this.LastRequestBody = await request.Content.ReadAsStringAsync().ConfigureAwait(false);
+#else
+ this.LastRequestBody = await request.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
+#endif
+ }
+
+ string path = request.RequestUri?.AbsolutePath ?? "";
+
+ // Route based on path and method
+ if (path.Contains("/memory-stores/") && path.Contains("/search") && request.Method == HttpMethod.Post)
+ {
+ return CreateResponse(this._searchStatusCode, this._searchMemoriesResponse);
+ }
+
+ if (path.Contains("/memory-stores/") && path.Contains("/memories") && request.Method == HttpMethod.Post)
+ {
+ return CreateResponse(this._updateStatusCode, this._updateMemoriesResponse);
+ }
+
+ if (path.Contains("/memory-stores/") && path.Contains("/scopes") && request.Method == HttpMethod.Delete)
+ {
+ return CreateResponse(this._deleteStatusCode, "");
+ }
+
+ if (path.Contains("/memory-stores") && request.Method == HttpMethod.Post)
+ {
+ return CreateResponse(this._createStoreStatusCode, """{"name":"test-store","status":"active"}""");
+ }
+
+ if (path.Contains("/memory-stores/") && request.Method == HttpMethod.Get)
+ {
+ return CreateResponse(this._getStoreStatusCode, """{"name":"test-store","status":"active"}""");
+ }
+
+ // Default response
+ return CreateResponse(HttpStatusCode.NotFound, "{}");
+ }
+
+ private static HttpResponseMessage CreateResponse(HttpStatusCode statusCode, string? content)
+ {
+ return new HttpResponseMessage(statusCode)
+ {
+ Content = new StringContent(content ?? "{}", Encoding.UTF8, "application/json")
+ };
+ }
+}
+
+///
+/// Mock token credential for testing.
+///
+internal sealed class MockTokenCredential : TokenCredential
+{
+ public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
+ {
+ return new AccessToken("mock-token", DateTimeOffset.UtcNow.AddHours(1));
+ }
+
+ public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
+ {
+ return new ValueTask(new AccessToken("mock-token", DateTimeOffset.UtcNow.AddHours(1)));
+ }
+}
+
+///
+/// Source-generated JSON serializer context for unit test types.
+///
+[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
+[JsonSerializable(typeof(TestState))]
+[JsonSerializable(typeof(TestScope))]
+internal sealed partial class TestJsonContext : JsonSerializerContext
+{
+}
+
+///
+/// Test state class for deserialization tests.
+///
+internal sealed class TestState
+{
+ public TestScope? Scope { get; set; }
+}
+
+///
+/// Test scope class for deserialization tests.
+///
+internal sealed class TestScope
+{
+ public string? Scope { get; set; }
+}