diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentSessionStore.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentSessionStore.cs index d4f4a74d22d..0f2c8ac1a5d 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentSessionStore.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentSessionStore.cs @@ -27,11 +27,34 @@ public abstract class AgentSessionStore /// The session to save. /// The to monitor for cancellation requests. /// A task that represents the asynchronous save operation. +#pragma warning disable RS0026 // Overloaded by a sessionId-based convenience method. public abstract ValueTask SaveSessionAsync( AIAgent agent, AgentSessionStoreKey key, AgentSession session, CancellationToken cancellationToken = default); +#pragma warning restore RS0026 + + /// + /// Saves an agent session to persistent storage using a session identifier without partitions. + /// + /// The agent that owns this session. + /// The logical session identifier. + /// The session to save. + /// The to monitor for cancellation requests. + /// A task that represents the asynchronous save operation. + /// + /// This is a convenience overload that creates an from + /// and calls . + /// +#pragma warning disable RS0026 // Convenience overload that mirrors the AgentSessionStoreKey-based method. + public ValueTask SaveSessionAsync( + AIAgent agent, + string sessionId, + AgentSession session, + CancellationToken cancellationToken = default) + => this.SaveSessionAsync(agent, new AgentSessionStoreKey(sessionId), session, cancellationToken); +#pragma warning restore RS0026 /// /// Retrieves an agent session from persistent storage, or when no session is stored @@ -50,10 +73,35 @@ public abstract ValueTask SaveSessionAsync( /// branches observing one another's changes or modifying the stored state. Implementations that cache a /// live session must return an independent copy rather than the shared instance. /// +#pragma warning disable RS0026 // Overloaded by a sessionId-based convenience method. public abstract ValueTask GetSessionAsync( AIAgent agent, AgentSessionStoreKey key, CancellationToken cancellationToken = default); +#pragma warning restore RS0026 + + /// + /// Retrieves an agent session from persistent storage using a session identifier without partitions, + /// or when no session is stored for the given identifier. + /// + /// The agent that owns this session. + /// The logical session identifier. + /// The to monitor for cancellation requests. + /// + /// A task whose result contains the restored session, or when nothing is stored for + /// the given identifier. This method never creates a session. + /// + /// + /// This is a convenience overload that creates an from + /// and calls . + /// +#pragma warning disable RS0026 // Convenience overload that mirrors the AgentSessionStoreKey-based method. + public ValueTask GetSessionAsync( + AIAgent agent, + string sessionId, + CancellationToken cancellationToken = default) + => this.GetSessionAsync(agent, new AgentSessionStoreKey(sessionId), cancellationToken); +#pragma warning restore RS0026 /// /// Retrieves the stored session for the given identifiers, or creates a new one when none is stored. @@ -63,10 +111,12 @@ public abstract ValueTask SaveSessionAsync( /// The to monitor for cancellation requests. /// A task whose result is always a usable session. /// - /// The default implementation calls and creates a session through - /// only when the lookup returns . - /// Implementations that override receive this behavior automatically. + /// The default implementation calls + /// and creates a session through only when the lookup returns + /// . Implementations that override + /// receive this behavior automatically. /// +#pragma warning disable RS0026 // Overloaded by a sessionId-based convenience method. public virtual async ValueTask GetOrCreateSessionAsync( AIAgent agent, AgentSessionStoreKey key, @@ -77,6 +127,27 @@ public virtual async ValueTask GetOrCreateSessionAsync( return await this.GetSessionAsync(agent, key, cancellationToken).ConfigureAwait(false) ?? await agent.CreateSessionAsync(cancellationToken).ConfigureAwait(false); } +#pragma warning restore RS0026 + + /// + /// Retrieves the stored session for the given session identifier without partitions, or creates a new one + /// when none is stored. + /// + /// The agent that owns this session. + /// The logical session identifier. + /// The to monitor for cancellation requests. + /// A task whose result is always a usable session. + /// + /// This is a convenience overload that creates an from + /// and calls . + /// +#pragma warning disable RS0026 // Convenience overload that mirrors the AgentSessionStoreKey-based method. + public ValueTask GetOrCreateSessionAsync( + AIAgent agent, + string sessionId, + CancellationToken cancellationToken = default) + => this.GetOrCreateSessionAsync(agent, new AgentSessionStoreKey(sessionId), cancellationToken); +#pragma warning restore RS0026 /// /// Deletes a stored agent session, if present. @@ -88,10 +159,31 @@ public virtual async ValueTask GetOrCreateSessionAsync( /// /// Implementations delete the session and treat a missing session as a no-op. /// +#pragma warning disable RS0026 // Overloaded by a sessionId-based convenience method. public abstract ValueTask DeleteSessionAsync( AIAgent agent, AgentSessionStoreKey key, CancellationToken cancellationToken = default); +#pragma warning restore RS0026 + + /// + /// Deletes a stored agent session identified by a session identifier without partitions, if present. + /// + /// The agent that owns this session. + /// The logical session identifier. + /// The to monitor for cancellation requests. + /// A task that represents the asynchronous delete operation. + /// + /// This is a convenience overload that creates an from + /// and calls . + /// +#pragma warning disable RS0026 // Convenience overload that mirrors the AgentSessionStoreKey-based method. + public ValueTask DeleteSessionAsync( + AIAgent agent, + string sessionId, + CancellationToken cancellationToken = default) + => this.DeleteSessionAsync(agent, new AgentSessionStoreKey(sessionId), cancellationToken); +#pragma warning restore RS0026 /// Asks the store for an object of the specified type. /// The type of object being requested. diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/dotnet/src/Microsoft.Agents.AI.Abstractions/PublicAPI/net10.0/PublicAPI.Unshipped.txt index 085b5279485..e68014631c9 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/PublicAPI/net10.0/PublicAPI.Unshipped.txt +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/PublicAPI/net10.0/PublicAPI.Unshipped.txt @@ -7,6 +7,10 @@ [MAAI001]abstract Microsoft.Agents.AI.AgentSessionStore.SaveSessionAsync(Microsoft.Agents.AI.AIAgent! agent, Microsoft.Agents.AI.AgentSessionStoreKey! key, Microsoft.Agents.AI.AgentSession! session, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask [MAAI001]abstract Microsoft.Agents.AI.AgentSessionStore.DeleteSessionAsync(Microsoft.Agents.AI.AIAgent! agent, Microsoft.Agents.AI.AgentSessionStoreKey! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask [MAAI001]virtual Microsoft.Agents.AI.AgentSessionStore.GetOrCreateSessionAsync(Microsoft.Agents.AI.AIAgent! agent, Microsoft.Agents.AI.AgentSessionStoreKey! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +[MAAI001]Microsoft.Agents.AI.AgentSessionStore.GetSessionAsync(Microsoft.Agents.AI.AIAgent! agent, string! sessionId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +[MAAI001]Microsoft.Agents.AI.AgentSessionStore.SaveSessionAsync(Microsoft.Agents.AI.AIAgent! agent, string! sessionId, Microsoft.Agents.AI.AgentSession! session, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +[MAAI001]Microsoft.Agents.AI.AgentSessionStore.DeleteSessionAsync(Microsoft.Agents.AI.AIAgent! agent, string! sessionId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +[MAAI001]Microsoft.Agents.AI.AgentSessionStore.GetOrCreateSessionAsync(Microsoft.Agents.AI.AIAgent! agent, string! sessionId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask [MAAI001]Microsoft.Agents.AI.AgentSessionStoreKey [MAAI001]Microsoft.Agents.AI.AgentSessionStoreKey.AgentSessionStoreKey(string! sessionId, System.Collections.Generic.IReadOnlyDictionary? partitions = null) -> void [MAAI001]Microsoft.Agents.AI.AgentSessionStoreKey.Equals(Microsoft.Agents.AI.AgentSessionStoreKey? other) -> bool diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/PublicAPI/net472/PublicAPI.Unshipped.txt b/dotnet/src/Microsoft.Agents.AI.Abstractions/PublicAPI/net472/PublicAPI.Unshipped.txt index 085b5279485..e68014631c9 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/PublicAPI/net472/PublicAPI.Unshipped.txt +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/PublicAPI/net472/PublicAPI.Unshipped.txt @@ -7,6 +7,10 @@ [MAAI001]abstract Microsoft.Agents.AI.AgentSessionStore.SaveSessionAsync(Microsoft.Agents.AI.AIAgent! agent, Microsoft.Agents.AI.AgentSessionStoreKey! key, Microsoft.Agents.AI.AgentSession! session, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask [MAAI001]abstract Microsoft.Agents.AI.AgentSessionStore.DeleteSessionAsync(Microsoft.Agents.AI.AIAgent! agent, Microsoft.Agents.AI.AgentSessionStoreKey! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask [MAAI001]virtual Microsoft.Agents.AI.AgentSessionStore.GetOrCreateSessionAsync(Microsoft.Agents.AI.AIAgent! agent, Microsoft.Agents.AI.AgentSessionStoreKey! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +[MAAI001]Microsoft.Agents.AI.AgentSessionStore.GetSessionAsync(Microsoft.Agents.AI.AIAgent! agent, string! sessionId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +[MAAI001]Microsoft.Agents.AI.AgentSessionStore.SaveSessionAsync(Microsoft.Agents.AI.AIAgent! agent, string! sessionId, Microsoft.Agents.AI.AgentSession! session, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +[MAAI001]Microsoft.Agents.AI.AgentSessionStore.DeleteSessionAsync(Microsoft.Agents.AI.AIAgent! agent, string! sessionId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +[MAAI001]Microsoft.Agents.AI.AgentSessionStore.GetOrCreateSessionAsync(Microsoft.Agents.AI.AIAgent! agent, string! sessionId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask [MAAI001]Microsoft.Agents.AI.AgentSessionStoreKey [MAAI001]Microsoft.Agents.AI.AgentSessionStoreKey.AgentSessionStoreKey(string! sessionId, System.Collections.Generic.IReadOnlyDictionary? partitions = null) -> void [MAAI001]Microsoft.Agents.AI.AgentSessionStoreKey.Equals(Microsoft.Agents.AI.AgentSessionStoreKey? other) -> bool diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/dotnet/src/Microsoft.Agents.AI.Abstractions/PublicAPI/net8.0/PublicAPI.Unshipped.txt index 085b5279485..e68014631c9 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -7,6 +7,10 @@ [MAAI001]abstract Microsoft.Agents.AI.AgentSessionStore.SaveSessionAsync(Microsoft.Agents.AI.AIAgent! agent, Microsoft.Agents.AI.AgentSessionStoreKey! key, Microsoft.Agents.AI.AgentSession! session, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask [MAAI001]abstract Microsoft.Agents.AI.AgentSessionStore.DeleteSessionAsync(Microsoft.Agents.AI.AIAgent! agent, Microsoft.Agents.AI.AgentSessionStoreKey! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask [MAAI001]virtual Microsoft.Agents.AI.AgentSessionStore.GetOrCreateSessionAsync(Microsoft.Agents.AI.AIAgent! agent, Microsoft.Agents.AI.AgentSessionStoreKey! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +[MAAI001]Microsoft.Agents.AI.AgentSessionStore.GetSessionAsync(Microsoft.Agents.AI.AIAgent! agent, string! sessionId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +[MAAI001]Microsoft.Agents.AI.AgentSessionStore.SaveSessionAsync(Microsoft.Agents.AI.AIAgent! agent, string! sessionId, Microsoft.Agents.AI.AgentSession! session, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +[MAAI001]Microsoft.Agents.AI.AgentSessionStore.DeleteSessionAsync(Microsoft.Agents.AI.AIAgent! agent, string! sessionId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +[MAAI001]Microsoft.Agents.AI.AgentSessionStore.GetOrCreateSessionAsync(Microsoft.Agents.AI.AIAgent! agent, string! sessionId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask [MAAI001]Microsoft.Agents.AI.AgentSessionStoreKey [MAAI001]Microsoft.Agents.AI.AgentSessionStoreKey.AgentSessionStoreKey(string! sessionId, System.Collections.Generic.IReadOnlyDictionary? partitions = null) -> void [MAAI001]Microsoft.Agents.AI.AgentSessionStoreKey.Equals(Microsoft.Agents.AI.AgentSessionStoreKey? other) -> bool diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/PublicAPI/net9.0/PublicAPI.Unshipped.txt b/dotnet/src/Microsoft.Agents.AI.Abstractions/PublicAPI/net9.0/PublicAPI.Unshipped.txt index 085b5279485..e68014631c9 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/PublicAPI/net9.0/PublicAPI.Unshipped.txt +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/PublicAPI/net9.0/PublicAPI.Unshipped.txt @@ -7,6 +7,10 @@ [MAAI001]abstract Microsoft.Agents.AI.AgentSessionStore.SaveSessionAsync(Microsoft.Agents.AI.AIAgent! agent, Microsoft.Agents.AI.AgentSessionStoreKey! key, Microsoft.Agents.AI.AgentSession! session, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask [MAAI001]abstract Microsoft.Agents.AI.AgentSessionStore.DeleteSessionAsync(Microsoft.Agents.AI.AIAgent! agent, Microsoft.Agents.AI.AgentSessionStoreKey! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask [MAAI001]virtual Microsoft.Agents.AI.AgentSessionStore.GetOrCreateSessionAsync(Microsoft.Agents.AI.AIAgent! agent, Microsoft.Agents.AI.AgentSessionStoreKey! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +[MAAI001]Microsoft.Agents.AI.AgentSessionStore.GetSessionAsync(Microsoft.Agents.AI.AIAgent! agent, string! sessionId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +[MAAI001]Microsoft.Agents.AI.AgentSessionStore.SaveSessionAsync(Microsoft.Agents.AI.AIAgent! agent, string! sessionId, Microsoft.Agents.AI.AgentSession! session, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +[MAAI001]Microsoft.Agents.AI.AgentSessionStore.DeleteSessionAsync(Microsoft.Agents.AI.AIAgent! agent, string! sessionId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +[MAAI001]Microsoft.Agents.AI.AgentSessionStore.GetOrCreateSessionAsync(Microsoft.Agents.AI.AIAgent! agent, string! sessionId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask [MAAI001]Microsoft.Agents.AI.AgentSessionStoreKey [MAAI001]Microsoft.Agents.AI.AgentSessionStoreKey.AgentSessionStoreKey(string! sessionId, System.Collections.Generic.IReadOnlyDictionary? partitions = null) -> void [MAAI001]Microsoft.Agents.AI.AgentSessionStoreKey.Equals(Microsoft.Agents.AI.AgentSessionStoreKey? other) -> bool diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/dotnet/src/Microsoft.Agents.AI.Abstractions/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index 085b5279485..e68014631c9 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -7,6 +7,10 @@ [MAAI001]abstract Microsoft.Agents.AI.AgentSessionStore.SaveSessionAsync(Microsoft.Agents.AI.AIAgent! agent, Microsoft.Agents.AI.AgentSessionStoreKey! key, Microsoft.Agents.AI.AgentSession! session, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask [MAAI001]abstract Microsoft.Agents.AI.AgentSessionStore.DeleteSessionAsync(Microsoft.Agents.AI.AIAgent! agent, Microsoft.Agents.AI.AgentSessionStoreKey! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask [MAAI001]virtual Microsoft.Agents.AI.AgentSessionStore.GetOrCreateSessionAsync(Microsoft.Agents.AI.AIAgent! agent, Microsoft.Agents.AI.AgentSessionStoreKey! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +[MAAI001]Microsoft.Agents.AI.AgentSessionStore.GetSessionAsync(Microsoft.Agents.AI.AIAgent! agent, string! sessionId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +[MAAI001]Microsoft.Agents.AI.AgentSessionStore.SaveSessionAsync(Microsoft.Agents.AI.AIAgent! agent, string! sessionId, Microsoft.Agents.AI.AgentSession! session, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +[MAAI001]Microsoft.Agents.AI.AgentSessionStore.DeleteSessionAsync(Microsoft.Agents.AI.AIAgent! agent, string! sessionId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +[MAAI001]Microsoft.Agents.AI.AgentSessionStore.GetOrCreateSessionAsync(Microsoft.Agents.AI.AIAgent! agent, string! sessionId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask [MAAI001]Microsoft.Agents.AI.AgentSessionStoreKey [MAAI001]Microsoft.Agents.AI.AgentSessionStoreKey.AgentSessionStoreKey(string! sessionId, System.Collections.Generic.IReadOnlyDictionary? partitions = null) -> void [MAAI001]Microsoft.Agents.AI.AgentSessionStoreKey.Equals(Microsoft.Agents.AI.AgentSessionStoreKey? other) -> bool diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AgentSessionStoreTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AgentSessionStoreTests.cs index d113eb9110c..f3ce91ebdf3 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AgentSessionStoreTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AgentSessionStoreTests.cs @@ -120,6 +120,108 @@ await Assert.ThrowsAsync( () => store.GetOrCreateSessionAsync(null!, new AgentSessionStoreKey("conversation-1")).AsTask()); } + [Fact] + public async Task GetSessionAsync_SessionIdOverload_ForwardsKeyWithoutPartitionsAsync() + { + // Arrange + var storedSession = new TestAgentSession(); + var agent = new Mock().Object; + using var cts = new CancellationTokenSource(); + var store = new Mock(); + store.Setup(s => s.GetSessionAsync(agent, It.IsAny(), cts.Token)) + .ReturnsAsync(storedSession); + + // Act + AgentSession? session = await store.Object.GetSessionAsync(agent, "conversation-1", cts.Token); + + // Assert + Assert.Same(storedSession, session); + store.Verify( + s => s.GetSessionAsync(agent, It.Is(k => IsUnpartitionedKey(k, "conversation-1")), cts.Token), + Times.Once); + } + + [Fact] + public async Task SaveSessionAsync_SessionIdOverload_ForwardsKeyWithoutPartitionsAsync() + { + // Arrange + var session = new TestAgentSession(); + var agent = new Mock().Object; + using var cts = new CancellationTokenSource(); + var store = new Mock(); + store.Setup(s => s.SaveSessionAsync(agent, It.IsAny(), session, cts.Token)) + .Returns(default(ValueTask)); + + // Act + await store.Object.SaveSessionAsync(agent, "conversation-1", session, cts.Token); + + // Assert + store.Verify( + s => s.SaveSessionAsync(agent, It.Is(k => IsUnpartitionedKey(k, "conversation-1")), session, cts.Token), + Times.Once); + } + + [Fact] + public async Task GetOrCreateSessionAsync_SessionIdOverload_ForwardsKeyWithoutPartitionsAsync() + { + // Arrange + var storedSession = new TestAgentSession(); + var agent = new Mock().Object; + using var cts = new CancellationTokenSource(); + var store = new Mock(); + store.Setup(s => s.GetOrCreateSessionAsync(agent, It.IsAny(), cts.Token)) + .ReturnsAsync(storedSession); + + // Act + AgentSession session = await store.Object.GetOrCreateSessionAsync(agent, "conversation-1", cts.Token); + + // Assert + Assert.Same(storedSession, session); + store.Verify( + s => s.GetOrCreateSessionAsync(agent, It.Is(k => IsUnpartitionedKey(k, "conversation-1")), cts.Token), + Times.Once); + } + + [Fact] + public async Task DeleteSessionAsync_SessionIdOverload_ForwardsKeyWithoutPartitionsAsync() + { + // Arrange + var agent = new Mock().Object; + using var cts = new CancellationTokenSource(); + var store = new Mock(); + store.Setup(s => s.DeleteSessionAsync(agent, It.IsAny(), cts.Token)) + .Returns(default(ValueTask)); + + // Act + await store.Object.DeleteSessionAsync(agent, "conversation-1", cts.Token); + + // Assert + store.Verify( + s => s.DeleteSessionAsync(agent, It.Is(k => IsUnpartitionedKey(k, "conversation-1")), cts.Token), + Times.Once); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task SessionIdOverloads_InvalidSessionId_ThrowAsync(string? sessionId) + { + // Arrange + var store = new TestAgentSessionStore(session: null); + var agent = new Mock().Object; + var session = new TestAgentSession(); + + // Act and assert + await Assert.ThrowsAnyAsync(() => store.GetSessionAsync(agent, sessionId!).AsTask()); + await Assert.ThrowsAnyAsync(() => store.SaveSessionAsync(agent, sessionId!, session).AsTask()); + await Assert.ThrowsAnyAsync(() => store.GetOrCreateSessionAsync(agent, sessionId!).AsTask()); + await Assert.ThrowsAnyAsync(() => store.DeleteSessionAsync(agent, sessionId!).AsTask()); + } + + private static bool IsUnpartitionedKey(AgentSessionStoreKey key, string sessionId) + => string.Equals(key.SessionId, sessionId, StringComparison.Ordinal) && key.Partitions is null; + private sealed class TestAgentSessionStore(AgentSession? session) : AgentSessionStore { public AgentSessionStoreKey? LastKey { get; private set; }