// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
///
/// Provides an in-memory implementation of with support for message reduction and collection semantics.
///
///
///
/// stores chat messages entirely in local memory, providing fast access and manipulation
/// capabilities. It implements both for agent integration and
/// for direct collection manipulation.
///
///
/// This store maintains all messages in memory. For long-running conversations or high-volume scenarios, consider using
/// message reduction strategies or alternative storage implementations.
///
///
[DebuggerDisplay("Count = {Count}")]
[DebuggerTypeProxy(typeof(DebugView))]
public sealed class InMemoryChatMessageStore : ChatMessageStore, IList
{
private List _messages;
///
/// Initializes a new instance of the class.
///
///
/// This constructor creates a basic in-memory store without message reduction capabilities.
/// Messages will be stored exactly as added without any automatic processing or reduction.
///
public InMemoryChatMessageStore()
{
this._messages = [];
}
///
/// Initializes a new instance of the class from previously serialized state.
///
/// A representing the serialized state of the message store.
/// Optional settings for customizing the JSON deserialization process.
/// The is not a valid JSON object or cannot be deserialized.
///
/// This constructor enables restoration of message stores from previously saved state, allowing
/// conversation history to be preserved across application restarts or migrated between instances.
/// The store will be configured with default settings and message reduction before retrieval.
///
public InMemoryChatMessageStore(JsonElement serializedStoreState, JsonSerializerOptions? jsonSerializerOptions = null)
: this(null, serializedStoreState, jsonSerializerOptions, ChatReducerTriggerEvent.BeforeMessagesRetrieval)
{
}
///
/// Initializes a new instance of the class.
///
///
/// A instance used to process, reduce, or optimize chat messages.
/// This can be used to implement strategies like message summarization, truncation, or cleanup.
///
///
/// Specifies when the message reducer should be invoked. The default is ,
/// which applies reduction logic when messages are retrieved for agent consumption.
///
/// is .
///
/// Message reducers enable automatic management of message storage by implementing strategies to
/// keep memory usage under control while preserving important conversation context.
///
public InMemoryChatMessageStore(IChatReducer chatReducer, ChatReducerTriggerEvent reducerTriggerEvent = ChatReducerTriggerEvent.BeforeMessagesRetrieval)
: this(chatReducer, default, null, reducerTriggerEvent)
{
Throw.IfNull(chatReducer);
}
///
/// Initializes a new instance of the class, with an existing state from a serialized JSON element.
///
/// An optional instance used to process or reduce chat messages. If null, no reduction logic will be applied.
/// A representing the serialized state of the store.
/// Optional settings for customizing the JSON deserialization process.
/// The event that should trigger the reducer invocation.
public InMemoryChatMessageStore(IChatReducer? chatReducer, JsonElement serializedStoreState, JsonSerializerOptions? jsonSerializerOptions = null, ChatReducerTriggerEvent reducerTriggerEvent = ChatReducerTriggerEvent.BeforeMessagesRetrieval)
{
this.ChatReducer = chatReducer;
this.ReducerTriggerEvent = reducerTriggerEvent;
if (serializedStoreState.ValueKind is JsonValueKind.Object)
{
var jso = jsonSerializerOptions ?? AgentAbstractionsJsonUtilities.DefaultOptions;
var state = serializedStoreState.Deserialize(
jso.GetTypeInfo(typeof(StoreState))) as StoreState;
if (state?.Messages is { } messages)
{
this._messages = messages;
return;
}
}
this._messages = [];
}
///
/// Gets the chat reducer used to process or reduce chat messages. If null, no reduction logic will be applied.
///
public IChatReducer? ChatReducer { get; }
///
/// Gets the event that triggers the reducer invocation in this store.
///
public ChatReducerTriggerEvent ReducerTriggerEvent { get; }
///
public int Count => this._messages.Count;
///
public bool IsReadOnly => ((IList)this._messages).IsReadOnly;
///
public ChatMessage this[int index]
{
get => this._messages[index];
set => this._messages[index] = value;
}
///
public override async ValueTask> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
_ = Throw.IfNull(context);
if (this.ReducerTriggerEvent is ChatReducerTriggerEvent.BeforeMessagesRetrieval && this.ChatReducer is not null)
{
this._messages = (await this.ChatReducer.ReduceAsync(this._messages, cancellationToken).ConfigureAwait(false)).ToList();
}
return this._messages;
}
///
public override async ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
{
_ = Throw.IfNull(context);
if (context.InvokeException is not null)
{
return;
}
// Add request, AI context provider, and response messages to the store
var allNewMessages = context.RequestMessages.Concat(context.AIContextProviderMessages ?? []).Concat(context.ResponseMessages ?? []);
this._messages.AddRange(allNewMessages);
if (this.ReducerTriggerEvent is ChatReducerTriggerEvent.AfterMessageAdded && this.ChatReducer is not null)
{
this._messages = (await this.ChatReducer.ReduceAsync(this._messages, cancellationToken).ConfigureAwait(false)).ToList();
}
}
///
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
{
StoreState state = new()
{
Messages = this._messages,
};
var jso = jsonSerializerOptions ?? AgentAbstractionsJsonUtilities.DefaultOptions;
return JsonSerializer.SerializeToElement(state, jso.GetTypeInfo(typeof(StoreState)));
}
///
public int IndexOf(ChatMessage item)
=> this._messages.IndexOf(item);
///
public void Insert(int index, ChatMessage item)
=> this._messages.Insert(index, item);
///
public void RemoveAt(int index)
=> this._messages.RemoveAt(index);
///
public void Add(ChatMessage item)
=> this._messages.Add(item);
///
public void Clear()
=> this._messages.Clear();
///
public bool Contains(ChatMessage item)
=> this._messages.Contains(item);
///
public void CopyTo(ChatMessage[] array, int arrayIndex)
=> this._messages.CopyTo(array, arrayIndex);
///
public bool Remove(ChatMessage item)
=> this._messages.Remove(item);
///
public IEnumerator GetEnumerator()
=> this._messages.GetEnumerator();
///
IEnumerator IEnumerable.GetEnumerator()
=> this.GetEnumerator();
internal sealed class StoreState
{
public List Messages { get; set; } = [];
}
///
/// Defines the events that can trigger a reducer in the .
///
public enum ChatReducerTriggerEvent
{
///
/// Trigger the reducer when a new message is added.
/// will only complete when reducer processing is done.
///
AfterMessageAdded,
///
/// Trigger the reducer before messages are retrieved from the store.
/// The reducer will process the messages before they are returned to the caller.
///
BeforeMessagesRetrieval
}
private sealed class DebugView(InMemoryChatMessageStore store)
{
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public ChatMessage[] Items => store._messages.ToArray();
}
}