chore: import upstream snapshot with attribution
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:25 +08:00
commit db620d33df
5151 changed files with 925932 additions and 0 deletions
@@ -0,0 +1,507 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>
/// Provides the base abstraction for all AI agents, defining the core interface for agent interactions and conversation management.
/// </summary>
/// <remarks>
/// <see cref="AIAgent"/> serves as the foundational class for implementing AI agents that can participate in conversations
/// and process user requests. An agent instance may participate in multiple concurrent conversations, and each conversation
/// may involve multiple agents working together.
/// <para>
/// <strong>Security considerations:</strong> An <see cref="AIAgent"/> orchestrates data flow across trust boundaries —
/// messages are sent to external AI services, context providers, chat history stores, and function tools. Agent Framework
/// passes messages through as-is without validation or sanitization. Developers must be aware that:
/// <list type="bullet">
/// <item><description>User-supplied messages may contain prompt injection attempts designed to manipulate LLM behavior.</description></item>
/// <item><description>LLM responses should be treated as untrusted output — they may contain hallucinations, malicious payloads (e.g., scripts, SQL),
/// or content influenced by indirect prompt injection. Always validate and sanitize LLM output before rendering in HTML, executing as code,
/// or using in database queries.</description></item>
/// <item><description>Messages with different roles carry different trust levels: <c>system</c> messages have the highest trust and must be developer-controlled;
/// <c>user</c>, <c>assistant</c>, and <c>tool</c> messages should be treated as untrusted.</description></item>
/// </list>
/// </para>
/// </remarks>
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public abstract partial class AIAgent
{
private static readonly AsyncLocal<AgentRunContext?> s_currentContext = new();
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private string DebuggerDisplay =>
this.Name is { } name ? $"Id = {this.Id}, Name = {name}" : $"Id = {this.Id}";
/// <summary>
/// Gets the unique identifier for this agent instance.
/// </summary>
/// <value>
/// A unique string identifier for the agent. For in-memory agents, this defaults to a randomly-generated ID,
/// while service-backed agents typically use the identifier assigned by the backing service.
/// </value>
/// <remarks>
/// Agent identifiers are used for tracking, telemetry, and distinguishing between different
/// agent instances in multi-agent scenarios. They should remain stable for the lifetime
/// of the agent instance.
/// </remarks>
public string Id { get => this.IdCore ?? field; } = Guid.NewGuid().ToString("N");
/// <summary>
/// Gets a custom identifier for the agent, which can be overridden by derived classes.
/// </summary>
/// <value>
/// A string representing the agent's identifier, or <see langword="null"/> if the default ID should be used.
/// </value>
/// <remarks>
/// Derived classes can override this property to provide a custom identifier.
/// When <see langword="null"/> is returned, the <see cref="Id"/> property will use the default randomly-generated identifier.
/// </remarks>
protected virtual string? IdCore => null;
/// <summary>
/// Gets the human-readable name of the agent.
/// </summary>
/// <value>
/// The agent's name, or <see langword="null"/> if no name has been assigned.
/// </value>
/// <remarks>
/// The agent name is typically used for display purposes and to help users identify
/// the agent's purpose or capabilities in user interfaces.
/// </remarks>
public virtual string? Name { get; }
/// <summary>
/// Gets a description of the agent's purpose, capabilities, or behavior.
/// </summary>
/// <value>
/// A descriptive text explaining what the agent does, or <see langword="null"/> if no description is available.
/// </value>
/// <remarks>
/// The description helps models and users understand the agent's intended purpose and capabilities,
/// which is particularly useful in multi-agent systems.
/// </remarks>
public virtual string? Description { get; }
/// <summary>
/// Gets or sets the <see cref="AgentRunContext"/> for the current agent run.
/// </summary>
/// <remarks>
/// This value flows across async calls.
/// </remarks>
public static AgentRunContext? CurrentRunContext
{
get => s_currentContext.Value;
protected set => s_currentContext.Value = value;
}
/// <summary>Asks the <see cref="AIAgent"/> for an object of the specified type <paramref name="serviceType"/>.</summary>
/// <param name="serviceType">The type of object being requested.</param>
/// <param name="serviceKey">An optional key that can be used to help identify the target service.</param>
/// <returns>The found object, otherwise <see langword="null"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="serviceType"/> is <see langword="null"/>.</exception>
/// <remarks>
/// The purpose of this method is to allow for the retrieval of strongly-typed services that might be provided by the <see cref="AIAgent"/>,
/// including itself or any services it might be wrapping. For example, to access the <see cref="AIAgentMetadata"/> for the instance,
/// <see cref="GetService"/> may be used to request it.
/// </remarks>
public virtual object? GetService(Type serviceType, object? serviceKey = null)
{
_ = Throw.IfNull(serviceType);
return serviceKey is null && serviceType.IsInstanceOfType(this)
? this
: null;
}
/// <summary>Asks the <see cref="AIAgent"/> for an object of type <typeparamref name="TService"/>.</summary>
/// <typeparam name="TService">The type of the object to be retrieved.</typeparam>
/// <param name="serviceKey">An optional key that can be used to help identify the target service.</param>
/// <returns>The found object, otherwise <see langword="null"/>.</returns>
/// <remarks>
/// The purpose of this method is to allow for the retrieval of strongly typed services that may be provided by the <see cref="AIAgent"/>,
/// including itself or any services it might be wrapping.
/// </remarks>
public TService? GetService<TService>(object? serviceKey = null)
=> this.GetService(typeof(TService), serviceKey) is TService service ? service : default;
/// <summary>
/// Creates a new conversation session that is compatible with this agent.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A value task that represents the asynchronous operation. The task result contains a new <see cref="AgentSession"/> instance ready for use with this agent.</returns>
/// <remarks>
/// <para>
/// This method creates a fresh conversation session that can be used to maintain state
/// and context for interactions with this agent. Each session represents an independent
/// conversation session.
/// </para>
/// <para>
/// If the agent supports multiple session types, this method returns the default or
/// configured session type. For service-backed agents, the actual session creation
/// may be deferred until first use to optimize performance.
/// </para>
/// </remarks>
public ValueTask<AgentSession> CreateSessionAsync(CancellationToken cancellationToken = default)
=> this.CreateSessionCoreAsync(cancellationToken);
/// <summary>
/// Core implementation of session creation logic.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A value task that represents the asynchronous operation. The task result contains a new <see cref="AgentSession"/> instance ready for use with this agent.</returns>
/// <remarks>
/// This is the primary session creation method that implementations must override.
/// </remarks>
protected abstract ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Serializes an agent session to its JSON representation.
/// </summary>
/// <param name="session">The <see cref="AgentSession"/> to serialize.</param>
/// <param name="jsonSerializerOptions">Optional settings to customize the serialization process.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A value task that represents the asynchronous operation. The task result contains a <see cref="JsonElement"/> with the serialized session state.</returns>
/// <exception cref="ArgumentNullException"><paramref name="session"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">The type of <paramref name="session"/> is not supported by this agent.</exception>
/// <remarks>
/// This method enables saving conversation sessions to persistent storage,
/// allowing conversations to resume across application restarts or be migrated between
/// different agent instances. Use <see cref="DeserializeSessionAsync"/> to restore the session.
/// <para>
/// <strong>Security consideration:</strong> Serialized sessions may contain conversation content, session identifiers,
/// and other potentially sensitive data including PII. Ensure that serialized session data is stored securely with
/// appropriate access controls and encryption at rest.
/// </para>
/// </remarks>
public ValueTask<JsonElement> SerializeSessionAsync(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> this.SerializeSessionCoreAsync(session, jsonSerializerOptions, cancellationToken);
/// <summary>
/// Core implementation of session serialization logic.
/// </summary>
/// <param name="session">The <see cref="AgentSession"/> to serialize.</param>
/// <param name="jsonSerializerOptions">Optional settings to customize the serialization process.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A value task that represents the asynchronous operation. The task result contains a <see cref="JsonElement"/> with the serialized session state.</returns>
/// <remarks>
/// This is the primary session serialization method that implementations must override.
/// </remarks>
protected abstract ValueTask<JsonElement> SerializeSessionCoreAsync(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default);
/// <summary>
/// Deserializes an agent session from its JSON serialized representation.
/// </summary>
/// <param name="serializedState">A <see cref="JsonElement"/> containing the serialized session state.</param>
/// <param name="jsonSerializerOptions">Optional settings to customize the deserialization process.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A value task that represents the asynchronous operation. The task result contains a restored <see cref="AgentSession"/> instance with the state from <paramref name="serializedState"/>.</returns>
/// <exception cref="ArgumentException">The <paramref name="serializedState"/> is not in the expected format.</exception>
/// <exception cref="JsonException">The serialized data is invalid or cannot be deserialized.</exception>
/// <remarks>
/// This method enables restoration of conversation sessions from previously saved state,
/// allowing conversations to resume across application restarts or be migrated between
/// different agent instances.
/// <para>
/// <strong>Security consideration:</strong> Restoring a session from an untrusted source is equivalent to accepting untrusted input.
/// Serialized sessions may contain conversation content, session identifiers, and potentially sensitive data. A compromised
/// storage backend could alter message roles to escalate trust, or inject adversarial content that influences LLM behavior.
/// Treat serialized session data as sensitive and ensure it is stored and transmitted securely.
/// </para>
/// </remarks>
public ValueTask<AgentSession> DeserializeSessionAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> this.DeserializeSessionCoreAsync(serializedState, jsonSerializerOptions, cancellationToken);
/// <summary>
/// Core implementation of session deserialization logic.
/// </summary>
/// <param name="serializedState">A <see cref="JsonElement"/> containing the serialized session state.</param>
/// <param name="jsonSerializerOptions">Optional settings to customize the deserialization process.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A value task that represents the asynchronous operation. The task result contains a restored <see cref="AgentSession"/> instance with the state from <paramref name="serializedState"/>.</returns>
/// <remarks>
/// This is the primary session deserialization method that implementations must override.
/// </remarks>
protected abstract ValueTask<AgentSession> DeserializeSessionCoreAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default);
/// <summary>
/// Run the agent with no message assuming that all required instructions are already provided to the agent or on the session.
/// </summary>
/// <param name="session">
/// The conversation session to use for this invocation. If <see langword="null"/>, a new session will be created.
/// The session will be updated with any response messages generated during invocation.
/// </param>
/// <param name="options">Optional configuration parameters for controlling the agent's invocation behavior.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains an <see cref="AgentResponse"/> with the agent's output.</returns>
/// <remarks>
/// This overload is useful when the agent has sufficient context from previous messages in the session
/// or from its initial configuration to generate a meaningful response without additional input.
/// </remarks>
public Task<AgentResponse> RunAsync(
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default) =>
this.RunAsync([], session, options, cancellationToken);
/// <summary>
/// Runs the agent with a text message from the user.
/// </summary>
/// <param name="message">The user message to send to the agent.</param>
/// <param name="session">
/// The conversation session to use for this invocation. If <see langword="null"/>, a new session will be created.
/// The session will be updated with the input message and any response messages generated during invocation.
/// </param>
/// <param name="options">Optional configuration parameters for controlling the agent's invocation behavior.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains an <see cref="AgentResponse"/> with the agent's output.</returns>
/// <exception cref="ArgumentException"><paramref name="message"/> is <see langword="null"/>, empty, or contains only whitespace.</exception>
/// <remarks>
/// The provided text will be wrapped in a <see cref="ChatMessage"/> with the <see cref="ChatRole.User"/> role
/// before being sent to the agent. This is a convenience method for simple text-based interactions.
/// </remarks>
public Task<AgentResponse> RunAsync(
string message,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
_ = Throw.IfNullOrWhitespace(message);
return this.RunAsync(new ChatMessage(ChatRole.User, message), session, options, cancellationToken);
}
/// <summary>
/// Runs the agent with a single chat message.
/// </summary>
/// <param name="message">The chat message to send to the agent.</param>
/// <param name="session">
/// The conversation session to use for this invocation. If <see langword="null"/>, a new session will be created.
/// The session will be updated with the input message and any response messages generated during invocation.
/// </param>
/// <param name="options">Optional configuration parameters for controlling the agent's invocation behavior.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains an <see cref="AgentResponse"/> with the agent's output.</returns>
/// <exception cref="ArgumentNullException"><paramref name="message"/> is <see langword="null"/>.</exception>
public Task<AgentResponse> RunAsync(
ChatMessage message,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
_ = Throw.IfNull(message);
return this.RunAsync([message], session, options, cancellationToken);
}
/// <summary>
/// Runs the agent with a collection of chat messages, providing the core invocation logic that all other overloads delegate to.
/// </summary>
/// <param name="messages">The collection of messages to send to the agent for processing.</param>
/// <param name="session">
/// The conversation session to use for this invocation. If <see langword="null"/>, a new session will be created.
/// The session will be updated with the input messages and any response messages generated during invocation.
/// </param>
/// <param name="options">Optional configuration parameters for controlling the agent's invocation behavior.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains an <see cref="AgentResponse"/> with the agent's output.</returns>
/// <remarks>
/// <para>
/// This method delegates to <see cref="RunCoreAsync"/> to perform the actual agent invocation. It handles collections of messages,
/// allowing for complex conversational scenarios including multi-turn interactions, function calls, and
/// context-rich conversations.
/// </para>
/// <para>
/// The messages are processed in the order provided and become part of the conversation history.
/// The agent's response will also be added to <paramref name="session"/> if one is provided.
/// </para>
/// <para>
/// <strong>Security consideration:</strong> Agent Framework does not validate or sanitize message content — it is passed through
/// to the underlying AI service as-is. If input messages include untrusted user content, developers should be aware of prompt injection risks.
/// System-role messages must be developer-controlled and should never contain end-user input.
/// </para>
/// </remarks>
public Task<AgentResponse> RunAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
CurrentRunContext = new(this, session, messages as IReadOnlyCollection<ChatMessage> ?? messages.ToList(), options);
return this.RunCoreAsync(messages, session, options, cancellationToken);
}
/// <summary>
/// Core implementation of the agent invocation logic with a collection of chat messages.
/// </summary>
/// <param name="messages">The collection of messages to send to the agent for processing.</param>
/// <param name="session">
/// The conversation session to use for this invocation. If <see langword="null"/>, a new session will be created.
/// The session will be updated with the input messages and any response messages generated during invocation.
/// </param>
/// <param name="options">Optional configuration parameters for controlling the agent's invocation behavior.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains an <see cref="AgentResponse"/> with the agent's output.</returns>
/// <remarks>
/// <para>
/// This is the primary invocation method that implementations must override. It handles collections of messages,
/// allowing for complex conversational scenarios including multi-turn interactions, function calls, and
/// context-rich conversations.
/// </para>
/// <para>
/// The messages are processed in the order provided and become part of the conversation history.
/// The agent's response will also be added to <paramref name="session"/> if one is provided.
/// </para>
/// </remarks>
protected abstract Task<AgentResponse> RunCoreAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default);
/// <summary>
/// Runs the agent in streaming mode without providing new input messages, relying on existing context and instructions.
/// </summary>
/// <param name="session">
/// The conversation session to use for this invocation. If <see langword="null"/>, a new session will be created.
/// The session will be updated with any response messages generated during invocation.
/// </param>
/// <param name="options">Optional configuration parameters for controlling the agent's invocation behavior.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>An asynchronous enumerable of <see cref="AgentResponseUpdate"/> instances representing the streaming response.</returns>
public IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default) =>
this.RunStreamingAsync([], session, options, cancellationToken);
/// <summary>
/// Runs the agent in streaming mode with a text message from the user.
/// </summary>
/// <param name="message">The user message to send to the agent.</param>
/// <param name="session">
/// The conversation session to use for this invocation. If <see langword="null"/>, a new session will be created.
/// The session will be updated with the input message and any response messages generated during invocation.
/// </param>
/// <param name="options">Optional configuration parameters for controlling the agent's invocation behavior.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>An asynchronous enumerable of <see cref="AgentResponseUpdate"/> instances representing the streaming response.</returns>
/// <exception cref="ArgumentException"><paramref name="message"/> is <see langword="null"/>, empty, or contains only whitespace.</exception>
/// <remarks>
/// The provided text will be wrapped in a <see cref="ChatMessage"/> with the <see cref="ChatRole.User"/> role.
/// Streaming invocation provides real-time updates as the agent generates its response.
/// </remarks>
public IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
string message,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
_ = Throw.IfNullOrWhitespace(message);
return this.RunStreamingAsync(new ChatMessage(ChatRole.User, message), session, options, cancellationToken);
}
/// <summary>
/// Runs the agent in streaming mode with a single chat message.
/// </summary>
/// <param name="message">The chat message to send to the agent.</param>
/// <param name="session">
/// The conversation session to use for this invocation. If <see langword="null"/>, a new session will be created.
/// The session will be updated with the input message and any response messages generated during invocation.
/// </param>
/// <param name="options">Optional configuration parameters for controlling the agent's invocation behavior.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>An asynchronous enumerable of <see cref="AgentResponseUpdate"/> instances representing the streaming response.</returns>
/// <exception cref="ArgumentNullException"><paramref name="message"/> is <see langword="null"/>.</exception>
public IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
ChatMessage message,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
_ = Throw.IfNull(message);
return this.RunStreamingAsync([message], session, options, cancellationToken);
}
/// <summary>
/// Runs the agent in streaming mode with a collection of chat messages, providing the core streaming invocation logic.
/// </summary>
/// <param name="messages">The collection of messages to send to the agent for processing.</param>
/// <param name="session">
/// The conversation session to use for this invocation. If <see langword="null"/>, a new session will be created.
/// The session will be updated with the input messages and any response updates generated during invocation.
/// </param>
/// <param name="options">Optional configuration parameters for controlling the agent's invocation behavior.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>An asynchronous enumerable of <see cref="AgentResponseUpdate"/> instances representing the streaming response.</returns>
/// <remarks>
/// <para>
/// This method delegates to <see cref="RunCoreStreamingAsync"/> to perform the actual streaming invocation. It provides real-time
/// updates as the agent processes the input and generates its response, enabling more responsive user experiences.
/// </para>
/// <para>
/// Each <see cref="AgentResponseUpdate"/> represents a portion of the complete response, allowing consumers
/// to display partial results, implement progressive loading, or provide immediate feedback to users.
/// </para>
/// <para>
/// <strong>Security consideration:</strong> Agent Framework does not validate or sanitize message content — it is passed through
/// to the underlying AI service as-is. If input messages include untrusted user content, developers should be aware of prompt injection risks.
/// System-role messages must be developer-controlled and should never contain end-user input.
/// </para>
/// </remarks>
public async IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
AgentRunContext context = new(this, session, messages as IReadOnlyCollection<ChatMessage> ?? messages.ToList(), options);
CurrentRunContext = context;
await foreach (var update in this.RunCoreStreamingAsync(messages, session, options, cancellationToken).ConfigureAwait(false))
{
yield return update;
// Restore context again when resuming after the caller code executes.
CurrentRunContext = context;
}
}
/// <summary>
/// Core implementation of the agent streaming invocation logic with a collection of chat messages.
/// </summary>
/// <param name="messages">The collection of messages to send to the agent for processing.</param>
/// <param name="session">
/// The conversation session to use for this invocation. If <see langword="null"/>, a new session will be created.
/// The session will be updated with the input messages and any response updates generated during invocation.
/// </param>
/// <param name="options">Optional configuration parameters for controlling the agent's invocation behavior.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>An asynchronous enumerable of <see cref="AgentResponseUpdate"/> instances representing the streaming response.</returns>
/// <remarks>
/// <para>
/// This is the primary streaming invocation method that implementations must override. It provides real-time
/// updates as the agent processes the input and generates its response, enabling more responsive user experiences.
/// </para>
/// <para>
/// Each <see cref="AgentResponseUpdate"/> represents a portion of the complete response, allowing consumers
/// to display partial results, implement progressive loading, or provide immediate feedback to users.
/// </para>
/// </remarks>
protected abstract IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default);
}
@@ -0,0 +1,40 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>
/// Provides metadata information about an <see cref="AIAgent"/> instance.
/// </summary>
/// <remarks>
/// This class contains descriptive information about an agent that can be used for identification,
/// telemetry, and logging purposes.
/// </remarks>
[DebuggerDisplay("ProviderName = {ProviderName}")]
public sealed class AIAgentMetadata
{
/// <summary>
/// Initializes a new instance of the <see cref="AIAgentMetadata"/> class.
/// </summary>
/// <param name="providerName">
/// The name of the agent provider, if applicable. Where possible, this should map to the
/// appropriate name defined in the OpenTelemetry Semantic Conventions for Generative AI systems.
/// </param>
public AIAgentMetadata(string? providerName = null)
{
this.ProviderName = providerName;
}
/// <summary>
/// Gets the name of the agent provider.
/// </summary>
/// <value>
/// The provider name that identifies the underlying service or implementation powering the agent.
/// </value>
/// <remarks>
/// Where possible, this maps to the appropriate name defined in the
/// OpenTelemetry Semantic Conventions for Generative AI systems.
/// </remarks>
public string? ProviderName { get; }
}
@@ -0,0 +1,140 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>
/// Provides structured output methods for <see cref="AIAgent"/> that enable requesting responses in a specific type format.
/// </summary>
public abstract partial class AIAgent
{
/// <summary>
/// Run the agent with no message assuming that all required instructions are already provided to the agent or on the session, and requesting a response of the specified type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type of structured output to request.</typeparam>
/// <param name="session">
/// The conversation session to use for this invocation. If <see langword="null"/>, a new session will be created.
/// The session will be updated with any response messages generated during invocation.
/// </param>
/// <param name="serializerOptions">Optional JSON serializer options to use for deserializing the response.</param>
/// <param name="options">Optional configuration parameters for controlling the agent's invocation behavior.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains an <see cref="AgentResponse{T}"/> with the agent's output.</returns>
/// <remarks>
/// This overload is useful when the agent has sufficient context from previous messages in the session
/// or from its initial configuration to generate a meaningful response without additional input.
/// </remarks>
public Task<AgentResponse<T>> RunAsync<T>(
AgentSession? session = null,
JsonSerializerOptions? serializerOptions = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default) =>
this.RunAsync<T>([], session, serializerOptions, options, cancellationToken);
/// <summary>
/// Runs the agent with a text message from the user, requesting a response of the specified type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type of structured output to request.</typeparam>
/// <param name="message">The user message to send to the agent.</param>
/// <param name="session">
/// The conversation session to use for this invocation. If <see langword="null"/>, a new session will be created.
/// The session will be updated with the input message and any response messages generated during invocation.
/// </param>
/// <param name="serializerOptions">Optional JSON serializer options to use for deserializing the response.</param>
/// <param name="options">Optional configuration parameters for controlling the agent's invocation behavior.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains an <see cref="AgentResponse{T}"/> with the agent's output.</returns>
/// <exception cref="ArgumentException"><paramref name="message"/> is <see langword="null"/>, empty, or contains only whitespace.</exception>
/// <remarks>
/// The provided text will be wrapped in a <see cref="ChatMessage"/> with the <see cref="ChatRole.User"/> role
/// before being sent to the agent. This is a convenience method for simple text-based interactions.
/// </remarks>
public Task<AgentResponse<T>> RunAsync<T>(
string message,
AgentSession? session = null,
JsonSerializerOptions? serializerOptions = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
_ = Throw.IfNullOrWhitespace(message);
return this.RunAsync<T>(new ChatMessage(ChatRole.User, message), session, serializerOptions, options, cancellationToken);
}
/// <summary>
/// Runs the agent with a single chat message, requesting a response of the specified type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type of structured output to request.</typeparam>
/// <param name="message">The chat message to send to the agent.</param>
/// <param name="session">
/// The conversation session to use for this invocation. If <see langword="null"/>, a new session will be created.
/// The session will be updated with the input message and any response messages generated during invocation.
/// </param>
/// <param name="serializerOptions">Optional JSON serializer options to use for deserializing the response.</param>
/// <param name="options">Optional configuration parameters for controlling the agent's invocation behavior.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains an <see cref="AgentResponse{T}"/> with the agent's output.</returns>
/// <exception cref="ArgumentNullException"><paramref name="message"/> is <see langword="null"/>.</exception>
public Task<AgentResponse<T>> RunAsync<T>(
ChatMessage message,
AgentSession? session = null,
JsonSerializerOptions? serializerOptions = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
_ = Throw.IfNull(message);
return this.RunAsync<T>([message], session, serializerOptions, options, cancellationToken);
}
/// <summary>
/// Runs the agent with a collection of chat messages, requesting a response of the specified type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type of structured output to request.</typeparam>
/// <param name="messages">The collection of messages to send to the agent for processing.</param>
/// <param name="session">
/// The conversation session to use for this invocation. If <see langword="null"/>, a new session will be created.
/// The session will be updated with the input messages and any response messages generated during invocation.
/// </param>
/// <param name="serializerOptions">Optional JSON serializer options to use for deserializing the response.</param>
/// <param name="options">Optional configuration parameters for controlling the agent's invocation behavior.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains an <see cref="AgentResponse{T}"/> with the agent's output.</returns>
/// <remarks>
/// <para>
/// This method handles collections of messages, allowing for complex conversational scenarios including
/// multi-turn interactions, function calls, and context-rich conversations.
/// </para>
/// <para>
/// The messages are processed in the order provided and become part of the conversation history.
/// The agent's response will also be added to <paramref name="session"/> if one is provided.
/// </para>
/// </remarks>
public async Task<AgentResponse<T>> RunAsync<T>(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
JsonSerializerOptions? serializerOptions = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
serializerOptions ??= AgentAbstractionsJsonUtilities.DefaultOptions;
var responseFormat = ChatResponseFormat.ForJsonSchema<T>(serializerOptions);
(responseFormat, bool isWrappedInObject) = StructuredOutputSchemaUtilities.WrapNonObjectSchema(responseFormat);
options = options?.Clone() ?? new AgentRunOptions();
options.ResponseFormat = responseFormat;
AgentResponse response = await this.RunAsync(messages, session, options, cancellationToken).ConfigureAwait(false);
return new AgentResponse<T>(response, serializerOptions) { IsWrappedInObject = isWrappedInObject };
}
}
@@ -0,0 +1,117 @@
// Copyright (c) Microsoft. All rights reserved.
#if NET
using System;
#endif
using System.Collections.Generic;
using System.Linq;
#if NET
using System.Runtime.CompilerServices;
#else
using System.Text;
#endif
namespace Microsoft.Extensions.AI;
/// <summary>Internal extensions for working with <see cref="AIContent"/>.</summary>
internal static class AIContentExtensions
{
/// <summary>Concatenates the text of all <see cref="TextContent"/> instances in the list.</summary>
public static string ConcatText(this IEnumerable<AIContent> contents)
{
if (contents is IList<AIContent> list)
{
int count = list.Count;
switch (count)
{
case 0:
return string.Empty;
case 1:
return (list[0] as TextContent)?.Text ?? string.Empty;
default:
#if NET
DefaultInterpolatedStringHandler builder = new(count, 0, null, stackalloc char[512]);
for (int i = 0; i < count; i++)
{
if (list[i] is TextContent text)
{
builder.AppendLiteral(text.Text);
}
}
return builder.ToStringAndClear();
#else
StringBuilder builder = new();
for (int i = 0; i < count; i++)
{
if (list[i] is TextContent text)
{
builder.Append(text.Text);
}
}
return builder.ToString();
#endif
}
}
return string.Concat(contents.OfType<TextContent>());
}
/// <summary>Concatenates the <see cref="ChatMessage.Text"/> of all <see cref="ChatMessage"/> instances in the list.</summary>
/// <remarks>A newline separator is added between each non-empty piece of text.</remarks>
public static string ConcatText(this IList<ChatMessage> messages)
{
int count = messages.Count;
switch (count)
{
case 0:
return string.Empty;
case 1:
return messages[0].Text;
default:
#if NET
DefaultInterpolatedStringHandler builder = new(count, 0, null, stackalloc char[512]);
bool needsSeparator = false;
for (int i = 0; i < count; i++)
{
string text = messages[i].Text;
if (text.Length > 0)
{
if (needsSeparator)
{
builder.AppendLiteral(Environment.NewLine);
}
builder.AppendLiteral(text);
needsSeparator = true;
}
}
return builder.ToStringAndClear();
#else
StringBuilder builder = new();
for (int i = 0; i < count; i++)
{
string text = messages[i].Text;
if (text.Length > 0)
{
if (builder.Length > 0)
{
builder.AppendLine();
}
builder.Append(text);
}
}
return builder.ToString();
#endif
}
}
}
@@ -0,0 +1,109 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI;
/// <summary>
/// Represents additional context information that can be dynamically provided to AI models during agent invocations.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="AIContext"/> serves as a container for contextual information that <see cref="AIContextProvider"/> instances
/// can supply to enhance AI model interactions. This context is merged with
/// the agent's base configuration before being passed to the underlying AI model.
/// </para>
/// <para>
/// The context system enables dynamic, runtime-specific enhancements to agent capabilities including:
/// <list type="bullet">
/// <item><description>Adding relevant background information from knowledge bases</description></item>
/// <item><description>Injecting task-specific instructions or guidelines</description></item>
/// <item><description>Providing specialized tools or functions for the current interaction</description></item>
/// <item><description>Including contextual messages that inform the AI about the current situation</description></item>
/// </list>
/// </para>
/// <para>
/// Context information is transient by default and applies only to the current invocation, however messages
/// added through the <see cref="Messages"/> property will be permanently incorporated into the conversation history.
/// </para>
/// </remarks>
public sealed class AIContext
{
/// <summary>
/// Gets or sets additional instructions to provide to the AI model for the current invocation.
/// </summary>
/// <value>
/// Instructions text that will be combined with any existing agent instructions or system prompts,
/// or <see langword="null"/> if no additional instructions should be provided.
/// </value>
/// <remarks>
/// <para>
/// These instructions are transient and apply only to the current AI model invocation. They are combined
/// with any existing agent instructions, system prompts, and conversation history to provide comprehensive
/// context to the AI model.
/// </para>
/// <para>
/// Instructions can be used to:
/// <list type="bullet">
/// <item><description>Provide context-specific behavioral guidance</description></item>
/// <item><description>Add domain-specific knowledge or constraints</description></item>
/// <item><description>Modify the agent's persona or response style for the current interaction</description></item>
/// <item><description>Include situational awareness information</description></item>
/// </list>
/// </para>
/// </remarks>
public string? Instructions { get; set; }
/// <summary>
/// Gets or sets the sequence of messages to use for the current invocation.
/// </summary>
/// <value>
/// A sequence of <see cref="ChatMessage"/> instances to be used for the current invocation,
/// or <see langword="null"/> if no messages should be used.
/// </value>
/// <remarks>
/// <para>
/// Unlike <see cref="Instructions"/> and <see cref="Tools"/>, messages added through this property may become
/// permanent additions to the conversation history.
/// If chat history is managed by the underlying AI service, these messages will become part of chat history.
/// If chat history is managed using a <see cref="ChatHistoryProvider"/>, these messages will be passed to the
/// <see cref="ChatHistoryProvider.InvokedCoreAsync(ChatHistoryProvider.InvokedContext, System.Threading.CancellationToken)"/> method,
/// and the provider can choose which of these messages to permanently add to the conversation history.
/// </para>
/// <para>
/// This property is useful for:
/// <list type="bullet">
/// <item><description>Injecting relevant historical context e.g. memories</description></item>
/// <item><description>Injecting relevant background information e.g. via Retrieval Augmented Generation</description></item>
/// <item><description>Adding system messages that provide ongoing context</description></item>
/// </list>
/// </para>
/// </remarks>
public IEnumerable<ChatMessage>? Messages { get; set; }
/// <summary>
/// Gets or sets a sequence of tools or functions to make available to the AI model for the current invocation.
/// </summary>
/// <value>
/// A sequence of <see cref="AITool"/> instances that will be available to the AI model during the current invocation,
/// or <see langword="null"/> if no additional tools should be provided.
/// </value>
/// <remarks>
/// <para>
/// These tools are transient and apply only to the current AI model invocation. Any existing tools
/// are provided as input to the <see cref="AIContextProvider"/> instances, so context providers can choose to modify or replace the existing tools
/// as needed based on the current context. The resulting set of tools is then passed to the underlying AI model, which may choose to utilize them when generating responses.
/// </para>
/// <para>
/// Context-specific tools enable:
/// <list type="bullet">
/// <item><description>Providing specialized functions based on user intent or conversation context</description></item>
/// <item><description>Adding domain-specific capabilities for particular types of queries</description></item>
/// <item><description>Enabling access to external services or data sources relevant to the current task</description></item>
/// <item><description>Offering interactive capabilities tailored to the current conversation state</description></item>
/// </list>
/// </para>
/// </remarks>
public IEnumerable<AITool>? Tools { get; set; }
}
@@ -0,0 +1,511 @@
// 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;
/// <summary>
/// Provides an abstract base class for components that enhance AI context during agent invocations.
/// </summary>
/// <remarks>
/// <para>
/// An AI context provider is a component that participates in the agent invocation lifecycle by:
/// <list type="bullet">
/// <item><description>Listening to changes in conversations</description></item>
/// <item><description>Providing additional context to agents during invocation</description></item>
/// <item><description>Supplying additional function tools for enhanced capabilities</description></item>
/// <item><description>Processing invocation results for state management or learning</description></item>
/// </list>
/// </para>
/// <para>
/// Context providers operate through a two-phase lifecycle: they are called at the start of invocation via
/// <see cref="InvokingAsync"/> to provide context, and optionally called at the end of invocation via
/// <see cref="InvokedAsync"/> to process results.
/// </para>
/// <para>
/// <strong>Security considerations:</strong> Context providers may inject messages with any role, including <c>system</c>, which
/// has the highest trust level and directly shapes LLM behavior. Developers must ensure that all providers attached to an agent
/// are trusted. Agent Framework does not validate or filter the data returned by providers — it is accepted as-is and merged into
/// the request context. If a provider retrieves data from an external source (e.g., a vector database or memory service), be aware
/// that a compromised data source could introduce adversarial content designed to manipulate LLM behavior via indirect prompt injection.
/// Implementers should validate and sanitize data retrieved from external sources before returning it.
/// </para>
/// </remarks>
public abstract class AIContextProvider
{
private static IEnumerable<ChatMessage> DefaultExternalOnlyFilter(IEnumerable<ChatMessage> messages)
=> messages.Where(m => m.GetAgentRequestMessageSourceType() == AgentRequestMessageSourceType.External);
private static IEnumerable<ChatMessage> DefaultNoopFilter(IEnumerable<ChatMessage> messages)
=> messages;
private IReadOnlyList<string>? _stateKeys;
/// <summary>
/// Initializes a new instance of the <see cref="AIContextProvider"/> class.
/// </summary>
/// <param name="provideInputMessageFilter">An optional filter function to apply to input messages before providing context via <see cref="ProvideAIContextAsync"/>. If not set, defaults to including only <see cref="AgentRequestMessageSourceType.External"/> messages.</param>
/// <param name="storeInputRequestMessageFilter">An optional filter function to apply to request messages before storing context via <see cref="StoreAIContextAsync"/>. If not set, defaults to including only <see cref="AgentRequestMessageSourceType.External"/> messages.</param>
/// <param name="storeInputResponseMessageFilter">An optional filter function to apply to response messages before storing context via <see cref="StoreAIContextAsync"/>. If not set, defaults to a no-op filter that includes all response messages.</param>
protected AIContextProvider(
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? provideInputMessageFilter = null,
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? storeInputRequestMessageFilter = null,
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? storeInputResponseMessageFilter = null)
{
this.ProvideInputMessageFilter = provideInputMessageFilter ?? DefaultExternalOnlyFilter;
this.StoreInputRequestMessageFilter = storeInputRequestMessageFilter ?? DefaultExternalOnlyFilter;
this.StoreInputResponseMessageFilter = storeInputResponseMessageFilter ?? DefaultNoopFilter;
}
/// <summary>
/// Gets the filter function to apply to input messages before providing context via <see cref="ProvideAIContextAsync"/>.
/// </summary>
protected Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>> ProvideInputMessageFilter { get; }
/// <summary>
/// Gets the filter function to apply to request messages before storing context via <see cref="StoreAIContextAsync"/>.
/// </summary>
protected Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>> StoreInputRequestMessageFilter { get; }
/// <summary>
/// Gets the filter function to apply to response messages before storing context via <see cref="StoreAIContextAsync"/>.
/// </summary>
protected Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>> StoreInputResponseMessageFilter { get; }
/// <summary>
/// Gets the set of keys used to store the provider state in the <see cref="AgentSession.StateBag"/>.
/// </summary>
/// <remarks>
/// The default value is a single-element set containing the name of the concrete type (e.g. <c>"TextSearchProvider"</c>).
/// 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.
/// </remarks>
public virtual IReadOnlyList<string> StateKeys => this._stateKeys ??= [this.GetType().Name];
/// <summary>
/// Called at the start of agent invocation to provide additional context.
/// </summary>
/// <param name="context">Contains the request context including the caller provided messages that will be used by the agent for this invocation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the <see cref="AIContext"/> with additional context to be used by the agent during this invocation.</returns>
/// <remarks>
/// <para>
/// Implementers can load any additional context required at this time, such as:
/// <list type="bullet">
/// <item><description>Retrieving relevant information from knowledge bases</description></item>
/// <item><description>Adding system instructions or prompts</description></item>
/// <item><description>Providing function tools for the current invocation</description></item>
/// <item><description>Injecting contextual messages from conversation history</description></item>
/// </list>
/// </para>
/// <para>
/// <strong>Security consideration:</strong> Data retrieved from external sources (e.g., vector databases, memory services, or
/// knowledge bases) may contain adversarial content designed to influence LLM behavior via indirect prompt injection.
/// Implementers should validate data integrity and consider the trustworthiness of the data source.
/// </para>
/// </remarks>
public ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
=> this.InvokingCoreAsync(Throw.IfNull(context), cancellationToken);
/// <summary>
/// Called at the start of agent invocation to provide additional context.
/// </summary>
/// <param name="context">Contains the request context including the caller provided messages that will be used by the agent for this invocation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the <see cref="AIContext"/> with additional context to be used by the agent during this invocation.</returns>
/// <remarks>
/// <para>
/// Implementers can load any additional context required at this time, such as:
/// <list type="bullet">
/// <item><description>Retrieving relevant information from knowledge bases</description></item>
/// <item><description>Adding system instructions or prompts</description></item>
/// <item><description>Providing function tools for the current invocation</description></item>
/// <item><description>Injecting contextual messages from conversation history</description></item>
/// </list>
/// </para>
/// <para>
/// The default implementation of this method filters the input messages using the configured provide-input message filter
/// (which defaults to including only <see cref="AgentRequestMessageSourceType.External"/> messages),
/// then calls <see cref="ProvideAIContextAsync"/> to get additional context,
/// stamps any messages from the returned context with <see cref="AgentRequestMessageSourceType.AIContextProvider"/> source attribution,
/// and merges the returned context with the original (unfiltered) input context (concatenating instructions, messages, and tools).
/// For most scenarios, overriding <see cref="ProvideAIContextAsync"/> is sufficient to provide additional context,
/// while still benefiting from the default filtering, merging and source stamping behavior.
/// However, for scenarios that require more control over context filtering, merging or source stamping, overriding this method
/// allows you to directly control the full <see cref="AIContext"/> returned for the invocation.
/// </para>
/// </remarks>
protected virtual async ValueTask<AIContext> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
var inputContext = context.AIContext;
// Create a filtered context for ProvideAIContextAsync, filtering input messages
// to exclude non-external messages (e.g. chat history, other AI context provider messages).
#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 filteredContext = new InvokingContext(
context.Agent,
context.Session,
new AIContext
{
Instructions = inputContext.Instructions,
Messages = inputContext.Messages is not null ? this.ProvideInputMessageFilter(inputContext.Messages) : null,
Tools = inputContext.Tools
});
#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.
var provided = await this.ProvideAIContextAsync(filteredContext, cancellationToken).ConfigureAwait(false);
var mergedInstructions = (inputContext.Instructions, provided.Instructions) switch
{
(null, null) => null,
(string a, null) => a,
(null, string b) => b,
(string a, string b) => a + "\n" + b
};
var providedMessages = provided.Messages is not null
? provided.Messages.Select(m => m.WithAgentRequestMessageSource(AgentRequestMessageSourceType.AIContextProvider, this.GetType().FullName!))
: null;
var mergedMessages = (inputContext.Messages, providedMessages) switch
{
(null, null) => null,
(var a, null) => a,
(null, var b) => b,
(var a, var b) => a.Concat(b)
};
var mergedTools = (inputContext.Tools, provided.Tools) switch
{
(null, null) => null,
(var a, null) => a,
(null, var b) => b,
(var a, var b) => a.Concat(b)
};
return new AIContext
{
Instructions = mergedInstructions,
Messages = mergedMessages,
Tools = mergedTools
};
}
/// <summary>
/// When overridden in a derived class, provides additional AI context to be merged with the input context for the current invocation.
/// </summary>
/// <remarks>
/// <para>
/// This method is called from <see cref="InvokingCoreAsync"/>.
/// Note that <see cref="InvokingCoreAsync"/> can be overridden to directly control context merging and source stamping, in which case
/// it is up to the implementer to call this method as needed to retrieve the additional context.
/// </para>
/// <para>
/// In contrast with <see cref="InvokingCoreAsync"/>, this method only returns additional context to be merged with the input,
/// while <see cref="InvokingCoreAsync"/> is responsible for returning the full merged <see cref="AIContext"/> for the invocation.
/// </para>
/// <para>
/// <strong>Security consideration:</strong> Any messages, tools, or instructions returned by this method will be merged into the
/// AI request context. If data is retrieved from external or untrusted sources, implementers should validate and sanitize it
/// to prevent indirect prompt injection attacks.
/// </para>
/// </remarks>
/// <param name="context">Contains the request context including the caller provided messages that will be used by the agent for this invocation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>
/// A task that represents the asynchronous operation. The task result contains an <see cref="AIContext"/>
/// with additional context to be merged with the input context.
/// </returns>
protected virtual ValueTask<AIContext> ProvideAIContextAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
return new ValueTask<AIContext>(new AIContext());
}
/// <summary>
/// Called at the end of the agent invocation to process the invocation results.
/// </summary>
/// <param name="context">Contains the invocation context including request messages, response messages, and any exception that occurred.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
/// <remarks>
/// <para>
/// Implementers can use the request and response messages in the provided <paramref name="context"/> to:
/// <list type="bullet">
/// <item><description>Update state based on conversation outcomes</description></item>
/// <item><description>Extract and store memories or preferences from user messages</description></item>
/// <item><description>Log or audit conversation details</description></item>
/// <item><description>Perform cleanup or finalization tasks</description></item>
/// </list>
/// </para>
/// <para>
/// The <see cref="AIContextProvider"/> is passed a reference to the <see cref="AgentSession"/> via <see cref="InvokingContext"/> and <see cref="InvokedContext"/>
/// allowing it to store state in the <see cref="AgentSession.StateBag"/>. Since an <see cref="AIContextProvider"/> 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 <see cref="AgentSession.StateBag"/>.
/// </para>
/// <para>
/// This method is called regardless of whether the invocation succeeded or failed.
/// To check if the invocation was successful, inspect the <see cref="InvokedContext.InvokeException"/> property.
/// </para>
/// </remarks>
public ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
=> this.InvokedCoreAsync(Throw.IfNull(context), cancellationToken);
/// <summary>
/// Called at the end of the agent invocation to process the invocation results.
/// </summary>
/// <param name="context">Contains the invocation context including request messages, response messages, and any exception that occurred.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
/// <remarks>
/// <para>
/// Implementers can use the request and response messages in the provided <paramref name="context"/> to:
/// <list type="bullet">
/// <item><description>Update internal state based on conversation outcomes</description></item>
/// <item><description>Extract and store memories or preferences from user messages</description></item>
/// <item><description>Log or audit conversation details</description></item>
/// <item><description>Perform cleanup or finalization tasks</description></item>
/// </list>
/// </para>
/// <para>
/// This method is called regardless of whether the invocation succeeded or failed.
/// To check if the invocation was successful, inspect the <see cref="InvokedContext.InvokeException"/> property.
/// </para>
/// <para>
/// The default implementation of this method skips execution for any invocation failures,
/// filters the request messages using the configured store-input request message filter
/// (which defaults to including only <see cref="AgentRequestMessageSourceType.External"/> messages),
/// filters the response messages using the configured store-input response message filter
/// (which defaults to a no-op, so all response messages are processed),
/// and calls <see cref="StoreAIContextAsync"/> to process the invocation results.
/// For most scenarios, overriding <see cref="StoreAIContextAsync"/> is sufficient to process invocation results,
/// 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 processing of invocation results.
/// </para>
/// </remarks>
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.StoreAIContextAsync(subContext, cancellationToken);
}
/// <summary>
/// When overridden in a derived class, processes invocation results at the end of the agent invocation.
/// </summary>
/// <param name="context">Contains the invocation context including request messages, response messages, and any exception that occurred.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
/// <remarks>
/// <para>
/// This method is called from <see cref="InvokedCoreAsync"/>.
/// Note that <see cref="InvokedCoreAsync"/> can be overridden to directly control error handling, in which case
/// it is up to the implementer to call this method as needed to process the invocation results.
/// </para>
/// <para>
/// In contrast with <see cref="InvokedCoreAsync"/>, this method only processes the invocation results,
/// while <see cref="InvokedCoreAsync"/> is also responsible for error handling.
/// </para>
/// <para>
/// The default implementation of <see cref="InvokedCoreAsync"/> only calls this method if the invocation succeeded.
/// </para>
/// <para>
/// <strong>Security consideration:</strong> Messages being processed/stored may contain PII and sensitive conversation content.
/// Implementers should ensure appropriate encryption at rest and access controls for the storage backend.
/// </para>
/// </remarks>
protected virtual ValueTask StoreAIContextAsync(InvokedContext context, CancellationToken cancellationToken = default) =>
default;
/// <summary>Asks the <see cref="AIContextProvider"/> for an object of the specified type <paramref name="serviceType"/>.</summary>
/// <param name="serviceType">The type of object being requested.</param>
/// <param name="serviceKey">An optional key that can be used to help identify the target service.</param>
/// <returns>The found object, otherwise <see langword="null"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="serviceType"/> is <see langword="null"/>.</exception>
/// <remarks>
/// The purpose of this method is to allow for the retrieval of strongly-typed services that might be provided by the <see cref="AIContextProvider"/>,
/// including itself or any services it might be wrapping. This enables advanced scenarios where consumers need access to
/// specific provider implementations or their internal services.
/// </remarks>
public virtual object? GetService(Type serviceType, object? serviceKey = null)
{
_ = Throw.IfNull(serviceType);
return serviceKey is null && serviceType.IsInstanceOfType(this)
? this
: null;
}
/// <summary>Asks the <see cref="AIContextProvider"/> for an object of type <typeparamref name="TService"/>.</summary>
/// <typeparam name="TService">The type of the object to be retrieved.</typeparam>
/// <param name="serviceKey">An optional key that can be used to help identify the target service.</param>
/// <returns>The found object, otherwise <see langword="null"/>.</returns>
/// <remarks>
/// The purpose of this method is to allow for the retrieval of strongly typed services that may be provided by the <see cref="AIContextProvider"/>,
/// including itself or any services it might be wrapping. This is a convenience overload of <see cref="GetService(Type, object?)"/>.
/// </remarks>
public TService? GetService<TService>(object? serviceKey = null)
=> this.GetService(typeof(TService), serviceKey) is TService service ? service : default;
/// <summary>
/// Contains the context information provided to <see cref="InvokingCoreAsync(InvokingContext, CancellationToken)"/>.
/// </summary>
/// <remarks>
/// This class provides context about the invocation before the underlying AI model is invoked, including the messages
/// that will be used. Context providers can use this information to determine what additional context
/// should be provided for the invocation.
/// </remarks>
public sealed class InvokingContext
{
/// <summary>
/// Initializes a new instance of the <see cref="InvokingContext"/> class.
/// </summary>
/// <param name="agent">The agent being invoked.</param>
/// <param name="session">The session associated with the agent invocation.</param>
/// <param name="aiContext">The AI context to be used by the agent for this invocation.</param>
/// <exception cref="ArgumentNullException"><paramref name="agent"/> or <paramref name="aiContext"/> is <see langword="null"/>.</exception>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public InvokingContext(
AIAgent agent,
AgentSession? session,
AIContext aiContext)
{
this.Agent = Throw.IfNull(agent);
this.Session = session;
this.AIContext = Throw.IfNull(aiContext);
}
/// <summary>
/// Gets the agent that is being invoked.
/// </summary>
public AIAgent Agent { get; }
/// <summary>
/// Gets the agent session associated with the agent invocation.
/// </summary>
public AgentSession? Session { get; }
/// <summary>
/// Gets the <see cref="AIContext"/> being built for the current invocation. Context providers can modify
/// and return or return a new <see cref="AIContext"/> instance to provide additional context for the invocation.
/// </summary>
/// <remarks>
/// <para>
/// If multiple <see cref="AIContextProvider"/> instances are used in the same invocation, each <see cref="AIContextProvider"/>
/// will receive the context returned by the previous <see cref="AIContextProvider"/> allowing them to build on top of each other's context.
/// </para>
/// <para>
/// The first <see cref="AIContextProvider"/> in the invocation pipeline will receive an <see cref="AIContext"/> instance
/// that already contains the caller provided messages that will be used by the agent for this invocation.
/// </para>
/// <para>
/// It may also contain messages from chat history, if a <see cref="ChatHistoryProvider"/> is being used.
/// </para>
/// </remarks>
public AIContext AIContext { get; }
}
/// <summary>
/// Contains the context information provided to <see cref="InvokedCoreAsync(InvokedContext, CancellationToken)"/>.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public sealed class InvokedContext
{
/// <summary>
/// Initializes a new instance of the <see cref="InvokedContext"/> class for a successful invocation.
/// </summary>
/// <param name="agent">The agent that was invoked.</param>
/// <param name="session">The session associated with the agent invocation.</param>
/// <param name="requestMessages">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.</param>
/// <param name="responseMessages">The response messages generated during this invocation.</param>
/// <exception cref="ArgumentNullException"><paramref name="agent"/>, <paramref name="requestMessages"/>, or <paramref name="responseMessages"/> is <see langword="null"/>.</exception>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public InvokedContext(
AIAgent agent,
AgentSession? session,
IEnumerable<ChatMessage> requestMessages,
IEnumerable<ChatMessage> responseMessages)
{
this.Agent = Throw.IfNull(agent);
this.Session = session;
this.RequestMessages = Throw.IfNull(requestMessages);
this.ResponseMessages = Throw.IfNull(responseMessages);
}
/// <summary>
/// Initializes a new instance of the <see cref="InvokedContext"/> class for a failed invocation.
/// </summary>
/// <param name="agent">The agent that was invoked.</param>
/// <param name="session">The session associated with the agent invocation.</param>
/// <param name="requestMessages">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.</param>
/// <param name="invokeException">The exception that caused the invocation to fail.</param>
/// <exception cref="ArgumentNullException"><paramref name="agent"/>, <paramref name="requestMessages"/>, or <paramref name="invokeException"/> is <see langword="null"/>.</exception>
public InvokedContext(
AIAgent agent,
AgentSession? session,
IEnumerable<ChatMessage> requestMessages,
Exception invokeException)
{
this.Agent = Throw.IfNull(agent);
this.Session = session;
this.RequestMessages = Throw.IfNull(requestMessages);
this.InvokeException = Throw.IfNull(invokeException);
}
/// <summary>
/// Gets the agent that is being invoked.
/// </summary>
public AIAgent Agent { get; }
/// <summary>
/// Gets the agent session associated with the agent invocation.
/// </summary>
public AgentSession? Session { get; }
/// <summary>
/// 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.
/// </summary>
/// <value>
/// A collection of <see cref="ChatMessage"/> instances representing all messages that were used by the agent for this invocation.
/// </value>
public IEnumerable<ChatMessage> RequestMessages { get; }
/// <summary>
/// Gets the collection of response messages generated during this invocation if the invocation succeeded.
/// </summary>
/// <value>
/// A collection of <see cref="ChatMessage"/> instances representing the response,
/// or <see langword="null"/> if the invocation failed.
/// </value>
public IEnumerable<ChatMessage>? ResponseMessages { get; }
/// <summary>
/// Gets the <see cref="Exception"/> that was thrown during the invocation, if the invocation failed.
/// </summary>
/// <value>
/// The exception that caused the invocation to fail, or <see langword="null"/> if the invocation succeeded.
/// </value>
public Exception? InvokeException { get; }
}
}
@@ -0,0 +1,99 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>
/// Contains extension methods to allow storing and retrieving properties using the type name of the property as the key.
/// </summary>
public static class AdditionalPropertiesExtensions
{
/// <summary>
/// Adds an additional property using the type name of the property as the key.
/// </summary>
/// <typeparam name="T">The type of the property to add.</typeparam>
/// <param name="additionalProperties">The dictionary of additional properties.</param>
/// <param name="value">The value to add.</param>
public static void Add<T>(this AdditionalPropertiesDictionary additionalProperties, T value)
{
_ = Throw.IfNull(additionalProperties);
additionalProperties.Add(typeof(T).FullName!, value);
}
/// <summary>
/// Attempts to add a property using the type name of the property as the key.
/// </summary>
/// <remarks>
/// This method uses the full name of the type parameter as the key. If the key already exists,
/// the value is not updated and the method returns <see langword="false"/>.
/// </remarks>
/// <typeparam name="T">The type of the property to add.</typeparam>
/// <param name="additionalProperties">The dictionary of additional properties.</param>
/// <param name="value">The value to add.</param>
/// <returns>
/// <see langword="true"/> if the value was added successfully; <see langword="false"/> if the key already exists.
/// </returns>
public static bool TryAdd<T>(this AdditionalPropertiesDictionary additionalProperties, T value)
{
_ = Throw.IfNull(additionalProperties);
return additionalProperties.TryAdd(typeof(T).FullName!, value);
}
/// <summary>
/// Attempts to retrieve a value from the additional properties dictionary using the type name of the property as the key.
/// </summary>
/// <remarks>
/// This method uses the full name of the type parameter as the key when searching the dictionary.
/// </remarks>
/// <typeparam name="T">The type of the property to be retrieved.</typeparam>
/// <param name="additionalProperties">The dictionary containing additional properties.</param>
/// <param name="value">
/// When this method returns, contains the value retrieved from the dictionary, if found and successfully converted to the requested type;
/// otherwise, the default value of <typeparamref name="T"/>.
/// </param>
/// <returns>
/// <see langword="true"/> if a non-<see langword="null"/> value was found
/// in the dictionary and converted to the requested type; otherwise, <see langword="false"/>.
/// </returns>
public static bool TryGetValue<T>(this AdditionalPropertiesDictionary additionalProperties, [NotNullWhen(true)] out T? value)
{
_ = Throw.IfNull(additionalProperties);
return additionalProperties.TryGetValue(typeof(T).FullName!, out value);
}
/// <summary>
/// Determines whether the additional properties dictionary contains a property with the name of the provided type as the key.
/// </summary>
/// <typeparam name="T">The type of the property to check for.</typeparam>
/// <param name="additionalProperties">The dictionary of additional properties.</param>
/// <returns>
/// <see langword="true"/> if the dictionary contains a property with the name of the provided type as the key; otherwise, <see langword="false"/>.
/// </returns>
public static bool Contains<T>(this AdditionalPropertiesDictionary additionalProperties)
{
_ = Throw.IfNull(additionalProperties);
return additionalProperties.ContainsKey(typeof(T).FullName!);
}
/// <summary>
/// Removes a property from the additional properties dictionary using the name of the provided type as the key.
/// </summary>
/// <typeparam name="T">The type of the property to remove.</typeparam>
/// <param name="additionalProperties">The dictionary of additional properties.</param>
/// <returns>
/// <see langword="true"/> if the property was successfully removed; otherwise, <see langword="false"/>.
/// </returns>
public static bool Remove<T>(this AdditionalPropertiesDictionary additionalProperties)
{
_ = Throw.IfNull(additionalProperties);
return additionalProperties.Remove(typeof(T).FullName!);
}
}
@@ -0,0 +1,90 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI;
/// <summary>
/// Provides utility methods and configurations for JSON serialization operations within the Microsoft Agent Framework.
/// </summary>
public static partial class AgentAbstractionsJsonUtilities
{
/// <summary>
/// Gets the default <see cref="JsonSerializerOptions"/> instance used for JSON serialization operations of agent abstraction types.
/// </summary>
/// <remarks>
/// <para>
/// For Native AOT or applications disabling <see cref="JsonSerializer.IsReflectionEnabledByDefault"/>, this instance
/// includes source generated contracts for all common exchange types contained in this library.
/// </para>
/// <para>
/// It additionally turns on the following settings:
/// <list type="number">
/// <item><description>Enables <see cref="JsonSerializerDefaults.Web"/> defaults.</description></item>
/// <item><description>Enables <see cref="JsonIgnoreCondition.WhenWritingNull"/> as the default ignore condition for properties.</description></item>
/// <item><description>Enables <see cref="JsonNumberHandling.AllowReadingFromString"/> as the default number handling for number types.</description></item>
/// <item><description>
/// Enables <see cref="JavaScriptEncoder.UnsafeRelaxedJsonEscaping"/> when escaping JSON strings.
/// Consuming applications must ensure that JSON outputs are adequately escaped before embedding in other document formats, such as HTML and XML.
/// </description></item>
/// </list>
/// </para>
/// </remarks>
public static JsonSerializerOptions DefaultOptions { get; } = CreateDefaultOptions();
/// <summary>
/// Creates and configures the default JSON serialization options for agent abstraction types.
/// </summary>
/// <returns>The configured options.</returns>
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050:RequiresDynamicCode", Justification = "Converter is guarded by IsReflectionEnabledByDefault check.")]
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access", Justification = "Converter is guarded by IsReflectionEnabledByDefault check.")]
private static JsonSerializerOptions CreateDefaultOptions()
{
// Copy the configuration from the source generated context.
JsonSerializerOptions options = new(JsonContext.Default.Options)
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, // same as AIJsonUtilities
};
// Chain in the resolvers from both AIJsonUtilities and our source generated context.
// We want AIJsonUtilities first to ensure any M.E.AI types are handled via its resolver.
options.TypeInfoResolverChain.Clear();
options.TypeInfoResolverChain.Add(AIJsonUtilities.DefaultOptions.TypeInfoResolver!);
options.TypeInfoResolverChain.Add(JsonContext.Default.Options.TypeInfoResolver!);
// If reflection-based serialization is enabled by default, this includes
// the default type info resolver that utilizes reflection, but we need to manually
// apply the same converter AIJsonUtilities adds for string-based enum serialization,
// as that's not propagated as part of the resolver.
if (JsonSerializer.IsReflectionEnabledByDefault)
{
options.Converters.Add(new JsonStringEnumConverter());
}
options.MakeReadOnly();
return options;
}
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web,
UseStringEnumConverter = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
NumberHandling = JsonNumberHandling.AllowReadingFromString)]
// Agent abstraction types
[JsonSerializable(typeof(AgentRunOptions))]
[JsonSerializable(typeof(AgentResponse))]
[JsonSerializable(typeof(AgentResponse[]))]
[JsonSerializable(typeof(AgentResponseUpdate))]
[JsonSerializable(typeof(AgentResponseUpdate[]))]
[JsonSerializable(typeof(InMemoryChatHistoryProvider.State))]
[JsonSerializable(typeof(AgentSessionStateBag))]
[JsonSerializable(typeof(ConcurrentDictionary<string, AgentSessionStateBagValue>))]
[ExcludeFromCodeCoverage]
private sealed partial class JsonContext : JsonSerializerContext;
}
@@ -0,0 +1,113 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI;
/// <summary>
/// Represents attribution information for the source of an agent request message for a specific run, including the component type and
/// identifier.
/// </summary>
/// <remarks>
/// Use this struct to identify which component provided a message during an agent run.
/// This is useful to allow filtering of messages based on their source, such as distinguishing between user input, middleware-generated messages, and chat history.
/// </remarks>
public readonly struct AgentRequestMessageSourceAttribution : IEquatable<AgentRequestMessageSourceAttribution>
{
/// <summary>
/// Provides the key used in <see cref="ChatMessage.AdditionalProperties"/> to store the <see cref="AgentRequestMessageSourceAttribution"/>
/// associated with the agent request message.
/// </summary>
public static readonly string AdditionalPropertiesKey = "_attribution";
/// <summary>
/// Initializes a new instance of the <see cref="AgentRequestMessageSourceAttribution"/> struct with the specified source type and identifier.
/// </summary>
/// <param name="sourceType">The <see cref="AgentRequestMessageSourceType"/> of the component that provided the message.</param>
/// <param name="sourceId">The unique identifier of the component that provided the message.</param>
public AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType sourceType, string? sourceId)
{
this.SourceType = sourceType;
this.SourceId = sourceId;
}
/// <summary>
/// Gets the type of component that provided the message for the current agent run.
/// </summary>
public AgentRequestMessageSourceType SourceType { get; }
/// <summary>
/// Gets the unique identifier of the component that provided the message for the current agent run.
/// </summary>
public string? SourceId { get; }
/// <summary>
/// Determines whether the specified <see cref="AgentRequestMessageSourceAttribution"/> is equal to the current instance.
/// </summary>
/// <param name="other">The <see cref="AgentRequestMessageSourceAttribution"/> to compare with the current instance.</param>
/// <returns><see langword="true"/> if the specified instance is equal to the current instance; otherwise, <see langword="false"/>.</returns>
public bool Equals(AgentRequestMessageSourceAttribution other)
{
return this.SourceType == other.SourceType &&
string.Equals(this.SourceId, other.SourceId, StringComparison.Ordinal);
}
/// <summary>
/// Determines whether the specified object is equal to the current instance.
/// </summary>
/// <param name="obj">The object to compare with the current instance.</param>
/// <returns><see langword="true"/> if the specified object is equal to the current instance; otherwise, <see langword="false"/>.</returns>
public override bool Equals(object? obj)
{
return obj is AgentRequestMessageSourceAttribution other && this.Equals(other);
}
/// <summary>
/// Returns a string representation of the current instance.
/// </summary>
/// <returns>A string containing the source type and source identifier.</returns>
public override string ToString()
{
return this.SourceId is null
? $"{this.SourceType}"
: $"{this.SourceType}:{this.SourceId}";
}
/// <summary>
/// Returns a hash code for the current instance.
/// </summary>
/// <returns>A hash code for the current instance.</returns>
public override int GetHashCode()
{
unchecked
{
int hash = 17;
hash = (hash * 31) + this.SourceType.GetHashCode();
hash = (hash * 31) + (this.SourceId?.GetHashCode() ?? 0);
return hash;
}
}
/// <summary>
/// Determines whether two <see cref="AgentRequestMessageSourceAttribution"/> instances are equal.
/// </summary>
/// <param name="left">The first instance to compare.</param>
/// <param name="right">The second instance to compare.</param>
/// <returns><see langword="true"/> if the instances are equal; otherwise, <see langword="false"/>.</returns>
public static bool operator ==(AgentRequestMessageSourceAttribution left, AgentRequestMessageSourceAttribution right)
{
return left.Equals(right);
}
/// <summary>
/// Determines whether two <see cref="AgentRequestMessageSourceAttribution"/> instances are not equal.
/// </summary>
/// <param name="left">The first instance to compare.</param>
/// <param name="right">The second instance to compare.</param>
/// <returns><see langword="true"/> if the instances are not equal; otherwise, <see langword="false"/>.</returns>
public static bool operator !=(AgentRequestMessageSourceAttribution left, AgentRequestMessageSourceAttribution right)
{
return !left.Equals(right);
}
}
@@ -0,0 +1,91 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>
/// Represents the source of an agent request message.
/// </summary>
/// <remarks>
/// Input messages for a specific agent run can originate from various sources.
/// This type helps to identify whether a message came from outside the agent pipeline,
/// whether it was produced by middleware, or came from chat history.
/// </remarks>
public readonly struct AgentRequestMessageSourceType : IEquatable<AgentRequestMessageSourceType>
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentRequestMessageSourceType"/> struct.
/// </summary>
/// <param name="value">The string value representing the source of the agent request message.</param>
public AgentRequestMessageSourceType(string value) => this.Value = Throw.IfNullOrWhitespace(value);
/// <summary>
/// Get the string value representing the source of the agent request message.
/// </summary>
public string Value { get { return field ?? External.Value; } }
/// <summary>
/// The message came from outside the agent pipeline (e.g., user input).
/// </summary>
public static AgentRequestMessageSourceType External { get; } = new AgentRequestMessageSourceType(nameof(External));
/// <summary>
/// The message was produced by middleware.
/// </summary>
public static AgentRequestMessageSourceType AIContextProvider { get; } = new AgentRequestMessageSourceType(nameof(AIContextProvider));
/// <summary>
/// The message came from chat history.
/// </summary>
public static AgentRequestMessageSourceType ChatHistory { get; } = new AgentRequestMessageSourceType(nameof(ChatHistory));
/// <summary>
/// Determines whether this instance and another specified <see cref="AgentRequestMessageSourceType"/> object have the same value.
/// </summary>
/// <param name="other">The <see cref="AgentRequestMessageSourceType"/> to compare to this instance.</param>
/// <returns><see langword="true"/> if the value of the <paramref name="other"/> parameter is the same as the value of this instance; otherwise, <see langword="false"/>.</returns>
public bool Equals(AgentRequestMessageSourceType other)
{
return string.Equals(this.Value, other.Value, StringComparison.Ordinal);
}
/// <summary>
/// Determines whether this instance and a specified object have the same value.
/// </summary>
/// <param name="obj">The object to compare to this instance.</param>
/// <returns><see langword="true"/> if <paramref name="obj"/> is a <see cref="AgentRequestMessageSourceType"/> and its value is the same as this instance; otherwise, <see langword="false"/>.</returns>
public override bool Equals(object? obj) => obj is AgentRequestMessageSourceType other && this.Equals(other);
/// <summary>
/// Returns the string representation of this instance.
/// </summary>
/// <returns>The string value representing the source of the agent request message.</returns>
public override string ToString() => this.Value;
/// <summary>
/// Returns the hash code for this instance.
/// </summary>
/// <returns>A 32-bit signed integer hash code.</returns>
public override int GetHashCode() => this.Value?.GetHashCode() ?? 0;
/// <summary>
/// Determines whether two specified <see cref="AgentRequestMessageSourceType"/> objects have the same value.
/// </summary>
/// <param name="left">The first <see cref="AgentRequestMessageSourceType"/> to compare.</param>
/// <param name="right">The second <see cref="AgentRequestMessageSourceType"/> to compare.</param>
/// <returns><see langword="true"/> if the value of <paramref name="left"/> is the same as the value of <paramref name="right"/>; otherwise, <see langword="false"/>.</returns>
public static bool operator ==(AgentRequestMessageSourceType left, AgentRequestMessageSourceType right)
{
return left.Equals(right);
}
/// <summary>
/// Determines whether two specified <see cref="AgentRequestMessageSourceType"/> objects have different values.
/// </summary>
/// <param name="left">The first <see cref="AgentRequestMessageSourceType"/> to compare.</param>
/// <param name="right">The second <see cref="AgentRequestMessageSourceType"/> to compare.</param>
/// <returns><see langword="true"/> if the value of <paramref name="left"/> is different from the value of <paramref name="right"/>; otherwise, <see langword="false"/>.</returns>
public static bool operator !=(AgentRequestMessageSourceType left, AgentRequestMessageSourceType right) => !(left == right);
}
@@ -0,0 +1,311 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using Microsoft.Extensions.AI;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>
/// Represents the response to an <see cref="AIAgent"/> run request, containing messages and metadata about the interaction.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="AgentResponse"/> provides one or more response messages and metadata about the response.
/// A typical response will contain a single message, however a response may contain multiple messages
/// in a variety of scenarios. For example, if the agent internally invokes functions or tools, performs
/// RAG retrievals or has other complex logic, a single run by the agent may produce many messages showing
/// the intermediate progress that the agent made towards producing the agent result.
/// </para>
/// <para>
/// To get the text result of the response, use the <see cref="Text"/> property or simply call <see cref="ToString()"/> on the <see cref="AgentResponse"/>.
/// </para>
/// </remarks>
public class AgentResponse
{
/// <summary>The response messages.</summary>
private IList<ChatMessage>? _messages;
/// <summary>Initializes a new instance of the <see cref="AgentResponse"/> class.</summary>
public AgentResponse()
{
}
/// <summary>Initializes a new instance of the <see cref="AgentResponse"/> class.</summary>
/// <param name="message">The response message to include in this response.</param>
/// <exception cref="ArgumentNullException"><paramref name="message"/> is <see langword="null"/>.</exception>
public AgentResponse(ChatMessage message)
{
_ = Throw.IfNull(message);
this.Messages.Add(message);
}
/// <summary>
/// Initializes a new instance of the <see cref="AgentResponse"/> class from an existing <see cref="ChatResponse"/>.
/// </summary>
/// <param name="response">The <see cref="ChatResponse"/> from which to populate this <see cref="AgentResponse"/>.</param>
/// <exception cref="ArgumentNullException"><paramref name="response"/> is <see langword="null"/>.</exception>
/// <remarks>
/// This constructor creates an agent response that wraps an existing <see cref="ChatResponse"/>, preserving all
/// metadata and storing the original response in <see cref="RawRepresentation"/> for access to
/// the underlying implementation details.
/// </remarks>
public AgentResponse(ChatResponse response)
{
_ = Throw.IfNull(response);
this.AdditionalProperties = response.AdditionalProperties;
this.CreatedAt = response.CreatedAt;
this.FinishReason = response.FinishReason;
this.Messages = response.Messages;
this.RawRepresentation = response;
this.ResponseId = response.ResponseId;
this.Usage = response.Usage;
this.ContinuationToken = response.ContinuationToken;
}
/// <summary>
/// Initializes a new instance of the <see cref="AgentResponse"/> class from an existing <see cref="AgentResponse"/>.
/// </summary>
/// <param name="response">The <see cref="AgentResponse"/> from which to copy properties.</param>
/// <exception cref="ArgumentNullException"><paramref name="response"/> is <see langword="null"/>.</exception>
/// <remarks>
/// This constructor creates a copy of an existing agent response, preserving all
/// metadata and storing the original response in <see cref="RawRepresentation"/> for access to
/// the underlying implementation details.
/// </remarks>
protected AgentResponse(AgentResponse response)
{
_ = Throw.IfNull(response);
this.AdditionalProperties = response.AdditionalProperties;
this.CreatedAt = response.CreatedAt;
this.FinishReason = response.FinishReason;
this.Messages = response.Messages;
this.RawRepresentation = response;
this.ResponseId = response.ResponseId;
this.Usage = response.Usage;
this.ContinuationToken = response.ContinuationToken;
}
/// <summary>
/// Initializes a new instance of the <see cref="AgentResponse"/> class with the specified collection of messages.
/// </summary>
/// <param name="messages">The collection of response messages, or <see langword="null"/> to create an empty response.</param>
public AgentResponse(IList<ChatMessage>? messages)
{
this._messages = messages;
}
/// <summary>
/// Gets or sets the collection of messages to be represented by this response.
/// </summary>
/// <value>
/// A collection of <see cref="ChatMessage"/> instances representing the agent's response.
/// If the backing collection is <see langword="null"/>, accessing this property will create an empty list.
/// </value>
/// <remarks>
/// <para>
/// This property provides access to all messages generated during the agent's execution. While most
/// responses contain a single assistant message, complex agent behaviors may produce multiple messages
/// showing intermediate steps, function calls, or different types of content.
/// </para>
/// <para>
/// The collection is mutable and can be modified after creation. Setting this property to <see langword="null"/>
/// will cause subsequent access to return an empty list.
/// </para>
/// </remarks>
[AllowNull]
public IList<ChatMessage> Messages
{
get => this._messages ??= new List<ChatMessage>(1);
set => this._messages = value;
}
/// <summary>
/// Gets the concatenated text content of all messages in this response.
/// </summary>
/// <value>
/// A string containing the combined text from all <see cref="TextContent"/> instances
/// across all messages in <see cref="Messages"/>, or an empty string if no text content is present.
/// </value>
/// <remarks>
/// This property provides a convenient way to access the textual response without needing to
/// iterate through individual messages and content items. Non-text content is ignored.
/// </remarks>
[JsonIgnore]
public string Text => this._messages?.ConcatText() ?? string.Empty;
/// <summary>
/// Gets or sets the identifier of the agent that generated this response.
/// </summary>
/// <value>
/// A unique string identifier for the agent, or <see langword="null"/> if not specified.
/// </value>
/// <remarks>
/// This identifier helps track which agent generated the response in multi-agent scenarios
/// or for debugging and telemetry purposes.
/// </remarks>
public string? AgentId { get; set; }
/// <summary>
/// Gets or sets the unique identifier for this specific response.
/// </summary>
/// <value>
/// A unique string identifier for this response instance, or <see langword="null"/> if not assigned.
/// </value>
public string? ResponseId { get; set; }
/// <summary>
/// Gets or sets the continuation token for getting the result of a background agent response.
/// </summary>
/// <remarks>
/// <see cref="AIAgent"/> implementations that support background responses will return
/// a continuation token if background responses are allowed in <see cref="AgentRunOptions.AllowBackgroundResponses"/>
/// and the result of the response has not been obtained yet. If the response has completed and the result has been obtained,
/// the token will be <see langword="null"/>.
/// <para>
/// This property should be used in conjunction with <see cref="AgentRunOptions.ContinuationToken"/> to
/// continue to poll for the completion of the response. Pass this token to
/// <see cref="AgentRunOptions.ContinuationToken"/> on subsequent calls to <see cref="AIAgent.RunAsync(AgentSession?, AgentRunOptions?, System.Threading.CancellationToken)"/>
/// to poll for completion.
/// </para>
/// </remarks>
[Experimental(DiagnosticIds.Experiments.AIResponseContinuations)]
public ResponseContinuationToken? ContinuationToken { get; set; }
/// <summary>
/// Gets or sets the timestamp indicating when this response was created.
/// </summary>
/// <value>
/// A <see cref="DateTimeOffset"/> representing when the response was generated,
/// or <see langword="null"/> if not specified.
/// </value>
/// <remarks>
/// The creation timestamp is useful for auditing, logging, and understanding
/// the chronology of agentic interactions.
/// </remarks>
public DateTimeOffset? CreatedAt { get; set; }
/// <summary>
/// Gets or sets the reason for the agent response finishing.
/// </summary>
/// <value>
/// A <see cref="ChatFinishReason"/> value indicating why the response finished (e.g., stop, length, content filter, tool calls),
/// or <see langword="null"/> if the finish reason is not available.
/// </value>
/// <remarks>
/// <para>
/// This property is particularly useful for detecting non-normal completions, such as content filtering
/// or token limit truncation, which may require special handling by the caller.
/// </para>
/// </remarks>
public ChatFinishReason? FinishReason { get; set; }
/// <summary>
/// Gets or sets the resource usage information for generating this response.
/// </summary>
/// <value>
/// A <see cref="UsageDetails"/> instance containing token counts and other usage metrics,
/// or <see langword="null"/> if usage information is not available.
/// </value>
public UsageDetails? Usage { get; set; }
/// <summary>Gets or sets the raw representation of the run response from an underlying implementation.</summary>
/// <remarks>
/// If a <see cref="AgentResponse"/> is created to represent some underlying object from another object
/// model, this property can be used to store that original object. This can be useful for debugging or
/// for enabling a consumer to access the underlying object model if needed.
/// </remarks>
[JsonIgnore]
public object? RawRepresentation { get; set; }
/// <summary>
/// Gets or sets additional properties associated with this response.
/// </summary>
/// <value>
/// An <see cref="AdditionalPropertiesDictionary"/> containing custom properties,
/// or <see langword="null"/> if no additional properties are present.
/// </value>
/// <remarks>
/// Additional properties provide a way to include custom metadata or provider-specific
/// information that doesn't fit into the standard response schema. This is useful for
/// preserving implementation-specific details or extending the response with custom data.
/// </remarks>
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
/// <inheritdoc />
public override string ToString() => this.Text;
/// <summary>
/// Converts this <see cref="AgentResponse"/> into a collection of <see cref="AgentResponseUpdate"/> instances
/// suitable for streaming scenarios.
/// </summary>
/// <returns>
/// An array of <see cref="AgentResponseUpdate"/> instances that collectively represent
/// the same information as this response.
/// </returns>
/// <remarks>
/// <para>
/// This method is useful for converting complete responses back into streaming format,
/// which may be needed for scenarios that require uniform handling of both streaming
/// and non-streaming agent responses.
/// </para>
/// <para>
/// Each message in <see cref="Messages"/> becomes a separate update, and usage information
/// is included as an additional update if present. The order of updates preserves the
/// original message sequence.
/// </para>
/// </remarks>
public AgentResponseUpdate[] ToAgentResponseUpdates()
{
AgentResponseUpdate? extra = null;
if (this.AdditionalProperties is not null || this.Usage is not null)
{
extra = new AgentResponseUpdate
{
AdditionalProperties = this.AdditionalProperties,
};
if (this.Usage is { } usage)
{
extra.Contents.Add(new UsageContent(usage));
}
}
int messageCount = this._messages?.Count ?? 0;
var updates = new AgentResponseUpdate[messageCount + (extra is not null ? 1 : 0)];
int i;
for (i = 0; i < messageCount; i++)
{
ChatMessage message = this._messages![i];
updates[i] = new AgentResponseUpdate
{
AdditionalProperties = message.AdditionalProperties,
AuthorName = message.AuthorName,
Contents = message.Contents,
RawRepresentation = message.RawRepresentation,
Role = message.Role,
FinishReason = this.FinishReason,
AgentId = this.AgentId,
ResponseId = this.ResponseId,
MessageId = message.MessageId,
CreatedAt = message.CreatedAt ?? this.CreatedAt,
};
}
if (extra is not null)
{
updates[i] = extra;
}
return updates;
}
}
@@ -0,0 +1,213 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>
/// Provides extension methods for working with <see cref="AgentResponse"/> and <see cref="AgentResponseUpdate"/> instances.
/// </summary>
public static class AgentResponseExtensions
{
/// <summary>
/// Creates a <see cref="ChatResponse"/> from an <see cref="AgentResponse"/> instance.
/// </summary>
/// <param name="response">The <see cref="AgentResponse"/> to convert.</param>
/// <returns>A <see cref="ChatResponse"/> built from the specified <paramref name="response"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="response"/> is <see langword="null"/>.</exception>
/// <remarks>
/// If the <paramref name="response"/>'s <see cref="AgentResponse.RawRepresentation"/> is already a
/// <see cref="ChatResponse"/> instance, that instance is returned directly.
/// Otherwise, a new <see cref="ChatResponse"/> is created and populated with the data from the <paramref name="response"/>.
/// The resulting instance is a shallow copy; any reference-type members (e.g. <see cref="AgentResponse.Messages"/>)
/// will be shared between the two instances.
/// </remarks>
public static ChatResponse AsChatResponse(this AgentResponse response)
{
Throw.IfNull(response);
return
response.RawRepresentation as ChatResponse ??
new()
{
AdditionalProperties = response.AdditionalProperties,
CreatedAt = response.CreatedAt,
FinishReason = response.FinishReason,
Messages = response.Messages,
RawRepresentation = response,
ResponseId = response.ResponseId,
Usage = response.Usage,
ContinuationToken = response.ContinuationToken,
};
}
/// <summary>
/// Creates a <see cref="ChatResponseUpdate"/> from an <see cref="AgentResponseUpdate"/> instance.
/// </summary>
/// <param name="responseUpdate">The <see cref="AgentResponseUpdate"/> to convert.</param>
/// <returns>A <see cref="ChatResponseUpdate"/> built from the specified <paramref name="responseUpdate"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="responseUpdate"/> is <see langword="null"/>.</exception>
/// <remarks>
/// If the <paramref name="responseUpdate"/>'s <see cref="AgentResponseUpdate.RawRepresentation"/> is already a
/// <see cref="ChatResponseUpdate"/> instance, that instance is returned directly.
/// Otherwise, a new <see cref="ChatResponseUpdate"/> is created and populated with the data from the <paramref name="responseUpdate"/>.
/// The resulting instance is a shallow copy; any reference-type members (e.g. <see cref="AgentResponseUpdate.Contents"/>)
/// will be shared between the two instances.
/// </remarks>
public static ChatResponseUpdate AsChatResponseUpdate(this AgentResponseUpdate responseUpdate)
{
Throw.IfNull(responseUpdate);
return
responseUpdate.RawRepresentation as ChatResponseUpdate ??
new()
{
AdditionalProperties = responseUpdate.AdditionalProperties,
AuthorName = responseUpdate.AuthorName,
Contents = responseUpdate.Contents,
CreatedAt = responseUpdate.CreatedAt,
FinishReason = responseUpdate.FinishReason,
MessageId = responseUpdate.MessageId,
RawRepresentation = responseUpdate,
ResponseId = responseUpdate.ResponseId,
Role = responseUpdate.Role,
ContinuationToken = responseUpdate.ContinuationToken,
};
}
/// <summary>
/// Creates an asynchronous enumerable of <see cref="ChatResponseUpdate"/> instances from an asynchronous
/// enumerable of <see cref="AgentResponseUpdate"/> instances.
/// </summary>
/// <param name="responseUpdates">The sequence of <see cref="AgentResponseUpdate"/> instances to convert.</param>
/// <returns>An asynchronous enumerable of <see cref="ChatResponseUpdate"/> instances built from <paramref name="responseUpdates"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="responseUpdates"/> is <see langword="null"/>.</exception>
/// <remarks>
/// Each <see cref="AgentResponseUpdate"/> is converted to a <see cref="ChatResponseUpdate"/> using
/// <see cref="AsChatResponseUpdate"/>.
/// </remarks>
public static async IAsyncEnumerable<ChatResponseUpdate> AsChatResponseUpdatesAsync(
this IAsyncEnumerable<AgentResponseUpdate> responseUpdates)
{
Throw.IfNull(responseUpdates);
await foreach (var responseUpdate in responseUpdates.ConfigureAwait(false))
{
yield return responseUpdate.AsChatResponseUpdate();
}
}
/// <summary>
/// Combines a sequence of <see cref="AgentResponseUpdate"/> instances into a single <see cref="AgentResponse"/>.
/// </summary>
/// <param name="updates">The sequence of updates to be combined into a single response.</param>
/// <returns>A single <see cref="AgentResponse"/> that represents the combined state of all the updates.</returns>
/// <exception cref="ArgumentNullException"><paramref name="updates"/> is <see langword="null"/>.</exception>
/// <remarks>
/// As part of combining <paramref name="updates"/> into a single <see cref="AgentResponse"/>, the method will attempt to reconstruct
/// <see cref="ChatMessage"/> instances. This includes using <see cref="AgentResponseUpdate.MessageId"/> to determine
/// message boundaries, as well as coalescing contiguous <see cref="AIContent"/> items where applicable, e.g. multiple
/// <see cref="TextContent"/> instances in a row may be combined into a single <see cref="TextContent"/>.
/// </remarks>
public static AgentResponse ToAgentResponse(
this IEnumerable<AgentResponseUpdate> updates)
{
_ = Throw.IfNull(updates);
AgentResponseDetails additionalDetails = new();
ChatResponse chatResponse =
AsChatResponseUpdatesWithAdditionalDetails(updates, additionalDetails)
.ToChatResponse();
return new AgentResponse(chatResponse)
{
AgentId = additionalDetails.AgentId,
};
}
/// <summary>
/// Asynchronously combines a sequence of <see cref="AgentResponseUpdate"/> instances into a single <see cref="AgentResponse"/>.
/// </summary>
/// <param name="updates">The asynchronous sequence of updates to be combined into a single response.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a single <see cref="AgentResponse"/> that represents the combined state of all the updates.</returns>
/// <exception cref="ArgumentNullException"><paramref name="updates"/> is <see langword="null"/>.</exception>
/// <remarks>
/// <para>
/// This is the asynchronous version of <see cref="ToAgentResponse(IEnumerable{AgentResponseUpdate})"/>.
/// It performs the same combining logic but operates on an asynchronous enumerable of updates.
/// </para>
/// <para>
/// As part of combining <paramref name="updates"/> into a single <see cref="AgentResponse"/>, the method will attempt to reconstruct
/// <see cref="ChatMessage"/> instances. This includes using <see cref="AgentResponseUpdate.MessageId"/> to determine
/// message boundaries, as well as coalescing contiguous <see cref="AIContent"/> items where applicable, e.g. multiple
/// <see cref="TextContent"/> instances in a row may be combined into a single <see cref="TextContent"/>.
/// </para>
/// </remarks>
public static Task<AgentResponse> ToAgentResponseAsync(
this IAsyncEnumerable<AgentResponseUpdate> updates,
CancellationToken cancellationToken = default)
{
_ = Throw.IfNull(updates);
return ToAgentResponseAsync(updates, cancellationToken);
static async Task<AgentResponse> ToAgentResponseAsync(
IAsyncEnumerable<AgentResponseUpdate> updates,
CancellationToken cancellationToken)
{
AgentResponseDetails additionalDetails = new();
ChatResponse chatResponse = await
AsChatResponseUpdatesWithAdditionalDetailsAsync(updates, additionalDetails, cancellationToken)
.ToChatResponseAsync(cancellationToken)
.ConfigureAwait(false);
return new AgentResponse(chatResponse)
{
AgentId = additionalDetails.AgentId,
};
}
}
private static IEnumerable<ChatResponseUpdate> AsChatResponseUpdatesWithAdditionalDetails(
IEnumerable<AgentResponseUpdate> updates,
AgentResponseDetails additionalDetails)
{
foreach (var update in updates)
{
UpdateAdditionalDetails(update, additionalDetails);
yield return update.AsChatResponseUpdate();
}
}
private static async IAsyncEnumerable<ChatResponseUpdate> AsChatResponseUpdatesWithAdditionalDetailsAsync(
IAsyncEnumerable<AgentResponseUpdate> updates,
AgentResponseDetails additionalDetails,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
await foreach (var update in updates.WithCancellation(cancellationToken).ConfigureAwait(false))
{
UpdateAdditionalDetails(update, additionalDetails);
yield return update.AsChatResponseUpdate();
}
}
private static void UpdateAdditionalDetails(AgentResponseUpdate update, AgentResponseDetails details)
{
if (update.AgentId is { Length: > 0 })
{
details.AgentId = update.AgentId;
}
}
private sealed class AgentResponseDetails
{
public string? AgentId { get; set; }
}
}
@@ -0,0 +1,178 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>
/// Represents a single streaming response chunk from an <see cref="AIAgent"/>.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="AgentResponseUpdate"/> is so named because it represents updates
/// that layer on each other to form a single agent response. Conceptually, this combines the roles of
/// <see cref="AgentResponse"/> and <see cref="ChatMessage"/> in streaming output.
/// </para>
/// <para>
/// To get the text result of this response chunk, use the <see cref="Text"/> property or simply call <see cref="ToString()"/> on the <see cref="AgentResponseUpdate"/>.
/// </para>
/// <para>
/// The relationship between <see cref="AgentResponse"/> and <see cref="AgentResponseUpdate"/> is
/// codified in the <see cref="AgentResponseExtensions.ToAgentResponseAsync"/> and
/// <see cref="AgentResponse.ToAgentResponseUpdates"/>, which enable bidirectional conversions
/// between the two. Note, however, that the provided conversions may be lossy, for example if multiple
/// updates all have different <see cref="RawRepresentation"/> objects whereas there's only one slot for
/// such an object available in <see cref="AgentResponse.RawRepresentation"/>.
/// </para>
/// </remarks>
[DebuggerDisplay("[{Role}] {ContentForDebuggerDisplay}{EllipsesForDebuggerDisplay,nq}")]
public class AgentResponseUpdate
{
/// <summary>The response update content items.</summary>
private IList<AIContent>? _contents;
/// <summary>Initializes a new instance of the <see cref="AgentResponseUpdate"/> class.</summary>
[JsonConstructor]
public AgentResponseUpdate()
{
}
/// <summary>Initializes a new instance of the <see cref="AgentResponseUpdate"/> class.</summary>
/// <param name="role">The role of the author of the update.</param>
/// <param name="content">The text content of the update.</param>
public AgentResponseUpdate(ChatRole? role, string? content)
: this(role, content is null ? null : [new TextContent(content)])
{
}
/// <summary>Initializes a new instance of the <see cref="AgentResponseUpdate"/> class.</summary>
/// <param name="role">The role of the author of the update.</param>
/// <param name="contents">The contents of the update.</param>
public AgentResponseUpdate(ChatRole? role, IList<AIContent>? contents)
{
this.Role = role;
this._contents = contents;
}
/// <summary>Initializes a new instance of the <see cref="AgentResponseUpdate"/> class.</summary>
/// <param name="chatResponseUpdate">The <see cref="ChatResponseUpdate"/> from which to seed this <see cref="AgentResponseUpdate"/>.</param>
public AgentResponseUpdate(ChatResponseUpdate chatResponseUpdate)
{
_ = Throw.IfNull(chatResponseUpdate);
this.AdditionalProperties = chatResponseUpdate.AdditionalProperties;
this.AuthorName = chatResponseUpdate.AuthorName;
this.Contents = chatResponseUpdate.Contents;
this.CreatedAt = chatResponseUpdate.CreatedAt;
this.FinishReason = chatResponseUpdate.FinishReason;
this.MessageId = chatResponseUpdate.MessageId;
this.RawRepresentation = chatResponseUpdate;
this.ResponseId = chatResponseUpdate.ResponseId;
this.Role = chatResponseUpdate.Role;
this.ContinuationToken = chatResponseUpdate.ContinuationToken;
}
/// <summary>Gets or sets the name of the author of the response update.</summary>
public string? AuthorName
{
get => field;
set => field = string.IsNullOrWhiteSpace(value) ? null : value;
}
/// <summary>Gets or sets the role of the author of the response update.</summary>
public ChatRole? Role { get; set; }
/// <summary>Gets the text of this update.</summary>
/// <remarks>
/// This property concatenates the text of all <see cref="TextContent"/> objects in <see cref="Contents"/>.
/// </remarks>
[JsonIgnore]
public string Text => this._contents is not null ? this._contents.ConcatText() : string.Empty;
/// <summary>Gets or sets the agent run response update content items.</summary>
[AllowNull]
public IList<AIContent> Contents
{
get => this._contents ??= [];
set => this._contents = value;
}
/// <summary>Gets or sets the raw representation of the response update from an underlying implementation.</summary>
/// <remarks>
/// If a <see cref="AgentResponseUpdate"/> is created to represent some underlying object from another object
/// model, this property can be used to store that original object. This can be useful for debugging or
/// for enabling a consumer to access the underlying object model if needed.
/// </remarks>
[JsonIgnore]
public object? RawRepresentation { get; set; }
/// <summary>Gets or sets additional properties for the update.</summary>
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
/// <summary>Gets or sets the ID of the agent that produced the response.</summary>
public string? AgentId { get; set; }
/// <summary>Gets or sets the ID of the response of which this update is a part.</summary>
public string? ResponseId { get; set; }
/// <summary>Gets or sets the ID of the message of which this update is a part.</summary>
/// <remarks>
/// A single streaming response may be composed of multiple messages, each of which may be represented
/// by multiple updates. This property is used to group those updates together into messages.
///
/// Some providers may consider streaming responses to be a single message, and in that case
/// the value of this property may be the same as the response ID.
///
/// This value is used when <see cref="AgentResponseExtensions.ToAgentResponseAsync(IAsyncEnumerable{AgentResponseUpdate}, System.Threading.CancellationToken)"/>
/// groups <see cref="AgentResponseUpdate"/> instances into <see cref="AgentResponse"/> instances.
/// The value must be unique to each call to the underlying provider, and must be shared by
/// all updates that are part of the same logical message within a streaming response.
/// </remarks>
public string? MessageId { get; set; }
/// <summary>Gets or sets a timestamp for the response update.</summary>
public DateTimeOffset? CreatedAt { get; set; }
/// <summary>
/// Gets or sets the continuation token for resuming the streamed agent response of which this update is a part.
/// </summary>
/// <remarks>
/// <see cref="AIAgent"/> implementations that support background responses will return
/// a continuation token on each update if background responses are allowed in <see cref="AgentRunOptions.AllowBackgroundResponses"/>
/// except for the last update, for which the token will be <see langword="null"/>.
/// <para>
/// This property should be used for stream resumption, where the continuation token of the latest received update should be
/// passed to <see cref="AgentRunOptions.ContinuationToken"/> on subsequent calls to <see cref="AIAgent.RunStreamingAsync(AgentSession?, AgentRunOptions?, System.Threading.CancellationToken)"/>
/// to resume streaming from the point of interruption.
/// </para>
/// </remarks>
public ResponseContinuationToken? ContinuationToken { get; set; }
/// <summary>
/// Gets or sets the reason for the agent response finishing.
/// </summary>
/// <value>
/// A <see cref="ChatFinishReason"/> value indicating why the response finished (e.g., stop, length, content filter, tool calls),
/// or <see langword="null"/> if the finish reason is not available or not yet determined (mid-stream).
/// </value>
public ChatFinishReason? FinishReason { get; set; }
/// <inheritdoc/>
public override string ToString() => this.Text;
/// <summary>Gets a <see cref="AIContent"/> object to display in the debugger display.</summary>
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
[ExcludeFromCodeCoverage]
private AIContent? ContentForDebuggerDisplay => this._contents is { Count: > 0 } ? this._contents[0] : null;
/// <summary>Gets an indication for the debugger display of whether there's more content.</summary>
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
[ExcludeFromCodeCoverage]
private string EllipsesForDebuggerDisplay => this._contents is { Count: > 1 } ? ", ..." : string.Empty;
}
@@ -0,0 +1,98 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
#if NET
using System.Buffers;
#endif
#if NET
using System.Text;
#endif
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>
/// Represents the response of the specified type <typeparamref name="T"/> to an <see cref="AIAgent"/> run request.
/// </summary>
/// <typeparam name="T">The type of value expected from the agent.</typeparam>
public class AgentResponse<T> : AgentResponse
{
private readonly JsonSerializerOptions _serializerOptions;
/// <summary>
/// Initializes a new instance of the <see cref="AgentResponse{T}"/> class.
/// </summary>
/// <param name="response">The <see cref="AgentResponse"/> from which to populate this <see cref="AgentResponse{T}"/>.</param>
/// <param name="serializerOptions">The <see cref="JsonSerializerOptions"/> to use when deserializing the result.</param>
/// <exception cref="ArgumentNullException"><paramref name="serializerOptions"/> is <see langword="null"/>.</exception>
public AgentResponse(AgentResponse response, JsonSerializerOptions serializerOptions) : base(response)
{
_ = Throw.IfNull(serializerOptions);
this._serializerOptions = serializerOptions;
}
/// <summary>
/// Gets or sets a value indicating whether the JSON schema has an extra object wrapper.
/// </summary>
/// <remarks>
/// The wrapper is required for any non-JSON-object-typed values such as numbers, enum values, and arrays.
/// </remarks>
public bool IsWrappedInObject { get; init; }
/// <summary>
/// Gets the result value of the agent response as an instance of <typeparamref name="T"/>.
/// </summary>
[JsonIgnore]
public virtual T Result
{
get
{
var json = this.Text;
if (string.IsNullOrEmpty(json))
{
throw new InvalidOperationException("The response did not contain JSON to be deserialized.");
}
if (this.IsWrappedInObject)
{
json = StructuredOutputSchemaUtilities.UnwrapResponseData(json!);
}
T? deserialized = DeserializeFirstTopLevelObject(json!, (JsonTypeInfo<T>)this._serializerOptions.GetTypeInfo(typeof(T)));
if (deserialized is null)
{
throw new InvalidOperationException("The deserialized response is null.");
}
return deserialized;
}
}
private static T? DeserializeFirstTopLevelObject(string json, JsonTypeInfo<T> typeInfo)
{
#if NET
// We need to deserialize only the first top-level object as a workaround for a common LLM backend
// issue. GPT 3.5 Turbo commonly returns multiple top-level objects after doing a function call.
// See https://community.openai.com/t/2-json-objects-returned-when-using-function-calling-and-json-mode/574348
var utf8ByteLength = Encoding.UTF8.GetByteCount(json);
var buffer = ArrayPool<byte>.Shared.Rent(utf8ByteLength);
try
{
var utf8SpanLength = Encoding.UTF8.GetBytes(json, 0, json.Length, buffer, 0);
var reader = new Utf8JsonReader(new ReadOnlySpan<byte>(buffer, 0, utf8SpanLength), new() { AllowMultipleValues = true });
return JsonSerializer.Deserialize(ref reader, typeInfo);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
#else
return JsonSerializer.Deserialize(json, typeInfo);
#endif
}
}
@@ -0,0 +1,42 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>Provides context for an in-flight agent run.</summary>
public sealed class AgentRunContext
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentRunContext"/> class.
/// </summary>
/// <param name="agent">The <see cref="AIAgent"/> that is executing the current run.</param>
/// <param name="session">The <see cref="AgentSession"/> that is associated with the current run if any.</param>
/// <param name="requestMessages">The request messages passed into the current run.</param>
/// <param name="agentRunOptions">The <see cref="AgentRunOptions"/> that was passed to the current run.</param>
public AgentRunContext(
AIAgent agent,
AgentSession? session,
IReadOnlyCollection<ChatMessage> requestMessages,
AgentRunOptions? agentRunOptions)
{
this.Agent = Throw.IfNull(agent);
this.Session = session;
this.RequestMessages = Throw.IfNull(requestMessages);
this.RunOptions = agentRunOptions;
}
/// <summary>Gets the <see cref="AIAgent"/> that is executing the current run.</summary>
public AIAgent Agent { get; }
/// <summary>Gets the <see cref="AgentSession"/> that is associated with the current run.</summary>
public AgentSession? Session { get; }
/// <summary>Gets the request messages passed into the current run.</summary>
public IReadOnlyCollection<ChatMessage> RequestMessages { get; }
/// <summary>Gets the <see cref="AgentRunOptions"/> that was passed to the current run.</summary>
public AgentRunOptions? RunOptions { get; }
}
@@ -0,0 +1,128 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.AI;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>
/// Provides optional parameters and configuration settings for controlling agent run behavior.
/// </summary>
/// <remarks>
/// <para>
/// Implementations of <see cref="AIAgent"/> may provide subclasses of <see cref="AgentRunOptions"/> with additional options specific to that agent type.
/// </para>
/// </remarks>
public class AgentRunOptions
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentRunOptions"/> class.
/// </summary>
public AgentRunOptions()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AgentRunOptions"/> class by copying values from the specified options.
/// </summary>
/// <param name="options">The options instance from which to copy values.</param>
/// <exception cref="ArgumentNullException"><paramref name="options"/> is <see langword="null"/>.</exception>
protected AgentRunOptions(AgentRunOptions options)
{
_ = Throw.IfNull(options);
this.ContinuationToken = options.ContinuationToken;
this.AllowBackgroundResponses = options.AllowBackgroundResponses;
this.AdditionalProperties = options.AdditionalProperties?.Clone();
this.ResponseFormat = options.ResponseFormat;
}
/// <summary>
/// Gets or sets the continuation token for resuming and getting the result of the agent response identified by this token.
/// </summary>
/// <remarks>
/// This property is used for background responses that can be activated via the <see cref="AllowBackgroundResponses"/>
/// property if the <see cref="AIAgent"/> implementation supports them.
/// Streamed background responses, such as those returned by default by <see cref="AIAgent.RunStreamingAsync(AgentSession?, AgentRunOptions?, System.Threading.CancellationToken)"/>
/// can be resumed if interrupted. This means that a continuation token obtained from the <see cref="AgentResponseUpdate.ContinuationToken"/>
/// of an update just before the interruption occurred can be passed to this property to resume the stream from the point of interruption.
/// Non-streamed background responses, such as those returned by <see cref="AIAgent.RunAsync(AgentSession?, AgentRunOptions?, System.Threading.CancellationToken)"/>,
/// can be polled for completion by obtaining the token from the <see cref="AgentResponse.ContinuationToken"/> property
/// and passing it via this property on subsequent calls to <see cref="AIAgent.RunAsync(AgentSession?, AgentRunOptions?, System.Threading.CancellationToken)"/>.
/// </remarks>
[Experimental(DiagnosticIds.Experiments.AIResponseContinuations)]
public ResponseContinuationToken? ContinuationToken { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the background responses are allowed.
/// </summary>
/// <remarks>
/// <para>
/// Background responses allow running long-running operations or tasks asynchronously in the background that can be resumed by streaming APIs
/// and polled for completion by non-streaming APIs.
/// </para>
/// <para>
/// When this property is set to true, non-streaming APIs may start a background operation and return an initial
/// response with a continuation token. Subsequent calls to the same API should be made in a polling manner with
/// the continuation token to get the final result of the operation.
/// </para>
/// <para>
/// When this property is set to true, streaming APIs may also start a background operation and begin streaming
/// response updates until the operation is completed. If the streaming connection is interrupted, the
/// continuation token obtained from the last update that has one should be supplied to a subsequent call to the same streaming API
/// to resume the stream from the point of interruption and continue receiving updates until the operation is completed.
/// </para>
/// <para>
/// This property only takes effect if the implementation it's used with supports background responses.
/// If the implementation does not support background responses, this property will be ignored.
/// </para>
/// </remarks>
public bool? AllowBackgroundResponses { get; set; }
/// <summary>
/// Gets or sets additional properties associated with these options.
/// </summary>
/// <value>
/// An <see cref="AdditionalPropertiesDictionary"/> containing custom properties,
/// or <see langword="null"/> if no additional properties are present.
/// </value>
/// <remarks>
/// Additional properties provide a way to include custom metadata or provider-specific
/// information that doesn't fit into the standard options schema. This is useful for
/// preserving implementation-specific details or extending the options with custom data.
/// </remarks>
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
/// <summary>
/// Gets or sets the response format.
/// </summary>
/// <remarks>
/// If <see langword="null"/>, no response format is specified and the agent will use its default.
/// This property can be set to <see cref="ChatResponseFormat.Text"/> to specify that the response should be unstructured text,
/// to <see cref="ChatResponseFormat.Json"/> to specify that the response should be structured JSON data, or
/// an instance of <see cref="ChatResponseFormatJson"/> constructed with a specific JSON schema to request that the
/// response be structured JSON data according to that schema. It is up to the agent implementation if or how
/// to honor the request. If the agent implementation doesn't recognize the specific kind of <see cref="ChatResponseFormat"/>,
/// it can be ignored.
/// </remarks>
public ChatResponseFormat? ResponseFormat { get; set; }
/// <summary>
/// Produces a clone of the current <see cref="AgentRunOptions"/> instance.
/// </summary>
/// <returns>
/// A clone of the current <see cref="AgentRunOptions"/> instance.
/// </returns>
/// <remarks>
/// <para>
/// The clone will have the same values for all properties as the original instance. Any collections, like <see cref="AdditionalProperties"/>,
/// are shallow-cloned, meaning a new collection instance is created, but any references contained by the collections are shared with the original.
/// </para>
/// <para>
/// Derived types should override <see cref="Clone"/> to return an instance of the derived type.
/// </para>
/// </remarks>
public virtual AgentRunOptions Clone() => new(this);
}
@@ -0,0 +1,119 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>
/// Base abstraction for all agent threads.
/// </summary>
/// <remarks>
/// <para>
/// An <see cref="AgentSession"/> contains the state of a specific conversation with an agent which may include:
/// <list type="bullet">
/// <item><description>Conversation history or a reference to externally stored conversation history.</description></item>
/// <item><description>Memories or a reference to externally stored memories.</description></item>
/// <item><description>Any other state that the agent needs to persist across runs for a conversation.</description></item>
/// </list>
/// </para>
/// <para>
/// An <see cref="AgentSession"/> may also have behaviors attached to it that may include:
/// <list type="bullet">
/// <item><description>Customized storage of state.</description></item>
/// <item><description>Data extraction from and injection into a conversation.</description></item>
/// <item><description>Chat history reduction, e.g. where messages needs to be summarized or truncated to reduce the size.</description></item>
/// </list>
/// An <see cref="AgentSession"/> is always constructed by an <see cref="AIAgent"/> so that the <see cref="AIAgent"/>
/// can attach any necessary behaviors to the <see cref="AgentSession"/>. See the <see cref="AIAgent.CreateSessionAsync(System.Threading.CancellationToken)"/>
/// and <see cref="AIAgent.DeserializeSessionAsync(JsonElement, JsonSerializerOptions?, System.Threading.CancellationToken)"/> methods for more information.
/// </para>
/// <para>
/// Because of these behaviors, an <see cref="AgentSession"/> may not be reusable across different agents, since each agent
/// may add different behaviors to the <see cref="AgentSession"/> it creates.
/// </para>
/// <para>
/// To support conversations that may need to survive application restarts or separate service requests, an <see cref="AgentSession"/> can be serialized
/// and deserialized, so that it can be saved in a persistent store.
/// The <see cref="AIAgent"/> provides the <see cref="AIAgent.SerializeSessionAsync(AgentSession, JsonSerializerOptions?, System.Threading.CancellationToken)"/> method to serialize the session to a
/// <see cref="JsonElement"/> and the <see cref="AIAgent.DeserializeSessionAsync(JsonElement, JsonSerializerOptions?, System.Threading.CancellationToken)"/> method
/// can be used to deserialize the session.
/// </para>
/// <para>
/// <strong>Security considerations:</strong> Serialized sessions may contain conversation content, session identifiers,
/// and other potentially sensitive data including PII. Developers should:
/// <list type="bullet">
/// <item><description>Treat serialized session data as sensitive and store it securely with appropriate access controls and encryption at rest.</description></item>
/// <item><description>Treat restoring a session from an untrusted source as equivalent to accepting untrusted input. A compromised storage backend
/// could alter message roles to escalate trust, or inject adversarial content that influences LLM behavior.</description></item>
/// </list>
/// </para>
/// </remarks>
/// <seealso cref="AIAgent"/>
/// <seealso cref="AIAgent.CreateSessionAsync(System.Threading.CancellationToken)"/>
/// <seealso cref="AIAgent.DeserializeSessionAsync(JsonElement, JsonSerializerOptions?, System.Threading.CancellationToken)"/>
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public abstract class AgentSession
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentSession"/> class.
/// </summary>
protected AgentSession()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AgentSession"/> class.
/// </summary>
protected AgentSession(AgentSessionStateBag stateBag)
{
this.StateBag = Throw.IfNull(stateBag);
}
/// <summary>
/// Gets any arbitrary state associated with this session.
/// </summary>
/// <remarks>
/// Data stored in the <see cref="StateBag"/> will be included when the session is serialized.
/// Avoid storing secrets, credentials, or highly sensitive data in the state bag without appropriate encryption,
/// as this data may be persisted to external storage.
/// </remarks>
[JsonPropertyName("stateBag")]
public AgentSessionStateBag StateBag { get; protected set; } = new();
/// <summary>Asks the <see cref="AgentSession"/> for an object of the specified type <paramref name="serviceType"/>.</summary>
/// <param name="serviceType">The type of object being requested.</param>
/// <param name="serviceKey">An optional key that can be used to help identify the target service.</param>
/// <returns>The found object, otherwise <see langword="null"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="serviceType"/> is <see langword="null"/>.</exception>
/// <remarks>
/// The purpose of this method is to allow for the retrieval of strongly-typed services that might be provided by the <see cref="AgentSession"/>,
/// including itself or any services it might be wrapping. For example, to access a <see cref="ChatHistoryProvider"/> if available for the instance,
/// <see cref="GetService"/> may be used to request it.
/// </remarks>
public virtual object? GetService(Type serviceType, object? serviceKey = null)
{
_ = Throw.IfNull(serviceType);
return serviceKey is null && serviceType.IsInstanceOfType(this)
? this
: null;
}
/// <summary>Asks the <see cref="AgentSession"/> for an object of type <typeparamref name="TService"/>.</summary>
/// <typeparam name="TService">The type of the object to be retrieved.</typeparam>
/// <param name="serviceKey">An optional key that can be used to help identify the target service.</param>
/// <returns>The found object, otherwise <see langword="null"/>.</returns>
/// <remarks>
/// The purpose of this method is to allow for the retrieval of strongly typed services that may be provided by the <see cref="AgentSession"/>,
/// including itself or any services it might be wrapping.
/// </remarks>
public TService? GetService<TService>(object? serviceKey = null)
=> this.GetService(typeof(TService), serviceKey) is TService service ? service : default;
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private string DebuggerDisplay => $"StateBag Count = {this.StateBag.Count}";
}
@@ -0,0 +1,67 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>
/// Provides extension methods for <see cref="AgentSession"/>.
/// </summary>
public static class AgentSessionExtensions
{
/// <summary>
/// Attempts to retrieve the in-memory chat history messages associated with the specified agent session, if the agent is storing memories in the session using the <see cref="InMemoryChatHistoryProvider"/>
/// </summary>
/// <remarks>
/// This method is only applicable when using <see cref="InMemoryChatHistoryProvider"/> and if the service does not require in-service chat history storage.
/// </remarks>
/// <param name="session">The agent session from which to retrieve in-memory chat history.</param>
/// <param name="messages">When this method returns, contains the list of chat history messages if available; otherwise, null.</param>
/// <param name="stateKey">An optional key used to identify the chat history state in the session's state bag. If null, the default key for
/// in-memory chat history is used.</param>
/// <param name="jsonSerializerOptions">Optional JSON serializer options to use when accessing the session state. If null, default options are used.</param>
/// <returns><see langword="true"/> if the in-memory chat history messages were found and retrieved; <see langword="false"/> otherwise.</returns>
public static bool TryGetInMemoryChatHistory(this AgentSession session, [MaybeNullWhen(false)] out List<ChatMessage> messages, string? stateKey = null, JsonSerializerOptions? jsonSerializerOptions = null)
{
_ = Throw.IfNull(session);
if (session.StateBag.TryGetValue(stateKey ?? nameof(InMemoryChatHistoryProvider), out InMemoryChatHistoryProvider.State? state, jsonSerializerOptions ?? AgentAbstractionsJsonUtilities.DefaultOptions) && state?.Messages is not null)
{
messages = state.Messages;
return true;
}
messages = null;
return false;
}
/// <summary>
/// Sets the in-memory chat message history for the specified agent session, replacing any existing messages.
/// </summary>
/// <remarks>
/// This method is only applicable when using <see cref="InMemoryChatHistoryProvider"/> and if the service does not require in-service chat history storage.
/// If messages are set, but a different <see cref="ChatHistoryProvider"/> is used, or if chat history is stored in the underlying AI service, the messages will be ignored.
/// </remarks>
/// <param name="session">The agent session whose in-memory chat history will be updated.</param>
/// <param name="messages">The list of chat messages to store in memory for the session. Replaces any existing messages for the specified
/// state key.</param>
/// <param name="stateKey">The key used to identify the in-memory chat history within the session's state bag. If null, a default key is
/// used.</param>
/// <param name="jsonSerializerOptions">The serializer options used when accessing or storing the state. If null, default options are applied.</param>
public static void SetInMemoryChatHistory(this AgentSession session, List<ChatMessage> messages, string? stateKey = null, JsonSerializerOptions? jsonSerializerOptions = null)
{
_ = Throw.IfNull(session);
if (session.StateBag.TryGetValue(stateKey ?? nameof(InMemoryChatHistoryProvider), out InMemoryChatHistoryProvider.State? state, jsonSerializerOptions ?? AgentAbstractionsJsonUtilities.DefaultOptions) && state is not null)
{
state.Messages = messages;
return;
}
session.StateBag.SetValue(stateKey ?? nameof(InMemoryChatHistoryProvider), new InMemoryChatHistoryProvider.State() { Messages = messages }, jsonSerializerOptions ?? AgentAbstractionsJsonUtilities.DefaultOptions);
}
}
@@ -0,0 +1,145 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Concurrent;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>
/// Provides a thread-safe key-value store for managing session-scoped state with support for type-safe access and JSON
/// serialization options.
/// </summary>
/// <remarks>
/// SessionState enables storing and retrieving objects associated with a session using string keys.
/// Values can be accessed in a type-safe manner and are serialized or deserialized using configurable JSON serializer
/// options. This class is designed for concurrent access and is safe to use across multiple threads.
/// </remarks>
[JsonConverter(typeof(AgentSessionStateBagJsonConverter))]
public class AgentSessionStateBag
{
private readonly ConcurrentDictionary<string, AgentSessionStateBagValue> _state;
/// <summary>
/// Initializes a new instance of the <see cref="AgentSessionStateBag"/> class.
/// </summary>
public AgentSessionStateBag()
{
this._state = new ConcurrentDictionary<string, AgentSessionStateBagValue>();
}
/// <summary>
/// Initializes a new instance of the <see cref="AgentSessionStateBag"/> class.
/// </summary>
/// <param name="state">The initial state dictionary.</param>
internal AgentSessionStateBag(ConcurrentDictionary<string, AgentSessionStateBagValue>? state)
{
this._state = state ?? new ConcurrentDictionary<string, AgentSessionStateBagValue>();
}
/// <summary>
/// Gets the number of key-value pairs contained in the session state.
/// </summary>
public int Count => this._state.Count;
/// <summary>
/// Tries to get a value from the session state.
/// </summary>
/// <typeparam name="T">The type of the value to retrieve.</typeparam>
/// <param name="key">The key from which to retrieve the value.</param>
/// <param name="value">The value if found and convertible to the required type; otherwise, null.</param>
/// <param name="jsonSerializerOptions">The JSON serializer options to use for serializing/deserializing the value.</param>
/// <returns><see langword="true"/> if the value was successfully retrieved, <see langword="false"/> otherwise.</returns>
public bool TryGetValue<T>(string key, out T? value, JsonSerializerOptions? jsonSerializerOptions = null)
where T : class
{
_ = Throw.IfNullOrWhitespace(key);
var jso = jsonSerializerOptions ?? AgentAbstractionsJsonUtilities.DefaultOptions;
if (this._state.TryGetValue(key, out var stateValue))
{
return stateValue.TryReadDeserializedValue(out value, jso);
}
value = null;
return false;
}
/// <summary>
/// Gets a value from the session state.
/// </summary>
/// <typeparam name="T">The type of value to get.</typeparam>
/// <param name="key">The key from which to retrieve the value.</param>
/// <param name="jsonSerializerOptions">The JSON serializer options to use for serializing/deserialing the value.</param>
/// <returns>The retrieved value or null if not found.</returns>
/// <exception cref="InvalidOperationException">The value could not be deserialized into the required type.</exception>
public T? GetValue<T>(string key, JsonSerializerOptions? jsonSerializerOptions = null)
where T : class
{
_ = Throw.IfNullOrWhitespace(key);
var jso = jsonSerializerOptions ?? AgentAbstractionsJsonUtilities.DefaultOptions;
if (this._state.TryGetValue(key, out var stateValue))
{
return stateValue.ReadDeserializedValue<T>(jso);
}
return null;
}
/// <summary>
/// Sets a value in the session state.
/// </summary>
/// <typeparam name="T">The type of the value to set.</typeparam>
/// <param name="key">The key to store the value under.</param>
/// <param name="value">The value to set.</param>
/// <param name="jsonSerializerOptions">The JSON serializer options to use for serializing the value.</param>
public void SetValue<T>(string key, T? value, JsonSerializerOptions? jsonSerializerOptions = null)
where T : class
{
_ = Throw.IfNullOrWhitespace(key);
var jso = jsonSerializerOptions ?? AgentAbstractionsJsonUtilities.DefaultOptions;
var stateValue = this._state.GetOrAdd(key, _ =>
new AgentSessionStateBagValue(value, typeof(T), jso));
stateValue.SetDeserialized(value, typeof(T), jso);
}
/// <summary>
/// Tries to remove a value from the session state.
/// </summary>
/// <param name="key">The key of the value to remove.</param>
/// <returns><see langword="true"/> if the value was successfully removed; otherwise, <see langword="false"/>.</returns>
public bool TryRemoveValue(string key)
=> this._state.TryRemove(Throw.IfNullOrWhitespace(key), out _);
/// <summary>
/// Serializes all session state values to a JSON object.
/// </summary>
/// <returns>A <see cref="JsonElement"/> representing the serialized session state.</returns>
/// <exception cref="InvalidOperationException">Thrown when a session state value is not properly initialized.</exception>
public JsonElement Serialize()
{
return JsonSerializer.SerializeToElement(this._state, AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ConcurrentDictionary<string, AgentSessionStateBagValue>)));
}
/// <summary>
/// Deserializes a JSON object into an <see cref="AgentSessionStateBag"/> instance.
/// </summary>
/// <param name="jsonElement">The element to deserialize.</param>
/// <returns>The deserialized <see cref="AgentSessionStateBag"/>.</returns>
public static AgentSessionStateBag Deserialize(JsonElement jsonElement)
{
if (jsonElement.ValueKind is JsonValueKind.Undefined or JsonValueKind.Null)
{
return new AgentSessionStateBag();
}
return new AgentSessionStateBag(
jsonElement.Deserialize(AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ConcurrentDictionary<string, AgentSessionStateBagValue>))) as ConcurrentDictionary<string, AgentSessionStateBagValue>
?? new ConcurrentDictionary<string, AgentSessionStateBagValue>());
}
}
@@ -0,0 +1,28 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Microsoft.Agents.AI;
/// <summary>
/// Custom JSON converter for <see cref="AgentSessionStateBag"/> that serializes and deserializes
/// the internal dictionary contents rather than the container object's public properties.
/// </summary>
public sealed class AgentSessionStateBagJsonConverter : JsonConverter<AgentSessionStateBag>
{
/// <inheritdoc/>
public override AgentSessionStateBag Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var element = JsonElement.ParseValue(ref reader);
return AgentSessionStateBag.Deserialize(element);
}
/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, AgentSessionStateBag value, JsonSerializerOptions options)
{
var element = value.Serialize();
element.WriteTo(writer);
}
}
@@ -0,0 +1,182 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Microsoft.Agents.AI;
/// <summary>
/// Used to store a value in session state.
/// </summary>
[JsonConverter(typeof(AgentSessionStateBagValueJsonConverter))]
internal class AgentSessionStateBagValue
{
private readonly object _lock = new();
private DeserializedCache? _cache;
private JsonElement _jsonValue;
/// <summary>
/// Initializes a new instance of the SessionStateValue class with the specified value.
/// </summary>
/// <param name="jsonValue">The serialized value to associate with the session state.</param>
public AgentSessionStateBagValue(JsonElement jsonValue)
{
this.JsonValue = jsonValue;
}
/// <summary>
/// Initializes a new instance of the SessionStateValue class with the specified value.
/// </summary>
/// <param name="deserializedValue">The value to associate with the session state. Can be any object, including null.</param>
/// <param name="valueType">The type of the value.</param>
/// <param name="jsonSerializerOptions">The JSON serializer options to use for serializing the value.</param>
public AgentSessionStateBagValue(object? deserializedValue, Type valueType, JsonSerializerOptions jsonSerializerOptions)
{
this._cache = new DeserializedCache(deserializedValue, valueType, jsonSerializerOptions);
}
/// <summary>
/// Gets or sets the value associated with this instance.
/// </summary>
public JsonElement JsonValue
{
get
{
lock (this._lock)
{
// We are assuming here that JsonValue will only be read when the object is being serialized,
// which means that we will only call SerializeToElement when serializing and therefore it's
// OK to serialize on each read if the cache is set.
if (this._cache is { } cache)
{
this._jsonValue = JsonSerializer.SerializeToElement(cache.Value, cache.Options.GetTypeInfo(cache.ValueType));
}
return this._jsonValue;
}
}
set
{
lock (this._lock)
{
this._jsonValue = value;
this._cache = null;
}
}
}
/// <summary>
/// Tries to read the deserialized value of this session state value.
/// Returns false if the value could not be deserialized into the required type, or if the value is undefined.
/// Returns true and sets the out parameter to null if the value is null.
/// </summary>
public bool TryReadDeserializedValue<T>(out T? value, JsonSerializerOptions? jsonSerializerOptions = null)
where T : class
{
var jso = jsonSerializerOptions ?? AgentAbstractionsJsonUtilities.DefaultOptions;
lock (this._lock)
{
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;
case DeserializedCache { ValueType: Type cacheValueType } when cacheValueType != typeof(T):
value = null;
return false;
}
switch (this._jsonValue)
{
case JsonElement jsonElement when jsonElement.ValueKind == JsonValueKind.Undefined:
value = null;
return false;
case JsonElement jsonElement when jsonElement.ValueKind == JsonValueKind.Null:
value = null;
return true;
default:
T? result = this._jsonValue.Deserialize(jso.GetTypeInfo(typeof(T))) as T;
if (result is null)
{
value = null;
return false;
}
this._cache = new DeserializedCache(result, typeof(T), jso);
value = result;
return true;
}
}
}
/// <summary>
/// Reads the deserialized value of this session state value, throwing an exception if the value could not be deserialized into the required type or is undefined.
/// </summary>
public T? ReadDeserializedValue<T>(JsonSerializerOptions? jsonSerializerOptions = null)
where T : class
{
var jso = jsonSerializerOptions ?? AgentAbstractionsJsonUtilities.DefaultOptions;
lock (this._lock)
{
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):
throw new InvalidOperationException($"The type of the cached value is {cacheValueType.FullName}, but the requested type is {typeof(T).FullName}.");
}
switch (this._jsonValue)
{
case JsonElement jsonElement when jsonElement.ValueKind == JsonValueKind.Null || jsonElement.ValueKind == JsonValueKind.Undefined:
return null;
default:
T? result = this._jsonValue.Deserialize(jso.GetTypeInfo(typeof(T))) as T;
if (result is null)
{
throw new InvalidOperationException($"Failed to deserialize session state value to type {typeof(T).FullName}.");
}
this._cache = new DeserializedCache(result, typeof(T), jso);
return result;
}
}
}
/// <summary>
/// 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.
/// </summary>
public void SetDeserialized<T>(T? deserializedValue, Type valueType, JsonSerializerOptions jsonSerializerOptions)
{
lock (this._lock)
{
this._cache = new DeserializedCache(deserializedValue, valueType, jsonSerializerOptions);
}
}
private readonly struct DeserializedCache
{
public DeserializedCache(object? value, Type valueType, JsonSerializerOptions options)
{
this.Value = value;
this.ValueType = valueType;
this.Options = options;
}
public object? Value { get; }
public Type ValueType { get; }
public JsonSerializerOptions Options { get; }
}
}
@@ -0,0 +1,27 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Microsoft.Agents.AI;
/// <summary>
/// Custom JSON converter for <see cref="AgentSessionStateBagValue"/> that serializes and deserializes
/// the <see cref="AgentSessionStateBagValue.JsonValue"/> directly rather than wrapping it in a container object.
/// </summary>
internal sealed class AgentSessionStateBagValueJsonConverter : JsonConverter<AgentSessionStateBagValue>
{
/// <inheritdoc/>
public override AgentSessionStateBagValue Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var element = JsonElement.ParseValue(ref reader);
return new AgentSessionStateBagValue(element);
}
/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, AgentSessionStateBagValue value, JsonSerializerOptions options)
{
value.JsonValue.WriteTo(writer);
}
}
@@ -0,0 +1,478 @@
// 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;
/// <summary>
/// Provides an abstract base class for fetching chat messages from, and adding chat messages to, chat history for the purposes of agent execution.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="ChatHistoryProvider"/> defines the contract that an <see cref="AIAgent"/> 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.
/// </para>
/// <para>
/// Key responsibilities include:
/// <list type="bullet">
/// <item><description>Storing chat messages with proper ordering and metadata preservation</description></item>
/// <item><description>Retrieving messages in chronological order for agent context</description></item>
/// <item><description>Managing storage limits through truncation, summarization, or other strategies</description></item>
/// </list>
/// </para>
/// <para>
/// The <see cref="ChatHistoryProvider"/> is passed a reference to the <see cref="AgentSession"/> via <see cref="InvokingContext"/> and <see cref="InvokedContext"/>
/// allowing it to store state in the <see cref="AgentSession.StateBag"/>. Since a <see cref="ChatHistoryProvider"/> 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 <see cref="AgentSession.StateBag"/>.
/// </para>
/// <para>
/// A <see cref="ChatHistoryProvider"/> is only relevant for scenarios where the underlying AI service that the agent is using
/// does not use in-service chat history storage.
/// </para>
/// <para>
/// <strong>Security considerations:</strong> 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.
/// </para>
/// </remarks>
public abstract class ChatHistoryProvider
{
private static IEnumerable<ChatMessage> DefaultExcludeChatHistoryFilter(IEnumerable<ChatMessage> messages)
=> messages.Where(m => m.GetAgentRequestMessageSourceType() != AgentRequestMessageSourceType.ChatHistory);
private static IEnumerable<ChatMessage> DefaultNoopFilter(IEnumerable<ChatMessage> messages)
=> messages;
private IReadOnlyList<string>? _stateKeys;
private readonly Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? _provideOutputMessageFilter;
private readonly Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>> _storeInputRequestMessageFilter;
private readonly Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>> _storeInputResponseMessageFilter;
/// <summary>
/// Initializes a new instance of the <see cref="ChatHistoryProvider"/> class.
/// </summary>
/// <param name="provideOutputMessageFilter">An optional filter function to apply to messages when retrieving them from the chat history.</param>
/// <param name="storeInputRequestMessageFilter">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 <see cref="AgentRequestMessageSourceType.ChatHistory"/>.</param>
/// <param name="storeInputResponseMessageFilter">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.</param>
protected ChatHistoryProvider(
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? provideOutputMessageFilter = null,
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? storeInputRequestMessageFilter = null,
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? storeInputResponseMessageFilter = null)
{
this._provideOutputMessageFilter = provideOutputMessageFilter;
this._storeInputRequestMessageFilter = storeInputRequestMessageFilter ?? DefaultExcludeChatHistoryFilter;
this._storeInputResponseMessageFilter = storeInputResponseMessageFilter ?? DefaultNoopFilter;
}
/// <summary>
/// Gets the set of keys used to store the provider state in the <see cref="AgentSession.StateBag"/>.
/// </summary>
/// <remarks>
/// The default value is a single-element set containing the name of the concrete type (e.g. <c>"InMemoryChatHistoryProvider"</c>).
/// 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.
/// </remarks>
public virtual IReadOnlyList<string> StateKeys => this._stateKeys ??= [this.GetType().Name];
/// <summary>
/// Called at the start of agent invocation to provide messages for the next agent invocation.
/// </summary>
/// <param name="context">Contains the request context including the caller provided messages that will be used by the agent for this invocation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>
/// A task that represents the asynchronous operation. The task result contains a collection of <see cref="ChatMessage"/>
/// instances that will be used for the agent invocation.
/// </returns>
/// <remarks>
/// <para>
/// If the total message history becomes very large, implementations should apply appropriate strategies to manage
/// storage constraints, such as:
/// <list type="bullet">
/// <item><description>Truncating older messages while preserving recent context</description></item>
/// <item><description>Summarizing message groups to maintain essential context</description></item>
/// <item><description>Implementing sliding window approaches for message retention</description></item>
/// <item><description>Archiving old messages while keeping active conversation context</description></item>
/// </list>
/// </para>
/// </remarks>
public ValueTask<IEnumerable<ChatMessage>> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
=> this.InvokingCoreAsync(Throw.IfNull(context), cancellationToken);
/// <summary>
/// Called at the start of agent invocation to provide messages for the next agent invocation.
/// </summary>
/// <param name="context">Contains the request context including the caller provided messages that will be used by the agent for this invocation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>
/// A task that represents the asynchronous operation. The task result contains a collection of <see cref="ChatMessage"/>
/// instances that will be used for the agent invocation.
/// </returns>
/// <remarks>
/// <para>
/// If the total message history becomes very large, implementations should apply appropriate strategies to manage
/// storage constraints, such as:
/// <list type="bullet">
/// <item><description>Truncating older messages while preserving recent context</description></item>
/// <item><description>Summarizing message groups to maintain essential context</description></item>
/// <item><description>Implementing sliding window approaches for message retention</description></item>
/// <item><description>Archiving old messages while keeping active conversation context</description></item>
/// </list>
/// </para>
/// <para>
/// The default implementation of this method, calls <see cref="ProvideChatHistoryAsync"/> 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 <see cref="ProvideChatHistoryAsync"/> 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.
/// </para>
/// </remarks>
protected virtual async ValueTask<IEnumerable<ChatMessage>> 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);
}
/// <summary>
/// When overridden in a derived class, provides the chat history messages to be used for the current invocation.
/// </summary>
/// <remarks>
/// <para>
/// This method is called from <see cref="InvokingCoreAsync"/>.
/// Note that <see cref="InvokingCoreAsync"/> 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.
/// </para>
/// <para>
/// In contrast with <see cref="InvokingCoreAsync"/>, this method only returns additional messages to be added to the request,
/// while <see cref="InvokingCoreAsync"/> is responsible for returning the full set of messages to be used for the invocation (including caller provided messages).
/// </para>
/// <para>
/// 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.
/// </para>
/// <para>
/// <strong>Security consideration:</strong> 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 <c>user</c> messages to
/// <c>system</c> messages) or inject adversarial content that influences LLM behavior.
/// </para>
/// </remarks>
/// <param name="context">Contains the request context including the caller provided messages that will be used by the agent for this invocation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>
/// A task that represents the asynchronous operation. The task result contains a collection of <see cref="ChatMessage"/>
/// instances in ascending chronological order (oldest first).
/// </returns>
protected virtual ValueTask<IEnumerable<ChatMessage>> ProvideChatHistoryAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
return new ValueTask<IEnumerable<ChatMessage>>([]);
}
/// <summary>
/// Called at the end of the agent invocation to add new messages to the chat history.
/// </summary>
/// <param name="context">Contains the invocation context including request messages, response messages, and any exception that occurred.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous add operation.</returns>
/// <remarks>
/// <para>
/// Messages should be added in the order they were generated to maintain proper chronological sequence.
/// The <see cref="ChatHistoryProvider"/> is responsible for preserving message ordering and ensuring that subsequent calls to
/// <see cref="InvokingCoreAsync"/> return messages in the correct chronological order.
/// </para>
/// <para>
/// Implementations may perform additional processing during message addition, such as:
/// <list type="bullet">
/// <item><description>Validating message content and metadata</description></item>
/// <item><description>Applying storage optimizations or compression</description></item>
/// <item><description>Triggering background maintenance operations</description></item>
/// </list>
/// </para>
/// <para>
/// This method is called regardless of whether the invocation succeeded or failed.
/// To check if the invocation was successful, inspect the <see cref="InvokedContext.InvokeException"/> property.
/// </para>
/// </remarks>
public ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default) =>
this.InvokedCoreAsync(Throw.IfNull(context), cancellationToken);
/// <summary>
/// Called at the end of the agent invocation to add new messages to the chat history.
/// </summary>
/// <param name="context">Contains the invocation context including request messages, response messages, and any exception that occurred.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous add operation.</returns>
/// <remarks>
/// <para>
/// Messages should be added in the order they were generated to maintain proper chronological sequence.
/// The <see cref="ChatHistoryProvider"/> is responsible for preserving message ordering and ensuring that subsequent calls to
/// <see cref="InvokingCoreAsync"/> return messages in the correct chronological order.
/// </para>
/// <para>
/// Implementations may perform additional processing during message addition, such as:
/// <list type="bullet">
/// <item><description>Validating message content and metadata</description></item>
/// <item><description>Applying storage optimizations or compression</description></item>
/// <item><description>Triggering background maintenance operations</description></item>
/// </list>
/// </para>
/// <para>
/// This method is called regardless of whether the invocation succeeded or failed.
/// To check if the invocation was successful, inspect the <see cref="InvokedContext.InvokeException"/> property.
/// </para>
/// <para>
/// 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 <see cref="StoreChatHistoryAsync"/> to store new chat history messages.
/// For most scenarios, overriding <see cref="StoreChatHistoryAsync"/> 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.
/// </para>
/// </remarks>
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);
}
/// <summary>
/// When overridden in a derived class, adds new messages to the chat history at the end of the agent invocation.
/// </summary>
/// <param name="context">Contains the invocation context including request messages, response messages, and any exception that occurred.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous add operation.</returns>
/// <remarks>
/// <para>
/// Messages should be added in the order they were generated to maintain proper chronological sequence.
/// The <see cref="ChatHistoryProvider"/> is responsible for preserving message ordering and ensuring that subsequent calls to
/// <see cref="InvokingCoreAsync"/> return messages in the correct chronological order.
/// </para>
/// <para>
/// Implementations may perform additional processing during message addition, such as:
/// <list type="bullet">
/// <item><description>Validating message content and metadata</description></item>
/// <item><description>Applying storage optimizations or compression</description></item>
/// <item><description>Triggering background maintenance operations</description></item>
/// </list>
/// </para>
/// <para>
/// This method is called from <see cref="InvokedCoreAsync"/>.
/// Note that <see cref="InvokedCoreAsync"/> 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.
/// </para>
/// <para>
/// In contrast with <see cref="InvokedCoreAsync"/>, this method only stores messages,
/// while <see cref="InvokedCoreAsync"/> is also responsible for messages filtering and error handling.
/// </para>
/// <para>
/// The default implementation of <see cref="InvokedCoreAsync"/> only calls this method if the invocation succeeded.
/// </para>
/// <para>
/// <strong>Security consideration:</strong> Messages being stored may contain PII and sensitive conversation content.
/// Implementers should ensure appropriate encryption at rest and access controls for the storage backend.
/// </para>
/// </remarks>
protected virtual ValueTask StoreChatHistoryAsync(InvokedContext context, CancellationToken cancellationToken = default) =>
default;
/// <summary>Asks the <see cref="ChatHistoryProvider"/> for an object of the specified type <paramref name="serviceType"/>.</summary>
/// <param name="serviceType">The type of object being requested.</param>
/// <param name="serviceKey">An optional key that can be used to help identify the target service.</param>
/// <returns>The found object, otherwise <see langword="null"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="serviceType"/> is <see langword="null"/>.</exception>
/// <remarks>
/// The purpose of this method is to allow for the retrieval of strongly-typed services that might be provided by the <see cref="ChatHistoryProvider"/>,
/// including itself or any services it might be wrapping.
/// </remarks>
public virtual object? GetService(Type serviceType, object? serviceKey = null)
{
_ = Throw.IfNull(serviceType);
return serviceKey is null && serviceType.IsInstanceOfType(this)
? this
: null;
}
/// <summary>Asks the <see cref="ChatHistoryProvider"/> for an object of type <typeparamref name="TService"/>.</summary>
/// <typeparam name="TService">The type of the object to be retrieved.</typeparam>
/// <param name="serviceKey">An optional key that can be used to help identify the target service.</param>
/// <returns>The found object, otherwise <see langword="null"/>.</returns>
/// <remarks>
/// The purpose of this method is to allow for the retrieval of strongly typed services that may be provided by the <see cref="ChatHistoryProvider"/>,
/// including itself or any services it might be wrapping.
/// </remarks>
public TService? GetService<TService>(object? serviceKey = null)
=> this.GetService(typeof(TService), serviceKey) is TService service ? service : default;
/// <summary>
/// Contains the context information provided to <see cref="InvokingCoreAsync(InvokingContext, CancellationToken)"/>.
/// </summary>
/// <remarks>
/// This class provides context about the invocation including the new messages that will be used.
/// A <see cref="ChatHistoryProvider"/> can use this information to determine what messages should be provided
/// for the invocation.
/// </remarks>
public sealed class InvokingContext
{
/// <summary>
/// Initializes a new instance of the <see cref="InvokingContext"/> class with the specified request messages.
/// </summary>
/// <param name="agent">The agent being invoked.</param>
/// <param name="session">The session associated with the agent invocation.</param>
/// <param name="requestMessages">The messages to be used by the agent for this invocation.</param>
/// <exception cref="ArgumentNullException"><paramref name="requestMessages"/> is <see langword="null"/>.</exception>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public InvokingContext(
AIAgent agent,
AgentSession? session,
IEnumerable<ChatMessage> requestMessages)
{
this.Agent = Throw.IfNull(agent);
this.Session = session;
this.RequestMessages = Throw.IfNull(requestMessages);
}
/// <summary>
/// Gets the agent that is being invoked.
/// </summary>
public AIAgent Agent { get; }
/// <summary>
/// Gets the agent session associated with the agent invocation.
/// </summary>
public AgentSession? Session { get; }
/// <summary>
/// Gets the messages that will be used by the agent for this invocation. <see cref="ChatHistoryProvider"/> instances can modify
/// and return or return a new message list to add additional messages for the invocation.
/// </summary>
/// <value>
/// A collection of <see cref="ChatMessage"/> instances representing the messages that will be used by the agent for this invocation.
/// </value>
/// <remarks>
/// <para>
/// If multiple <see cref="ChatHistoryProvider"/> instances are used in the same invocation, each <see cref="ChatHistoryProvider"/>
/// will receive the messages returned by the previous <see cref="ChatHistoryProvider"/> allowing them to build on top of each other's context.
/// </para>
/// <para>
/// The first <see cref="ChatHistoryProvider"/> in the invocation pipeline will receive the
/// caller provided messages.
/// </para>
/// </remarks>
public IEnumerable<ChatMessage> RequestMessages { get; set { field = Throw.IfNull(value); } }
}
/// <summary>
/// Contains the context information provided to <see cref="InvokedCoreAsync(InvokedContext, CancellationToken)"/>.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public sealed class InvokedContext
{
/// <summary>
/// Initializes a new instance of the <see cref="InvokedContext"/> class for a successful invocation.
/// </summary>
/// <param name="agent">The agent that was invoked.</param>
/// <param name="session">The session associated with the agent invocation.</param>
/// <param name="requestMessages">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.</param>
/// <param name="responseMessages">The response messages generated during this invocation.</param>
/// <exception cref="ArgumentNullException"><paramref name="agent"/>, <paramref name="requestMessages"/>, or <paramref name="responseMessages"/> is <see langword="null"/>.</exception>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public InvokedContext(
AIAgent agent,
AgentSession? session,
IEnumerable<ChatMessage> requestMessages,
IEnumerable<ChatMessage> responseMessages)
{
this.Agent = Throw.IfNull(agent);
this.Session = session;
this.RequestMessages = Throw.IfNull(requestMessages);
this.ResponseMessages = Throw.IfNull(responseMessages);
}
/// <summary>
/// Initializes a new instance of the <see cref="InvokedContext"/> class for a failed invocation.
/// </summary>
/// <param name="agent">The agent that was invoked.</param>
/// <param name="session">The session associated with the agent invocation.</param>
/// <param name="requestMessages">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.</param>
/// <param name="invokeException">The exception that caused the invocation to fail.</param>
/// <exception cref="ArgumentNullException"><paramref name="agent"/>, <paramref name="requestMessages"/>, or <paramref name="invokeException"/> is <see langword="null"/>.</exception>
public InvokedContext(
AIAgent agent,
AgentSession? session,
IEnumerable<ChatMessage> requestMessages,
Exception invokeException)
{
this.Agent = Throw.IfNull(agent);
this.Session = session;
this.RequestMessages = Throw.IfNull(requestMessages);
this.InvokeException = Throw.IfNull(invokeException);
}
/// <summary>
/// Gets the agent that is being invoked.
/// </summary>
public AIAgent Agent { get; }
/// <summary>
/// Gets the agent session associated with the agent invocation.
/// </summary>
public AgentSession? Session { get; }
/// <summary>
/// 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.
/// </summary>
/// <value>
/// A collection of <see cref="ChatMessage"/> instances representing new messages that were provided by the caller.
/// This does not include any <see cref="ChatHistoryProvider"/> supplied messages.
/// </value>
public IEnumerable<ChatMessage> RequestMessages { get; }
/// <summary>
/// Gets the collection of response messages generated during this invocation if the invocation succeeded.
/// </summary>
/// <value>
/// A collection of <see cref="ChatMessage"/> instances representing the response,
/// or <see langword="null"/> if the invocation failed.
/// </value>
public IEnumerable<ChatMessage>? ResponseMessages { get; }
/// <summary>
/// Gets the <see cref="Exception"/> that was thrown during the invocation, if the invocation failed.
/// </summary>
/// <value>
/// The exception that caused the invocation to fail, or <see langword="null"/> if the invocation succeeded.
/// </value>
public Exception? InvokeException { get; }
}
}
@@ -0,0 +1,75 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI;
/// <summary>
/// Contains extension methods for <see cref="ChatMessage"/>
/// </summary>
public static class ChatMessageExtensions
{
/// <summary>
/// Gets the source type of the provided <see cref="ChatMessage"/> in the context of messages passed into an agent run.
/// </summary>
/// <param name="message">The <see cref="ChatMessage"/> for which we need the source type.</param>
/// <returns>An <see cref="AgentRequestMessageSourceType"/> value indicating the source type of the <see cref="ChatMessage"/>. Defaults to <see
/// cref="AgentRequestMessageSourceType.External"/> if no explicit source is defined.</returns>
public static AgentRequestMessageSourceType GetAgentRequestMessageSourceType(this ChatMessage message)
{
if (message.AdditionalProperties?.TryGetValue(AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, out var attribution) is true
&& attribution is AgentRequestMessageSourceAttribution typedAttribution)
{
return typedAttribution.SourceType;
}
return AgentRequestMessageSourceType.External;
}
/// <summary>
/// Gets the source id of the provided <see cref="ChatMessage"/> in the context of messages passed into an agent run.
/// </summary>
/// <param name="message">The <see cref="ChatMessage"/> for which we need the source id.</param>
/// <returns>An <see cref="string"/> value indicating the source id of the <see cref="ChatMessage"/>. Defaults to <see langword="null"/>
/// if no explicit source id is defined.</returns>
public static string? GetAgentRequestMessageSourceId(this ChatMessage message)
{
if (message.AdditionalProperties?.TryGetValue(AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, out var attribution) is true
&& attribution is AgentRequestMessageSourceAttribution typedAttribution)
{
return typedAttribution.SourceId;
}
return null;
}
/// <summary>
/// Ensure that the provided message is tagged with the provided source type and source id in the context of a specific agent run.
/// </summary>
/// <param name="message">The message to tag.</param>
/// <param name="sourceType">The source type to tag the message with.</param>
/// <param name="sourceId">The source id to tag the message with.</param>
/// <returns>The tagged message.</returns>
/// <remarks>
/// If the message is already tagged with the provided source type and source id, it is returned as is.
/// Otherwise, a cloned message is returned with the appropriate tagging in the AdditionalProperties.
/// </remarks>
public static ChatMessage WithAgentRequestMessageSource(this ChatMessage message, AgentRequestMessageSourceType sourceType, string? sourceId = null)
{
if (message.AdditionalProperties != null
// Check if the message was already tagged with the required source type and source id
&& message.AdditionalProperties.TryGetValue(AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, out var messageSourceAttribution)
&& messageSourceAttribution is AgentRequestMessageSourceAttribution typedMessageSourceAttribution
&& typedMessageSourceAttribution.SourceType == sourceType
&& typedMessageSourceAttribution.SourceId == sourceId)
{
return message;
}
message = message.Clone();
message.AdditionalProperties ??= new();
message.AdditionalProperties[AgentRequestMessageSourceAttribution.AdditionalPropertiesKey] =
new AgentRequestMessageSourceAttribution(sourceType, sourceId);
return message;
}
}
@@ -0,0 +1,102 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>
/// Provides an abstract base class for AI agents that delegate operations to an inner agent
/// instance while allowing for extensibility and customization.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="DelegatingAIAgent"/> implements the decorator pattern for <see cref="AIAgent"/>s, enabling the creation of agent pipelines
/// where each layer can add functionality while delegating core operations to an underlying agent. This pattern is
/// fundamental to building composable agent architectures.
/// </para>
/// <para>
/// The default implementation provides transparent pass-through behavior, forwarding all operations to the inner agent.
/// Derived classes can override specific methods to add custom behavior while maintaining compatibility with the agent interface.
/// </para>
/// </remarks>
public abstract class DelegatingAIAgent : AIAgent
{
/// <summary>
/// Initializes a new instance of the <see cref="DelegatingAIAgent"/> class with the specified inner agent.
/// </summary>
/// <param name="innerAgent">The underlying agent instance that will handle the core operations.</param>
/// <exception cref="ArgumentNullException"><paramref name="innerAgent"/> is <see langword="null"/>.</exception>
/// <remarks>
/// The inner agent serves as the foundation of the delegation chain. All operations not overridden by
/// derived classes will be forwarded to this agent.
/// </remarks>
protected DelegatingAIAgent(AIAgent innerAgent)
{
this.InnerAgent = Throw.IfNull(innerAgent);
}
/// <summary>
/// Gets the inner agent instance that receives delegated operations.
/// </summary>
/// <value>
/// The underlying <see cref="AIAgent"/> instance that handles core agent operations.
/// </value>
/// <remarks>
/// Derived classes can use this property to access the inner agent for custom delegation scenarios
/// or to forward operations with additional processing.
/// </remarks>
protected AIAgent InnerAgent { get; }
/// <inheritdoc />
protected override string? IdCore => this.InnerAgent.Id;
/// <inheritdoc />
public override string? Name => this.InnerAgent.Name;
/// <inheritdoc />
public override string? Description => this.InnerAgent.Description;
/// <inheritdoc />
public override object? GetService(Type serviceType, object? serviceKey = null)
{
_ = Throw.IfNull(serviceType);
// If the key is non-null, we don't know what it means so pass through to the inner service.
return
serviceKey is null && serviceType.IsInstanceOfType(this) ? this :
this.InnerAgent.GetService(serviceType, serviceKey);
}
/// <inheritdoc />
protected override ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default) => this.InnerAgent.CreateSessionAsync(cancellationToken);
/// <inheritdoc />
protected override ValueTask<JsonElement> SerializeSessionCoreAsync(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> this.InnerAgent.SerializeSessionAsync(session, jsonSerializerOptions, cancellationToken);
/// <inheritdoc />
protected override ValueTask<AgentSession> DeserializeSessionCoreAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> this.InnerAgent.DeserializeSessionAsync(serializedState, jsonSerializerOptions, cancellationToken);
/// <inheritdoc />
protected override Task<AgentResponse> RunCoreAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
=> this.InnerAgent.RunAsync(messages, session, options, cancellationToken);
/// <inheritdoc />
protected override IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
=> this.InnerAgent.RunStreamingAsync(messages, session, options, cancellationToken);
}
@@ -0,0 +1,134 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>
/// Provides an in-memory implementation of <see cref="ChatHistoryProvider"/> with support for message reduction.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="InMemoryChatHistoryProvider"/> stores chat messages in the <see cref="AgentSession.StateBag"/>,
/// providing fast access and manipulation capabilities integrated with session state management.
/// </para>
/// <para>
/// This <see cref="ChatHistoryProvider"/> maintains all messages in memory. For long-running conversations or high-volume scenarios, consider using
/// message reduction strategies or alternative storage implementations.
/// </para>
/// </remarks>
public sealed class InMemoryChatHistoryProvider : ChatHistoryProvider
{
private readonly ProviderSessionState<State> _sessionState;
private IReadOnlyList<string>? _stateKeys;
/// <summary>
/// Initializes a new instance of the <see cref="InMemoryChatHistoryProvider"/> class.
/// </summary>
/// <param name="options">
/// Optional configuration options that control the provider's behavior, including state initialization,
/// message reduction, and serialization settings. If <see langword="null"/>, default settings will be used.
/// </param>
public InMemoryChatHistoryProvider(InMemoryChatHistoryProviderOptions? options = null)
: base(
options?.ProvideOutputMessageFilter,
options?.StorageInputRequestMessageFilter,
options?.StorageInputResponseMessageFilter)
{
this._sessionState = new ProviderSessionState<State>(
options?.StateInitializer ?? (_ => new State()),
options?.StateKey ?? this.GetType().Name,
options?.JsonSerializerOptions);
this.ChatReducer = options?.ChatReducer;
this.ReducerTriggerEvent = options?.ReducerTriggerEvent ?? InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.BeforeMessagesRetrieval;
}
/// <inheritdoc />
public override IReadOnlyList<string> StateKeys => this._stateKeys ??= [this._sessionState.StateKey];
/// <summary>
/// Gets the chat reducer used to process or reduce chat messages. If null, no reduction logic will be applied.
/// </summary>
public IChatReducer? ChatReducer { get; }
/// <summary>
/// Gets the event that triggers the reducer invocation in this provider.
/// </summary>
public InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent ReducerTriggerEvent { get; }
/// <summary>
/// Gets the chat messages stored for the specified session.
/// </summary>
/// <param name="session">The agent session containing the state.</param>
/// <returns>A list of chat messages, or an empty list if no state is found.</returns>
public List<ChatMessage> GetMessages(AgentSession? session)
=> this._sessionState.GetOrInitializeState(session).Messages;
/// <summary>
/// Sets the chat messages for the specified session.
/// </summary>
/// <param name="session">The agent session containing the state.</param>
/// <param name="messages">The messages to store.</param>
/// <exception cref="ArgumentNullException"><paramref name="messages"/> is <see langword="null"/>.</exception>
public void SetMessages(AgentSession? session, List<ChatMessage> messages)
{
Throw.IfNull(messages);
State state = this._sessionState.GetOrInitializeState(session);
state.Messages = messages;
}
/// <inheritdoc />
protected override async ValueTask<IEnumerable<ChatMessage>> ProvideChatHistoryAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
State state = this._sessionState.GetOrInitializeState(context.Session);
if (this.ReducerTriggerEvent is InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.BeforeMessagesRetrieval && this.ChatReducer is not null)
{
// Apply pre-retrieval reduction if configured
await ReduceMessagesAsync(this.ChatReducer, state, cancellationToken).ConfigureAwait(false);
}
return state.Messages;
}
/// <inheritdoc />
protected override async ValueTask StoreChatHistoryAsync(InvokedContext context, CancellationToken cancellationToken = default)
{
State state = this._sessionState.GetOrInitializeState(context.Session);
// Add request and response messages to the provider
var allNewMessages = (context.RequestMessages ?? []).Concat(context.ResponseMessages ?? []);
state.Messages.AddRange(allNewMessages);
if (this.ReducerTriggerEvent is InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.AfterMessageAdded && this.ChatReducer is not null)
{
// Apply pre-write reduction strategy if configured
await ReduceMessagesAsync(this.ChatReducer, state, cancellationToken).ConfigureAwait(false);
}
}
private static async Task ReduceMessagesAsync(IChatReducer reducer, State state, CancellationToken cancellationToken = default)
{
state.Messages = [.. await reducer.ReduceAsync(state.Messages, cancellationToken).ConfigureAwait(false)];
}
/// <summary>
/// Represents the state of a <see cref="InMemoryChatHistoryProvider"/> stored in the <see cref="AgentSession.StateBag"/>.
/// </summary>
public sealed class State
{
/// <summary>
/// Gets or sets the list of chat messages.
/// </summary>
[JsonPropertyName("messages")]
public List<ChatMessage> Messages { get; set; } = [];
}
}
@@ -0,0 +1,105 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Text.Json;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI;
/// <summary>
/// Represents configuration options for <see cref="InMemoryChatHistoryProvider"/>.
/// </summary>
public sealed class InMemoryChatHistoryProviderOptions
{
/// <summary>
/// Gets or sets an optional delegate that initializes the provider state on the first invocation.
/// If <see langword="null"/>, a default initializer that creates an empty state will be used.
/// </summary>
public Func<AgentSession?, InMemoryChatHistoryProvider.State>? StateInitializer { get; set; }
/// <summary>
/// Gets or sets an optional <see cref="IChatReducer"/> instance used to process, reduce, or optimize chat messages.
/// This can be used to implement strategies like message summarization, truncation, or cleanup.
/// </summary>
public IChatReducer? ChatReducer { get; set; }
/// <summary>
/// Gets or sets when the message reducer should be invoked.
/// The default is <see cref="ChatReducerTriggerEvent.BeforeMessagesRetrieval"/>,
/// which applies reduction logic when messages are retrieved for agent consumption.
/// </summary>
/// <remarks>
/// Message reducers enable automatic management of message storage by implementing strategies to
/// keep memory usage under control while preserving important conversation context.
/// </remarks>
public ChatReducerTriggerEvent ReducerTriggerEvent { get; set; } = ChatReducerTriggerEvent.BeforeMessagesRetrieval;
/// <summary>
/// Gets or sets an optional key to use for storing the state in the <see cref="AgentSession.StateBag"/>.
/// If <see langword="null"/>, a default key will be used.
/// </summary>
public string? StateKey { get; set; }
/// <summary>
/// Gets or sets optional JSON serializer options for serializing the state of this provider.
/// This is valuable for cases like when the chat history contains custom <see cref="AIContent"/> types
/// and source generated serializers are required, or Native AOT / Trimming is required.
/// </summary>
public JsonSerializerOptions? JsonSerializerOptions { get; set; }
/// <summary>
/// Gets or sets an optional filter function applied to request messages before they are added to storage
/// during <see cref="ChatHistoryProvider.InvokedAsync"/>.
/// </summary>
/// <value>
/// When <see langword="null"/>, the provider defaults to excluding messages with
/// <see cref="AgentRequestMessageSourceType.ChatHistory"/> source type to avoid
/// storing messages that came from chat history in the first place.
/// Depending on your requirements, you could provide a different filter, that also excludes
/// messages from e.g. AI context providers.
/// </value>
public Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? StorageInputRequestMessageFilter { get; set; }
/// <summary>
/// Gets or sets an optional filter function applied to response messages before they are added to storage
/// during <see cref="ChatHistoryProvider.InvokedAsync"/>.
/// </summary>
/// <value>
/// When <see langword="null"/>, no filtering is applied to response messages before they are stored.
/// If you want to avoid persisting certain messages (for example, those with
/// <see cref="AgentRequestMessageSourceType.ChatHistory"/> source type or produced by AI context providers),
/// provide a filter that returns only the messages you want to keep.
/// </value>
public Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? StorageInputResponseMessageFilter { get; set; }
/// <summary>
/// Gets or sets an optional filter function applied to messages produced by this provider
/// during <see cref="ChatHistoryProvider.InvokingAsync"/>.
/// </summary>
/// <remarks>
/// This filter is only applied to the messages that the provider itself produces (from its internal storage).
/// </remarks>
/// <value>
/// When <see langword="null"/>, no filtering is applied to the output messages.
/// </value>
public Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? ProvideOutputMessageFilter { get; set; }
/// <summary>
/// Defines the events that can trigger a reducer in the <see cref="InMemoryChatHistoryProvider"/>.
/// </summary>
public enum ChatReducerTriggerEvent
{
/// <summary>
/// Trigger the reducer when a new message is added.
/// <see cref="AIContextProvider.InvokedAsync"/> will only complete when reducer processing is done.
/// </summary>
AfterMessageAdded,
/// <summary>
/// Trigger the reducer before messages are retrieved from the provider.
/// The reducer will process the messages before they are returned to the caller.
/// </summary>
BeforeMessagesRetrieval
}
}
@@ -0,0 +1,212 @@
// 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;
/// <summary>
/// Provides an abstract base class for components that enhance AI context during agent invocations by supplying additional chat messages.
/// </summary>
/// <remarks>
/// <para>
/// A message AI context provider is a component that participates in the agent invocation lifecycle by:
/// <list type="bullet">
/// <item><description>Listening to changes in conversations</description></item>
/// <item><description>Providing additional messages to agents during invocation</description></item>
/// <item><description>Processing invocation results for state management or learning</description></item>
/// </list>
/// </para>
/// <para>
/// Context providers operate through a two-phase lifecycle: they are called at the start of invocation via
/// <see cref="AIContextProvider.InvokingAsync"/> to provide context, and optionally called at the end of invocation via
/// <see cref="AIContextProvider.InvokedAsync"/> to process results.
/// </para>
/// </remarks>
public abstract class MessageAIContextProvider : AIContextProvider
{
/// <summary>
/// Initializes a new instance of the <see cref="MessageAIContextProvider"/> class.
/// </summary>
/// <param name="provideInputMessageFilter">An optional filter function to apply to input messages before providing messages via <see cref="ProvideMessagesAsync"/>. If not set, defaults to including only <see cref="AgentRequestMessageSourceType.External"/> messages.</param>
/// <param name="storeInputRequestMessageFilter">An optional filter function to apply to request messages before storing messages via <see cref="AIContextProvider.StoreAIContextAsync"/>. If not set, defaults to including only <see cref="AgentRequestMessageSourceType.External"/> messages.</param>
/// <param name="storeInputResponseMessageFilter">An optional filter function to apply to response messages before storing messages via <see cref="AIContextProvider.StoreAIContextAsync"/>. If not set, defaults to including all response messages (no filtering).</param>
protected MessageAIContextProvider(
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? provideInputMessageFilter = null,
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? storeInputRequestMessageFilter = null,
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? storeInputResponseMessageFilter = null)
: base(provideInputMessageFilter, storeInputRequestMessageFilter, storeInputResponseMessageFilter)
{
}
/// <inheritdoc/>
protected override async ValueTask<AIContext> ProvideAIContextAsync(AIContextProvider.InvokingContext context, CancellationToken cancellationToken = default)
{
// Call ProvideMessagesAsync directly to return only additional messages.
// The base AIContextProvider.InvokingCoreAsync handles merging with the original input and stamping.
#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.
return new AIContext
{
Messages = await this.ProvideMessagesAsync(
new InvokingContext(context.Agent, context.Session, context.AIContext.Messages ?? []),
cancellationToken).ConfigureAwait(false)
};
#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.
}
/// <summary>
/// Called at the start of agent invocation to provide additional messages.
/// </summary>
/// <param name="context">Contains the request context including the caller provided messages that will be used by the agent for this invocation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the <see cref="IEnumerable{ChatMessage}"/> to be used by the agent during this invocation.</returns>
/// <remarks>
/// <para>
/// Implementers can load any additional messages required at this time, such as:
/// <list type="bullet">
/// <item><description>Retrieving relevant information from knowledge bases</description></item>
/// <item><description>Adding system instructions or prompts</description></item>
/// <item><description>Injecting contextual messages from conversation history</description></item>
/// </list>
/// </para>
/// </remarks>
public ValueTask<IEnumerable<ChatMessage>> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
=> this.InvokingCoreAsync(Throw.IfNull(context), cancellationToken);
/// <summary>
/// Called at the start of agent invocation to provide additional messages.
/// </summary>
/// <param name="context">Contains the request context including the caller provided messages that will be used by the agent for this invocation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the <see cref="IEnumerable{ChatMessage}"/> to be used by the agent during this invocation.</returns>
/// <remarks>
/// <para>
/// Implementers can load any additional messages required at this time, such as:
/// <list type="bullet">
/// <item><description>Retrieving relevant information from knowledge bases</description></item>
/// <item><description>Adding system instructions or prompts</description></item>
/// <item><description>Injecting contextual messages from conversation history</description></item>
/// </list>
/// </para>
/// <para>
/// The default implementation of this method filters the input messages using the configured provide-input message filter
/// (which defaults to including only <see cref="AgentRequestMessageSourceType.External"/> messages),
/// then calls <see cref="ProvideMessagesAsync"/> to get additional messages,
/// stamps any messages with <see cref="AgentRequestMessageSourceType.AIContextProvider"/> source attribution,
/// and merges the returned messages with the original (unfiltered) input messages.
/// For most scenarios, overriding <see cref="ProvideMessagesAsync"/> is sufficient to provide additional messages,
/// while still benefiting from the default filtering, merging and source stamping 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 <see cref="IEnumerable{ChatMessage}"/> returned for the invocation.
/// </para>
/// </remarks>
protected virtual async ValueTask<IEnumerable<ChatMessage>> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
var inputMessages = context.RequestMessages;
// Create a filtered context for ProvideMessagesAsync, filtering input messages
// to exclude non-external messages (e.g. chat history, other AI context provider messages).
#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 filteredContext = new InvokingContext(
context.Agent,
context.Session,
this.ProvideInputMessageFilter(inputMessages));
#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.
var providedMessages = await this.ProvideMessagesAsync(filteredContext, cancellationToken).ConfigureAwait(false);
// Stamp and merge provided messages.
providedMessages = providedMessages.Select(m => m.WithAgentRequestMessageSource(AgentRequestMessageSourceType.AIContextProvider, this.GetType().FullName!));
return inputMessages.Concat(providedMessages);
}
/// <summary>
/// When overridden in a derived class, provides additional messages to be merged with the input messages for the current invocation.
/// </summary>
/// <remarks>
/// <para>
/// This method is called from <see cref="InvokingCoreAsync(InvokingContext, CancellationToken)"/>.
/// Note that <see cref="InvokingCoreAsync(InvokingContext, CancellationToken)"/> can be overridden to directly control messages merging and source stamping, in which case
/// it is up to the implementer to call this method as needed to retrieve the additional messages.
/// </para>
/// <para>
/// In contrast with <see cref="InvokingCoreAsync(InvokingContext, CancellationToken)"/>, this method only returns additional messages to be merged with the input,
/// while <see cref="InvokingCoreAsync(InvokingContext, CancellationToken)"/> is responsible for returning the full merged <see cref="IEnumerable{ChatMessage}"/> for the invocation.
/// </para>
/// </remarks>
/// <param name="context">Contains the request context including the caller provided messages that will be used by the agent for this invocation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>
/// A task that represents the asynchronous operation. The task result contains an <see cref="IEnumerable{ChatMessage}"/>
/// with additional messages to be merged with the input messages.
/// </returns>
protected virtual ValueTask<IEnumerable<ChatMessage>> ProvideMessagesAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
return new ValueTask<IEnumerable<ChatMessage>>([]);
}
/// <summary>
/// Contains the context information provided to <see cref="InvokingCoreAsync(InvokingContext, CancellationToken)"/>.
/// </summary>
/// <remarks>
/// This class provides context about the invocation before the underlying AI model is invoked, including the messages
/// that will be used. Message AI Context providers can use this information to determine what additional messages
/// should be provided for the invocation.
/// </remarks>
public new sealed class InvokingContext
{
/// <summary>
/// Initializes a new instance of the <see cref="InvokingContext"/> class with the specified request messages.
/// </summary>
/// <param name="agent">The agent being invoked.</param>
/// <param name="session">The session associated with the agent invocation.</param>
/// <param name="requestMessages">The messages to be used by the agent for this invocation.</param>
/// <exception cref="ArgumentNullException"><paramref name="agent"/> or <paramref name="requestMessages"/> is <see langword="null"/>.</exception>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public InvokingContext(
AIAgent agent,
AgentSession? session,
IEnumerable<ChatMessage> requestMessages)
{
this.Agent = Throw.IfNull(agent);
this.Session = session;
this.RequestMessages = Throw.IfNull(requestMessages);
}
/// <summary>
/// Gets the agent that is being invoked.
/// </summary>
public AIAgent Agent { get; }
/// <summary>
/// Gets the agent session associated with the agent invocation.
/// </summary>
public AgentSession? Session { get; }
/// <summary>
/// Gets the messages that will be used by the agent for this invocation. <see cref="MessageAIContextProvider"/> instances can modify
/// and return or return a new message list to add additional messages for the invocation.
/// </summary>
/// <value>
/// A collection of <see cref="ChatMessage"/> instances representing the messages that will be used by the agent for this invocation.
/// </value>
/// <remarks>
/// <para>
/// If multiple <see cref="MessageAIContextProvider"/> instances are used in the same invocation, each <see cref="MessageAIContextProvider"/>
/// will receive the messages returned by the previous <see cref="MessageAIContextProvider"/> allowing them to build on top of each other's context.
/// </para>
/// <para>
/// The first <see cref="MessageAIContextProvider"/> in the invocation pipeline will receive the
/// caller provided messages.
/// </para>
/// </remarks>
public IEnumerable<ChatMessage> RequestMessages { get; set { field = Throw.IfNull(value); } }
}
}
@@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>Microsoft.Agents.AI</RootNamespace>
<NoWarn>$(NoWarn);MEAI001</NoWarn>
<IsReleased>true</IsReleased>
</PropertyGroup>
<PropertyGroup>
<InjectSharedThrow>true</InjectSharedThrow>
<InjectSharedStructuredOutput>true</InjectSharedStructuredOutput>
<InjectDiagnosticClassesOnLegacy>true</InjectDiagnosticClassesOnLegacy>
<InjectTrimAttributesOnLegacy>true</InjectTrimAttributesOnLegacy>
<InjectIsExternalInitOnLegacy>true</InjectIsExternalInitOnLegacy>
<InjectRequiredMemberOnLegacy>true</InjectRequiredMemberOnLegacy>
<InjectCompilerFeatureRequiredOnLegacy>true</InjectCompilerFeatureRequiredOnLegacy>
<InjectExperimentalAttributeOnLegacy>true</InjectExperimentalAttributeOnLegacy>
<InjectSharedDiagnosticIds>true</InjectSharedDiagnosticIds>
</PropertyGroup>
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
<PropertyGroup>
<!-- NuGet Package Settings -->
<Title>Microsoft Agent Framework Abstractions</Title>
<Description>Provides Microsoft Agent Framework interfaces and abstractions.</Description>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="Microsoft.Agents.AI.Abstractions.UnitTests" />
</ItemGroup>
</Project>
@@ -0,0 +1,87 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Text.Json;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>
/// Provides strongly-typed state management for providers, enabling reading and writing of provider-specific state
/// to and from an <see cref="AgentSession"/>'s <see cref="AgentSessionStateBag"/>.
/// </summary>
/// <typeparam name="TState">The type of the state to be maintained. Must be a reference type.</typeparam>
/// <remarks>
/// <para>
/// This class encapsulates the logic for initializing, retrieving, and persisting provider state in the session's StateBag
/// using a configurable key and JSON serialization options. It is intended to be used as a composed field within provider
/// implementations (e.g., <see cref="AIContextProvider"/> or <see cref="ChatHistoryProvider"/> subclasses) to avoid
/// duplicating state management logic across provider type hierarchies.
/// </para>
/// <para>
/// State is stored in the <see cref="AgentSession.StateBag"/> using the <see cref="StateKey"/> property as the key,
/// enabling multiple providers to maintain independent state within the same session.
/// </para>
/// </remarks>
public class ProviderSessionState<TState>
where TState : class
{
private readonly Func<AgentSession?, TState> _stateInitializer;
private readonly JsonSerializerOptions _jsonSerializerOptions;
/// <summary>
/// Initializes a new instance of the <see cref="ProviderSessionState{TState}"/> class.
/// </summary>
/// <param name="stateInitializer">A function to initialize the state when it is not yet present in the session's StateBag.</param>
/// <param name="stateKey">The key used to store the state in the session's StateBag.</param>
/// <param name="jsonSerializerOptions">Options for JSON serialization and deserialization of the state.</param>
public ProviderSessionState(
Func<AgentSession?, TState> stateInitializer,
string stateKey,
JsonSerializerOptions? jsonSerializerOptions = null)
{
this._stateInitializer = Throw.IfNull(stateInitializer);
this.StateKey = Throw.IfNullOrWhitespace(stateKey);
this._jsonSerializerOptions = jsonSerializerOptions ?? AgentAbstractionsJsonUtilities.DefaultOptions;
}
/// <summary>
/// Gets the key used to store the provider state in the <see cref="AgentSession.StateBag"/>.
/// </summary>
public string StateKey { get; }
/// <summary>
/// Gets the state from the session's StateBag, or initializes it using the state initializer if not present.
/// </summary>
/// <param name="session">The agent session containing the StateBag.</param>
/// <returns>The provider state.</returns>
public TState GetOrInitializeState(AgentSession? session)
{
if (session?.StateBag.TryGetValue<TState>(this.StateKey, out var state, this._jsonSerializerOptions) is true && state is not null)
{
return state;
}
state = this._stateInitializer(session);
if (session is not null)
{
session.StateBag.SetValue(this.StateKey, state, this._jsonSerializerOptions);
}
return state;
}
/// <summary>
/// Saves the specified state to the session's StateBag using the configured state key and JSON serializer options.
/// If the session is null, this method does nothing.
/// </summary>
/// <param name="session">The agent session containing the StateBag.</param>
/// <param name="state">The state to be saved.</param>
public void SaveState(AgentSession? session, TState state)
{
if (session is not null)
{
session.StateBag.SetValue(this.StateKey, state, this._jsonSerializerOptions);
}
}
}