Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ protected override async Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessag
List<ChatMessage> responseMessages = CloneAndToUpperCase(messages, this.Name).ToList();

// Notify the session of the input and output messages.
var invokedContext = new ChatHistoryProvider.InvokedContext(messages, storeMessages)
var invokedContext = new ChatHistoryProvider.InvokedContext(messages)
{
ResponseMessages = responseMessages
};
Expand Down Expand Up @@ -84,7 +84,7 @@ protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingA
List<ChatMessage> responseMessages = CloneAndToUpperCase(messages, this.Name).ToList();

// Notify the session of the input and output messages.
var invokedContext = new ChatHistoryProvider.InvokedContext(messages, storeMessages)
var invokedContext = new ChatHistoryProvider.InvokedContext(messages)
{
ResponseMessages = responseMessages
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ public UserInfoMemory(IChatClient chatClient, JsonElement serializedState, JsonS

public UserInfo UserInfo { get; set; }

public override async ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
protected override async ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default)
{
// Try and extract the user name and age from the message if we don't have it already and it's a user message.
if ((this.UserInfo.UserName is null || this.UserInfo.UserAge is null) && context.RequestMessages.Any(x => x.Role == ChatRole.User))
Expand All @@ -122,7 +122,7 @@ public override async ValueTask InvokedAsync(InvokedContext context, Cancellatio
}
}

public override ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
protected override ValueTask<AIContext> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
StringBuilder instructions = new();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public VectorChatHistoryProvider(VectorStore vectorStore, JsonElement serialized

public string? SessionDbKey { get; private set; }

public override async ValueTask<IEnumerable<ChatMessage>> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
protected override async ValueTask<IEnumerable<ChatMessage>> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
var collection = this._vectorStore.GetCollection<string, ChatHistoryItem>("ChatHistory");
await collection.EnsureCollectionExistsAsync(cancellationToken);
Expand All @@ -105,7 +105,7 @@ public override async ValueTask<IEnumerable<ChatMessage>> InvokingAsync(Invoking
return messages;
}

public override async ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
protected override async ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default)
{
// Don't store messages if the request failed.
if (context.InvokeException is not null)
Expand All @@ -120,7 +120,7 @@ public override async ValueTask InvokedAsync(InvokedContext context, Cancellatio

// Add both request and response messages to the store
// Optionally messages produced by the AIContextProvider can also be persisted (not shown).
var allNewMessages = context.RequestMessages.Concat(context.AIContextProviderMessages ?? []).Concat(context.ResponseMessages ?? []);
var allNewMessages = context.RequestMessages.Concat(context.ResponseMessages ?? []);

await collection.UpsertAsync(allNewMessages.Select(x => new ChatHistoryItem()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public TodoListAIContextProvider(JsonElement jsonElement, JsonSerializerOptions?
}
}

public override ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
protected override ValueTask<AIContext> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
StringBuilder outputMessageBuilder = new();
outputMessageBuilder.AppendLine("Your todo list contains the following items:");
Expand Down Expand Up @@ -132,7 +132,7 @@ public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptio
/// </summary>
internal sealed class CalendarSearchAIContextProvider(Func<Task<string[]>> loadNextThreeCalendarEvents) : AIContextProvider
{
public override async ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
protected override async ValueTask<AIContext> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
var events = await loadNextThreeCalendarEvents();

Expand Down Expand Up @@ -179,7 +179,7 @@ public AggregatingAIContextProvider(ProviderFactory[] providerFactories, JsonEle
.ToList();
}

public override async ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
protected override async ValueTask<AIContext> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
// Invoke all the sub providers.
var tasks = this._providers.Select(provider => provider.InvokingAsync(context, cancellationToken).AsTask());
Expand Down
90 changes: 74 additions & 16 deletions dotnet/src/Microsoft.Agents.AI.Abstractions/AIContextProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -48,7 +49,51 @@ public abstract class AIContextProvider
/// </list>
/// </para>
/// </remarks>
public abstract ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default);
public async ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
var aiContext = await this.InvokingCoreAsync(context, cancellationToken).ConfigureAwait(false);
if (aiContext.Messages is null)
{
return aiContext;
}

aiContext.Messages = aiContext.Messages.Select(message =>
{
if (message.AdditionalProperties != null
&& message.AdditionalProperties.TryGetValue(AgentRequestMessageSource.AdditionalPropertiesKey, out var source)
&& source is AgentRequestMessageSource typedSource
&& typedSource == AgentRequestMessageSource.AIContextProvider)
{
return message;
}

message = message.Clone();
message.AdditionalProperties ??= new();
message.AdditionalProperties[AgentRequestMessageSource.AdditionalPropertiesKey] = AgentRequestMessageSource.AIContextProvider;
return message;
}).ToList();

return aiContext;
}

/// <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>
/// </remarks>
protected abstract ValueTask<AIContext> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default);
Comment on lines +52 to +96
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing InvokingAsync from an abstract method to a non-virtual wrapper over the new protected InvokingCoreAsync is an API surface change that breaks existing derived implementations (both at compile-time and potentially at runtime for pre-compiled consumers). This likely needs to be treated and documented as a breaking change (including updating release notes / PR title), and guidance should be provided to external implementers on how to migrate their overrides to InvokingCoreAsync.

Copilot uses AI. Check for mistakes.

/// <summary>
/// Called at the end of the agent invocation to process the invocation results.
Expand All @@ -71,7 +116,31 @@ public abstract class AIContextProvider
/// To check if the invocation was successful, inspect the <see cref="InvokedContext.InvokeException"/> property.
/// </para>
/// </remarks>
public virtual ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
public ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
=> this.InvokedCoreAsync(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>
/// </remarks>
protected virtual ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default)
=> default;

/// <summary>
Expand Down Expand Up @@ -117,7 +186,7 @@ public virtual JsonElement Serialize(JsonSerializerOptions? jsonSerializerOption
=> this.GetService(typeof(TService), serviceKey) is TService service ? service : default;

/// <summary>
/// Contains the context information provided to <see cref="InvokingAsync(InvokingContext, CancellationToken)"/>.
/// 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
Expand Down Expand Up @@ -146,7 +215,7 @@ public InvokingContext(IEnumerable<ChatMessage> requestMessages)
}

/// <summary>
/// Contains the context information provided to <see cref="InvokedAsync(InvokedContext, CancellationToken)"/>.
/// Contains the context information provided to <see cref="InvokedCoreAsync(InvokedContext, CancellationToken)"/>.
/// </summary>
/// <remarks>
/// This class provides context about a completed agent invocation, including both the
Expand All @@ -159,12 +228,10 @@ public sealed class InvokedContext
/// Initializes a new instance of the <see cref="InvokedContext"/> class with the specified request messages.
/// </summary>
/// <param name="requestMessages">The caller provided messages that were used by the agent for this invocation.</param>
/// <param name="aiContextProviderMessages">The messages provided by the <see cref="AIContextProvider"/> for this invocation, if any.</param>
/// <exception cref="ArgumentNullException"><paramref name="requestMessages"/> is <see langword="null"/>.</exception>
public InvokedContext(IEnumerable<ChatMessage> requestMessages, IEnumerable<ChatMessage>? aiContextProviderMessages)
public InvokedContext(IEnumerable<ChatMessage> requestMessages)
{
this.RequestMessages = requestMessages ?? throw new ArgumentNullException(nameof(requestMessages));
this.AIContextProviderMessages = aiContextProviderMessages;
}

/// <summary>
Expand All @@ -176,15 +243,6 @@ public InvokedContext(IEnumerable<ChatMessage> requestMessages, IEnumerable<Chat
/// </value>
public IEnumerable<ChatMessage> RequestMessages { get; set { field = Throw.IfNull(value); } }

/// <summary>
/// Gets the messages provided by the <see cref="AIContextProvider"/> for this invocation, if any.
/// </summary>
/// <value>
/// A collection of <see cref="ChatMessage"/> instances that were provided by the <see cref="AIContextProvider"/>,
/// and were used by the agent as part of the invocation.
/// </value>
public IEnumerable<ChatMessage>? AIContextProviderMessages { get; set; }

/// <summary>
/// Gets the collection of response messages generated during this invocation if the invocation succeeded.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using Microsoft.Extensions.AI;
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The using Microsoft.Extensions.AI; directive appears to be unused in this file and may trigger build warnings; consider removing it to keep the file warning-free and consistent with the rest of the codebase.

Suggested change
using Microsoft.Extensions.AI;

Copilot uses AI. Check for mistakes.
using Microsoft.Shared.Diagnostics;

namespace Microsoft.Agents.AI;

/// <summary>
/// An enumeration representing the source of an agent request message.
/// </summary>
/// <remarks>
/// Input messages for a specific agent run can originate from various sources.
/// This enumeration helps to identify whether a message came from outside the agent pipeline,
Comment on lines +10 to +14
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The summary currently describes AgentRequestMessageSource as an "enumeration", but this type is implemented as a sealed class; consider updating the wording to avoid confusion for consumers and keep documentation aligned with the implementation.

Suggested change
/// An enumeration representing the source of an agent request message.
/// </summary>
/// <remarks>
/// Input messages for a specific agent run can originate from various sources.
/// This enumeration helps to identify whether a message came from outside the agent pipeline,
/// 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,

Copilot uses AI. Check for mistakes.
/// whether it was produced by middleware, or came from chat history.
/// </remarks>
public sealed class AgentRequestMessageSource : IEquatable<AgentRequestMessageSource>
{
/// <summary>
/// Provides the key used in <see cref="ChatMessage.AdditionalProperties"/> to store the source of the agent request message.
/// </summary>
public static readonly string AdditionalPropertiesKey = "Agent.RequestMessageSource";

/// <summary>
/// Initializes a new instance of the <see cref="AgentRequestMessageSource"/> class.
/// </summary>
/// <param name="value">The string value representing the source of the agent request message.</param>
public AgentRequestMessageSource(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; }

/// <summary>
/// The message came from outside the agent pipeline (e.g., user input).
/// </summary>
public static AgentRequestMessageSource External { get; } = new AgentRequestMessageSource(nameof(External));

/// <summary>
/// The message was produced by middleware.
/// </summary>
public static AgentRequestMessageSource AIContextProvider { get; } = new AgentRequestMessageSource(nameof(AIContextProvider));

/// <summary>
/// The message came from chat history.
/// </summary>
public static AgentRequestMessageSource ChatHistory { get; } = new AgentRequestMessageSource(nameof(ChatHistory));

/// <summary>
/// Determines whether this instance and another specified <see cref="AgentRequestMessageSource"/> object have the same value.
/// </summary>
/// <param name="other">The <see cref="AgentRequestMessageSource"/> 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(AgentRequestMessageSource? other)
{
if (other is null)
{
return false;
}

if (ReferenceEquals(this, other))
{
return true;
}

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="AgentRequestMessageSource"/> and its value is the same as this instance; otherwise, <see langword="false"/>.</returns>
public override bool Equals(object? obj) => this.Equals(obj as AgentRequestMessageSource);

/// <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="AgentRequestMessageSource"/> objects have the same value.
/// </summary>
/// <param name="left">The first <see cref="AgentRequestMessageSource"/> to compare.</param>
/// <param name="right">The second <see cref="AgentRequestMessageSource"/> 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 ==(AgentRequestMessageSource? left, AgentRequestMessageSource? right)
{
if (left is null)
{
return right is null;
}

return left.Equals(right);
}

/// <summary>
/// Determines whether two specified <see cref="AgentRequestMessageSource"/> objects have different values.
/// </summary>
/// <param name="left">The first <see cref="AgentRequestMessageSource"/> to compare.</param>
/// <param name="right">The second <see cref="AgentRequestMessageSource"/> 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 !=(AgentRequestMessageSource? left, AgentRequestMessageSource? right) => !(left == right);
}
Loading
Loading