// 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);
}
}