// Copyright (c) Microsoft. All rights reserved. using System.Collections.Concurrent; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace Microsoft.Agents.AI.Hosting; /// /// Provides an in-memory implementation of for development and testing scenarios. /// /// /// /// This implementation stores threads in memory using a concurrent dictionary and is suitable for: /// /// Single-instance development scenarios /// Testing and prototyping /// Scenarios where session persistence across restarts is not required /// /// /// /// Warning: All stored threads will be lost when the application restarts. /// For production use with multiple instances or persistence across restarts, use a durable storage implementation /// such as Redis, SQL Server, or Azure Cosmos DB. /// /// /// Multi-user warning. This store keys threads by /// (agent.Id, conversationId) only — it has no principal/owner dimension. When /// the conversation identifier originates from the wire (for example, an AG-UI /// RunAgentInput.ThreadId or an A2A contextId), any caller who knows /// or guesses another caller's identifier can resume that other caller's persisted /// thread. Multi-user hosts must wrap this store in /// (typically by calling /// UseClaimsBasedSessionIsolation(...) from /// Microsoft.Agents.AI.Hosting.AspNetCore or by registering a custom /// ) so that the conversation namespace is /// scoped per principal. See the trust-model remarks on /// for the full background. /// /// public sealed class InMemoryAgentSessionStore : AgentSessionStore { private readonly ConcurrentDictionary _threads = new(); /// public override async ValueTask SaveSessionAsync(AIAgent agent, string conversationId, AgentSession session, CancellationToken cancellationToken = default) { var key = GetKey(conversationId, agent.Id); this._threads[key] = await agent.SerializeSessionAsync(session, cancellationToken: cancellationToken).ConfigureAwait(false); } /// public override async ValueTask GetSessionAsync(AIAgent agent, string conversationId, CancellationToken cancellationToken = default) { var key = GetKey(conversationId, agent.Id); JsonElement? sessionContent = this._threads.TryGetValue(key, out var existingSession) ? existingSession : null; return sessionContent switch { null => await agent.CreateSessionAsync(cancellationToken).ConfigureAwait(false), _ => await agent.DeserializeSessionAsync(sessionContent.Value, cancellationToken: cancellationToken).ConfigureAwait(false), }; } private static string GetKey(string conversationId, string agentId) => $"{agentId}:{conversationId}"; }