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, ".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 and 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.
Step 1: Install the hosting packages
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.
Step 2: Register the agent and a session store
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 |
Step 3: Don't copy the bare 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.
Step 4: Scope sessions to the signed-in user
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.
Step 5: Survive restarts with your own store
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:
builder.AddAIAgent("assistant", (_, _) => agent)
.WithSessionStore(new FileAgentSessionStore("sessions")); // isolation on by default, as in Step 4
Three things I found when I ran this:
-
The build fails without the
#pragma.AgentSessionStoreandAgentSessionStoreKeyare flaggedMAAI001("for evaluation purposes only and is subject to change or removal"), which is an error by default. -
Give the agent a stable
Id. The docs say to key records byagent.Idand the session ID. WithoutIdinChatClientAgentOptions, 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. WithId = "assistant", turn 2 after a restart saw turn 1. - Include the key's partitions. With isolation on, the store key carries extra partitions. Leaving them out of the file name would let two users share a file.
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.
What about memory across threads?
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 covers the Python package. It also has a REST API. There's no .NET package, so I'm not covering it here.
Troubleshooting
-
Every turn forgets the last one. No session store is registered, or the client is sending a new
threadId. Send the samethreadIdand the previousrunIdasparentRunId. - 500: "Agent isolation key is required". Isolation is on, but there's no provider. See Steps 3 and 4.
-
500: "No service for type IHttpContextAccessor". Add
builder.Services.AddHttpContextAccessor(). -
History is lost after a restart. You're on the in-memory store, or your agent has no stable
Id. -
MapAGUIdoesn't compile. UseMapAGUIServer.
Common questions
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.
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).
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).
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.
Sources
- GitHub issue microsoft/agent-framework#2517 and its closing comment (Aug 18, 2026)
- Microsoft Learn: AG-UI getting started (integrations/by-component/ui/ag-ui/getting-started) and self-hosting (hosting/self-hosting)
- microsoft/agent-framework:
dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs - NuGet: Microsoft.Agents.AI.Hosting.AGUI.AspNetCore and Microsoft.Agents.AI.Hosting.AspNetCore 1.26.0-preview.261023.1, Microsoft.Agents.AI 1.26.0
- MemorySync docs: docs.memorysync.io/api/memory
Try it
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.
Top comments (0)