diff --git a/.gitignore b/.gitignore
index e6b9efb40e..258a8c0704 100644
--- a/.gitignore
+++ b/.gitignore
@@ -214,6 +214,7 @@ WARP.md
**/memory-bank/
**/projectBrief.md
**/tmpclaude*
+.kiro/
# Dependency-bound validation reports
python/scripts/dependency-*-results.json
python/scripts/dependencies/dependency-*-results.json
diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index fc57fdea08..76a4444bc1 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -138,10 +138,15 @@
+
+
+
+
+
diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 98c364cd53..6afa318012 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -194,6 +194,8 @@
+
+
@@ -625,6 +627,7 @@
+
@@ -678,5 +681,6 @@
+
diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey/AgentWithMemory_Step03_MemoryUsingValkey.csproj b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey/AgentWithMemory_Step03_MemoryUsingValkey.csproj
new file mode 100644
index 0000000000..1217591bc1
--- /dev/null
+++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey/AgentWithMemory_Step03_MemoryUsingValkey.csproj
@@ -0,0 +1,22 @@
+
+
+
+ Exe
+ net10.0
+
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey/Program.cs b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey/Program.cs
new file mode 100644
index 0000000000..6faa02a0f3
--- /dev/null
+++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey/Program.cs
@@ -0,0 +1,55 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample demonstrates using Valkey for persistent chat history with the Agent Framework.
+// ValkeyChatHistoryProvider persists conversation history across sessions using Valkey lists.
+//
+// Prerequisites:
+// - A running Valkey server (any version):
+// docker run -d --name valkey -p 6379:6379 valkey/valkey:latest
+// - Azure OpenAI endpoint and deployment configured via environment variables
+
+using Azure.AI.OpenAI;
+using Azure.Identity;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.Valkey;
+using Microsoft.Extensions.AI;
+using OpenAI.Chat;
+using Valkey.Glide;
+
+var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
+var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
+var valkeyConnection = Environment.GetEnvironmentVariable("VALKEY_CONNECTION") ?? "localhost:6379";
+
+var connection = await ConnectionMultiplexer.ConnectAsync(valkeyConnection);
+
+Console.WriteLine("=== ValkeyChatHistoryProvider — Persistent Chat History ===\n");
+
+var historyProvider = new ValkeyChatHistoryProvider(
+ connection,
+ _ => new ValkeyChatHistoryProvider.State($"sample-{Guid.NewGuid():N}"),
+ new ValkeyChatHistoryProviderOptions
+ {
+ KeyPrefix = "sample_chat",
+ MaxMessages = 20
+ });
+
+AIAgent historyAgent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
+ .GetChatClient(deploymentName)
+ .AsAIAgent(new ChatClientAgentOptions()
+ {
+ ChatOptions = new() { Instructions = "You are a helpful assistant that remembers our conversation." },
+ ChatHistoryProvider = historyProvider
+ });
+
+AgentSession session1 = await historyAgent.CreateSessionAsync();
+Console.WriteLine(await historyAgent.RunAsync("Hello! My name is Alex and I'm a software engineer.", session1));
+Console.WriteLine(await historyAgent.RunAsync("I'm working on a project using Valkey for caching.", session1));
+Console.WriteLine(await historyAgent.RunAsync("What do you remember about me?", session1));
+
+var messageCount = await historyProvider.GetMessageCountAsync(session1);
+Console.WriteLine($"\n Stored {messageCount} messages in Valkey.\n");
+
+// Clean up
+connection.Dispose();
+
+Console.WriteLine("Done!");
diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey/README.md b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey/README.md
new file mode 100644
index 0000000000..08f65ecffa
--- /dev/null
+++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey/README.md
@@ -0,0 +1,30 @@
+# Agent with Memory Using Valkey
+
+This sample demonstrates using Valkey for persistent chat history with the Agent Framework.
+
+## Components
+
+- **ValkeyChatHistoryProvider** — Persists conversation history across sessions using Valkey lists. Works with any Valkey or Redis OSS server (no search module required).
+
+## Prerequisites
+
+- Azure OpenAI endpoint and deployment
+- A running Valkey server (any version):
+
+```bash
+docker run -d --name valkey -p 6379:6379 valkey/valkey:latest
+```
+
+## Environment Variables
+
+| Variable | Description | Default |
+|---|---|---|
+| `AZURE_OPENAI_ENDPOINT` | Azure OpenAI endpoint URL | (required) |
+| `AZURE_OPENAI_DEPLOYMENT_NAME` | Model deployment name | `gpt-5.4-mini` |
+| `VALKEY_CONNECTION` | Valkey connection string | `localhost:6379` |
+
+## Running
+
+```bash
+dotnet run
+```
diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock.csproj b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock.csproj
new file mode 100644
index 0000000000..274ac15c97
--- /dev/null
+++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock.csproj
@@ -0,0 +1,20 @@
+
+
+
+ Exe
+ net10.0
+
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock/Program.cs b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock/Program.cs
new file mode 100644
index 0000000000..6f3027681f
--- /dev/null
+++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock/Program.cs
@@ -0,0 +1,57 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample demonstrates using Valkey for persistent chat history with the Agent Framework,
+// powered by Amazon Bedrock.
+//
+// Prerequisites:
+// - A running Valkey server (any version):
+// docker run -d --name valkey -p 6379:6379 valkey/valkey:latest
+// - AWS credentials configured (environment variables, AWS profile, or IAM role)
+// - Access to an Amazon Bedrock model (e.g., Anthropic Claude)
+
+using Amazon;
+using Amazon.BedrockRuntime;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.Valkey;
+using Microsoft.Extensions.AI;
+using Valkey.Glide;
+
+var awsRegion = Environment.GetEnvironmentVariable("AWS_REGION") ?? "us-east-1";
+var modelId = Environment.GetEnvironmentVariable("BEDROCK_MODEL_ID") ?? "anthropic.claude-3-5-sonnet-20241022-v2:0";
+var valkeyConnection = Environment.GetEnvironmentVariable("VALKEY_CONNECTION") ?? "localhost:6379";
+
+// Create the Bedrock runtime client.
+var bedrockRuntime = new AmazonBedrockRuntimeClient(RegionEndpoint.GetBySystemName(awsRegion));
+IChatClient chatClient = bedrockRuntime.AsIChatClient(modelId);
+
+var connection = await ConnectionMultiplexer.ConnectAsync(valkeyConnection);
+
+Console.WriteLine("=== ValkeyChatHistoryProvider — Persistent Chat History (Bedrock) ===\n");
+
+var historyProvider = new ValkeyChatHistoryProvider(
+ connection,
+ _ => new ValkeyChatHistoryProvider.State($"bedrock-sample-{Guid.NewGuid():N}"),
+ new ValkeyChatHistoryProviderOptions
+ {
+ KeyPrefix = "bedrock_chat",
+ MaxMessages = 20
+ });
+
+AIAgent historyAgent = chatClient.AsAIAgent(new ChatClientAgentOptions()
+{
+ ChatOptions = new() { Instructions = "You are a helpful assistant that remembers our conversation." },
+ ChatHistoryProvider = historyProvider
+});
+
+AgentSession session1 = await historyAgent.CreateSessionAsync();
+Console.WriteLine(await historyAgent.RunAsync("Hello! My name is Alex and I'm a software engineer.", session1));
+Console.WriteLine(await historyAgent.RunAsync("I'm working on a project using Valkey for caching.", session1));
+Console.WriteLine(await historyAgent.RunAsync("What do you remember about me?", session1));
+
+var messageCount = await historyProvider.GetMessageCountAsync(session1);
+Console.WriteLine($"\n Stored {messageCount} messages in Valkey.\n");
+
+// Clean up
+connection.Dispose();
+
+Console.WriteLine("Done!");
diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock/README.md b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock/README.md
new file mode 100644
index 0000000000..06d4012bd9
--- /dev/null
+++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock/README.md
@@ -0,0 +1,41 @@
+# Agent with Memory Using Valkey + Amazon Bedrock
+
+This sample demonstrates using Valkey for persistent chat history with the Agent Framework, powered by Amazon Bedrock via the `AWSSDK.Extensions.Bedrock.MEAI` adapter.
+
+## Components
+
+- **ValkeyChatHistoryProvider** — Persists conversation history across sessions using Valkey lists. Works with any Valkey or Redis OSS server (no search module required).
+- **Amazon Bedrock** — Provides the LLM via `AWSSDK.Extensions.Bedrock.MEAI`, which implements `IChatClient` from `Microsoft.Extensions.AI`.
+
+## Prerequisites
+
+- AWS credentials configured (environment variables, AWS CLI profile, or IAM role)
+- Access to an Amazon Bedrock model (e.g., Anthropic Claude 3.5 Sonnet)
+- A running Valkey server (any version):
+
+```bash
+docker run -d --name valkey -p 6379:6379 valkey/valkey:latest
+```
+
+## Environment Variables
+
+| Variable | Description | Default |
+|---|---|---|
+| `AWS_REGION` | AWS region for Bedrock | `us-east-1` |
+| `BEDROCK_MODEL_ID` | Bedrock model identifier | `anthropic.claude-3-5-sonnet-20241022-v2:0` |
+| `VALKEY_CONNECTION` | Valkey connection string | `localhost:6379` |
+| `AWS_ACCESS_KEY_ID` | AWS access key (if not using profile/role) | — |
+| `AWS_SECRET_ACCESS_KEY` | AWS secret key (if not using profile/role) | — |
+
+## Running
+
+```bash
+# Using default AWS credential chain (profile, env vars, or IAM role)
+dotnet run
+
+# Or with explicit credentials
+export AWS_ACCESS_KEY_ID="your-access-key"
+export AWS_SECRET_ACCESS_KEY="your-secret-key"
+export AWS_REGION="us-east-1"
+dotnet run
+```
diff --git a/dotnet/src/Microsoft.Agents.AI.Valkey/Microsoft.Agents.AI.Valkey.csproj b/dotnet/src/Microsoft.Agents.AI.Valkey/Microsoft.Agents.AI.Valkey.csproj
new file mode 100644
index 0000000000..e819c3f51c
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Valkey/Microsoft.Agents.AI.Valkey.csproj
@@ -0,0 +1,40 @@
+
+
+
+ $(TargetFrameworksCore)
+ Microsoft.Agents.AI.Valkey
+ alpha
+ $(NoWarn);CA1873
+
+
+
+ true
+ true
+
+
+
+
+
+ false
+
+
+
+
+ Microsoft Agent Framework - Valkey integration
+ Provides Valkey integration for Microsoft Agent Framework, including chat history persistence and context provider with full-text search.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/src/Microsoft.Agents.AI.Valkey/ValkeyChatHistoryProvider.cs b/dotnet/src/Microsoft.Agents.AI.Valkey/ValkeyChatHistoryProvider.cs
new file mode 100644
index 0000000000..088d66ed47
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Valkey/ValkeyChatHistoryProvider.cs
@@ -0,0 +1,225 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Logging;
+using Microsoft.Shared.Diagnostics;
+using Valkey.Glide;
+
+namespace Microsoft.Agents.AI.Valkey;
+
+///
+/// Provides a Valkey-backed implementation of for persistent chat history storage.
+///
+///
+///
+/// Uses basic Valkey list operations via Valkey.Glide.
+/// No search module is required — this provider works with any Valkey server.
+///
+///
+/// Data retention: Stored messages have no TTL and persist indefinitely.
+/// Use to limit per-conversation storage, and
+/// for explicit cleanup. Callers are responsible for implementing data retention policies.
+///
+///
+/// Security considerations:
+///
+/// - PII and sensitive data: Chat history stored in Valkey may contain PII and sensitive
+/// conversation content. Ensure the Valkey server is configured with appropriate access controls and encryption in transit
+/// (TLS). The property can limit stored messages per conversation.
+/// - Compromised store risks: Agent Framework does not validate or filter messages loaded
+/// from the store — they are accepted as-is. If the Valkey store is compromised, adversarial content could be injected
+/// into the conversation context.
+///
+///
+///
+public sealed class ValkeyChatHistoryProvider : ChatHistoryProvider
+{
+ private readonly ProviderSessionState _sessionState;
+ private IReadOnlyList? _stateKeys;
+ private readonly IConnectionMultiplexer _connection;
+ private readonly string _keyPrefix;
+ private readonly int? _maxMessages;
+ private readonly int? _maxMessagesToRetrieve;
+ private readonly JsonSerializerOptions _jsonSerializerOptions;
+ private readonly ILogger? _logger;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// An existing instance.
+ /// A delegate that initializes the provider state on the first invocation.
+ /// Optional configuration options.
+ /// Optional logger factory.
+ public ValkeyChatHistoryProvider(
+ IConnectionMultiplexer connection,
+ Func stateInitializer,
+ ValkeyChatHistoryProviderOptions? options = null,
+ ILoggerFactory? loggerFactory = null)
+ : base(options?.ProvideOutputMessageFilter, options?.StoreInputRequestMessageFilter, options?.StoreInputResponseMessageFilter)
+ {
+ this._sessionState = new ProviderSessionState(
+ Throw.IfNull(stateInitializer),
+ options?.StateKey ?? this.GetType().Name,
+ options?.JsonSerializerOptions);
+ this._connection = Throw.IfNull(connection);
+ this._keyPrefix = options?.KeyPrefix ?? "chat_history";
+ this._maxMessages = options?.MaxMessages;
+ this._maxMessagesToRetrieve = options?.MaxMessagesToRetrieve;
+ this._jsonSerializerOptions = options?.JsonSerializerOptions ?? AgentAbstractionsJsonUtilities.DefaultOptions;
+ this._logger = loggerFactory?.CreateLogger();
+ }
+
+ ///
+ public override IReadOnlyList StateKeys => this._stateKeys ??= [this._sessionState.StateKey];
+
+ ///
+ protected override async ValueTask> ProvideChatHistoryAsync(InvokingContext context, CancellationToken cancellationToken = default)
+ {
+ Throw.IfNull(context);
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var state = this._sessionState.GetOrInitializeState(context.Session);
+ var db = this._connection.GetDatabase();
+ var key = this.BuildKey(state);
+
+ // Fetch only the tail when MaxMessagesToRetrieve is set [Low: avoid fetching all then trimming]
+ ValkeyValue[] values;
+ if (this._maxMessagesToRetrieve.HasValue)
+ {
+ values = await db.ListRangeAsync(key, -this._maxMessagesToRetrieve.Value, -1).ConfigureAwait(false);
+ }
+ else
+ {
+ values = await db.ListRangeAsync(key).ConfigureAwait(false);
+ }
+
+ var messages = new List(values.Length);
+
+ foreach (var value in values)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (value.IsNullOrEmpty)
+ {
+ continue;
+ }
+
+ try
+ {
+ var message = JsonSerializer.Deserialize(value.ToString(), this._jsonSerializerOptions.GetTypeInfo(typeof(ChatMessage))) as ChatMessage;
+ if (message is not null)
+ {
+ messages.Add(message);
+ }
+ }
+ catch (JsonException ex)
+ {
+ // Skip malformed entries rather than crashing the session [VERIFY-002]
+ this._logger?.LogWarning(ex, "ValkeyChatHistoryProvider: Skipping malformed message in conversation '{ConversationId}'.", state.ConversationId);
+ }
+ }
+
+ this._logger?.LogInformation(
+ "ValkeyChatHistoryProvider: Retrieved {Count} messages for conversation.",
+ messages.Count);
+
+ return messages;
+ }
+
+ ///
+ protected override async ValueTask StoreChatHistoryAsync(InvokedContext context, CancellationToken cancellationToken = default)
+ {
+ Throw.IfNull(context);
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var state = this._sessionState.GetOrInitializeState(context.Session);
+ var messageList = context.RequestMessages.Concat(context.ResponseMessages ?? []).ToList();
+ if (messageList.Count == 0)
+ {
+ return;
+ }
+
+ var db = this._connection.GetDatabase();
+ var key = this.BuildKey(state);
+
+ // Batch push — single round-trip [Medium-8]
+ var serialized = new ValkeyValue[messageList.Count];
+ for (int i = 0; i < messageList.Count; i++)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ serialized[i] = JsonSerializer.Serialize(messageList[i], this._jsonSerializerOptions.GetTypeInfo(typeof(ChatMessage)));
+ }
+
+ await db.ListRightPushAsync(key, serialized).ConfigureAwait(false);
+
+ // Trim to max messages if configured
+ if (this._maxMessages.HasValue)
+ {
+ await db.ListTrimAsync(key, -this._maxMessages.Value, -1).ConfigureAwait(false);
+ }
+
+ this._logger?.LogInformation(
+ "ValkeyChatHistoryProvider: Stored {Count} messages for conversation.",
+ messageList.Count);
+ }
+
+ ///
+ /// Clears all messages for the specified session's conversation.
+ ///
+ /// The session containing the conversation state.
+ /// Cancellation token.
+ /// A task representing the asynchronous operation.
+ public async Task ClearMessagesAsync(AgentSession? session, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ var state = this._sessionState.GetOrInitializeState(session);
+ var db = this._connection.GetDatabase();
+ var key = this.BuildKey(state);
+ await db.KeyDeleteAsync(key).ConfigureAwait(false);
+ }
+
+ ///
+ /// Gets the count of stored messages for the specified session's conversation.
+ ///
+ /// The session containing the conversation state.
+ /// Cancellation token.
+ /// The number of stored messages.
+ public async Task GetMessageCountAsync(AgentSession? session, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ var state = this._sessionState.GetOrInitializeState(session);
+ var db = this._connection.GetDatabase();
+ var key = this.BuildKey(state);
+ return await db.ListLengthAsync(key).ConfigureAwait(false);
+ }
+
+ private string BuildKey(State state) => $"{this._keyPrefix}:{state.ConversationId}";
+
+ ///
+ /// Represents the per-session state of a .
+ ///
+ public sealed class State
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The unique identifier for this conversation thread.
+ [JsonConstructor]
+ public State(string conversationId)
+ {
+ this.ConversationId = Throw.IfNullOrWhitespace(conversationId);
+ }
+
+ ///
+ /// Gets the conversation ID associated with this state.
+ ///
+ public string ConversationId { get; }
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Valkey/ValkeyChatHistoryProviderOptions.cs b/dotnet/src/Microsoft.Agents.AI.Valkey/ValkeyChatHistoryProviderOptions.cs
new file mode 100644
index 0000000000..eabe4680cf
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Valkey/ValkeyChatHistoryProviderOptions.cs
@@ -0,0 +1,56 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Text.Json;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.Valkey;
+
+///
+/// Options for configuring .
+///
+public sealed class ValkeyChatHistoryProviderOptions
+{
+ ///
+ /// Gets or sets the prefix for Valkey keys. Defaults to "chat_history".
+ ///
+ public string KeyPrefix { get; set; } = "chat_history";
+
+ ///
+ /// Gets or sets the maximum number of messages to retain per conversation.
+ /// When exceeded, oldest messages are automatically trimmed. Null means unlimited.
+ ///
+ public int? MaxMessages { get; set; }
+
+ ///
+ /// Gets or sets the maximum number of messages to retrieve from the provider.
+ /// Null means no limit.
+ ///
+ public int? MaxMessagesToRetrieve { get; set; }
+
+ ///
+ /// Gets or sets an optional key for storing state in the session's StateBag.
+ ///
+ public string? StateKey { get; set; }
+
+ ///
+ /// Gets or sets optional JSON serializer options for serializing the state of this provider.
+ ///
+ public JsonSerializerOptions? JsonSerializerOptions { get; set; }
+
+ ///
+ /// Gets or sets an optional filter for messages when retrieving from history.
+ ///
+ public Func, IEnumerable>? ProvideOutputMessageFilter { get; set; }
+
+ ///
+ /// Gets or sets an optional filter for request messages before storing.
+ ///
+ public Func, IEnumerable>? StoreInputRequestMessageFilter { get; set; }
+
+ ///
+ /// Gets or sets an optional filter for response messages before storing.
+ ///
+ public Func, IEnumerable>? StoreInputResponseMessageFilter { get; set; }
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Valkey.UnitTests/Microsoft.Agents.AI.Valkey.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.Valkey.UnitTests/Microsoft.Agents.AI.Valkey.UnitTests.csproj
new file mode 100644
index 0000000000..4dd4ff1f6c
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Valkey.UnitTests/Microsoft.Agents.AI.Valkey.UnitTests.csproj
@@ -0,0 +1,11 @@
+
+
+
+ net10.0
+
+
+
+
+
+
+
diff --git a/dotnet/tests/Microsoft.Agents.AI.Valkey.UnitTests/TestHelpers.cs b/dotnet/tests/Microsoft.Agents.AI.Valkey.UnitTests/TestHelpers.cs
new file mode 100644
index 0000000000..1f320ec34f
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Valkey.UnitTests/TestHelpers.cs
@@ -0,0 +1,44 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using Microsoft.Extensions.AI;
+using Moq;
+
+namespace Microsoft.Agents.AI.Valkey.UnitTests;
+
+internal sealed class TestAgentSession : AgentSession
+{
+ public TestAgentSession()
+ {
+ this.StateBag = new AgentSessionStateBag();
+ }
+}
+
+internal static class TestHelpers
+{
+ internal static readonly AIAgent MockAgent = new Mock().Object;
+
+ internal static ChatHistoryProvider.InvokingContext CreateChatHistoryInvokingContext(
+ IEnumerable? requestMessages = null)
+ {
+#pragma warning disable MAAI001
+ return new ChatHistoryProvider.InvokingContext(
+ MockAgent,
+ new TestAgentSession(),
+ requestMessages ?? [new ChatMessage(ChatRole.User, "test")]);
+#pragma warning restore MAAI001
+ }
+
+ internal static ChatHistoryProvider.InvokedContext CreateChatHistoryInvokedContext(
+ IEnumerable requestMessages,
+ IEnumerable responseMessages)
+ {
+#pragma warning disable MAAI001
+ return new ChatHistoryProvider.InvokedContext(
+ MockAgent,
+ new TestAgentSession(),
+ requestMessages,
+ responseMessages);
+#pragma warning restore MAAI001
+ }
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Valkey.UnitTests/ValkeyChatHistoryProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.Valkey.UnitTests/ValkeyChatHistoryProviderTests.cs
new file mode 100644
index 0000000000..d624b58fa1
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Valkey.UnitTests/ValkeyChatHistoryProviderTests.cs
@@ -0,0 +1,249 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Linq;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.AI;
+using Moq;
+using Valkey.Glide;
+
+namespace Microsoft.Agents.AI.Valkey.UnitTests;
+
+///
+/// Unit tests for .
+///
+public sealed class ValkeyChatHistoryProviderTests
+{
+ private static Mock CreateMockConnection(Mock? dbMock = null)
+ {
+ var mockConnection = new Mock();
+ dbMock ??= new Mock();
+ mockConnection.Setup(c => c.GetDatabase()).Returns(dbMock.Object);
+ return mockConnection;
+ }
+
+ // --- Constructor tests ---
+
+ [Fact]
+ public void Constructor_WithConnection_SetsProperties()
+ {
+ // Arrange & Act
+ var provider = new ValkeyChatHistoryProvider(
+ CreateMockConnection().Object,
+ static (_) => new ValkeyChatHistoryProvider.State("conv-1"),
+ new ValkeyChatHistoryProviderOptions { KeyPrefix = "test_prefix" });
+
+ // Assert
+ Assert.NotNull(provider);
+ }
+
+ [Fact]
+ public void Constructor_WithConnection_NullConnection_Throws()
+ {
+ // Act & Assert
+ Assert.Throws(() =>
+ new ValkeyChatHistoryProvider(
+ null!,
+ static (_) => new ValkeyChatHistoryProvider.State("conv-1")));
+ }
+
+ [Fact]
+ public void Constructor_WithConnection_NullStateInitializer_Throws()
+ {
+ // Act & Assert
+ Assert.Throws(() =>
+ new ValkeyChatHistoryProvider(
+ CreateMockConnection().Object,
+ null!));
+ }
+
+ // --- State tests ---
+
+ [Fact]
+ public void State_NullConversationId_Throws()
+ {
+ Assert.Throws(() => new ValkeyChatHistoryProvider.State(null!));
+ }
+
+ [Fact]
+ public void State_EmptyConversationId_Throws()
+ {
+ Assert.Throws(() => new ValkeyChatHistoryProvider.State(""));
+ }
+
+ [Fact]
+ public void State_ValidConversationId_SetsProperty()
+ {
+ var state = new ValkeyChatHistoryProvider.State("my-conversation");
+ Assert.Equal("my-conversation", state.ConversationId);
+ }
+
+ [Fact]
+ public void State_JsonConstructor_RoundTrips()
+ {
+ // Arrange
+ var original = new ValkeyChatHistoryProvider.State("test-conv");
+
+ // Act
+ var json = JsonSerializer.Serialize(original);
+ var deserialized = JsonSerializer.Deserialize(json);
+
+ // Assert
+ Assert.NotNull(deserialized);
+ Assert.Equal("test-conv", deserialized.ConversationId);
+ }
+
+ // --- StateKeys tests ---
+
+ [Fact]
+ public void StateKeys_ReturnsProviderTypeName()
+ {
+ var provider = new ValkeyChatHistoryProvider(
+ CreateMockConnection().Object,
+ _ => new ValkeyChatHistoryProvider.State("conv-1"));
+
+ var keys = provider.StateKeys;
+ Assert.Single(keys);
+ Assert.Equal(nameof(ValkeyChatHistoryProvider), keys[0]);
+ }
+
+ [Fact]
+ public void StateKeys_WithCustomKey_ReturnsCustomKey()
+ {
+ var provider = new ValkeyChatHistoryProvider(
+ CreateMockConnection().Object,
+ _ => new ValkeyChatHistoryProvider.State("conv-1"),
+ new ValkeyChatHistoryProviderOptions { StateKey = "custom_key" });
+
+ var keys = provider.StateKeys;
+ Assert.Single(keys);
+ Assert.Equal("custom_key", keys[0]);
+ }
+
+ // --- ProvideChatHistoryAsync tests ---
+
+ [Fact]
+ public async Task ProvideChatHistoryAsync_ReturnsDeserializedMessagesAsync()
+ {
+ // Arrange
+ var dbMock = new Mock();
+ var msg1 = new ChatMessage(ChatRole.User, "hello");
+ var msg2 = new ChatMessage(ChatRole.Assistant, "hi there");
+ var values = new ValkeyValue[]
+ {
+ JsonSerializer.Serialize(msg1),
+ JsonSerializer.Serialize(msg2)
+ };
+ dbMock.Setup(d => d.ListRangeAsync(It.IsAny(), It.IsAny(), It.IsAny()))
+ .ReturnsAsync(values);
+
+ var provider = new ValkeyChatHistoryProvider(
+ CreateMockConnection(dbMock).Object,
+ _ => new ValkeyChatHistoryProvider.State("conv-1"));
+
+ var context = TestHelpers.CreateChatHistoryInvokingContext();
+
+ // Act — should not throw
+ var result = await provider.InvokingAsync(context);
+ var messages = result.ToList();
+
+ // Assert — only the valid message + request message
+ Assert.True(messages.Count >= 1);
+ }
+
+ [Fact]
+ public async Task ProvideChatHistoryAsync_WithMaxMessagesToRetrieve_UsesRangeQueryAsync()
+ {
+ // Arrange
+ var dbMock = new Mock();
+ dbMock.Setup(d => d.ListRangeAsync(It.IsAny(), It.IsAny(), It.IsAny()))
+ .ReturnsAsync([]);
+
+ var provider = new ValkeyChatHistoryProvider(
+ CreateMockConnection(dbMock).Object,
+ _ => new ValkeyChatHistoryProvider.State("conv-1"),
+ new ValkeyChatHistoryProviderOptions { MaxMessagesToRetrieve = 5 });
+
+ var context = TestHelpers.CreateChatHistoryInvokingContext();
+
+ // Act
+ await provider.InvokingAsync(context);
+
+ // Assert — should use -5, -1 range
+ dbMock.Verify(d => d.ListRangeAsync(
+ It.IsAny(), -5, -1), Times.Once);
+ }
+
+ [Fact]
+ public async Task ProvideChatHistoryAsync_CancellationToken_ThrowsAsync()
+ {
+ // Arrange
+ var provider = new ValkeyChatHistoryProvider(
+ CreateMockConnection().Object,
+ _ => new ValkeyChatHistoryProvider.State("conv-1"));
+
+ var cts = new CancellationTokenSource();
+ cts.Cancel();
+
+ var context = TestHelpers.CreateChatHistoryInvokingContext();
+
+ // Act & Assert
+ await Assert.ThrowsAsync(() =>
+ provider.InvokingAsync(context, cts.Token).AsTask());
+ }
+
+ // --- StoreChatHistoryAsync tests ---
+
+ [Fact]
+ public async Task StoreChatHistoryAsync_BatchPushesMessagesAsync()
+ {
+ // Arrange
+ var dbMock = new Mock();
+ dbMock.Setup(d => d.ListRightPushAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(2);
+
+ var provider = new ValkeyChatHistoryProvider(
+ CreateMockConnection(dbMock).Object,
+ _ => new ValkeyChatHistoryProvider.State("conv-1"));
+
+ var context = TestHelpers.CreateChatHistoryInvokedContext(
+ [new ChatMessage(ChatRole.User, "hello")],
+ [new ChatMessage(ChatRole.Assistant, "hi")]);
+
+ // Act
+ await provider.InvokedAsync(context);
+
+ // Assert — batch push called once with array
+ dbMock.Verify(d => d.ListRightPushAsync(
+ It.IsAny(), It.IsAny()), Times.Once);
+ }
+
+ [Fact]
+ public async Task StoreChatHistoryAsync_WithMaxMessages_TrimsAsync()
+ {
+ // Arrange
+ var dbMock = new Mock();
+ dbMock.Setup(d => d.ListRightPushAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(1);
+ dbMock.Setup(d => d.ListTrimAsync(It.IsAny(), It.IsAny(), It.IsAny()))
+ .Returns(Task.CompletedTask);
+
+ var provider = new ValkeyChatHistoryProvider(
+ CreateMockConnection(dbMock).Object,
+ _ => new ValkeyChatHistoryProvider.State("conv-1"),
+ new ValkeyChatHistoryProviderOptions { MaxMessages = 10 });
+
+ var context = TestHelpers.CreateChatHistoryInvokedContext(
+ [new ChatMessage(ChatRole.User, "hello")],
+ [new ChatMessage(ChatRole.Assistant, "hi")]);
+
+ // Act
+ await provider.InvokedAsync(context);
+
+ // Assert — trim called unconditionally when MaxMessages is set
+ dbMock.Verify(d => d.ListTrimAsync(
+ It.IsAny(), -10, -1), Times.Once);
+ }
+}