mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: [BREAKING] Rename from ServiceStoredSimulatingChatClient to PerServiceCallChatHistoryPersistingChatClient (#4993)
* Rename from ServiceStoredSimulatingChatClient to PerServiceCallChatHistoryPersistingChatClient * Address PR comment
This commit is contained in:
@@ -139,8 +139,8 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
|
||||
this._logger = (loggerFactory ?? chatClient.GetService<ILoggerFactory>() ?? NullLoggerFactory.Instance).CreateLogger<ChatClientAgent>();
|
||||
|
||||
// Warn if using a custom chat client stack with simulated service stored persistence but no ServiceStoredSimulatingChatClient.
|
||||
this.WarnOnMissingServiceStoredSimulatingClient();
|
||||
// Warn if using a custom chat client stack with simulated service stored persistence but no PerServiceCallChatHistoryPersistingChatClient.
|
||||
this.WarnOnMissingPerServiceCallChatHistoryPersistingChatClient();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -454,7 +454,7 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
/// Notifies the <see cref="ChatHistoryProvider"/> and all <see cref="AIContextProviders"/> of successfully completed messages.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is also called by <see cref="ServiceStoredSimulatingChatClient"/> to persist messages per-service-call.
|
||||
/// This method is also called by <see cref="PerServiceCallChatHistoryPersistingChatClient"/> to persist messages per-service-call.
|
||||
/// </remarks>
|
||||
internal async Task NotifyProvidersOfNewMessagesAsync(
|
||||
ChatClientAgentSession session,
|
||||
@@ -486,7 +486,7 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
/// Notifies the <see cref="ChatHistoryProvider"/> and all <see cref="AIContextProviders"/> of a failure during a service call.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is also called by <see cref="ServiceStoredSimulatingChatClient"/> to report failures per-service-call.
|
||||
/// This method is also called by <see cref="PerServiceCallChatHistoryPersistingChatClient"/> to report failures per-service-call.
|
||||
/// </remarks>
|
||||
internal async Task NotifyProvidersOfFailureAsync(
|
||||
ChatClientAgentSession session,
|
||||
@@ -701,7 +701,7 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
throw new InvalidOperationException("A session must be provided when continuing a background response with a continuation token.");
|
||||
}
|
||||
|
||||
if ((continuationToken is not null || chatOptions?.AllowBackgroundResponses is true) && this.SimulatesServiceStoredChatHistory && this._logger.IsEnabled(LogLevel.Warning))
|
||||
if ((continuationToken is not null || chatOptions?.AllowBackgroundResponses is true) && this.RequiresPerServiceCallChatHistoryPersistence && this._logger.IsEnabled(LogLevel.Warning))
|
||||
{
|
||||
var warningAgentName = this.GetLoggingAgentName();
|
||||
this._logger.LogAgentChatClientBackgroundResponseFallback(this.Id, warningAgentName);
|
||||
@@ -740,10 +740,10 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
IEnumerable<ChatMessage> inputMessagesForChatClient = inputMessages;
|
||||
|
||||
// Populate the session messages only if we are not continuing an existing response as it's not allowed.
|
||||
// When SimulateServiceStoredChatHistory is active, the ServiceStoredSimulatingChatClient
|
||||
// When RequirePerServiceCallChatHistoryPersistence is active, the PerServiceCallChatHistoryPersistingChatClient
|
||||
// owns the chat history lifecycle — it loads history before each service call. The agent
|
||||
// must not load history itself, as that would result in duplicate messages.
|
||||
if (chatOptions?.ContinuationToken is null && !this.SimulatesServiceStoredChatHistory)
|
||||
if (chatOptions?.ContinuationToken is null && !this.RequiresPerServiceCallChatHistoryPersistence)
|
||||
{
|
||||
// Add any existing messages from the session to the messages to be sent to the chat client.
|
||||
// The ChatHistoryProvider returns the merged result (history + input messages).
|
||||
@@ -837,14 +837,14 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
/// Updates the session conversation ID at the end of an agent run.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When a <see cref="ServiceStoredSimulatingChatClient"/> handles per-service-call
|
||||
/// When a <see cref="PerServiceCallChatHistoryPersistingChatClient"/> handles per-service-call
|
||||
/// conversation ID updates, this end-of-run update is skipped. When the decorator is
|
||||
/// absent, the update is performed here. When <paramref name="forceUpdate"/> is <see langword="true"/>
|
||||
/// (continuation token scenarios), the update is always performed.
|
||||
/// </remarks>
|
||||
private void UpdateSessionConversationIdAtEndOfRun(ChatClientAgentSession session, string? responseConversationId, CancellationToken cancellationToken, bool forceUpdate = false)
|
||||
{
|
||||
if (!forceUpdate && this.SimulatesServiceStoredChatHistory)
|
||||
if (!forceUpdate && this.RequiresPerServiceCallChatHistoryPersistence)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -856,7 +856,7 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
/// Notifies providers of successfully completed messages at the end of an agent run.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When a <see cref="ServiceStoredSimulatingChatClient"/> handles per-service-call
|
||||
/// When a <see cref="PerServiceCallChatHistoryPersistingChatClient"/> handles per-service-call
|
||||
/// notification, this end-of-run notification is skipped. When no decorator is present,
|
||||
/// all messages are persisted.
|
||||
/// When <paramref name="forceNotify"/> is <see langword="true"/> (continuation token or
|
||||
@@ -871,7 +871,7 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
CancellationToken cancellationToken,
|
||||
bool forceNotify = false)
|
||||
{
|
||||
if (!forceNotify && this.SimulatesServiceStoredChatHistory)
|
||||
if (!forceNotify && this.RequiresPerServiceCallChatHistoryPersistence)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
@@ -883,7 +883,7 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
/// Notifies providers of a failure at the end of an agent run.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When a <see cref="ServiceStoredSimulatingChatClient"/> handles per-service-call
|
||||
/// When a <see cref="PerServiceCallChatHistoryPersistingChatClient"/> handles per-service-call
|
||||
/// notification (including failure), this end-of-run notification is skipped to avoid
|
||||
/// duplicate notification. In all other cases, failure is reported at the end of the run.
|
||||
/// </remarks>
|
||||
@@ -894,7 +894,7 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
ChatOptions? chatOptions,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (this.SimulatesServiceStoredChatHistory)
|
||||
if (this.RequiresPerServiceCallChatHistoryPersistence)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
@@ -905,14 +905,14 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the agent is configured to simulate service-stored chat history.
|
||||
/// When <see langword="true"/>, end-of-run persistence and history loading are skipped because a
|
||||
/// per-service-call decorator (such as <see cref="ServiceStoredSimulatingChatClient"/> or a
|
||||
/// per-service-call decorator (such as <see cref="PerServiceCallChatHistoryPersistingChatClient"/> or a
|
||||
/// user-supplied equivalent) is expected to handle the history lifecycle.
|
||||
/// </summary>
|
||||
private bool SimulatesServiceStoredChatHistory
|
||||
private bool RequiresPerServiceCallChatHistoryPersistence
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._agentOptions?.SimulateServiceStoredChatHistory is true;
|
||||
return this._agentOptions?.RequirePerServiceCallChatHistoryPersistence is true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -923,7 +923,7 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
/// The base class sets <see cref="AIAgent.CurrentRunContext"/> with the raw session parameter
|
||||
/// (which may be null) and restores it after each yield in streaming scenarios. After
|
||||
/// <see cref="PrepareSessionAndMessagesAsync"/> resolves or creates a session, we update the
|
||||
/// context so the <see cref="ServiceStoredSimulatingChatClient"/> decorator always has a valid session.
|
||||
/// context so the <see cref="PerServiceCallChatHistoryPersistingChatClient"/> decorator always has a valid session.
|
||||
/// The original agent from the context is preserved to maintain the top-of-stack agent in
|
||||
/// decorated agent scenarios.
|
||||
/// </remarks>
|
||||
@@ -939,19 +939,19 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
/// <summary>
|
||||
/// Checks for potential misconfiguration when using a custom chat client stack and logs warnings.
|
||||
/// </summary>
|
||||
private void WarnOnMissingServiceStoredSimulatingClient()
|
||||
private void WarnOnMissingPerServiceCallChatHistoryPersistingChatClient()
|
||||
{
|
||||
if (this._agentOptions?.UseProvidedChatClientAsIs is not true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._agentOptions?.SimulateServiceStoredChatHistory is not true)
|
||||
if (this._agentOptions?.RequirePerServiceCallChatHistoryPersistence is not true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var persistingClient = this.ChatClient.GetService<ServiceStoredSimulatingChatClient>();
|
||||
var persistingClient = this.ChatClient.GetService<PerServiceCallChatHistoryPersistingChatClient>();
|
||||
if (persistingClient is null && this._logger.IsEnabled(LogLevel.Warning))
|
||||
{
|
||||
var loggingAgentName = this.GetLoggingAgentName();
|
||||
@@ -998,7 +998,7 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is used by both the agent (during <see cref="PrepareSessionAndMessagesAsync"/>) and by
|
||||
/// <see cref="ServiceStoredSimulatingChatClient"/> to load history before each service call.
|
||||
/// <see cref="PerServiceCallChatHistoryPersistingChatClient"/> to load history before each service call.
|
||||
/// </remarks>
|
||||
internal async Task<IEnumerable<ChatMessage>> LoadChatHistoryAsync(
|
||||
ChatClientAgentSession session,
|
||||
|
||||
@@ -72,12 +72,12 @@ internal static partial class ChatClientAgentLogMessages
|
||||
|
||||
/// <summary>
|
||||
/// Logs a warning when <see cref="ChatClientAgentOptions.UseProvidedChatClientAsIs"/> is <see langword="true"/>
|
||||
/// and <see cref="ChatClientAgentOptions.SimulateServiceStoredChatHistory"/> is <see langword="true"/>,
|
||||
/// but no <see cref="ServiceStoredSimulatingChatClient"/> is found in the custom chat client stack.
|
||||
/// and <see cref="ChatClientAgentOptions.RequirePerServiceCallChatHistoryPersistence"/> is <see langword="true"/>,
|
||||
/// but no <see cref="PerServiceCallChatHistoryPersistingChatClient"/> is found in the custom chat client stack.
|
||||
/// </summary>
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Warning,
|
||||
Message = "Agent {AgentId}/{AgentName}: SimulateServiceStoredChatHistory is enabled with a custom chat client stack (UseProvidedChatClientAsIs), but no ServiceStoredSimulatingChatClient was found in the pipeline. Chat history will not be persisted by ChatClientAgent. Consider adding a ServiceStoredSimulatingChatClient to the pipeline using the UseServiceStoredChatHistorySimulation extension method if you have not added your own persistence mechanism.")]
|
||||
Message = "Agent {AgentId}/{AgentName}: RequirePerServiceCallChatHistoryPersistence is enabled with a custom chat client stack (UseProvidedChatClientAsIs), but no PerServiceCallChatHistoryPersistingChatClient was found in the pipeline. Chat history will not be persisted by ChatClientAgent. Consider adding a PerServiceCallChatHistoryPersistingChatClient to the pipeline using the UsePerServiceCallChatHistoryPersistence extension method if you have not added your own persistence mechanism.")]
|
||||
public static partial void LogAgentChatClientMissingPersistingClient(
|
||||
this ILogger logger,
|
||||
string agentId,
|
||||
@@ -92,7 +92,7 @@ internal static partial class ChatClientAgentLogMessages
|
||||
/// </summary>
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Warning,
|
||||
Message = "Agent {AgentId}/{AgentName}: SimulateServiceStoredChatHistory is enabled but we have to fall back to end-of-run persistence because the run involves background responses.")]
|
||||
Message = "Agent {AgentId}/{AgentName}: RequirePerServiceCallChatHistoryPersistence is enabled but we have to fall back to end-of-run persistence because the run involves background responses.")]
|
||||
public static partial void LogAgentChatClientBackgroundResponseFallback(
|
||||
this ILogger logger,
|
||||
string agentId,
|
||||
|
||||
@@ -92,38 +92,56 @@ public sealed class ChatClientAgentOptions
|
||||
public bool ThrowOnChatHistoryProviderConflict { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the <see cref="ChatClientAgent"/> should simulate
|
||||
/// service-stored chat history behavior using its configured <see cref="ChatHistoryProvider"/>.
|
||||
/// Gets or sets a value indicating whether the <see cref="ChatClientAgent"/> should persist
|
||||
/// chat history after each individual service call within the <see cref="FunctionInvokingChatClient"/>
|
||||
/// loop, rather than at the end of the full agent run.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// When set to <see langword="true"/>, a <see cref="ServiceStoredSimulatingChatClient"/> decorator is
|
||||
/// injected between the <see cref="FunctionInvokingChatClient"/> and the leaf <see cref="IChatClient"/>
|
||||
/// in the chat client pipeline. This decorator takes full ownership of the chat history lifecycle:
|
||||
/// it loads history from the <see cref="ChatHistoryProvider"/> before each service call and persists
|
||||
/// new messages after each service call. It also returns a sentinel <see cref="ChatOptions.ConversationId"/>
|
||||
/// on the response, causing the <see cref="FunctionInvokingChatClient"/> to treat the conversation
|
||||
/// as service-managed — clearing accumulated history and not injecting duplicate
|
||||
/// <see cref="FunctionCallContent"/> during approval-response processing.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This mode aligns the behavior of framework-managed chat history with service-stored chat history,
|
||||
/// ensuring consistency in how messages are stored and loaded, including during function calling loops
|
||||
/// and tool-call termination scenarios.
|
||||
/// When set to <see langword="true"/>, a <see cref="PerServiceCallChatHistoryPersistingChatClient"/>
|
||||
/// decorator becomes active in the chat client pipeline. It handles two complementary scenarios:
|
||||
/// </para>
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// <term>Framework-managed chat history</term>
|
||||
/// <description>
|
||||
/// The decorator loads history from the <see cref="ChatHistoryProvider"/> before each service call
|
||||
/// and persists new request and response messages after each call. It returns a sentinel
|
||||
/// <see cref="ChatOptions.ConversationId"/> on the response, causing the
|
||||
/// <see cref="FunctionInvokingChatClient"/> to treat the conversation as service-managed — clearing
|
||||
/// accumulated history between iterations and not injecting duplicate <see cref="FunctionCallContent"/>
|
||||
/// during approval-response processing.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term>AI Service-stored chat history</term>
|
||||
/// <description>
|
||||
/// When the service manages its own chat history (returning a real <see cref="ChatOptions.ConversationId"/>),
|
||||
/// the decorator updates <see cref="ChatClientAgentSession.ConversationId"/> after each service call so
|
||||
/// that intermediate ConversationId changes are captured immediately. For some services (e.g., the
|
||||
/// Conversations API with the Responses API), there is only one thread with one ID, so every service
|
||||
/// call updates it anyway and updating the <see cref="ChatClientAgentSession.ConversationId"/> has little effect
|
||||
/// since it's the same ID. For other services (e.g., Responses API with Response IDs), a new ID is generated
|
||||
/// with each service call, so updating the <see cref="ChatClientAgentSession.ConversationId"/> ensures that the
|
||||
/// latest ID is always captured, even mid-run.
|
||||
/// Enabling this option ensures consistent per-service-call behavior across all service types.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// When set to <see langword="false"/> (the default), the <see cref="ChatClientAgent"/> handles
|
||||
/// chat history persistence at the end of the full agent run via the <see cref="ChatHistoryProvider"/>
|
||||
/// pipeline.
|
||||
/// chat history persistence at the end of the full agent run via the <see cref="ChatHistoryProvider"/> if using
|
||||
/// framework-managed chat history. For AI service-stored chat history, the <see cref="ChatClientAgentSession.ConversationId"/>
|
||||
/// updates happen only at the end of the run.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// When setting the <see cref="UseProvidedChatClientAsIs"/> setting to <see langword="true"/> and
|
||||
/// <see cref="SimulateServiceStoredChatHistory"/> to <see langword="true"/>, ensure that your custom chat client stack includes a
|
||||
/// <see cref="ServiceStoredSimulatingChatClient"/> to enable per-service-call persistence.
|
||||
/// If no <see cref="ServiceStoredSimulatingChatClient"/> is provided, and you are not storing chat history via other means,
|
||||
/// <see cref="RequirePerServiceCallChatHistoryPersistence"/> to <see langword="true"/>, ensure that your custom chat client stack includes a
|
||||
/// <see cref="PerServiceCallChatHistoryPersistingChatClient"/> to enable per-service-call persistence.
|
||||
/// If no <see cref="PerServiceCallChatHistoryPersistingChatClient"/> is provided, and you are not storing chat history via other means,
|
||||
/// no chat history may be stored.
|
||||
/// When using a custom chat client stack, you can add a <see cref="ServiceStoredSimulatingChatClient"/>
|
||||
/// manually via the <see cref="ChatClientBuilderExtensions.UseServiceStoredChatHistorySimulation"/>
|
||||
/// When using a custom chat client stack, you can add a <see cref="PerServiceCallChatHistoryPersistingChatClient"/>
|
||||
/// manually via the <see cref="ChatClientBuilderExtensions.UsePerServiceCallChatHistoryPersistence"/>
|
||||
/// extension method.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
@@ -131,7 +149,7 @@ public sealed class ChatClientAgentOptions
|
||||
/// Default is <see langword="false"/>.
|
||||
/// </value>
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public bool SimulateServiceStoredChatHistory { get; set; }
|
||||
public bool RequirePerServiceCallChatHistoryPersistence { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="ChatClientAgentOptions"/> with the same values as this instance.
|
||||
@@ -149,6 +167,6 @@ public sealed class ChatClientAgentOptions
|
||||
ClearOnChatHistoryProviderConflict = this.ClearOnChatHistoryProviderConflict,
|
||||
WarnOnChatHistoryProviderConflict = this.WarnOnChatHistoryProviderConflict,
|
||||
ThrowOnChatHistoryProviderConflict = this.ThrowOnChatHistoryProviderConflict,
|
||||
SimulateServiceStoredChatHistory = this.SimulateServiceStoredChatHistory,
|
||||
RequirePerServiceCallChatHistoryPersistence = this.RequirePerServiceCallChatHistoryPersistence,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -86,21 +86,21 @@ public static class ChatClientBuilderExtensions
|
||||
services: services);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a <see cref="ServiceStoredSimulatingChatClient"/> to the chat client pipeline.
|
||||
/// Adds a <see cref="PerServiceCallChatHistoryPersistingChatClient"/> to the chat client pipeline.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This decorator should be positioned between the <see cref="FunctionInvokingChatClient"/> and the leaf
|
||||
/// <see cref="IChatClient"/> in the pipeline. It simulates service-stored chat history behavior by
|
||||
/// loading history before each service call, persisting after each call, and returning a sentinel
|
||||
/// <see cref="ChatOptions.ConversationId"/> on the response.
|
||||
/// <see cref="IChatClient"/> in the pipeline. It persists chat history after each individual service call
|
||||
/// and updates the session <see cref="ChatOptions.ConversationId"/> per call for both framework-managed
|
||||
/// and service-stored chat history scenarios.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This extension method is intended for use with custom chat client stacks when
|
||||
/// <see cref="ChatClientAgentOptions.UseProvidedChatClientAsIs"/> is <see langword="true"/>.
|
||||
/// When <see cref="ChatClientAgentOptions.UseProvidedChatClientAsIs"/> is <see langword="false"/> (the default),
|
||||
/// the <see cref="ChatClientAgent"/> automatically injects this decorator when
|
||||
/// <see cref="ChatClientAgentOptions.SimulateServiceStoredChatHistory"/> is <see langword="true"/>.
|
||||
/// the <see cref="ChatClientAgent"/> automatically includes this decorator in the pipeline and activates it when
|
||||
/// <see cref="ChatClientAgentOptions.RequirePerServiceCallChatHistoryPersistence"/> is <see langword="true"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This decorator only works within the context of a running <see cref="ChatClientAgent"/> and will throw an
|
||||
@@ -110,8 +110,8 @@ public static class ChatClientBuilderExtensions
|
||||
/// <param name="builder">The <see cref="ChatClientBuilder"/> to add the decorator to.</param>
|
||||
/// <returns>The <paramref name="builder"/> for chaining.</returns>
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public static ChatClientBuilder UseServiceStoredChatHistorySimulation(this ChatClientBuilder builder)
|
||||
public static ChatClientBuilder UsePerServiceCallChatHistoryPersistence(this ChatClientBuilder builder)
|
||||
{
|
||||
return builder.Use(innerClient => new ServiceStoredSimulatingChatClient(innerClient));
|
||||
return builder.Use(innerClient => new PerServiceCallChatHistoryPersistingChatClient(innerClient));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,16 +63,16 @@ public static class ChatClientExtensions
|
||||
});
|
||||
}
|
||||
|
||||
// ServiceStoredSimulatingChatClient is only injected when SimulateServiceStoredChatHistory is enabled.
|
||||
// PerServiceCallChatHistoryPersistingChatClient is only injected when RequirePerServiceCallChatHistoryPersistence is enabled.
|
||||
// It is registered after FunctionInvokingChatClient so that it sits between FIC and the leaf client.
|
||||
// ChatClientBuilder.Build applies factories in reverse order, making the first Use() call outermost.
|
||||
// By adding our decorator second, the resulting pipeline is:
|
||||
// FunctionInvokingChatClient → ServiceStoredSimulatingChatClient → leaf IChatClient
|
||||
// FunctionInvokingChatClient → PerServiceCallChatHistoryPersistingChatClient → leaf IChatClient
|
||||
// This allows the decorator to simulate service-stored chat history by loading history before
|
||||
// each service call, persisting after each call, and returning a sentinel ConversationId.
|
||||
if (options?.SimulateServiceStoredChatHistory is true)
|
||||
if (options?.RequirePerServiceCallChatHistoryPersistence is true)
|
||||
{
|
||||
chatBuilder.Use(innerClient => new ServiceStoredSimulatingChatClient(innerClient));
|
||||
chatBuilder.Use(innerClient => new PerServiceCallChatHistoryPersistingChatClient(innerClient));
|
||||
}
|
||||
|
||||
var agentChatClient = chatBuilder.Build(services);
|
||||
|
||||
+32
-16
@@ -11,23 +11,39 @@ using Microsoft.Extensions.AI;
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// A delegating chat client that simulates service-stored chat history behavior using
|
||||
/// framework-managed <see cref="ChatHistoryProvider"/> instances.
|
||||
/// A delegating chat client that persists chat history and updates session state after each
|
||||
/// individual service call within the <see cref="FunctionInvokingChatClient"/> loop.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This decorator is intended to operate between the <see cref="FunctionInvokingChatClient"/> and the leaf
|
||||
/// <see cref="IChatClient"/> in a <see cref="ChatClientAgent"/> pipeline.
|
||||
/// <see cref="IChatClient"/> in a <see cref="ChatClientAgent"/> pipeline. It is activated when
|
||||
/// <see cref="ChatClientAgentOptions.RequirePerServiceCallChatHistoryPersistence"/> is <see langword="true"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Before each service call, it loads chat history from the agent's <see cref="ChatHistoryProvider"/>
|
||||
/// and prepends it to the request messages. After each successful service call, it persists
|
||||
/// new request and response messages to the provider. It also returns a sentinel
|
||||
/// <see cref="ChatOptions.ConversationId"/> on the response so that the
|
||||
/// <see cref="FunctionInvokingChatClient"/> treats the conversation as service-managed —
|
||||
/// clearing accumulated history between iterations and not injecting duplicate
|
||||
/// <see cref="FunctionCallContent"/> during approval-response processing.
|
||||
/// When active, it handles two complementary scenarios:
|
||||
/// </para>
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// <term>Framework-managed chat history</term>
|
||||
/// <description>
|
||||
/// Before each service call, the decorator loads history from the agent's <see cref="ChatHistoryProvider"/>
|
||||
/// and prepends it to the request messages. After each successful call, it persists new messages to
|
||||
/// the provider and returns a sentinel <see cref="ChatOptions.ConversationId"/> so that
|
||||
/// <see cref="FunctionInvokingChatClient"/> treats the conversation as service-managed — clearing
|
||||
/// accumulated history between iterations and not injecting duplicate <see cref="FunctionCallContent"/>
|
||||
/// during approval-response processing.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term>Service-stored chat history</term>
|
||||
/// <description>
|
||||
/// When the underlying service manages its own chat history (real <see cref="ChatOptions.ConversationId"/>),
|
||||
/// the decorator updates <see cref="ChatClientAgentSession.ConversationId"/> after each service call so
|
||||
/// that intermediate ConversationId changes are captured immediately rather than only at the end of the run.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// This chat client must be used within the context of a running <see cref="ChatClientAgent"/>. It retrieves the
|
||||
/// current agent and session from <see cref="AIAgent.CurrentRunContext"/>, which is set automatically when an agent's
|
||||
@@ -38,7 +54,7 @@ namespace Microsoft.Agents.AI;
|
||||
/// available or if the agent is not a <see cref="ChatClientAgent"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal sealed class ServiceStoredSimulatingChatClient : DelegatingChatClient
|
||||
internal sealed class PerServiceCallChatHistoryPersistingChatClient : DelegatingChatClient
|
||||
{
|
||||
/// <summary>
|
||||
/// A sentinel value returned on <see cref="ChatResponse.ConversationId"/> to signal
|
||||
@@ -59,10 +75,10 @@ internal sealed class ServiceStoredSimulatingChatClient : DelegatingChatClient
|
||||
internal const string LocalHistoryConversationId = "_agent_local_chat_history";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ServiceStoredSimulatingChatClient"/> class.
|
||||
/// Initializes a new instance of the <see cref="PerServiceCallChatHistoryPersistingChatClient"/> class.
|
||||
/// </summary>
|
||||
/// <param name="innerClient">The underlying chat client that will handle the core operations.</param>
|
||||
public ServiceStoredSimulatingChatClient(IChatClient innerClient)
|
||||
public PerServiceCallChatHistoryPersistingChatClient(IChatClient innerClient)
|
||||
: base(innerClient)
|
||||
{
|
||||
}
|
||||
@@ -237,18 +253,18 @@ internal sealed class ServiceStoredSimulatingChatClient : DelegatingChatClient
|
||||
{
|
||||
var runContext = AIAgent.CurrentRunContext
|
||||
?? throw new InvalidOperationException(
|
||||
$"{nameof(ServiceStoredSimulatingChatClient)} can only be used within the context of a running AIAgent. " +
|
||||
$"{nameof(PerServiceCallChatHistoryPersistingChatClient)} can only be used within the context of a running AIAgent. " +
|
||||
"Ensure that the chat client is being invoked as part of an AIAgent.RunAsync or AIAgent.RunStreamingAsync call.");
|
||||
|
||||
var chatClientAgent = runContext.Agent.GetService<ChatClientAgent>()
|
||||
?? throw new InvalidOperationException(
|
||||
$"{nameof(ServiceStoredSimulatingChatClient)} can only be used with a {nameof(ChatClientAgent)}. " +
|
||||
$"{nameof(PerServiceCallChatHistoryPersistingChatClient)} can only be used with a {nameof(ChatClientAgent)}. " +
|
||||
$"The current agent is of type '{runContext.Agent.GetType().Name}'.");
|
||||
|
||||
if (runContext.Session is not ChatClientAgentSession chatClientAgentSession)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"{nameof(ServiceStoredSimulatingChatClient)} requires a {nameof(ChatClientAgentSession)}. " +
|
||||
$"{nameof(PerServiceCallChatHistoryPersistingChatClient)} requires a {nameof(ChatClientAgentSession)}. " +
|
||||
$"The current session is of type '{runContext.Session?.GetType().Name ?? "null"}'.");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user