Semantic Kernel had memory. It had RAG concepts, conversation history plugins, vector store integrations — all the ingredients. But it was heavy. Gravitational. You didn’t bolt on Semantic Kernel; you migrated into its orbit. Everything was our way or nothing.

Microsoft.Extensions.AI took a different bet. Not a framework — just abstractions. IChatClient, IEmbeddingGenerator, VectorStore. Small interfaces you can implement, wrap, or compose without buying into a whole worldview. It flipped the weight class from adopt our platform to bring your own pieces.

Memori is that bet in action. It adds durable memory to the pipeline the same way you add any other middleware — by filling a gap, not pulling you into a framework. It is the jelly between the layers you already chose. Small, composable, and very much not the answer to everything.

What changed in the .NET ecosystem

The reason a library like Memori is possible now and would have been awkward three years ago is that the .NET platform has added or matured several foundational pieces:

  • System.Numerics and System.Numerics.Tensors — local vector math without external dependencies
  • Microsoft.Extensions.AIIChatClient and IEmbeddingGenerator as unified abstractions across AI providers
  • Microsoft.Extensions.VectorData — a standard VectorStore contract that works with any backend

These three give .NET what Python has had for years: vector math, AI client abstractions, and storage contracts. But there is a fourth layer — less technical, more cultural — that matters just as much.

Microsoft Learn now surfaces through MCP. That means the documentation is agent-discoverable, not just human-readable. An AI coding agent can browse the same reference material a developer would — resolve APIs, fetch signatures, trace how pieces connect. In the era of vibe coding, where engineers build alongside agents as much as by hand, this consistency is the difference between explorable and searchable. The platform surface is something you can navigate through conversation.

More than that: the concepts themselves are no longer gated by runtime tribe. You do not need to be a Python data scientist or a C++ performance engineer to work with embeddings, vector search, or semantic memory. An AI agent can scaffold the implementation, explain the math at whatever depth you ask, and help iterate. We used agents to build deterministic n-gram embedders — the kind of thing that used to mean digging through academic papers and writing native interop. The same agent that helps you wire up IChatClient can walk you through cosine similarity.

LLMs are the great equalizer. The concepts are no longer bound to any particular library, framework, or runtime. You can learn them in conversation and implement them in whatever stack you already own.

Memori is an expression of that shift. It does not ship its own embedding provider, vector database, or chat model. It uses the ones the platform already defines — and trusts that an engineer who wants durable memory can explore, understand, and compose those pieces without being handed a monolithic framework.

Where Memori fits in

An IChatClient pipeline handles inference well. You send messages, you get a response. But nothing in that abstraction preserves what was learned across turns or sessions.

Standard workarounds have known failure modes:

  • Manual history cramming: The application appends all previous messages to every request. This works until context windows fill up, token costs grow, and irrelevant history dilutes the signal.
  • Stateless pipelines: Each turn is independent. The model never learns preferences, never remembers facts, and never synthesizes across sessions.
  • Provider-specific memory APIs: Some AI providers offer memory-like features, but they lock you into their ecosystem and don’t transfer if you switch providers for cost, latency, or capability reasons.

These are not design preferences. They are operational constraints that emerge at different scales. A demo works with any of them. A production service needs something different.

What Memori does instead

Memori adds a persistent, searchable memory layer to any IChatClient pipeline. It is built around four operations:

  • Capture: persist conversation messages to durable storage
  • Recall: retrieve relevant facts for a given query using vector, lexical, or hybrid search
  • Augmentation: extract structured memory (facts, triples, summaries) from captured conversations in the background
  • Injection: format recalled memory as prompt context and insert it into the chat pipeline automatically

These operations are available at multiple API levels — a design decision that came from experience with what different teams actually need.

The layer cake

Level 1: DI extensions
  AddMemori() with config binding and factory overrides
  → Minimal ceremony, one-call setup

Level 2: Middleware
  UseMemori() on IChatClient pipeline
  → The most common path — automatic recall-before, capture-after

Level 3: Facade
  MemoriEngine: attribution + capture + recall + augmentation in one class
  → Application code that wants programmatic control

Level 4: Raw abstractions
  IConversationStorage, VectorStoreCollection, IAugmentationClient
  → Storage backend authors, custom integration engineers

Each level builds on the one below. Teams can start at level 1 or 2 for a quick integration and drop to level 3 or 4 when they need finer control. The same storage contracts and semantics apply at every level.

This layered design was not the original plan. The first version of the library exposed only the facade and middleware, assuming most users would wire storage explicitly. Feedback showed that even the “quick start” required choosing too many abstractions on day one — IConversationStorage, InMemoryVectorStore, MemoryFactRecord, the facade constructor signature, the middleware wrapper. That was six new concepts before the first message could be sent.

So we added AddMemoriWithDefaults() — a single DI call that registers in-memory storage, an embedding generator, the facade, and the middleware in one step. The 10-line setup became one. Users can peel back layers as they outgrow the defaults.

A quick taste

The default path is intentionally short:

services.AddMemori(options =>
{
    options.SessionTimeout = TimeSpan.FromMinutes(30);
});

IChatClient client = new ChatClientBuilder(innerClient)
    .UseMemori(memori)
    .Build(serviceProvider);

// Turn 1 — capture happens after the model responds
await client.CompleteAsync([
    new ChatMessage(ChatRole.User, "My favorite color is blue.")
]);

// Turn 2 — Memori recalls the stored fact before the model call
var response = await client.CompleteAsync([
    new ChatMessage(ChatRole.User, "What is my favorite color?")
]);
// → "Your favorite color is blue."

The middleware handles recall, injection, and capture on every turn. The application only manages attribution (who does this memory belong to?) and sessions (which conversation is this part of?).

Storage contracts, not storage implementations

One of the earliest design decisions was also the most consequential: Memori would not ship first-party database providers.

That means there is no Memori.EntityFrameworkCore, no Memori.AzureAISearch, no Memori.Qdrant. The library defines the contract — IConversationStorage for history, VectorStoreCollection<string, MemoryFactRecord> for facts — and teams provide the implementation.

This decision came from a specific lesson. Early in development, the library had a single IStorage interface that covered everything: conversations, entities, embeddings, raw retrieval. It was monolithic and forced every provider implementation to deal with concerns it did not care about. A team using a relational database for conversations and a vector database for facts had no clean way to split the two.

Splitting IStorage into IConversationStorage (append-heavy, ordered, relational) and VectorStoreCollection (search-heavy, vector-indexed, key-value) fixed that. But it also meant the library could not provide a single “turnkey” storage backend — any production-capable database needs its own adapter.

That is the right tradeoff. A memory library should not be the bottleneck between your team and your chosen infrastructure. The contracts are simple enough that a motivated developer can implement IConversationStorage in an afternoon using their existing database. And any VectorStore provider — Azure AI Search, Qdrant, PostgreSQL with pgvector, SQLite with vec0 — works as the fact store without a Memori-specific adapter.

A useful rule of thumb

If you cannot tell whether your memory layer is storage-coupled or provider-coupled, you are not using an abstraction yet.

Storage-coupled means switching from PostgreSQL to Cosmos DB requires rewriting memory code. Provider-coupled means switching from OpenAI to Azure or Anthropic requires rewriting memory code. A storage-agnostic, provider-agnostic abstraction should isolate both concerns. Memori does, by design.

What comes next

This post is the motivation, not the full model.

The core design rests on a few abstractions that need their own treatment:

  • how the two-storage split actually works
  • how recall flows from query to prompt context
  • how the middleware preserves correct semantics for streaming and non-streaming paths
  • how background augmentation extracts facts without blocking the chat pipeline

That is the subject of the next post. The test of any memory abstraction is not whether it can store and retrieve — that is the easy part. The test is whether the operational semantics are clear enough that you trust it in production.

For a closer look at the code and examples, check out the Memori repo: https://github.com/khurram-uworx/Memori

NuGet: https://www.nuget.org/packages/Memori

Getting started: https://github.com/khurram-uworx/Memori/blob/main/GETTING-STARTED.md

Architecture and design notes: https://github.com/khurram-uworx/Memori/blob/main/ARCHITECTURE.md