// 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; }
}
}