From ac438bb4ec99b38012b61bae0534fc4a7881a7c7 Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Wed, 29 Jul 2026 17:33:27 +0000 Subject: [PATCH 1/2] Add FileMemoryProvider sample --- dotnet/agent-framework-dotnet.slnx | 1 + ...ithMemory_Step07_FileMemoryProvider.csproj | 19 ++++ .../Program.cs | 98 +++++++++++++++++++ .../README.md | 68 +++++++++++++ .../02-agents/AgentWithMemory/README.md | 1 + 5 files changed, 187 insertions(+) create mode 100644 dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step07_FileMemoryProvider/AgentWithMemory_Step07_FileMemoryProvider.csproj create mode 100644 dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step07_FileMemoryProvider/Program.cs create mode 100644 dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step07_FileMemoryProvider/README.md diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 9cc20db18bf..92bf92d4157 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -204,6 +204,7 @@ + diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step07_FileMemoryProvider/AgentWithMemory_Step07_FileMemoryProvider.csproj b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step07_FileMemoryProvider/AgentWithMemory_Step07_FileMemoryProvider.csproj new file mode 100644 index 00000000000..129c9026a2b --- /dev/null +++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step07_FileMemoryProvider/AgentWithMemory_Step07_FileMemoryProvider.csproj @@ -0,0 +1,19 @@ + + + + Exe + net10.0 + + enable + enable + + + + + + + + + + + diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step07_FileMemoryProvider/Program.cs b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step07_FileMemoryProvider/Program.cs new file mode 100644 index 00000000000..1a9dbdc1703 --- /dev/null +++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step07_FileMemoryProvider/Program.cs @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft. All rights reserved. + +// This sample shows how to give an agent file-based memory using the FileMemoryProvider. +// The FileMemoryProvider exposes a set of tools to the agent (write, read, delete, list, grep and replace) +// that allow it to store memories as individual files in an AgentFileStore. +// Because the files are stored outside of the conversation, the agent can recall them +// in later conversations, even after the original chat history is gone. +// +// The sample also shows how to control the folder that memory files are written to, +// by supplying a state initializer callback that sets the working folder for each session. + +#pragma warning disable MAAI001 // AgentFileStore and its implementations are experimental. + +using Azure.AI.Projects; +using Azure.Identity; +using Microsoft.Agents.AI; + +var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set."); +var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini"; + +// The id of the user that we are storing memories for. +// It is used below to give each user their own memory folder. +const string UserId = "UID1"; + +// Create the file store that the FileMemoryProvider will use to persist memory files. +// Here we use a file system backed store rooted at a local folder called "agent-memory", +// but any AgentFileStore implementation can be used, e.g. InMemoryAgentFileStore or a custom +// implementation backed by blob storage. +var memoryRoot = Path.Combine(AppContext.BaseDirectory, "agent-memory"); +var fileStore = new FileSystemAgentFileStore(memoryRoot); + +// The working folder that memories for this user will be written to, relative to the store root. +// The folder you choose determines the scope and lifetime of the memories: +// - A stable folder, like the per-user one below, gives you durable memories that are shared by +// every session for that user. That is what allows the second conversation further down to +// recall what the user said in the first. +// - A unique folder per session gives you memories that are isolated to a single session, e.g. +// generate one in the state initializer callback below: +// _ => new FileMemoryState { WorkingFolder = Guid.NewGuid().ToString() } +var workingFolder = $"users/{UserId}"; + +Console.WriteLine($"Memory files will be written to: {Path.Combine(memoryRoot, workingFolder)}"); +Console.WriteLine(); + +// Create the file memory provider. +// The second parameter is a state initializer callback that is invoked whenever the provider +// cannot find existing state in a session, i.e. typically the first time it is used with a new session. +// It allows us to configure the folder that memory files for that session are written to. +// If no callback is supplied, the working folder defaults to the root of the store, +// which means all sessions share a single, flat set of memory files. +using var fileMemoryProvider = new FileMemoryProvider( + fileStore, + _ => new FileMemoryState { WorkingFolder = workingFolder }); + +// Create the agent and attach the FileMemoryProvider so that the agent gets the file memory tools. +AIAgent agent = new AIProjectClient( + new Uri(endpoint), + // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. + // In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid + // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. + new DefaultAzureCredential()) + .AsAIAgent(new ChatClientAgentOptions + { + ChatOptions = new() + { + ModelId = deploymentName, + Instructions = "You are a helpful travel assistant. Remember what the user tells you about themselves so that you can give better recommendations later." + }, + Name = "TravelAssistant", + AIContextProviders = [fileMemoryProvider], + }); + +// First conversation: tell the agent something worth remembering. +// The agent should use the file_memory_write tool to store it as a file in the working folder. +AgentSession firstSession = await agent.CreateSessionAsync(); +Console.WriteLine("=== First conversation ==="); +Console.WriteLine(await agent.RunAsync( + "I'm vegetarian and I always travel with my dog. Please remember this for future trips.", + firstSession)); +Console.WriteLine(); + +// Show the memory files that the agent created on disk. +Console.WriteLine("=== Memory files on disk ==="); +foreach (var file in Directory.EnumerateFiles(Path.Combine(memoryRoot, workingFolder))) +{ + Console.WriteLine(Path.GetFileName(file)); +} + +Console.WriteLine(); + +// Second conversation: a brand new session with no chat history from the first conversation. +// The provider surfaces the memory index to the agent, and the agent can read the memory files +// using the file_memory_read tool, so it can still recall the user's preferences. +AgentSession secondSession = await agent.CreateSessionAsync(); +Console.WriteLine("=== Second conversation (new session) ==="); +Console.WriteLine(await agent.RunAsync( + "Suggest a hotel and a restaurant for my trip to Paris next week.", + secondSession)); diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step07_FileMemoryProvider/README.md b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step07_FileMemoryProvider/README.md new file mode 100644 index 00000000000..23d4d2434ee --- /dev/null +++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step07_FileMemoryProvider/README.md @@ -0,0 +1,68 @@ +# File Based Memory with FileMemoryProvider + +This sample demonstrates how to give an agent file-based memory using the `FileMemoryProvider`. + +The `FileMemoryProvider` is an `AIContextProvider` that exposes a set of memory tools to the agent, allowing the agent to decide what to remember and when to recall it. Each memory is stored as an individual file in an `AgentFileStore`, so memories survive beyond the lifetime of a single conversation. + +## Concepts + +- **`FileMemoryProvider`**: An `AIContextProvider` that adds the following tools to the agent: + + | Tool | Description | + |---|---| + | `file_memory_write` | Write a memory file with a name, content and optional description. | + | `file_memory_read` | Read the content of a memory file by name. | + | `file_memory_delete` | Delete a memory file by name. | + | `file_memory_ls` | List all memory files with their descriptions. | + | `file_memory_grep` | Search memory file contents using a regular expression. | + | `file_memory_replace` | Replace occurrences of a substring within a memory file. | + | `file_memory_replace_lines` | Replace whole lines within a memory file. | + + The provider also maintains a `memories.md` index file, which it injects into the conversation so the agent knows which memories are available without having to list them first. + +- **`AgentFileStore`**: The pluggable storage abstraction used by the provider. This sample uses `FileSystemAgentFileStore` to store memories on the local disk, but `InMemoryAgentFileStore` or a custom implementation (e.g. backed by blob storage) can be used instead. + +- **`FileMemoryState`**: The per-session state of the provider. Its `WorkingFolder` property determines the folder, relative to the store root, that memory files are written to. + +## Configuring the memory folder + +By default, all sessions share the root folder of the store, which means every session reads and writes the same flat set of memory files. + +To scope memories, e.g. per user, per tenant or per session, pass a state initializer callback to the `FileMemoryProvider` constructor. The callback receives the `AgentSession` and is invoked whenever the provider cannot find existing state in that session, i.e. typically the first time the provider is used with a new session: + +```csharp +using var fileMemoryProvider = new FileMemoryProvider( + fileStore, + session => new FileMemoryState { WorkingFolder = $"users/{userId}" }); +``` + +In this sample, memories are written to `agent-memory/users/UID1` under the application's base directory. Because the folder is derived from a fixed user id rather than the session, a new session for the same user picks up the memories written by earlier sessions. + +## Prerequisites + +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) +- A Microsoft Foundry project with a chat model deployment +- Run `az login` to authenticate with `DefaultAzureCredential` + +## Configuration + +Set the following environment variables: + +| Variable | Description | Default | +|---|---|---| +| `FOUNDRY_PROJECT_ENDPOINT` | Your Foundry project endpoint | *(required)* | +| `FOUNDRY_MODEL` | Chat model deployment name | `gpt-5.4-mini` | + +## Running the Sample + +```bash +dotnet run +``` + +## How it Works + +1. A `FileSystemAgentFileStore` is created, rooted at a local `agent-memory` folder. +2. A `FileMemoryProvider` is created over that store, with a state initializer that puts the memories for the current user in their own working folder. +3. The provider is attached to the agent via `ChatClientAgentOptions.AIContextProviders`, which gives the agent the `file_memory_*` tools and instructions for using them. +4. In the first conversation, the user shares some preferences and the agent calls `file_memory_write` to store them as a file in the working folder. The sample then lists the files that were created on disk. +5. In the second conversation, a brand new session is created with no chat history from the first conversation. The provider injects the memory index into the conversation, and the agent calls `file_memory_read` to recall the stored preferences when making its recommendations. diff --git a/dotnet/samples/02-agents/AgentWithMemory/README.md b/dotnet/samples/02-agents/AgentWithMemory/README.md index 752635bbb24..f1dc4064a58 100644 --- a/dotnet/samples/02-agents/AgentWithMemory/README.md +++ b/dotnet/samples/02-agents/AgentWithMemory/README.md @@ -10,6 +10,7 @@ These samples show how to create an agent with the Agent Framework that uses Mem |[Memory with Microsoft Foundry](./AgentWithMemory_Step04_MemoryUsingFoundry/)|This sample demonstrates how to create and run an agent that uses Microsoft Foundry's managed memory service to extract and retrieve individual memories.| |[Bounded Chat History with Overflow](./AgentWithMemory_Step05_BoundedChatHistory/)|This sample demonstrates how to create a bounded chat history provider that overflows older messages to a vector store and recalls them as memories.| |[Memory Using AgentMemory](./AgentWithMemory_Step06_MemoryUsingAgentMemory/)|This sample demonstrates a retail shopping assistant built with [`AgentMemory`](https://www.nuget.org/packages/AgentMemory), an unofficial .NET port of the Neo4j Labs graph-memory provider, to learn customer preferences and recommend products via graph traversal.| +|[File Based Memory](./AgentWithMemory_Step07_FileMemoryProvider/)|This sample demonstrates how to use the `FileMemoryProvider` to give an agent tools for storing and recalling memories as files, and how to configure the folder that those memory files are written to.| > **See also**: [Memory Search with Foundry Agents](../AgentProviders/foundry/Agent_Step22_MemorySearch/) - demonstrates using the built-in Memory Search tool with Microsoft Foundry agents. From 68303cd0a9809da7e536685e71beaf4341ab8efb Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Thu, 30 Jul 2026 08:43:45 +0000 Subject: [PATCH 2/2] Address PR comments --- dotnet/eng/verify-samples/AgentsSamples.cs | 22 +++++++++++++++++++ .../Harness/FileAccess/FileAccessProvider.cs | 3 ++- .../Harness/FileMemory/FileMemoryProvider.cs | 7 +++--- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/dotnet/eng/verify-samples/AgentsSamples.cs b/dotnet/eng/verify-samples/AgentsSamples.cs index 512ce660276..b19217230ae 100644 --- a/dotnet/eng/verify-samples/AgentsSamples.cs +++ b/dotnet/eng/verify-samples/AgentsSamples.cs @@ -510,6 +510,28 @@ internal static class AgentsSamples SkipReason = "Requires a running Neo4j instance; standalone sample outside the repo's CPM build.", }, + new SampleDefinition + { + Name = "AgentWithMemory_Step07_FileMemoryProvider", + ProjectPath = "samples/02-agents/AgentWithMemory/AgentWithMemory_Step07_FileMemoryProvider", + RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"], + OptionalEnvironmentVariables = ["FOUNDRY_MODEL"], + MustContain = + [ + "Memory files will be written to:", + "=== First conversation ===", + "=== Memory files on disk ===", + "=== Second conversation (new session) ===", + ], + ExpectedOutputDescription = + [ + "The output should acknowledge that the user is vegetarian and travels with a dog, indicating the agent stored these preferences.", + "The memory files section should list at least one memory file written by the agent, such as a file about the user's preferences.", + "The second conversation should recommend a hotel and a restaurant in Paris that are consistent with the remembered preferences, for example a pet-friendly hotel and a restaurant with vegetarian options, even though it is a new session.", + "The output should not contain error messages or stack traces.", + ], + }, + // ── AgentWithRAG ──────────────────────────────────────────────────── new SampleDefinition diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/FileAccess/FileAccessProvider.cs b/dotnet/src/Microsoft.Agents.AI/Harness/FileAccess/FileAccessProvider.cs index afdf4475c67..f42335b7986 100644 --- a/dotnet/src/Microsoft.Agents.AI/Harness/FileAccess/FileAccessProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI/Harness/FileAccess/FileAccessProvider.cs @@ -22,7 +22,8 @@ namespace Microsoft.Agents.AI; /// /// The gives agents the ability to work with files /// in a folder that the user has granted access to. Unlike , -/// which provides session-scoped memory that may be isolated per session, +/// which provides agent-managed memory files whose scope is determined by the working folder it is +/// configured with, /// operates on a shared, persistent folder whose contents are visible across sessions and agents. /// This makes it suitable for reading input data, writing output artifacts, and working with /// files that have a lifetime beyond any single agent session. diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/FileMemory/FileMemoryProvider.cs b/dotnet/src/Microsoft.Agents.AI/Harness/FileMemory/FileMemoryProvider.cs index b2cf88f9f7c..680fdfcef28 100644 --- a/dotnet/src/Microsoft.Agents.AI/Harness/FileMemory/FileMemoryProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI/Harness/FileMemory/FileMemoryProvider.cs @@ -69,8 +69,9 @@ public sealed class FileMemoryProvider : AIContextProvider, IDisposable private const string DefaultInstructions = """ ## File Based Memory - You have access to a session-scoped, file-based memory system via the `file_memory_*` tools for storing and retrieving information across interactions. - These files act as your working memory for the current session and are isolated from other sessions. + You have access to a file-based memory system via the `file_memory_*` tools for storing and retrieving information across interactions. + These files act as your working memory and persist beyond the current conversation, so they may already contain memories written earlier, + and anything you write now may remain available later. Use these tools to store plans, memories, processing results, or downloaded data. - Use descriptive file names (e.g., "projectarchitecture.md", "userpreferences.md"). @@ -507,7 +508,7 @@ private static bool IsInternalFile(string fileName) => /// /// Returns if the normalized file name points into a subdirectory. - /// File memory is a flat, session-scoped space, so nested names are rejected up front. + /// File memory is a flat namespace within the working folder, so nested names are rejected up front. /// private static bool IsNestedPath(string normalizedFileName) => normalizedFileName.IndexOf('/') >= 0;