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:
committed by
GitHub
Unverified
parent
9c57680f00
commit
6e7254bba7
@@ -42,7 +42,7 @@ The persistence timing and `FunctionResultContent` trimming behaviors are interr
|
||||
## Considered Options
|
||||
|
||||
- Option 1: Per-run persistence with opt-in FRC (FunctionResultContent) trimming
|
||||
- Option 2: Opt-in per-service-call persistence (via `SimulateServiceStoredChatHistory`)
|
||||
- Option 2: Opt-in per-service-call persistence (via `RequirePerServiceCallChatHistoryPersistence`)
|
||||
|
||||
## Pros and Cons of the Options
|
||||
|
||||
@@ -57,12 +57,12 @@ Keep the current default behavior of persisting chat history only at the end of
|
||||
- Bad, because if the process crashes mid-loop, all intermediate progress from the current run is lost, not satisfying driver C.
|
||||
- Bad, because this option alone does not provide a way for users to opt into per-service-call persistence, not satisfying driver E.
|
||||
|
||||
### Option 2: Opt-in per-service-call persistence (via `SimulateServiceStoredChatHistory`)
|
||||
### Option 2: Opt-in per-service-call persistence (via `RequirePerServiceCallChatHistoryPersistence`)
|
||||
|
||||
Introduce an optional SimulateServiceStoredChatHistory setting to persist chat history after each individual service call within the FIC loop, matching the AI service's behavior. Trailing `FunctionResultContent` trimming is unnecessary with this approach (it is naturally handled).
|
||||
Introduce an optional RequirePerServiceCallChatHistoryPersistence setting to persist chat history after each individual service call within the FIC loop, matching the AI service's behavior. Trailing `FunctionResultContent` trimming is unnecessary with this approach (it is naturally handled).
|
||||
|
||||
Settings:
|
||||
- `SimulateServiceStoredChatHistory` = `true`
|
||||
- `RequirePerServiceCallChatHistoryPersistence` = `true`
|
||||
|
||||
- Good, because the stored history matches the service's behavior when opting in for both timing and content, fully satisfying driver A.
|
||||
- Good, because intermediate progress is preserved if the process is interrupted, satisfying driver C.
|
||||
@@ -73,36 +73,49 @@ Settings:
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
Chosen option: **Option 2: Opt-in per-service-call persistence (via `SimulateServiceStoredChatHistory`)**. The existing per-run persistence behavior is retained as-is, requiring no changes from users. Per-service-call persistence is available as an opt-in feature via the `SimulateServiceStoredChatHistory` setting. This satisfies drivers B (atomicity) and D (simplicity) for the common case, while fully satisfying driver A (consistency) for users who opt into simulated service-stored behavior. Users who need per-service-call persistence for recoverability (driver C) can enable it explicitly.
|
||||
Chosen option: **Option 2: Opt-in per-service-call persistence (via `RequirePerServiceCallChatHistoryPersistence`)**. The existing per-run persistence behavior is retained as-is, requiring no changes from users. Per-service-call persistence is available as an opt-in feature via the `RequirePerServiceCallChatHistoryPersistence` setting. This satisfies drivers B (atomicity) and D (simplicity) for the common case, while fully satisfying driver A (consistency) for users who opt into simulated service-stored behavior. Users who need per-service-call persistence for recoverability (driver C) can enable it explicitly.
|
||||
|
||||
### Configuration Matrix
|
||||
|
||||
The behavior depends on the combination of `UseProvidedChatClientAsIs` and `SimulateServiceStoredChatHistory`:
|
||||
The behavior depends on the combination of `UseProvidedChatClientAsIs` and `RequirePerServiceCallChatHistoryPersistence`:
|
||||
|
||||
| `UseProvidedChatClientAsIs` | `SimulateServiceStoredChatHistory` | Behavior |
|
||||
| `UseProvidedChatClientAsIs` | `RequirePerServiceCallChatHistoryPersistence` | Behavior |
|
||||
|---|---|---|
|
||||
| `false` (default) | `false` (default) | **Per-run persistence.** Messages are persisted at the end of the full agent run via the `ChatHistoryProvider`. |
|
||||
| `false` | `true` | **Per-service-call persistence (simulated).** A `ServiceStoredSimulatingChatClient` middleware is automatically injected into the chat client pipeline between `FunctionInvokingChatClient` and the leaf `IChatClient`. Messages are persisted after each service call. A sentinel `ConversationId` causes FIC to treat the conversation as service-managed. |
|
||||
| `false` | `true` | **Per-service-call persistence (simulated).** A `PerServiceCallChatHistoryPersistingChatClient` middleware is automatically injected into the chat client pipeline between `FunctionInvokingChatClient` and the leaf `IChatClient`. Messages are persisted after each service call. A sentinel `ConversationId` causes FIC to treat the conversation as service-managed. |
|
||||
| `true` | `false` | **Per-run persistence.** No middleware is injected because the user has provided a custom chat client stack. Messages are persisted at the end of the run. |
|
||||
| `true` | `true` | **User responsibility.** The system checks whether the custom chat client stack includes a `ServiceStoredSimulatingChatClient`. If not, a warning is emitted — the user is expected to have added their own per-service-call persistence mechanism. End-of-run persistence is skipped. |
|
||||
| `true` | `true` | **User responsibility.** The system checks whether the custom chat client stack includes a `PerServiceCallChatHistoryPersistingChatClient`. If not, a warning is emitted — the user is expected to have added their own per-service-call persistence mechanism. End-of-run persistence is skipped. |
|
||||
|
||||
### Consequences
|
||||
|
||||
- Good, because per-run persistence is atomic by default — chat history is only updated when the full run succeeds, satisfying driver B.
|
||||
- Good, because the default mental model is simple: one run = one history update, satisfying driver D.
|
||||
- Good, because users who opt into `SimulateServiceStoredChatHistory` get stored history that matches the service's behavior for both timing and content, fully satisfying driver A.
|
||||
- Good, because users who opt into `RequirePerServiceCallChatHistoryPersistence` get stored history that matches the service's behavior for both timing and content, fully satisfying driver A.
|
||||
- Good, because per-service-call persistence preserves intermediate progress if the process is interrupted, satisfying driver C when opted in.
|
||||
- Good, because no separate `FunctionResultContent` trimming logic is needed when per-service-call persistence is active — it is naturally handled.
|
||||
- Good, because conflict detection (configurable via `ThrowOnChatHistoryProviderConflict`, `WarnOnChatHistoryProviderConflict`, `ClearOnChatHistoryProviderConflict`) prevents misconfiguration when a service returns a `ConversationId` alongside a configured `ChatHistoryProvider`.
|
||||
- Bad, because per-service-call persistence (when opted in) may leave chat history in an incomplete state if the run fails mid-loop (e.g., `FunctionCallContent` stored without corresponding `FunctionResultContent`), requiring manual recovery in rare cases.
|
||||
- Neutral, because users who want per-service-call consistency can opt in via `SimulateServiceStoredChatHistory = true`, satisfying driver E.
|
||||
- Neutral, because users who want per-service-call consistency can opt in via `RequirePerServiceCallChatHistoryPersistence = true`, satisfying driver E.
|
||||
- Neutral, because increased write frequency from per-service-call persistence may impact performance for some storage backends; this can be mitigated with a caching decorator.
|
||||
|
||||
### Implementation Notes
|
||||
|
||||
#### Conversation ID Consistency
|
||||
|
||||
We should introduce a separate `ConversationIdPersistingChatClient`, middleware which allows us to
|
||||
persist response `ConversationIds` during the FICC loop. This could be used with or without
|
||||
`ServiceStoredSimulatingChatClient`.
|
||||
When `RequirePerServiceCallChatHistoryPersistence` is enabled, the `PerServiceCallChatHistoryPersistingChatClient`
|
||||
decorator also updates `session.ConversationId` after each service call. This handles two scenarios:
|
||||
|
||||
1. **Framework-managed chat history** — the decorator sets a sentinel `ConversationId` on the response
|
||||
so that `FunctionInvokingChatClient` treats the conversation as service-managed (clearing accumulated
|
||||
history between iterations and not injecting duplicate `FunctionCallContent` during approval processing).
|
||||
|
||||
2. **Service-stored chat history** — when the service returns a real `ConversationId`, the decorator
|
||||
updates `session.ConversationId` immediately after each service call, rather than deferring the update
|
||||
to the end of the run. This ensures intermediate ConversationId changes are captured even if the
|
||||
process is interrupted mid-loop.
|
||||
|
||||
For some service-stored scenarios (e.g., the Conversations API with the Responses API), there is only
|
||||
one thread with one ID, so every service call returns the same ConversationId and this per-call update
|
||||
makes no practical difference. Enabling `RequirePerServiceCallChatHistoryPersistence` ensures consistent
|
||||
per-service-call behavior across all service types regardless of how they manage ConversationIds.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates how the ChatClientAgent persists chat history after each individual
|
||||
// call to the AI service, using the SimulateServiceStoredChatHistory option.
|
||||
// call to the AI service, using the RequirePerServiceCallChatHistoryPersistence option.
|
||||
// When an agent uses tools, FunctionInvokingChatClient may loop multiple times
|
||||
// (service call → tool execution → service call), and intermediate messages (tool calls and
|
||||
// results) are persisted after each service call. This allows you to inspect or recover them
|
||||
@@ -9,7 +9,7 @@
|
||||
// yet finalized (e.g., tool calls without results) being persisted, which may be undesirable in some cases.
|
||||
//
|
||||
// To use end-of-run persistence instead (atomic run semantics), remove the
|
||||
// SimulateServiceStoredChatHistory = true setting (or set it to false). End-of-run
|
||||
// RequirePerServiceCallChatHistoryPersistence = true setting (or set it to false). End-of-run
|
||||
// persistence is the default behavior.
|
||||
//
|
||||
// The sample runs two multi-turn conversations: one using non-streaming (RunAsync) and one
|
||||
@@ -54,7 +54,7 @@ static string GetTime([Description("The city name.")] string city) =>
|
||||
_ => $"{city}: time data not available."
|
||||
};
|
||||
|
||||
// Create the agent — per-service-call persistence is enabled via SimulateServiceStoredChatHistory.
|
||||
// Create the agent — per-service-call persistence is enabled via RequirePerServiceCallChatHistoryPersistence.
|
||||
// The in-memory ChatHistoryProvider is used by default when the service does not require service stored chat
|
||||
// history, so for those cases, we can inspect the chat history via session.TryGetInMemoryChatHistory().
|
||||
IChatClient chatClient = string.Equals(store, "TRUE", StringComparison.OrdinalIgnoreCase) ?
|
||||
@@ -64,7 +64,7 @@ AIAgent agent = chatClient.AsAIAgent(
|
||||
new ChatClientAgentOptions
|
||||
{
|
||||
Name = "WeatherAssistant",
|
||||
SimulateServiceStoredChatHistory = true,
|
||||
RequirePerServiceCallChatHistoryPersistence = true,
|
||||
ChatOptions = new()
|
||||
{
|
||||
Instructions = "You are a helpful assistant. When asked about multiple cities, call the appropriate tool for each city.",
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
# In-Function-Loop Checkpointing
|
||||
|
||||
This sample demonstrates how `ChatClientAgent` can persist chat history after each individual call to the AI service using the `SimulateServiceStoredChatHistory` option. This per-service-call persistence ensures intermediate progress is saved during the function invocation loop.
|
||||
This sample demonstrates how `ChatClientAgent` can persist chat history after each individual call to the AI service using the `RequirePerServiceCallChatHistoryPersistence` option. This per-service-call persistence ensures intermediate progress is saved during the function invocation loop.
|
||||
|
||||
## What This Sample Shows
|
||||
|
||||
When an agent uses tools, the `FunctionInvokingChatClient` loops multiple times (service call → tool execution → service call → …). By enabling `SimulateServiceStoredChatHistory = true`, chat history is persisted after each service call via the `ServiceStoredSimulatingChatClient` decorator:
|
||||
When an agent uses tools, the `FunctionInvokingChatClient` loops multiple times (service call → tool execution → service call → …). By enabling `RequirePerServiceCallChatHistoryPersistence = true`, chat history is persisted after each service call via the `PerServiceCallChatHistoryPersistingChatClient` decorator:
|
||||
|
||||
- A `ServiceStoredSimulatingChatClient` decorator is inserted into the chat client pipeline
|
||||
- A `PerServiceCallChatHistoryPersistingChatClient` decorator is inserted into the chat client pipeline
|
||||
- Before each service call, the decorator loads history from the `ChatHistoryProvider` and prepends it to the request
|
||||
- After each service call, the decorator notifies the `ChatHistoryProvider` (and any `AIContextProvider` instances) with the new messages
|
||||
- Only **new** messages are sent to providers on each notification — messages that were already persisted in an earlier call within the same run are deduplicated automatically
|
||||
|
||||
By default (without `SimulateServiceStoredChatHistory`), chat history is persisted at the end of the full agent run instead. To use per-service-call persistence, set `SimulateServiceStoredChatHistory = true` on `ChatClientAgentOptions`.
|
||||
By default (without `RequirePerServiceCallChatHistoryPersistence`), chat history is persisted at the end of the full agent run instead. To use per-service-call persistence, set `RequirePerServiceCallChatHistoryPersistence = true` on `ChatClientAgentOptions`.
|
||||
|
||||
With `SimulateServiceStoredChatHistory` = true, the behavior matches that of chat history stored in the underlying AI service exactly.
|
||||
With `RequirePerServiceCallChatHistoryPersistence` = true, the behavior matches that of chat history stored in the underlying AI service exactly.
|
||||
|
||||
Per-service-call persistence is useful for:
|
||||
- **Crash recovery** — if the process is interrupted mid-loop, the intermediate tool calls and results are already persisted
|
||||
@@ -29,7 +29,7 @@ The sample asks the agent about the weather and time in three cities. The model
|
||||
```
|
||||
ChatClientAgent
|
||||
└─ FunctionInvokingChatClient (handles tool call loop)
|
||||
└─ ServiceStoredSimulatingChatClient (persists after each service call)
|
||||
└─ PerServiceCallChatHistoryPersistingChatClient (persists after each service call)
|
||||
└─ Leaf IChatClient (Azure OpenAI)
|
||||
```
|
||||
|
||||
|
||||
@@ -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"}'.");
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace Microsoft.Agents.AI.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Shared test helper for <see cref="ChatClientAgent"/> integration tests that verify
|
||||
/// end-to-end behavior with <see cref="ServiceStoredSimulatingChatClient"/> and
|
||||
/// end-to-end behavior with <see cref="PerServiceCallChatHistoryPersistingChatClient"/> and
|
||||
/// <see cref="FunctionInvokingChatClient"/>.
|
||||
/// </summary>
|
||||
internal static class ChatClientAgentTestHelper
|
||||
|
||||
+3
-3
@@ -9,7 +9,7 @@ namespace Microsoft.Agents.AI.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Contains unit tests that verify the end-to-end approval flow behavior of the
|
||||
/// <see cref="ChatClientAgent"/> class with <see cref="ServiceStoredSimulatingChatClient"/>,
|
||||
/// <see cref="ChatClientAgent"/> class with <see cref="PerServiceCallChatHistoryPersistingChatClient"/>,
|
||||
/// ensuring that chat history is correctly persisted across multi-turn approval interactions.
|
||||
/// </summary>
|
||||
public class ChatClientAgent_ApprovalsTests
|
||||
@@ -48,7 +48,7 @@ public class ChatClientAgent_ApprovalsTests
|
||||
agentOptions: new()
|
||||
{
|
||||
ChatOptions = new() { Tools = [approvalTool] },
|
||||
SimulateServiceStoredChatHistory = true,
|
||||
RequirePerServiceCallChatHistoryPersistence = true,
|
||||
},
|
||||
callIndex: callIndex,
|
||||
capturedInputs: capturedInputs);
|
||||
@@ -260,7 +260,7 @@ public class ChatClientAgent_ApprovalsTests
|
||||
agentOptions: new()
|
||||
{
|
||||
ChatOptions = new() { Tools = [approvalTool] },
|
||||
SimulateServiceStoredChatHistory = true,
|
||||
RequirePerServiceCallChatHistoryPersistence = true,
|
||||
},
|
||||
callIndex: callIndex,
|
||||
capturedInputs: capturedInputs);
|
||||
|
||||
+2
-2
@@ -520,7 +520,7 @@ public class ChatClientAgent_ChatHistoryManagementTests
|
||||
agentOptions: new()
|
||||
{
|
||||
ChatOptions = new() { Instructions = "Be helpful" },
|
||||
SimulateServiceStoredChatHistory = true,
|
||||
RequirePerServiceCallChatHistoryPersistence = true,
|
||||
},
|
||||
expectedServiceCallCount: 1,
|
||||
expectedHistory:
|
||||
@@ -554,7 +554,7 @@ public class ChatClientAgent_ChatHistoryManagementTests
|
||||
agentOptions: new()
|
||||
{
|
||||
ChatOptions = new() { Tools = [tool] },
|
||||
SimulateServiceStoredChatHistory = true,
|
||||
RequirePerServiceCallChatHistoryPersistence = true,
|
||||
},
|
||||
expectedServiceCallCount: 2,
|
||||
expectedHistory:
|
||||
|
||||
+44
-44
@@ -13,15 +13,15 @@ using Moq.Protected;
|
||||
namespace Microsoft.Agents.AI.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Contains unit tests for the <see cref="ServiceStoredSimulatingChatClient"/> decorator,
|
||||
/// Contains unit tests for the <see cref="PerServiceCallChatHistoryPersistingChatClient"/> decorator,
|
||||
/// verifying that it persists messages via the <see cref="ChatHistoryProvider"/> after each
|
||||
/// individual service call by default, or marks messages for end-of-run persistence when the
|
||||
/// <see cref="ChatClientAgentOptions.SimulateServiceStoredChatHistory"/> option is enabled.
|
||||
/// <see cref="ChatClientAgentOptions.RequirePerServiceCallChatHistoryPersistence"/> option is enabled.
|
||||
/// </summary>
|
||||
public class ServiceStoredSimulatingChatClientTests
|
||||
public class PerServiceCallChatHistoryPersistingChatClientTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies that by default (SimulateServiceStoredChatHistory is false),
|
||||
/// Verifies that by default (RequirePerServiceCallChatHistoryPersistence is false),
|
||||
/// the ChatHistoryProvider receives messages after a successful non-streaming call.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
@@ -50,7 +50,7 @@ public class ServiceStoredSimulatingChatClientTests
|
||||
ChatClientAgent agent = new(mockService.Object, options: new()
|
||||
{
|
||||
ChatHistoryProvider = mockChatHistoryProvider.Object,
|
||||
SimulateServiceStoredChatHistory = true,
|
||||
RequirePerServiceCallChatHistoryPersistence = true,
|
||||
});
|
||||
|
||||
// Act
|
||||
@@ -97,7 +97,7 @@ public class ServiceStoredSimulatingChatClientTests
|
||||
ChatClientAgent agent = new(mockService.Object, options: new()
|
||||
{
|
||||
ChatHistoryProvider = mockChatHistoryProvider.Object,
|
||||
SimulateServiceStoredChatHistory = true,
|
||||
RequirePerServiceCallChatHistoryPersistence = true,
|
||||
});
|
||||
|
||||
// Act
|
||||
@@ -145,7 +145,7 @@ public class ServiceStoredSimulatingChatClientTests
|
||||
ChatClientAgent agent = new(mockService.Object, options: new()
|
||||
{
|
||||
ChatHistoryProvider = mockChatHistoryProvider.Object,
|
||||
SimulateServiceStoredChatHistory = true,
|
||||
RequirePerServiceCallChatHistoryPersistence = true,
|
||||
});
|
||||
|
||||
// Act
|
||||
@@ -163,7 +163,7 @@ public class ServiceStoredSimulatingChatClientTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the decorator is NOT injected by default (SimulateServiceStoredChatHistory is false).
|
||||
/// Verifies that the decorator is NOT injected by default (RequirePerServiceCallChatHistoryPersistence is false).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ChatClient_DoesNotContainDecorator_ByDefault()
|
||||
@@ -175,15 +175,15 @@ public class ServiceStoredSimulatingChatClientTests
|
||||
ChatClientAgent agent = new(mockService.Object, options: new());
|
||||
|
||||
// Assert
|
||||
var decorator = agent.ChatClient.GetService<ServiceStoredSimulatingChatClient>();
|
||||
var decorator = agent.ChatClient.GetService<PerServiceCallChatHistoryPersistingChatClient>();
|
||||
Assert.Null(decorator);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the decorator is injected when SimulateServiceStoredChatHistory is true.
|
||||
/// Verifies that the decorator is injected when RequirePerServiceCallChatHistoryPersistence is true.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ChatClient_ContainsDecorator_WhenSimulateServiceStoredChatHistory()
|
||||
public void ChatClient_ContainsDecorator_WhenRequirePerServiceCallChatHistoryPersistence()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockService = new();
|
||||
@@ -191,11 +191,11 @@ public class ServiceStoredSimulatingChatClientTests
|
||||
// Act
|
||||
ChatClientAgent agent = new(mockService.Object, options: new()
|
||||
{
|
||||
SimulateServiceStoredChatHistory = true,
|
||||
RequirePerServiceCallChatHistoryPersistence = true,
|
||||
});
|
||||
|
||||
// Assert
|
||||
var decorator = agent.ChatClient.GetService<ServiceStoredSimulatingChatClient>();
|
||||
var decorator = agent.ChatClient.GetService<PerServiceCallChatHistoryPersistingChatClient>();
|
||||
Assert.NotNull(decorator);
|
||||
}
|
||||
|
||||
@@ -215,27 +215,27 @@ public class ServiceStoredSimulatingChatClientTests
|
||||
});
|
||||
|
||||
// Assert
|
||||
var decorator = agent.ChatClient.GetService<ServiceStoredSimulatingChatClient>();
|
||||
var decorator = agent.ChatClient.GetService<PerServiceCallChatHistoryPersistingChatClient>();
|
||||
Assert.Null(decorator);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the SimulateServiceStoredChatHistory option is included in Clone().
|
||||
/// Verifies that the RequirePerServiceCallChatHistoryPersistence option is included in Clone().
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ChatClientAgentOptions_Clone_IncludesSimulateServiceStoredChatHistory()
|
||||
public void ChatClientAgentOptions_Clone_IncludesRequirePerServiceCallChatHistoryPersistence()
|
||||
{
|
||||
// Arrange
|
||||
var options = new ChatClientAgentOptions
|
||||
{
|
||||
SimulateServiceStoredChatHistory = true,
|
||||
RequirePerServiceCallChatHistoryPersistence = true,
|
||||
};
|
||||
|
||||
// Act
|
||||
var cloned = options.Clone();
|
||||
|
||||
// Assert
|
||||
Assert.True(cloned.SimulateServiceStoredChatHistory);
|
||||
Assert.True(cloned.RequirePerServiceCallChatHistoryPersistence);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -289,7 +289,7 @@ public class ServiceStoredSimulatingChatClientTests
|
||||
{
|
||||
ChatOptions = new() { Tools = [tool] },
|
||||
ChatHistoryProvider = mockChatHistoryProvider.Object,
|
||||
SimulateServiceStoredChatHistory = true,
|
||||
RequirePerServiceCallChatHistoryPersistence = true,
|
||||
}, services: new ServiceCollection().BuildServiceProvider());
|
||||
|
||||
// Act
|
||||
@@ -358,7 +358,7 @@ public class ServiceStoredSimulatingChatClientTests
|
||||
ChatClientAgent agent = new(mockService.Object, options: new()
|
||||
{
|
||||
ChatHistoryProvider = mockChatHistoryProvider.Object,
|
||||
SimulateServiceStoredChatHistory = true,
|
||||
RequirePerServiceCallChatHistoryPersistence = true,
|
||||
});
|
||||
|
||||
// Act
|
||||
@@ -407,7 +407,7 @@ public class ServiceStoredSimulatingChatClientTests
|
||||
ChatClientAgent agent = new(mockService.Object, options: new()
|
||||
{
|
||||
AIContextProviders = [mockContextProvider.Object],
|
||||
SimulateServiceStoredChatHistory = true,
|
||||
RequirePerServiceCallChatHistoryPersistence = true,
|
||||
});
|
||||
|
||||
// Act
|
||||
@@ -454,7 +454,7 @@ public class ServiceStoredSimulatingChatClientTests
|
||||
ChatClientAgent agent = new(mockService.Object, options: new()
|
||||
{
|
||||
AIContextProviders = [mockContextProvider.Object],
|
||||
SimulateServiceStoredChatHistory = true,
|
||||
RequirePerServiceCallChatHistoryPersistence = true,
|
||||
});
|
||||
|
||||
// Act
|
||||
@@ -513,7 +513,7 @@ public class ServiceStoredSimulatingChatClientTests
|
||||
{
|
||||
ChatHistoryProvider = mockChatHistoryProvider.Object,
|
||||
AIContextProviders = [mockContextProvider.Object],
|
||||
SimulateServiceStoredChatHistory = true,
|
||||
RequirePerServiceCallChatHistoryPersistence = true,
|
||||
});
|
||||
|
||||
// Act
|
||||
@@ -587,7 +587,7 @@ public class ServiceStoredSimulatingChatClientTests
|
||||
{
|
||||
ChatOptions = new() { Tools = [tool] },
|
||||
ChatHistoryProvider = mockChatHistoryProvider.Object,
|
||||
SimulateServiceStoredChatHistory = true,
|
||||
RequirePerServiceCallChatHistoryPersistence = true,
|
||||
}, services: new ServiceCollection().BuildServiceProvider());
|
||||
|
||||
// Act
|
||||
@@ -652,7 +652,7 @@ public class ServiceStoredSimulatingChatClientTests
|
||||
{
|
||||
ChatOptions = new() { Tools = [tool] },
|
||||
ChatHistoryProvider = mockChatHistoryProvider.Object,
|
||||
SimulateServiceStoredChatHistory = true,
|
||||
RequirePerServiceCallChatHistoryPersistence = true,
|
||||
}, services: new ServiceCollection().BuildServiceProvider());
|
||||
|
||||
// Act
|
||||
@@ -720,8 +720,8 @@ public class ServiceStoredSimulatingChatClientTests
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that when per-service-call persistence is active and no real conversation ID exists,
|
||||
/// <see cref="ChatClientAgent"/> sets the <see cref="ServiceStoredSimulatingChatClient.LocalHistoryConversationId"/>
|
||||
/// sentinel on the chat options and <see cref="ServiceStoredSimulatingChatClient"/> strips it before
|
||||
/// <see cref="ChatClientAgent"/> sets the <see cref="PerServiceCallChatHistoryPersistingChatClient.LocalHistoryConversationId"/>
|
||||
/// sentinel on the chat options and <see cref="PerServiceCallChatHistoryPersistingChatClient"/> strips it before
|
||||
/// forwarding to the inner client.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
@@ -741,7 +741,7 @@ public class ServiceStoredSimulatingChatClientTests
|
||||
ChatClientAgent agent = new(mockService.Object, options: new()
|
||||
{
|
||||
ChatOptions = new() { Instructions = "test" },
|
||||
SimulateServiceStoredChatHistory = true,
|
||||
RequirePerServiceCallChatHistoryPersistence = true,
|
||||
});
|
||||
|
||||
// Act
|
||||
@@ -773,7 +773,7 @@ public class ServiceStoredSimulatingChatClientTests
|
||||
ChatClientAgent agent = new(mockService.Object, options: new()
|
||||
{
|
||||
ChatOptions = new() { Instructions = "test" },
|
||||
SimulateServiceStoredChatHistory = true,
|
||||
RequirePerServiceCallChatHistoryPersistence = true,
|
||||
});
|
||||
|
||||
// Act
|
||||
@@ -808,7 +808,7 @@ public class ServiceStoredSimulatingChatClientTests
|
||||
|
||||
ChatClientAgent agent = new(mockService.Object, options: new()
|
||||
{
|
||||
SimulateServiceStoredChatHistory = true,
|
||||
RequirePerServiceCallChatHistoryPersistence = true,
|
||||
});
|
||||
|
||||
// Create a session with a real conversation ID.
|
||||
@@ -842,7 +842,7 @@ public class ServiceStoredSimulatingChatClientTests
|
||||
ChatClientAgent agent = new(mockService.Object, options: new()
|
||||
{
|
||||
ChatOptions = new() { Instructions = "test" },
|
||||
SimulateServiceStoredChatHistory = true,
|
||||
RequirePerServiceCallChatHistoryPersistence = true,
|
||||
});
|
||||
|
||||
// Act
|
||||
@@ -862,7 +862,7 @@ public class ServiceStoredSimulatingChatClientTests
|
||||
/// skip provider resolution in the agent (the decorator handles it).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RunAsync_SetsSentinelOnSession_WhenSimulateServiceStoredChatHistoryActiveAsync()
|
||||
public async Task RunAsync_SetsSentinelOnSession_WhenRequirePerServiceCallChatHistoryPersistenceActiveAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockService = new();
|
||||
@@ -875,7 +875,7 @@ public class ServiceStoredSimulatingChatClientTests
|
||||
|
||||
ChatClientAgent agent = new(mockService.Object, options: new()
|
||||
{
|
||||
SimulateServiceStoredChatHistory = true,
|
||||
RequirePerServiceCallChatHistoryPersistence = true,
|
||||
});
|
||||
|
||||
// Act
|
||||
@@ -883,7 +883,7 @@ public class ServiceStoredSimulatingChatClientTests
|
||||
await agent.RunAsync([new(ChatRole.User, "test")], session);
|
||||
|
||||
// Assert — session should have the sentinel conversation ID
|
||||
Assert.Equal(ServiceStoredSimulatingChatClient.LocalHistoryConversationId, session!.ConversationId);
|
||||
Assert.Equal(PerServiceCallChatHistoryPersistingChatClient.LocalHistoryConversationId, session!.ConversationId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -924,7 +924,7 @@ public class ServiceStoredSimulatingChatClientTests
|
||||
ChatClientAgent agent = new(mockService.Object, options: new()
|
||||
{
|
||||
ChatHistoryProvider = mockChatHistoryProvider.Object,
|
||||
SimulateServiceStoredChatHistory = true,
|
||||
RequirePerServiceCallChatHistoryPersistence = true,
|
||||
});
|
||||
|
||||
// Act & Assert — conflict detection should throw
|
||||
@@ -969,7 +969,7 @@ public class ServiceStoredSimulatingChatClientTests
|
||||
|
||||
ChatClientAgent agent = new(mockService.Object, options: new()
|
||||
{
|
||||
SimulateServiceStoredChatHistory = true,
|
||||
RequirePerServiceCallChatHistoryPersistence = true,
|
||||
AIContextProviders = [mockContextProvider.Object],
|
||||
});
|
||||
|
||||
@@ -1025,7 +1025,7 @@ public class ServiceStoredSimulatingChatClientTests
|
||||
|
||||
ChatClientAgent agent = new(mockService.Object, options: new()
|
||||
{
|
||||
SimulateServiceStoredChatHistory = true,
|
||||
RequirePerServiceCallChatHistoryPersistence = true,
|
||||
AIContextProviders = [mockContextProvider.Object],
|
||||
});
|
||||
|
||||
@@ -1077,7 +1077,7 @@ public class ServiceStoredSimulatingChatClientTests
|
||||
|
||||
ChatClientAgent agent = new(mockService.Object, options: new()
|
||||
{
|
||||
SimulateServiceStoredChatHistory = true,
|
||||
RequirePerServiceCallChatHistoryPersistence = true,
|
||||
AIContextProviders = [mockContextProvider.Object],
|
||||
});
|
||||
|
||||
@@ -1137,7 +1137,7 @@ public class ServiceStoredSimulatingChatClientTests
|
||||
// No ChatHistoryProvider — so conflict detection won't throw.
|
||||
ChatClientAgent agent = new(mockService.Object, options: new()
|
||||
{
|
||||
SimulateServiceStoredChatHistory = true,
|
||||
RequirePerServiceCallChatHistoryPersistence = true,
|
||||
AIContextProviders = [mockContextProvider.Object],
|
||||
});
|
||||
|
||||
@@ -1192,7 +1192,7 @@ public class ServiceStoredSimulatingChatClientTests
|
||||
// No ChatHistoryProvider — so conflict detection won't throw.
|
||||
ChatClientAgent agent = new(mockService.Object, options: new()
|
||||
{
|
||||
SimulateServiceStoredChatHistory = true,
|
||||
RequirePerServiceCallChatHistoryPersistence = true,
|
||||
AIContextProviders = [mockContextProvider.Object],
|
||||
});
|
||||
|
||||
@@ -1253,7 +1253,7 @@ public class ServiceStoredSimulatingChatClientTests
|
||||
ChatClientAgent agent = new(mockService.Object, options: new()
|
||||
{
|
||||
ChatHistoryProvider = mockChatHistoryProvider.Object,
|
||||
SimulateServiceStoredChatHistory = true,
|
||||
RequirePerServiceCallChatHistoryPersistence = true,
|
||||
});
|
||||
|
||||
// Act
|
||||
@@ -1270,7 +1270,7 @@ public class ServiceStoredSimulatingChatClientTests
|
||||
Assert.Equal("test", messageList[0].Text);
|
||||
|
||||
// Assert — session should NOT have the sentinel (agent handles ConversationId at end-of-run)
|
||||
Assert.NotEqual(ServiceStoredSimulatingChatClient.LocalHistoryConversationId, session!.ConversationId);
|
||||
Assert.NotEqual(PerServiceCallChatHistoryPersistingChatClient.LocalHistoryConversationId, session!.ConversationId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1291,7 +1291,7 @@ public class ServiceStoredSimulatingChatClientTests
|
||||
|
||||
ChatClientAgent agent = new(mockService.Object, options: new()
|
||||
{
|
||||
SimulateServiceStoredChatHistory = true,
|
||||
RequirePerServiceCallChatHistoryPersistence = true,
|
||||
});
|
||||
|
||||
// Act
|
||||
@@ -1309,6 +1309,6 @@ public class ServiceStoredSimulatingChatClientTests
|
||||
Assert.NotEmpty(updates);
|
||||
|
||||
// Assert — session should NOT have the sentinel
|
||||
Assert.NotEqual(ServiceStoredSimulatingChatClient.LocalHistoryConversationId, session!.ConversationId);
|
||||
Assert.NotEqual(PerServiceCallChatHistoryPersistingChatClient.LocalHistoryConversationId, session!.ConversationId);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user