// Copyright (c) Microsoft. All rights reserved. using System; using System.Threading; using System.Threading.Tasks; namespace Microsoft.Agents.AI.Hosting; /// /// A delegating that scopes session keys by an isolation key /// provided by a , ensuring that sessions are isolated /// per logical partition (e.g., user, tenant, or composite key). /// public class IsolationKeyScopedAgentSessionStore : DelegatingAgentSessionStore { private readonly SessionIsolationKeyProvider? _keyProvider; private readonly bool _strict; /// /// Initializes a new instance of the class. /// /// The underlying to delegate to. /// /// The used to retrieve the isolation key for the current context. /// /// The options for configuring the session store. If null, defaults are used. /// /// is . /// public IsolationKeyScopedAgentSessionStore( AgentSessionStore innerStore, SessionIsolationKeyProvider? keyProvider, IsolationKeyScopedAgentSessionStoreOptions? options = null) : base(innerStore) { this._keyProvider = keyProvider; options ??= new IsolationKeyScopedAgentSessionStoreOptions(); this._strict = options.Strict; } /// /// Asynchronously retrieves the isolation key from the provider and validates it if in strict mode. /// /// The cancellation token. /// /// The isolation key string, or if no key is available and non-strict mode is enabled. /// /// /// The provider returned and strict mode is enabled. /// private async ValueTask GetIsolationKeyAsync(CancellationToken cancellationToken) { string? key = this._keyProvider != null ? await this._keyProvider.GetSessionIsolationKeyAsync(cancellationToken).ConfigureAwait(false) : null; if (this._strict && key == null) { throw new InvalidOperationException("Session isolation key is required but was not provided by the configured SessionIsolationKeyProvider."); } return key; } /// /// Escapes special characters in the isolation key to ensure unambiguous scoped conversation IDs. /// /// The raw isolation key. /// The escaped isolation key. /// /// Backslashes are escaped first (\ becomes \\), then colons (: becomes \:). /// This ensures the scoped conversation ID format {key}::{conversationId} can be parsed correctly. /// private static string EscapeIsolationKey(string key) => key.Replace("\\", "\\\\").Replace(":", "\\:"); /// /// Constructs a scoped conversation ID by prefixing the bare conversation ID with the escaped isolation key. /// /// The original conversation ID. /// The cancellation token. /// /// The scoped conversation ID in the format {escapedKey}::{conversationId}, or the bare conversation ID /// if no isolation key is available and non-strict mode is enabled. /// private async ValueTask GetScopedConversationIdAsync(string bareConversationId, CancellationToken cancellationToken) { string? key = await this.GetIsolationKeyAsync(cancellationToken).ConfigureAwait(false); if (key == null) { return bareConversationId; } return $"{EscapeIsolationKey(key)}::{bareConversationId}"; } /// public override async ValueTask GetSessionAsync(AIAgent agent, string conversationId, CancellationToken cancellationToken = default) { string scopedConversationId = await this.GetScopedConversationIdAsync(conversationId, cancellationToken).ConfigureAwait(false); return await this.InnerStore.GetSessionAsync(agent, scopedConversationId, cancellationToken).ConfigureAwait(false); } /// public override async ValueTask SaveSessionAsync(AIAgent agent, string conversationId, AgentSession session, CancellationToken cancellationToken = default) { string scopedConversationId = await this.GetScopedConversationIdAsync(conversationId, cancellationToken).ConfigureAwait(false); await this.InnerStore.SaveSessionAsync(agent, scopedConversationId, session, cancellationToken).ConfigureAwait(false); } }