// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI; /// /// Provides an abstract base class for fetching chat messages from, and adding chat messages to, chat history for the purposes of agent execution. /// /// /// /// defines the contract that an can use to retrieve messsages from chat history /// and provide notification of newly produced messages. /// Implementations are responsible for managing message persistence, retrieval, and any necessary optimization /// strategies such as truncation, summarization, or archival. /// /// /// Key responsibilities include: /// /// Storing chat messages with proper ordering and metadata preservation /// Retrieving messages in chronological order for agent context /// Managing storage limits through truncation, summarization, or other strategies /// /// /// /// The is passed a reference to the via and /// allowing it to store state in the . Since a is used with many different sessions, it should /// not store any session-specific information within its own instance fields. Instead, any session-specific state should be stored in the associated . /// /// /// A is only relevant for scenarios where the underlying AI service that the agent is using /// does not use in-service chat history storage. /// /// /// Security considerations: Agent Framework does not validate or filter the messages returned by the provider /// during load — they are accepted as-is and treated identically to user-supplied messages. Implementers must ensure that only /// trusted data is returned. If the underlying storage is compromised, adversarial content could influence LLM behavior via /// indirect prompt injection — for example, injected messages could alter the conversation context or impersonate different roles. /// Messages stored in chat history may contain PII and sensitive conversation content; implementers should consider encryption /// at rest and appropriate access controls for the storage backend. /// /// public abstract class ChatHistoryProvider { private static IEnumerable DefaultExcludeChatHistoryFilter(IEnumerable messages) => messages.Where(m => m.GetAgentRequestMessageSourceType() != AgentRequestMessageSourceType.ChatHistory); private static IEnumerable DefaultNoopFilter(IEnumerable messages) => messages; private IReadOnlyList? _stateKeys; private readonly Func, IEnumerable>? _provideOutputMessageFilter; private readonly Func, IEnumerable> _storeInputRequestMessageFilter; private readonly Func, IEnumerable> _storeInputResponseMessageFilter; /// /// Initializes a new instance of the class. /// /// An optional filter function to apply to messages when retrieving them from the chat history. /// An optional filter function to apply to request messages before storing them in the chat history. If not set, defaults to excluding messages with source type . /// An optional filter function to apply to response messages before storing them in the chat history. If not set, defaults to a no-op filter that includes all response messages. protected ChatHistoryProvider( Func, IEnumerable>? provideOutputMessageFilter = null, Func, IEnumerable>? storeInputRequestMessageFilter = null, Func, IEnumerable>? storeInputResponseMessageFilter = null) { this._provideOutputMessageFilter = provideOutputMessageFilter; this._storeInputRequestMessageFilter = storeInputRequestMessageFilter ?? DefaultExcludeChatHistoryFilter; this._storeInputResponseMessageFilter = storeInputResponseMessageFilter ?? DefaultNoopFilter; } /// /// Gets the set of keys used to store the provider state in the . /// /// /// The default value is a single-element set containing the name of the concrete type (e.g. "InMemoryChatHistoryProvider"). /// Implementations may override this to provide custom keys, for example when multiple /// instances of the same provider type are used in the same session, or when a provider /// stores state under more than one key. /// public virtual IReadOnlyList StateKeys => this._stateKeys ??= [this.GetType().Name]; /// /// Called at the start of agent invocation to provide messages for the next agent invocation. /// /// Contains the request context including the caller provided messages that will be used by the agent for this invocation. /// The to monitor for cancellation requests. The default is . /// /// A task that represents the asynchronous operation. The task result contains a collection of /// instances that will be used for the agent invocation. /// /// /// /// If the total message history becomes very large, implementations should apply appropriate strategies to manage /// storage constraints, such as: /// /// Truncating older messages while preserving recent context /// Summarizing message groups to maintain essential context /// Implementing sliding window approaches for message retention /// Archiving old messages while keeping active conversation context /// /// /// public ValueTask> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default) => this.InvokingCoreAsync(Throw.IfNull(context), cancellationToken); /// /// Called at the start of agent invocation to provide messages for the next agent invocation. /// /// Contains the request context including the caller provided messages that will be used by the agent for this invocation. /// The to monitor for cancellation requests. The default is . /// /// A task that represents the asynchronous operation. The task result contains a collection of /// instances that will be used for the agent invocation. /// /// /// /// If the total message history becomes very large, implementations should apply appropriate strategies to manage /// storage constraints, such as: /// /// Truncating older messages while preserving recent context /// Summarizing message groups to maintain essential context /// Implementing sliding window approaches for message retention /// Archiving old messages while keeping active conversation context /// /// /// /// The default implementation of this method, calls to get the chat history messages, applies the optional retrieval output filter, /// and merges the returned messages with the caller provided messages (with chat history messages appearing first) before returning the full message list to be used for the invocation. /// For most scenarios, overriding is sufficient to return the desired chat history messages, while still benefiting from the default merging and filtering behavior. /// However, for scenarios that require more control over message filtering, merging or source stamping, overriding this method allows you to directly control the full set of messages returned for the invocation. /// /// protected virtual async ValueTask> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default) { var output = await this.ProvideChatHistoryAsync(context, cancellationToken).ConfigureAwait(false); if (this._provideOutputMessageFilter is not null) { output = this._provideOutputMessageFilter(output); } return output .Select(message => message.WithAgentRequestMessageSource(AgentRequestMessageSourceType.ChatHistory, this.GetType().FullName!)) .Concat(context.RequestMessages); } /// /// When overridden in a derived class, provides the chat history messages to be used for the current invocation. /// /// /// /// This method is called from . /// Note that can be overridden to directly control message filtering, merging and source stamping, in which case /// it is up to the implementer to call this method as needed to retrieve the unfiltered/unmerged chat history messages. /// /// /// In contrast with , this method only returns additional messages to be added to the request, /// while is responsible for returning the full set of messages to be used for the invocation (including caller provided messages). /// /// /// Messages are returned in chronological order to maintain proper conversation flow and context for the agent. /// The oldest messages appear first in the collection, followed by more recent messages. /// /// /// Security consideration: Messages loaded from storage should be treated with the same caution as user-supplied /// messages. A compromised storage backend could alter message roles to escalate trust (e.g., changing user messages to /// system messages) or inject adversarial content that influences LLM behavior. /// /// /// Contains the request context including the caller provided messages that will be used by the agent for this invocation. /// The to monitor for cancellation requests. The default is . /// /// A task that represents the asynchronous operation. The task result contains a collection of /// instances in ascending chronological order (oldest first). /// protected virtual ValueTask> ProvideChatHistoryAsync(InvokingContext context, CancellationToken cancellationToken = default) { return new ValueTask>([]); } /// /// Called at the end of the agent invocation to add new messages to the chat history. /// /// Contains the invocation context including request messages, response messages, and any exception that occurred. /// The to monitor for cancellation requests. The default is . /// A task that represents the asynchronous add operation. /// /// /// Messages should be added in the order they were generated to maintain proper chronological sequence. /// The is responsible for preserving message ordering and ensuring that subsequent calls to /// return messages in the correct chronological order. /// /// /// Implementations may perform additional processing during message addition, such as: /// /// Validating message content and metadata /// Applying storage optimizations or compression /// Triggering background maintenance operations /// /// /// /// This method is called regardless of whether the invocation succeeded or failed. /// To check if the invocation was successful, inspect the property. /// /// public ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default) => this.InvokedCoreAsync(Throw.IfNull(context), cancellationToken); /// /// Called at the end of the agent invocation to add new messages to the chat history. /// /// Contains the invocation context including request messages, response messages, and any exception that occurred. /// The to monitor for cancellation requests. The default is . /// A task that represents the asynchronous add operation. /// /// /// Messages should be added in the order they were generated to maintain proper chronological sequence. /// The is responsible for preserving message ordering and ensuring that subsequent calls to /// return messages in the correct chronological order. /// /// /// Implementations may perform additional processing during message addition, such as: /// /// Validating message content and metadata /// Applying storage optimizations or compression /// Triggering background maintenance operations /// /// /// /// This method is called regardless of whether the invocation succeeded or failed. /// To check if the invocation was successful, inspect the property. /// /// /// The default implementation of this method, skips execution for any invocation failures, filters messages using the optional storage input request and response message filters /// and calls to store new chat history messages. /// For most scenarios, overriding is sufficient to store chat history messages, while still benefiting from the default error handling and filtering behavior. /// However, for scenarios that require more control over error handling or message filtering, overriding this method allows you to directly control the messages that are stored for the invocation. /// /// protected virtual ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default) { if (context.InvokeException is not null) { return default; } #pragma warning disable MAAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. var subContext = new InvokedContext(context.Agent, context.Session, this._storeInputRequestMessageFilter(context.RequestMessages), this._storeInputResponseMessageFilter(context.ResponseMessages!)); #pragma warning restore MAAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. return this.StoreChatHistoryAsync(subContext, cancellationToken); } /// /// When overridden in a derived class, adds new messages to the chat history at the end of the agent invocation. /// /// Contains the invocation context including request messages, response messages, and any exception that occurred. /// The to monitor for cancellation requests. The default is . /// A task that represents the asynchronous add operation. /// /// /// Messages should be added in the order they were generated to maintain proper chronological sequence. /// The is responsible for preserving message ordering and ensuring that subsequent calls to /// return messages in the correct chronological order. /// /// /// Implementations may perform additional processing during message addition, such as: /// /// Validating message content and metadata /// Applying storage optimizations or compression /// Triggering background maintenance operations /// /// /// /// This method is called from . /// Note that can be overridden to directly control message filtering and error handling, in which case /// it is up to the implementer to call this method as needed to store messages. /// /// /// In contrast with , this method only stores messages, /// while is also responsible for messages filtering and error handling. /// /// /// The default implementation of only calls this method if the invocation succeeded. /// /// /// Security consideration: Messages being stored may contain PII and sensitive conversation content. /// Implementers should ensure appropriate encryption at rest and access controls for the storage backend. /// /// protected virtual ValueTask StoreChatHistoryAsync(InvokedContext context, CancellationToken cancellationToken = default) => default; /// Asks the for an object of the specified type . /// The type of object being requested. /// An optional key that can be used to help identify the target service. /// The found object, otherwise . /// is . /// /// The purpose of this method is to allow for the retrieval of strongly-typed services that might be provided by the , /// including itself or any services it might be wrapping. /// public virtual object? GetService(Type serviceType, object? serviceKey = null) { _ = Throw.IfNull(serviceType); return serviceKey is null && serviceType.IsInstanceOfType(this) ? this : null; } /// Asks the for an object of type . /// The type of the object to be retrieved. /// An optional key that can be used to help identify the target service. /// The found object, otherwise . /// /// The purpose of this method is to allow for the retrieval of strongly typed services that may be provided by the , /// including itself or any services it might be wrapping. /// public TService? GetService(object? serviceKey = null) => this.GetService(typeof(TService), serviceKey) is TService service ? service : default; /// /// Contains the context information provided to . /// /// /// This class provides context about the invocation including the new messages that will be used. /// A can use this information to determine what messages should be provided /// for the invocation. /// public sealed class InvokingContext { /// /// Initializes a new instance of the class with the specified request messages. /// /// The agent being invoked. /// The session associated with the agent invocation. /// The messages to be used by the agent for this invocation. /// is . [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public InvokingContext( AIAgent agent, AgentSession? session, IEnumerable requestMessages) { this.Agent = Throw.IfNull(agent); this.Session = session; this.RequestMessages = Throw.IfNull(requestMessages); } /// /// Gets the agent that is being invoked. /// public AIAgent Agent { get; } /// /// Gets the agent session associated with the agent invocation. /// public AgentSession? Session { get; } /// /// Gets the messages that will be used by the agent for this invocation. instances can modify /// and return or return a new message list to add additional messages for the invocation. /// /// /// A collection of instances representing the messages that will be used by the agent for this invocation. /// /// /// /// If multiple instances are used in the same invocation, each /// will receive the messages returned by the previous allowing them to build on top of each other's context. /// /// /// The first in the invocation pipeline will receive the /// caller provided messages. /// /// public IEnumerable RequestMessages { get; set { field = Throw.IfNull(value); } } } /// /// Contains the context information provided to . /// /// /// 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 { /// /// Initializes a new instance of the class for a successful invocation. /// /// The agent that was invoked. /// The session associated with the agent 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 . [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public InvokedContext( AIAgent agent, AgentSession? session, IEnumerable requestMessages, IEnumerable responseMessages) { this.Agent = Throw.IfNull(agent); this.Session = session; this.RequestMessages = Throw.IfNull(requestMessages); this.ResponseMessages = Throw.IfNull(responseMessages); } /// /// Initializes a new instance of the class for a failed invocation. /// /// The agent that was invoked. /// The session associated with the agent 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( AIAgent agent, AgentSession? session, IEnumerable requestMessages, Exception invokeException) { this.Agent = Throw.IfNull(agent); this.Session = session; this.RequestMessages = Throw.IfNull(requestMessages); this.InvokeException = Throw.IfNull(invokeException); } /// /// Gets the agent that is being invoked. /// public AIAgent Agent { get; } /// /// Gets the agent session associated with the agent invocation. /// public AgentSession? Session { get; } /// /// 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. /// This does not include any supplied messages. /// public IEnumerable RequestMessages { get; } /// /// Gets the collection of response messages generated during this invocation if the invocation succeeded. /// /// /// A collection of instances representing the response, /// or if the invocation failed. /// public IEnumerable? ResponseMessages { get; } /// /// Gets the that was thrown during the invocation, if the invocation failed. /// /// /// The exception that caused the invocation to fail, or if the invocation succeeded. /// public Exception? InvokeException { get; } } }