# Microsoft Agent Framework .NET: persisting AG-UI threads (from MapAGUI to MapAGUIServer and AgentSessionStore)

> Source: <https://dev.to/memorysync_rafay/microsoft-agent-framework-net-persisting-ag-ui-threads-from-mapagui-to-mapaguiserver-and-5h3f>
> Published: 2026-10-26 16:38:28+00:00

*Disclosure: I'm Mohammed, founder of MemorySync. I drafted this article with AI assistance and reviewed it for accuracy against the official Microsoft Agent Framework docs. The code was built and run locally against the 1.26.0-preview packages with a stub chat client (no real model call).*

If your AG-UI front end talks to an Agent Framework .NET server and the agent forgets the previous message on every turn, you've hit [microsoft/agent-framework#2517](https://github.com/microsoft/agent-framework/issues/2517), ".NET: Thread persistence does not work by default in MapAGUI". `MapAGUI` called the agent without a thread, so every request got a new thread and a new `ChatMessageStore`, even when the AG-UI `threadId` was the same. The only workaround was to carry the thread ID through `AsyncLocal` into a custom store.

The issue was closed on Aug 18, 2026. In the closing comment, a maintainer said it's resolved by the current `MapAGUIServer` hosting: the AG-UI `threadId` loads or creates an `AgentSession`, which is passed to the agent and saved after streaming. The old `AgentThread`/` ChatMessageStoreFactory` model was replaced by `AgentSession`/` AgentSessionStore`.

Below is that setup step by step, with the two things that tripped me up when I ran it. Framework behaviour comes from Microsoft's [AG-UI getting started](https://learn.microsoft.com/en-us/agent-framework/integrations/by-component/ui/ag-ui/getting-started) and [self-hosting](https://learn.microsoft.com/en-us/agent-framework/hosting/self-hosting/) docs, plus the package API. I built and ran the code with `Microsoft.Agents.AI.Hosting.AGUI.AspNetCore` 1.26.0-preview.261023.1 on .NET 10, using a stub `IChatClient` instead of a real model.

```
dotnet add package Microsoft.Agents.AI.Hosting.AGUI.AspNetCore --prerelease
dotnet add package Microsoft.Agents.AI.Hosting.AspNetCore --prerelease  # only needed for claims-based isolation (Step 4)
```

`MapAGUI` no longer exists in the current package (the build fails with CS1061). The method is `MapAGUIServer`.

Persistence is opt-in. Without an `AgentSessionStore`, every request still gets a new session, which is the behaviour from the issue. Register the agent through hosting DI, attach a store, and map it by name:

```
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting;
using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
using Microsoft.Extensions.AI;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddAGUIServer();

IChatClient chatClient = CreateChatClient(); // your Azure OpenAI, OpenAI or other IChatClient

AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
{
    Id = "assistant", // keep it stable (see Step 5)
    Name = "assistant",
    ChatOptions = new ChatOptions { Instructions = "You are a helpful assistant." },
});

builder.AddAIAgent("assistant", (_, _) => agent)
    .WithInMemorySessionStore(withIsolation: false); // local development, one trusted user only

WebApplication app = builder.Build();
app.MapAGUIServer("assistant", "/ag-ui");
await app.RunAsync();
```

The client sends only the new message on each turn, with the same `threadId` and the previous `runId` as `parentRunId`. With my stub model, two requests on `thread-1` behaved like this:

| Setup | Turn 2 ("What is my name?") saw | 
|---|---|
| No session store | only the new message | 
| `WithInMemorySessionStore(withIsolation: false)` | both messages | 

`WithInMemorySessionStore()` into an app without isolation
The issue's closing comment shows `.WithInMemorySessionStore()` with no arguments. With the current package that turns on isolation, and if nothing provides an isolation key, every request fails with a 500:

```
System.InvalidOperationException: Agent isolation key is required but was not provided by the configured AgentIsolationKeyProvider.
```

That's by design. The AG-UI `threadId` comes from the client, and the docs are explicit that it's a correlation ID, not authorization: anyone who guesses another user's `threadId` could resume their conversation. So either pass `withIsolation: false` for single-user local development, or configure isolation as in Step 4.

The self-hosting docs register a claims-based isolation provider from `Microsoft.Agents.AI.Hosting.AspNetCore`. By default it uses the `ClaimTypes.NameIdentifier` claim:

```
builder.Services.AddAuthentication(/* your scheme */);
builder.Services.AddAuthorization();
builder.Services.AddHttpContextAccessor(); // the provider reads the current user from HttpContext
builder.Services.UseClaimsBasedAgentIsolation();

builder.AddAIAgent("assistant", (_, _) => agent)
    .WithInMemorySessionStore(); // isolation stays on

WebApplication app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapAGUIServer("assistant", "/ag-ui").RequireAuthorization();
```

Two notes from running it. Without `AddHttpContextAccessor()` the request failed with "No service for type 'IHttpContextAccessor' has been registered." (that line is my addition, not in the docs). With it, and a test user signed in, the same `threadId` gave each user a separate conversation, and a request with no user returned 500. The provider doesn't authenticate anyone, so you still need real ASP.NET Core authentication.

The GitHub sample's comment mentions `UseClaimsBasedSessionIsolation`. In 1.26.0-preview the method is `UseClaimsBasedAgentIsolation`, which matches the Learn docs.

`InMemoryAgentSessionStore` loses everything when the process exits, and the docs say the framework doesn't ship a general-purpose durable store. You implement `AgentSessionStore` yourself. Here's a minimal file-backed one, using the agent's own session serialization as the docs recommend:

```
#pragma warning disable MAAI001 // AgentSessionStore is marked evaluation-only in the preview packages
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.Agents.AI;

sealed class FileAgentSessionStore(string dir) : AgentSessionStore
{
    string PathFor(AIAgent agent, AgentSessionStoreKey key)
    {
        string partitions = string.Join(",", (key.Partitions ?? new Dictionary<string, string>())
            .OrderBy(p => p.Key).Select(p => $"{p.Key}={p.Value}"));
        string raw = $"{agent.Id}|{key.SessionId}|{partitions}";
        Directory.CreateDirectory(dir);
        return Path.Combine(dir, Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(raw))) + ".json");
    }

    public override async ValueTask SaveSessionAsync(AIAgent agent, AgentSessionStoreKey key, AgentSession session, CancellationToken cancellationToken = default)
    {
        JsonElement state = await agent.SerializeSessionAsync(session, cancellationToken: cancellationToken);
        await File.WriteAllTextAsync(PathFor(agent, key), state.GetRawText(), cancellationToken);
    }

    public override async ValueTask<AgentSession?> GetSessionAsync(AIAgent agent, AgentSessionStoreKey key, CancellationToken cancellationToken = default)
    {
        string path = PathFor(agent, key);
        if (!File.Exists(path)) return null;
        using JsonDocument doc = JsonDocument.Parse(await File.ReadAllTextAsync(path, cancellationToken));
        return await agent.DeserializeSessionAsync(doc.RootElement.Clone(), cancellationToken: cancellationToken);
    }

    public override ValueTask DeleteSessionAsync(AIAgent agent, AgentSessionStoreKey key, CancellationToken cancellationToken = default)
    {
        File.Delete(PathFor(agent, key));
        return ValueTask.CompletedTask;
    }
}
```

Register it in place of the in-memory store:

``` js
builder.AddAIAgent("assistant", (_, _) => agent)
    .WithSessionStore(new FileAgentSessionStore("sessions")); // isolation on by default, as in Step 4
```

Three things I found when I ran this:

`#pragma`.` AgentSessionStore` and `AgentSessionStoreKey` are flagged `MAAI001` ("for evaluation purposes only and is subject to change or removal"), which is an error by default.`Id`.` agent.Id` and the session ID. Without `ChatClientAgentOptions`, the ID was a new GUID on every start, so after a restart the store looked in a different file and the conversation was gone. With `Id = "assistant"`, turn 2 after a restart saw turn 1.
A JSON file per conversation is fine for a demo. In production, put the same three methods over your database. The docs point out that a session can hold more than chat history (context-provider state, pending approvals), so persist the whole session, and protect it like user data.

All of this restores one conversation by its `threadId`. A new thread starts empty. If you want facts about a user to carry into new threads, that's long-term memory. MemorySync is a hosted memory layer, and its [Agent Framework guide](https://docs.memorysync.io/guides/agent-framework?utm_source=devto&utm_medium=social&utm_campaign=pivot_oct19&utm_content=agent-framework-dotnet-agui-thread-persistence-docs) covers the Python package. It also has a [REST API](https://docs.memorysync.io/api/memory?utm_source=devto&utm_medium=social&utm_campaign=pivot_oct19&utm_content=agent-framework-dotnet-agui-thread-persistence). There's no .NET package, so I'm not covering it here.

`threadId`. Send the same `runId` as `parentRunId`.` builder.Services.AddHttpContextAccessor()`.` MapAGUI` doesn't compile.`MapAGUIServer`.
**Does MapAGUIServer persist conversation history by default?**

No. Without an `AgentSessionStore`, sessions are ephemeral and each request gets a new server session. Configure a store, as in the closing comment of issue #2517 and the [self-hosting docs](https://learn.microsoft.com/en-us/agent-framework/hosting/self-hosting/).

**Is the AG-UI threadId safe to use as a session key?**

Not on its own. The docs call it protocol data, not an authorization credential. Scope sessions to the authenticated user, for example with `UseClaimsBasedAgentIsolation()` ([self-hosting docs](https://learn.microsoft.com/en-us/agent-framework/hosting/self-hosting/)).

**What replaced ChatMessageStoreFactory and AgentThread?**

`AgentSession` and `AgentSessionStore`. The store saves the whole session selected by a hosted request, not only its messages ([self-hosting docs](https://learn.microsoft.com/en-us/agent-framework/hosting/self-hosting/)).

**Does Agent Framework ship a durable session store?**

No. The docs say it doesn't include a general-purpose durable store, and `InMemoryAgentSessionStore` loses all sessions when the process exits. Implement `AgentSessionStore` over your own storage.

`dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs`
If you want to see a memory survive a full page reload, the MemorySync playground runs the save, reload and recall loop in your browser: [memorysync.io/try](https://memorysync.io/try?utm_source=devto&utm_medium=social&utm_campaign=pivot_oct19&utm_content=agent-framework-dotnet-agui-thread-persistence).
