{"slug": "microsoft-agent-framework-net-persisting-ag-ui-threads-from-mapagui-to-and", "title": "Microsoft Agent Framework .NET: persisting AG-UI threads (from MapAGUI to MapAGUIServer and AgentSessionStore)", "summary": "Mohammed, founder of MemorySync, documented how Microsoft Agent Framework .NET's MapAGUIServer hosting resolves GitHub issue #2517 by loading or creating an AgentSession from the AG-UI threadId and saving it after streaming, replacing the old AgentThread/ChatMessageStoreFactory model with AgentSession/AgentSessionStore. He showed that persistence is opt-in: without an AgentSessionStore each request gets a fresh session, while WithInMemorySessionStore(withIsolation: false) preserves prior messages across turns. He also flagged that calling WithInMemorySessionStore() with no arguments enables isolation and returns a 500 error when no isolation key is provided.", "body_md": "*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).*\n\nIf 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.\n\nThe 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`.\n\nBelow 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.\n\n```\ndotnet add package Microsoft.Agents.AI.Hosting.AGUI.AspNetCore --prerelease\ndotnet add package Microsoft.Agents.AI.Hosting.AspNetCore --prerelease  # only needed for claims-based isolation (Step 4)\n```\n\n`MapAGUI` no longer exists in the current package (the build fails with CS1061). The method is `MapAGUIServer`.\n\nPersistence 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:\n\n```\nusing Microsoft.Agents.AI;\nusing Microsoft.Agents.AI.Hosting;\nusing Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;\nusing Microsoft.Extensions.AI;\n\nWebApplicationBuilder builder = WebApplication.CreateBuilder(args);\nbuilder.Services.AddAGUIServer();\n\nIChatClient chatClient = CreateChatClient(); // your Azure OpenAI, OpenAI or other IChatClient\n\nAIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions\n{\n    Id = \"assistant\", // keep it stable (see Step 5)\n    Name = \"assistant\",\n    ChatOptions = new ChatOptions { Instructions = \"You are a helpful assistant.\" },\n});\n\nbuilder.AddAIAgent(\"assistant\", (_, _) => agent)\n    .WithInMemorySessionStore(withIsolation: false); // local development, one trusted user only\n\nWebApplication app = builder.Build();\napp.MapAGUIServer(\"assistant\", \"/ag-ui\");\nawait app.RunAsync();\n```\n\nThe 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:\n\n| Setup | Turn 2 (\"What is my name?\") saw | \n|---|---|\n| No session store | only the new message | \n| `WithInMemorySessionStore(withIsolation: false)` | both messages | \n\n`WithInMemorySessionStore()` into an app without isolation\nThe 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:\n\n```\nSystem.InvalidOperationException: Agent isolation key is required but was not provided by the configured AgentIsolationKeyProvider.\n```\n\nThat'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.\n\nThe self-hosting docs register a claims-based isolation provider from `Microsoft.Agents.AI.Hosting.AspNetCore`. By default it uses the `ClaimTypes.NameIdentifier` claim:\n\n```\nbuilder.Services.AddAuthentication(/* your scheme */);\nbuilder.Services.AddAuthorization();\nbuilder.Services.AddHttpContextAccessor(); // the provider reads the current user from HttpContext\nbuilder.Services.UseClaimsBasedAgentIsolation();\n\nbuilder.AddAIAgent(\"assistant\", (_, _) => agent)\n    .WithInMemorySessionStore(); // isolation stays on\n\nWebApplication app = builder.Build();\napp.UseAuthentication();\napp.UseAuthorization();\napp.MapAGUIServer(\"assistant\", \"/ag-ui\").RequireAuthorization();\n```\n\nTwo 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.\n\nThe GitHub sample's comment mentions `UseClaimsBasedSessionIsolation`. In 1.26.0-preview the method is `UseClaimsBasedAgentIsolation`, which matches the Learn docs.\n\n`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:\n\n```\n#pragma warning disable MAAI001 // AgentSessionStore is marked evaluation-only in the preview packages\nusing System.Security.Cryptography;\nusing System.Text;\nusing System.Text.Json;\nusing Microsoft.Agents.AI;\n\nsealed class FileAgentSessionStore(string dir) : AgentSessionStore\n{\n    string PathFor(AIAgent agent, AgentSessionStoreKey key)\n    {\n        string partitions = string.Join(\",\", (key.Partitions ?? new Dictionary<string, string>())\n            .OrderBy(p => p.Key).Select(p => $\"{p.Key}={p.Value}\"));\n        string raw = $\"{agent.Id}|{key.SessionId}|{partitions}\";\n        Directory.CreateDirectory(dir);\n        return Path.Combine(dir, Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(raw))) + \".json\");\n    }\n\n    public override async ValueTask SaveSessionAsync(AIAgent agent, AgentSessionStoreKey key, AgentSession session, CancellationToken cancellationToken = default)\n    {\n        JsonElement state = await agent.SerializeSessionAsync(session, cancellationToken: cancellationToken);\n        await File.WriteAllTextAsync(PathFor(agent, key), state.GetRawText(), cancellationToken);\n    }\n\n    public override async ValueTask<AgentSession?> GetSessionAsync(AIAgent agent, AgentSessionStoreKey key, CancellationToken cancellationToken = default)\n    {\n        string path = PathFor(agent, key);\n        if (!File.Exists(path)) return null;\n        using JsonDocument doc = JsonDocument.Parse(await File.ReadAllTextAsync(path, cancellationToken));\n        return await agent.DeserializeSessionAsync(doc.RootElement.Clone(), cancellationToken: cancellationToken);\n    }\n\n    public override ValueTask DeleteSessionAsync(AIAgent agent, AgentSessionStoreKey key, CancellationToken cancellationToken = default)\n    {\n        File.Delete(PathFor(agent, key));\n        return ValueTask.CompletedTask;\n    }\n}\n```\n\nRegister it in place of the in-memory store:\n\n``` js\nbuilder.AddAIAgent(\"assistant\", (_, _) => agent)\n    .WithSessionStore(new FileAgentSessionStore(\"sessions\")); // isolation on by default, as in Step 4\n```\n\nThree things I found when I ran this:\n\n`#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.\nA 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.\n\nAll 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.\n\n`threadId`. Send the same `runId` as `parentRunId`.` builder.Services.AddHttpContextAccessor()`.` MapAGUI` doesn't compile.`MapAGUIServer`.\n**Does MapAGUIServer persist conversation history by default?**\n\nNo. 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/).\n\n**Is the AG-UI threadId safe to use as a session key?**\n\nNot 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/)).\n\n**What replaced ChatMessageStoreFactory and AgentThread?**\n\n`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/)).\n\n**Does Agent Framework ship a durable session store?**\n\nNo. 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.\n\n`dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs`\nIf 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).", "url": "https://wpnews.pro/news/microsoft-agent-framework-net-persisting-ag-ui-threads-from-mapagui-to-and", "canonical_source": "https://dev.to/memorysync_rafay/microsoft-agent-framework-net-persisting-ag-ui-threads-from-mapagui-to-mapaguiserver-and-5h3f", "published_at": "2026-10-26 16:38:28+00:00", "updated_at": "2026-10-26 16:42:18.292264+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "agent-protocols"], "entities": ["Microsoft", "Microsoft Agent Framework", "MemorySync", "Mohammed", "AG-UI", "AgentSessionStore", "MapAGUIServer", ".NET"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/microsoft-agent-framework-net-persisting-ag-ui-threads-from-mapagui-to-and", "markdown": "https://wpnews.pro/news/microsoft-agent-framework-net-persisting-ag-ui-threads-from-mapagui-to-and.md", "text": "https://wpnews.pro/news/microsoft-agent-framework-net-persisting-ag-ui-threads-from-mapagui-to-and.txt", "jsonld": "https://wpnews.pro/news/microsoft-agent-framework-net-persisting-ag-ui-threads-from-mapagui-to-and.jsonld"}}