Table of Contents

Introduction

Hey Everyone!

You can find all the C# code samples here: MicrosoftAgentFramework GitHub Repo

Before you waste days or even weeks building custom database plumbing for AI memory from scratch, it is worth looking at what is already available. Today, I am breaking down one of the absolute best candidates for that job: Mem0.

Reading these posts might be helpful as they are also related to AI Memory:

This article is a complete guide to Getting Started with Mem0, and I will show you how to build .NET AI Memory profiles that persist across multiple chat sessions. While I will focus on a Mem0 Microsoft Agent Framework integration using C#, the core architectural concepts we discuss here are completely technology-agnostic. You can easily apply this knowledge to Python, Node.js, or any other stack you use.

Why do we need memory?

To understand why we need a dedicated memory layer, we first need to look at the main limitation of Large Language Models (LLMs): they are completely stateless.

If you open a chat window and tell the AI, “I live in Poland close to Krakow and I like to travel,” it knows that fact only during that specific chat session. If you start a brand-new chat session and ask, “What 3 places should I visit in the city I live next to?”, the AI will answer, “I do not know which country or city you live in.”

Below is an example you can find in the GitHub repo (Mem0ProviderExample) without any memory added.

[User]: I live in Poland close to Krakow and I like to travel.
[Agent]: Nice - Kraków is a great base for travel. You're close to a lot of good options in Poland and Europe.

A few ideas, depending on what kind of trips you like:

- **City breaks:** Warsaw, Wrocław, Gdańsk, Prague, Vienna, Budapest
- **Nature:** Tatra Mountains, Pieniny, Bieszczady, Slovak mountains, Alpine regions
- **Weekend road trips:** Zakopane, Ojców National Park, Wieliczka, Silesia region
- **Longer travel:** Italy, Croatia, Greece, Spain, and many cheap flights from nearby airports

If you want, I can help you with:
1. **trip ideas from Kraków**,
2. **budget travel**,
3. **train/flight suggestions**, or
4. **a personalized travel plan** based on your interests.

========================================================
[User]: What 3 places should I visit in the city I live next to and what food should I eat?
[Agent]: Sure - I can do that, but I need one detail first: **what city do you live next to?**

If you want, you can also tell me:
- your **country**
- what kind of places you like: **nature, history, museums, nightlife, food, family-friendly**
- any **dietary preferences** for the food recommendation

Reply with the city name and I'll give you **3 places to visit + what food to eat** there.

In standard applications, we solve this inside a single session by sending the entire chat history back and forth with every new prompt. But the moment a session closes or a new chat thread starts, that history disappears. True AI memory must persist across completely different sessions.

Memory vs. RAG (Retrieval-Augmented Generation)

It is common to confuse AI memory with RAG, but they serve two very different purposes:

  • RAG (Knowledge Base): This is about fetching external organizational data (like company documents or product manuals) to give the LLM more facts.
  • Episodic Memory (AI Memory): This is about tracking what happened before. It focuses on learning from user interactions, distilling raw chat logs, and retaining user preferences over long periods.

Intro to Mem0 with C# and MAF

The Mem0 platform provides official SDKs for Python and Node.js. Under the hood, these SDKs communicate with a managed cloud platform via a clean REST API. Since there is no official .NET client yet, I built a custom wrapper called Mem0MemoryProvider to handle Persistent AI Context within the Microsoft Agent Framework (MAF).

How the Context Provider Connects

When integrating memory into an AI framework, the orchestration relies on a dual-phase lifecycle block:

  1. The Search Phase (Before the LLM Call): When a user sends a prompt, the application intercepts it and invokes a search operation against Mem0. This fetches relevant past memories based on the user’s query and injects them directly into the LLM’s system prompt.
  2. The Storage Phase (After the LLM Call): Once the LLM generates its response, the application streams the full conversation snippet back to Mem0 using an add operation. Mem0 analyzes the text, extracts the core facts, and updates the database asynchronously.

Here is how this architecture looks in a real C# implementation:

public class Mem0MemoryProvider : MessageAIContextProvider, IDisposable
{
    private readonly HttpClient _httpClient;
    private readonly ProviderSessionState<Mem0ProviderState> _sessionState;
    private bool _disposed;

    public Mem0MemoryProvider(
        string apiKey,
        Func<AgentSession?, Mem0ProviderState> stateInitializer)
    {
        ArgumentNullException.ThrowIfNull(stateInitializer);

        _sessionState = new ProviderSessionState<Mem0ProviderState>(
            stateInitializer,
            stateKey: GetType().Name);

        _httpClient = new HttpClient
        {
            BaseAddress = new Uri("https://api.mem0.ai/")
        };
        _httpClient.DefaultRequestHeaders.Add("Authorization", $"Token {apiKey}");
    }

    protected override async ValueTask<IEnumerable<ChatMessage>> ProvideMessagesAsync(InvokingContext context, CancellationToken cancellationToken = default)
    {
		// trimmed for brevity, check the full code in the GitHub repo
	
        var searchPayload = new Dictionary<string, object>
        {
            ["query"] = userQuery
        };

        var state = _sessionState.GetOrInitializeState(context.Session);
        ApplySearchScope(searchPayload, state.SearchScope);

        var response = await _httpClient.PostAsJsonAsync("v3/memories/search/", searchPayload, cancellationToken);
        var memoryContext = "Relevant information from previous conversations:\n" +
                            string.Join("\n- ", retrievedMemories);

        return [new ChatMessage(ChatRole.System, memoryContext)];
    }

    protected override async ValueTask StoreAIContextAsync(InvokedContext context, CancellationToken cancellationToken = default)
    {
        // trimmed for brevity, check the full code in the GitHub repo

        var addPayload = new Dictionary<string, object>
        {
            ["messages"] = new[]
            {
                new { role = "user", content = lastUserMessage },
                new { role = "assistant", content = assistantResponse }
            }
        };

        var state = _sessionState.GetOrInitializeState(context.Session);
        ApplyStorageScope(addPayload, state.StorageScope);

        var response = await _httpClient.PostAsJsonAsync("v3/memories/add/", addPayload, cancellationToken);
    }
}

Understanding Memory Scopes (Entities)

A screenshot of the Mem0 platform dashboard interface filtering memories by the specific user entity "Michal 123" to demonstrate persistent AI context. The dashboard displays metrics showing 5 total memories across 19 total requests. The filtered table displays stored profiles categorized into health, personal details, and travel. Text annotations trace how individual logs map back to a .NET AI memory execution lifecycle, explicitly labeling rows for Session 1 (location and travel interests), Session 3 (unit preferences), an external Postman API call (fitness constraints), and Session 4 (training schedule request).

When you write or search for memories, you must tag your payloads with specific identifiers. These identifiers act as a strict database scope to keep data separated cleanly:

  • user_id: Identifies the individual human user (e.g., pulling from a JWT token).
  • app_id: Identifies the specific corporate application environment.
  • agent_id: Identifies the distinct AI persona.
  • run_id: Tracks the unique chat session or execution GUID.

This is how I assign these identifiers for the storageScope (add operation) and searchScope (search operation).

public Mem0ProviderExample()
{
    var openAiClient = new AzureOpenAIClient(new Uri(Environment.GetEnvironmentVariable("AZURE_OPEN_AI_RESPONSES_CLIENT_URI")!), new DefaultAzureCredential());

    var responesClient = openAiClient
        .GetResponsesClient()
        .AsIChatClientWithStoredOutputDisabled(Environment.GetEnvironmentVariable("AZURE_OPEN_AI_MODEL_DEPLOYMENT_NAME")!)
        .AsBuilder()
        .Use(inner => new InspectingChatClient(inner))
        .Build();

    _mem0MemoryProvider = new Mem0MemoryProvider(
            apiKey: Environment.GetEnvironmentVariable("MEM0_API_KEY")!,
            sessionState =>
            {
                var executionContext = (sessionState?.GetSessionExecutionContext()) ?? throw new InvalidOperationException("Execution context is not initialized");

                return new Mem0ProviderState(
                    storageScope: new Mem0ProviderScope
                    {
                        RunId = executionContext.RunId,
                        UserId = executionContext.UserId,
                        AppId = executionContext.ApplicationId,
                        AgentId = executionContext.AgentId
                    },
                    searchScope: new Mem0ProviderScope
                    {
                        UserId = executionContext.UserId,
                        AppId = executionContext.ApplicationId
                    });
            });

    _mafAgent = new ChatClientAgent(responesClient, new ChatClientAgentOptions()
    {
        Name = "Agent which uses Mem0",
        ChatOptions = new ChatOptions
        {
            Instructions = "You are a helpful assistant.",
            Reasoning = new ReasoningOptions() { Effort = ReasoningEffort.None, Output = ReasoningOutput.None }
        },
        AIContextProviders = [_mem0MemoryProvider]
    });
}

Live Code Walkthrough: Testing Cross-Session Isolation

To prove how this works, my example application simulates four consecutive user chat sessions. Each session runs on a completely fresh thread with a distinct run_id.

public async Task RunAsync()
{
    AgentSession firstSession = await _mafAgent.CreateSessionWithExecutionContext(new SessionExecutionContext()
    {
        RunId = Guid.NewGuid().ToString(),
        UserId = "Michal 123",
        ApplicationId = "travel_assistant",
        AgentId = "travel_assistant_agent"
    });

    Console.WriteLine("[User]: I live in Poland close to Krakow and I like to travel.");
    var response1 = await _mafAgent.RunAsync("I live in Poland close to Krakow and I like to travel", firstSession);
    Console.WriteLine($"[Agent]: {response1}");


    Console.WriteLine("\n========================================================");

    AgentSession secondSession = await _mafAgent.CreateSessionWithExecutionContext(new SessionExecutionContext()
    {
        RunId = Guid.NewGuid().ToString(),
        UserId = "Michal 123",
        ApplicationId = "travel_assistant",
        AgentId = "travel_assistant_agent"
    });

    Console.WriteLine("[User]: What 3 places should I visit in the city I live next to and what food should I eat?");
    var response2 = await _mafAgent.RunAsync("What 3 places should I visit in the city I live next to and what food should I eat?", secondSession);
    Console.WriteLine($"[Agent]: {response2}");

    Console.WriteLine("\n========================================================");

    AgentSession thirdSession = await _mafAgent.CreateSessionWithExecutionContext(new SessionExecutionContext()
    {
        RunId = Guid.NewGuid().ToString(),
        UserId = "Michal 123",
        ApplicationId = "gym_coach_assistant",
        AgentId = "gym_coach_assistant_agent"
    });

    Console.WriteLine("[User]: Based on the country I live in, should my strength tracking default to kilograms or pounds?");
    var response3 = await _mafAgent.RunAsync("Based on the country I live in, should my strength tracking default to kilograms or pounds?", thirdSession);
    Console.WriteLine($"[Agent]: {response3}");

    AgentSession fourthSession = await _mafAgent.CreateSessionWithExecutionContext(new SessionExecutionContext()
    {
        RunId = Guid.NewGuid().ToString(),
        UserId = "Michal 123",
        ApplicationId = "gym_coach_assistant",
        AgentId = "gym_coach_assistant_agent"
    });

    Console.WriteLine("\n========================================================");

    Console.WriteLine("[User]: prepare training for this week, each should have just 3 exercises");
    var response4 = await _mafAgent.RunAsync("prepare training A and B for this week, each should have just 3 exercises", fourthSession);
    Console.WriteLine($"[Agent]: {response4}");
}

Here is exactly what happens behind the scenes during execution:

  • Session 1 (Travel Assistant): The user states their location and travel interests. The provider catches the assistant’s reply and triggers an async add request to Mem0.
  • Session 2 (Travel Assistant): The user starts a brand-new thread and asks for local recommendations. The provider executes a search scoped to the current user_id and app_id. It successfully rehydrates the context, allowing the agent to correctly recommend spots around Krakow.
  • Session 3 (Gym Coach Assistant): The user switches to a completely different application profile (gym_coach_assistant). Because our search criteria enforces an intersection of both user and app IDs, the travel preferences are safely isolated. The gym coach agent remains unaware of the Krakow data and asks the user to clarify their location…
A screenshot of the Postman API client executing a POST request to the Mem0 V3 API endpoint (/v3/memories/add/) to manually write raw conversation data. The JSON request body outlines a user and assistant exchange about custom full-body workout constraints, explicitly configuring scope entities like user_id "Michal 123" and app_id "gym_coach_assistant". This demonstrates how to inject persistent AI context from an external background system independently of the primary application pipeline.
  • Session 4 (Gym Coach Assistant): Before running this session, I add a memory via Postman to simulate an external system (like a CRM) updating the user profile behind the scenes (it does not have to be always your agent). I add that the user prefers full-body workouts and can only train twice a week. When the user asks for a weekly training plan, the agent instantly uses this new context to prepare the correct routine.

How Mem0 works behind the scenes

Mem0 recently updated its core engine pipeline to version 3 (V3). Understanding what changed between versions gives great insight into building an optimized, production-ready memory engine.

The Ingestion Pipeline (V3 Upgrades)

A technical flowchart diagram showing how the Mem0 architecture manages persistent AI context during the memory extraction phase before the V3 engine update. The process shows a raw conversation stream moving through context lookup, an LLM call for fact extraction, a second LLM call for deduplication, embeddings generation, and entity linking before saving data into the final memory database.

In older versions of the engine, storing a memory required two separate LLM calls: one call to extract raw facts from a conversation stream, and a second call to evaluate if those facts should add, update, or delete an existing record.

To reduce high latency and lower running costs, the V3 engine completely removed that second LLM call. The new pipeline operates via a streamlined layout:

  • Fact Extraction: A single optimized LLM call processes the raw text to extract clean, rephrased statements.
  • Hash-Based Deduplication: Instead of relying on an expensive LLM evaluation to find duplicates, V3 uses strict hash matching to instantly filter out exact text replicas.
  • Append-Only Writes: The engine favors fast, batch vector database insertions over immediate row updates. Cleanups can be offloaded entirely to background optimization processes.
A technical flowchart illustrating the optimized V3 extraction phase of Mem0 for handling persistent AI context. The diagram shows how the second LLM call during the deduplication step is removed—allowing the pipeline to flow from a single LLM call for fact extraction directly to hash-based deduplication (add only), embeddings generation, and entity linking before saving data to the final memory database.

The Retrieval & Search Pipeline

When you query the v3/memories/search/ endpoint, Mem0 uses a multi-layered search infrastructure combining standard relational databases, vector storage, and advanced graph databases:

  • Primary Candidate Search: Semantic search (vector similarity) acts as the primary tool to gather candidate results from the vector database.
  • Keyword & Entity Boosting: Traditional keyword searches (like BM25) and graph-based entity lookups are executed simultaneously. Instead of merging independent results via Reciprocal Rank Fusion (RRF), Mem0 uses these matching values as active boost signals to adjust the confidence scores of your primary semantic candidates.
  • Temporal Memory Decay: You can enable a built-in time-decay function. This multiplies older memory scores by a decay factor fluctuating between 0.3 and 1.5. If a memory hasn’t been accessed or verified recently, its relevance score drifts downward over time, prioritizing fresh user behavior.
A technical flowchart diagram showcasing the retrieval phase ("search" API) of Mem0 used to provide persistent AI context. The diagram illustrates how the Query Engine processes requests in parallel across four paths: Semantic Search (contextual meaning), Keyword Search (deterministic tokens), Entity Search (object and identity), and Temporal Search (time-weighted results), before combining all signals into an aggregated context block for .NET AI memory integration.

Is it worth to use Mem0?

Personally, I really like this service because it prevents you from having to design complex relational schemas and embedding syncing pipelines manually. However, for strict enterprise settings, you must evaluate a clear architectural trade-off:

Enterprise Notice: I highly recommend avoiding the managed cloud-hosted version of Mem0 if you handle highly sensitive corporate or customer data due to security and compliance boundaries.

If you are dealing with strict data protection compliance, you should favor a self-hosted configuration or point the engine directly toward your own private cloud infrastructure. Alternatively, if your engineering team has the time and skills, you can replicate this exact pattern inside Azure by combining Azure AI Search (using hybrid search and semantic rerankers) with a graph database for tracking core metadata entities.

Summary

Mem0 is a fantastic, developer-friendly way to introduce persistent state management to stateless AI models. It completely changes how we approach chat history by acting as an intelligent, long-term memory layer rather than a basic text archive.

Thanks for reading and see you in the next post.

Categorized in:

Agents,