From 521d708c06b9c55d8ef99c80f99667592d1ebdab Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Fri, 13 Feb 2026 10:29:03 +0000 Subject: [PATCH 1/3] Address Feedback on statebag feature branch PR --- .../Program.cs | 8 +- .../AIContextProvider.cs | 9 +- .../AgentSessionStateBagValue.cs | 19 ++- .../ChatHistoryProvider.cs | 9 +- .../CHANGELOG.md | 1 + .../AgentSessionStateBagTests.cs | 109 ++++++++++++++++-- 6 files changed, 129 insertions(+), 26 deletions(-) diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Program.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Program.cs index 16fca255e2..583b5ad06b 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Program.cs +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Program.cs @@ -59,13 +59,13 @@ protected override async Task RunCoreAsync(IEnumerable responseMessages = CloneAndToUpperCase(messages, this.Name).ToList(); // Notify the session of the input and output messages. - var invokedContext = new ChatHistoryProvider.InvokedContext(this, session, messages) + var invokedContext = new ChatHistoryProvider.InvokedContext(this, session, userAndChatHistoryMessages) { ResponseMessages = responseMessages }; @@ -91,13 +91,13 @@ protected override async IAsyncEnumerable RunCoreStreamingA // Get existing messages from the store var invokingContext = new ChatHistoryProvider.InvokingContext(this, session, messages); - var storeMessages = await this.ChatHistoryProvider.InvokingAsync(invokingContext, cancellationToken); + var userAndChatHistoryMessages = await this.ChatHistoryProvider.InvokingAsync(invokingContext, cancellationToken); // Clone the input messages and turn them into response messages with upper case text. List responseMessages = CloneAndToUpperCase(messages, this.Name).ToList(); // Notify the session of the input and output messages. - var invokedContext = new ChatHistoryProvider.InvokedContext(this, session, messages) + var invokedContext = new ChatHistoryProvider.InvokedContext(this, session, userAndChatHistoryMessages) { ResponseMessages = responseMessages }; diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/AIContextProvider.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/AIContextProvider.cs index c7c17c4122..224b9c1241 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/AIContextProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/AIContextProvider.cs @@ -223,9 +223,9 @@ public InvokingContext( /// Contains the context information provided to . /// /// - /// This class provides context about a completed agent invocation, including both the - /// request messages that were used and the response messages that were generated. It also indicates - /// whether the invocation succeeded or failed. + /// This class provides context about a completed agent invocation, including the accumulated + /// request messages (user input, chat history and any others provided by AI context providers) that were used + /// and the response messages that were generated. It also indicates whether the invocation succeeded or failed. /// public sealed class InvokedContext { @@ -234,7 +234,8 @@ public sealed class InvokedContext /// /// The agent being invoked. /// The session associated with the agent invocation. - /// The messages that were used by the agent for this invocation. + /// The accumulated request messages (user input, chat history and any others provided by AI context providers) + /// that were used by the agent for this invocation. /// is . public InvokedContext( AIAgent agent, diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentSessionStateBagValue.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentSessionStateBagValue.cs index 6f97237516..30ac06bc48 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentSessionStateBagValue.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentSessionStateBagValue.cs @@ -78,10 +78,14 @@ public bool TryReadDeserializedValue(out T? value, JsonSerializerOptions? jso lock (this._lock) { - if (this._cache is { } cache) + switch (this._cache) { - value = cache.Value as T; - return true; + case DeserializedCache { Value: T cacheValue, ValueType: Type cacheValueType } when cacheValueType == typeof(T): + value = cacheValue; + return true; + case DeserializedCache { ValueType: Type cacheValueType } when cacheValueType != typeof(T): + value = null; + return false; } switch (this._jsonValue) @@ -118,9 +122,12 @@ public bool TryReadDeserializedValue(out T? value, JsonSerializerOptions? jso lock (this._lock) { - if (this._cache is { } cache) + switch (this._cache) { - return cache.Value as T; + case DeserializedCache { Value: T cacheValue, ValueType: Type cacheValueType } when cacheValueType == typeof(T): + return cacheValue; + case DeserializedCache { ValueType: Type cacheValueType } when cacheValueType != typeof(T): + throw new InvalidOperationException($"The type of the cached value is {cacheValueType.FullName}, but the requested type is {typeof(T).FullName}."); } switch (this._jsonValue) @@ -144,7 +151,7 @@ public bool TryReadDeserializedValue(out T? value, JsonSerializerOptions? jso /// Sets the deserialized value of this session state value, updating the cache accordingly. /// This does not update the JsonValue directly; the JsonValue will be updated on the next read or when the object is serialized. /// - public void SetDeserialized(object? deserializedValue, Type valueType, JsonSerializerOptions jsonSerializerOptions) + public void SetDeserialized(T? deserializedValue, Type valueType, JsonSerializerOptions jsonSerializerOptions) { lock (this._lock) { diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/ChatHistoryProvider.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/ChatHistoryProvider.cs index 408732162a..7c467505d8 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/ChatHistoryProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/ChatHistoryProvider.cs @@ -253,9 +253,9 @@ public InvokingContext( /// Contains the context information provided to . /// /// - /// This class provides context about a completed agent invocation, including both the - /// request messages that were used and the response messages that were generated. It also indicates - /// whether the invocation succeeded or failed. + /// This class provides context about a completed agent invocation, including the accumulated + /// request messages (user input, chat history and any others provided by AI context providers) that were used + /// and the response messages that were generated. It also indicates whether the invocation succeeded or failed. /// public sealed class InvokedContext { @@ -264,7 +264,8 @@ public sealed class InvokedContext /// /// The agent being invoked. /// The session associated with the agent invocation. - /// The caller provided messages that were used by the agent for this invocation. + /// The accumulated request messages (user input, chat history and any others provided by AI context providers) + /// that were used by the agent for this invocation. /// is . public InvokedContext( AIAgent agent, diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/CHANGELOG.md b/dotnet/src/Microsoft.Agents.AI.DurableTask/CHANGELOG.md index d8260fcb84..ea27e4fbd6 100644 --- a/dotnet/src/Microsoft.Agents.AI.DurableTask/CHANGELOG.md +++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/CHANGELOG.md @@ -12,6 +12,7 @@ - Renamed serializedSession parameter to serializedState on DeserializeSessionAsync for consistency ([#3681](https://github.com/microsoft/agent-framework/pull/3681)) - Introduce Core method pattern for Session management methods on AIAgent ([#3699](https://github.com/microsoft/agent-framework/pull/3699)) - Changed AIAgent.SerializeSession to AIAgent.SerializeSessionAsync ([#3879](https://github.com/microsoft/agent-framework/pull/3879)) +- Changed ChatHistory and AIContext Providers to have pipeline semantics ([#3806](https://github.com/microsoft/agent-framework/pull/3806/)) ## v1.0.0-preview.251204.1 diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AgentSessionStateBagTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AgentSessionStateBagTests.cs index b30af7acc6..a51f6dcb86 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AgentSessionStateBagTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AgentSessionStateBagTests.cs @@ -534,14 +534,9 @@ public async System.Threading.Tasks.Task ConcurrentReadsAndWrites_DoesNotThrowAs for (int i = 0; i < 200; i++) { int index = i; - if (index % 2 == 0) - { - tasks[i] = System.Threading.Tasks.Task.Run(() => stateBag.GetValue("key")); - } - else - { - tasks[i] = System.Threading.Tasks.Task.Run(() => stateBag.SetValue("key", $"value{index}")); - } + tasks[i] = (index % 2 == 0) + ? System.Threading.Tasks.Task.Run(() => stateBag.GetValue("key")) + : System.Threading.Tasks.Task.Run(() => stateBag.SetValue("key", $"value{index}")); } await System.Threading.Tasks.Task.WhenAll(tasks); @@ -650,6 +645,104 @@ public void Serialize_WithComplexObject_ReturnsJsonWithProperties() #endregion + #region Type Mismatch Tests + + [Fact] + public void TryGetValue_WithDifferentTypeAfterSet_ReturnsFalse() + { + // Arrange + var stateBag = new AgentSessionStateBag(); + stateBag.SetValue("key1", "hello"); + + // Act + var found = stateBag.TryGetValue("key1", out var result, TestJsonSerializerContext.Default.Options); + + // Assert + Assert.False(found); + Assert.Null(result); + } + + [Fact] + public void GetValue_WithDifferentTypeAfterSet_ThrowsInvalidOperationException() + { + // Arrange + var stateBag = new AgentSessionStateBag(); + stateBag.SetValue("key1", "hello"); + + // Act & Assert + Assert.Throws(() => stateBag.GetValue("key1", TestJsonSerializerContext.Default.Options)); + } + + [Fact] + public void TryGetValue_WithDifferentTypeAfterDeserializedRead_ReturnsFalse() + { + // Arrange + var stateBag = new AgentSessionStateBag(); + stateBag.SetValue("key1", "hello"); + + // First read caches the value as string + var cachedValue = stateBag.GetValue("key1"); + Assert.Equal("hello", cachedValue); + + // Act - request as a different type + var found = stateBag.TryGetValue("key1", out var result, TestJsonSerializerContext.Default.Options); + + // Assert + Assert.False(found); + Assert.Null(result); + } + + [Fact] + public void GetValue_WithDifferentTypeAfterDeserializedRoundtrip_ThrowsInvalidOperationException() + { + // Arrange + var originalStateBag = new AgentSessionStateBag(); + originalStateBag.SetValue("key1", "hello"); + + // Round-trip through serialization + var json = originalStateBag.Serialize(); + var restoredStateBag = AgentSessionStateBag.Deserialize(json); + + // First read caches the value as string + var cachedValue = restoredStateBag.GetValue("key1"); + Assert.Equal("hello", cachedValue); + + // Act & Assert - request as a different type + Assert.Throws(() => restoredStateBag.GetValue("key1", TestJsonSerializerContext.Default.Options)); + } + + [Fact] + public void TryGetValue_ComplexTypeAfterSetString_ReturnsFalse() + { + // Arrange + var stateBag = new AgentSessionStateBag(); + stateBag.SetValue("animal", "not an animal"); + + // Act + var found = stateBag.TryGetValue("animal", out var result, TestJsonSerializerContext.Default.Options); + + // Assert + Assert.False(found); + Assert.Null(result); + } + + [Fact] + public void GetValue_TypeMismatch_ExceptionMessageContainsBothTypeNames() + { + // Arrange + var stateBag = new AgentSessionStateBag(); + stateBag.SetValue("key1", "hello"); + + // Act + var exception = Assert.Throws(() => stateBag.GetValue("key1", TestJsonSerializerContext.Default.Options)); + + // Assert + Assert.Contains(typeof(string).FullName!, exception.Message); + Assert.Contains(typeof(Animal).FullName!, exception.Message); + } + + #endregion + #region JsonSerializer Integration Tests [Fact] From ae3f90ab915d76d4b8258e7233345e7bdecf78d6 Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Fri, 13 Feb 2026 10:40:17 +0000 Subject: [PATCH 2/3] Update dotnet/src/Microsoft.Agents.AI.DurableTask/CHANGELOG.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- dotnet/src/Microsoft.Agents.AI.DurableTask/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/CHANGELOG.md b/dotnet/src/Microsoft.Agents.AI.DurableTask/CHANGELOG.md index ea27e4fbd6..ff886e2ebe 100644 --- a/dotnet/src/Microsoft.Agents.AI.DurableTask/CHANGELOG.md +++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/CHANGELOG.md @@ -12,7 +12,7 @@ - Renamed serializedSession parameter to serializedState on DeserializeSessionAsync for consistency ([#3681](https://github.com/microsoft/agent-framework/pull/3681)) - Introduce Core method pattern for Session management methods on AIAgent ([#3699](https://github.com/microsoft/agent-framework/pull/3699)) - Changed AIAgent.SerializeSession to AIAgent.SerializeSessionAsync ([#3879](https://github.com/microsoft/agent-framework/pull/3879)) -- Changed ChatHistory and AIContext Providers to have pipeline semantics ([#3806](https://github.com/microsoft/agent-framework/pull/3806/)) +- Changed ChatHistory and AIContext Providers to have pipeline semantics ([#3806](https://github.com/microsoft/agent-framework/pull/3806)) ## v1.0.0-preview.251204.1 From 1e63959ff8cfee27c458c54149f550d6dadca59c Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Fri, 13 Feb 2026 10:45:25 +0000 Subject: [PATCH 3/3] Address PR comments --- .../Microsoft.Agents.AI.Abstractions/AIContextProvider.cs | 6 ++++-- .../AgentSessionStateBagValue.cs | 5 +++++ .../Microsoft.Agents.AI.Abstractions/ChatHistoryProvider.cs | 6 ++++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/AIContextProvider.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/AIContextProvider.cs index ff1190eb94..b201e9f8e1 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/AIContextProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/AIContextProvider.cs @@ -234,7 +234,8 @@ public sealed class InvokedContext /// /// The agent that was invoked. /// The session associated with the agent invocation. - /// The messages that were used by the agent for this invocation. + /// The accumulated request messages (user input, chat history and any others provided by AI context providers) + /// that were used by the agent for this invocation. /// The response messages generated during this invocation. /// , , or is . public InvokedContext( @@ -281,7 +282,8 @@ public InvokedContext( public AgentSession? Session { get; } /// - /// Gets the messages that were used by the agent for this invocation. + /// Gets the accumulated request messages (user input, chat history and any others provided by AI context providers) + /// that were used by the agent for this invocation. /// /// /// A collection of instances representing all messages that were used by the agent for this invocation. diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentSessionStateBagValue.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentSessionStateBagValue.cs index 30ac06bc48..0b4849aa1b 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentSessionStateBagValue.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentSessionStateBagValue.cs @@ -80,6 +80,9 @@ public bool TryReadDeserializedValue(out T? value, JsonSerializerOptions? jso { switch (this._cache) { + case DeserializedCache { Value: null, ValueType: Type cacheValueType } when cacheValueType == typeof(T): + value = null; + return true; case DeserializedCache { Value: T cacheValue, ValueType: Type cacheValueType } when cacheValueType == typeof(T): value = cacheValue; return true; @@ -124,6 +127,8 @@ public bool TryReadDeserializedValue(out T? value, JsonSerializerOptions? jso { switch (this._cache) { + case DeserializedCache { Value: null, ValueType: Type cacheValueType } when cacheValueType == typeof(T): + return null; case DeserializedCache { Value: T cacheValue, ValueType: Type cacheValueType } when cacheValueType == typeof(T): return cacheValue; case DeserializedCache { ValueType: Type cacheValueType } when cacheValueType != typeof(T): diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/ChatHistoryProvider.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/ChatHistoryProvider.cs index 0fe237a2c5..d16ca69528 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/ChatHistoryProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/ChatHistoryProvider.cs @@ -285,7 +285,8 @@ public InvokedContext( /// /// The agent that was invoked. /// The session associated with the agent invocation. - /// The caller provided messages that were used by the agent for this invocation. + /// The accumulated request messages (user input, chat history and any others provided by AI context providers) + /// that were used by the agent for this invocation. /// The exception that caused the invocation to fail. /// , , or is . public InvokedContext( @@ -311,7 +312,8 @@ public InvokedContext( public AgentSession? Session { get; } /// - /// Gets the caller provided messages that were used by the agent for this invocation. + /// Gets the accumulated request messages (user input, chat history and any others provided by AI context providers) + /// that were used by the agent for this invocation. /// /// /// A collection of instances representing new messages that were provided by the caller.