diff --git a/docs/decisions/0022-chat-history-persistence-consistency.md b/docs/decisions/0022-chat-history-persistence-consistency.md index 2784934817..5bd303a029 100644 --- a/docs/decisions/0022-chat-history-persistence-consistency.md +++ b/docs/decisions/0022-chat-history-persistence-consistency.md @@ -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. diff --git a/dotnet/samples/02-agents/Agents/Agent_Step19_InFunctionLoopCheckpointing/Program.cs b/dotnet/samples/02-agents/Agents/Agent_Step19_InFunctionLoopCheckpointing/Program.cs index 0c74ad86b1..4e526f8f41 100644 --- a/dotnet/samples/02-agents/Agents/Agent_Step19_InFunctionLoopCheckpointing/Program.cs +++ b/dotnet/samples/02-agents/Agents/Agent_Step19_InFunctionLoopCheckpointing/Program.cs @@ -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 @@ static string GetTime([Description("The city name.")] string city) => 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.", diff --git a/dotnet/samples/02-agents/Agents/Agent_Step19_InFunctionLoopCheckpointing/README.md b/dotnet/samples/02-agents/Agents/Agent_Step19_InFunctionLoopCheckpointing/README.md index 461f76aaf4..9b96562a66 100644 --- a/dotnet/samples/02-agents/Agents/Agent_Step19_InFunctionLoopCheckpointing/README.md +++ b/dotnet/samples/02-agents/Agents/Agent_Step19_InFunctionLoopCheckpointing/README.md @@ -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) ``` diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs index 713361d9be..44a136da3e 100644 --- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs @@ -139,8 +139,8 @@ public ChatClientAgent(IChatClient chatClient, ChatClientAgentOptions? options, this._logger = (loggerFactory ?? chatClient.GetService() ?? NullLoggerFactory.Instance).CreateLogger(); - // 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(); } /// @@ -454,7 +454,7 @@ protected override ValueTask DeserializeSessionCoreAsync(JsonEleme /// Notifies the and all of successfully completed messages. /// /// - /// This method is also called by to persist messages per-service-call. + /// This method is also called by to persist messages per-service-call. /// internal async Task NotifyProvidersOfNewMessagesAsync( ChatClientAgentSession session, @@ -486,7 +486,7 @@ internal async Task NotifyProvidersOfNewMessagesAsync( /// Notifies the and all of a failure during a service call. /// /// - /// This method is also called by to report failures per-service-call. + /// This method is also called by to report failures per-service-call. /// internal async Task NotifyProvidersOfFailureAsync( ChatClientAgentSession session, @@ -701,7 +701,7 @@ private async Task 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 @@ private async Task IEnumerable 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 @@ internal void UpdateSessionConversationId(ChatClientAgentSession session, string /// Updates the session conversation ID at the end of an agent run. /// /// - /// When a handles per-service-call + /// When a 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 is /// (continuation token scenarios), the update is always performed. /// 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 @@ private void UpdateSessionConversationIdAtEndOfRun(ChatClientAgentSession sessio /// Notifies providers of successfully completed messages at the end of an agent run. /// /// - /// When a handles per-service-call + /// When a handles per-service-call /// notification, this end-of-run notification is skipped. When no decorator is present, /// all messages are persisted. /// When is (continuation token or @@ -871,7 +871,7 @@ private Task NotifyProvidersOfNewMessagesAtEndOfRunAsync( CancellationToken cancellationToken, bool forceNotify = false) { - if (!forceNotify && this.SimulatesServiceStoredChatHistory) + if (!forceNotify && this.RequiresPerServiceCallChatHistoryPersistence) { return Task.CompletedTask; } @@ -883,7 +883,7 @@ private Task NotifyProvidersOfNewMessagesAtEndOfRunAsync( /// Notifies providers of a failure at the end of an agent run. /// /// - /// When a handles per-service-call + /// When a 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. /// @@ -894,7 +894,7 @@ private Task NotifyProvidersOfFailureAtEndOfRunAsync( ChatOptions? chatOptions, CancellationToken cancellationToken) { - if (this.SimulatesServiceStoredChatHistory) + if (this.RequiresPerServiceCallChatHistoryPersistence) { return Task.CompletedTask; } @@ -905,14 +905,14 @@ private Task NotifyProvidersOfFailureAtEndOfRunAsync( /// /// Gets a value indicating whether the agent is configured to simulate service-stored chat history. /// When , end-of-run persistence and history loading are skipped because a - /// per-service-call decorator (such as or a + /// per-service-call decorator (such as or a /// user-supplied equivalent) is expected to handle the history lifecycle. /// - private bool SimulatesServiceStoredChatHistory + private bool RequiresPerServiceCallChatHistoryPersistence { get { - return this._agentOptions?.SimulateServiceStoredChatHistory is true; + return this._agentOptions?.RequirePerServiceCallChatHistoryPersistence is true; } } @@ -923,7 +923,7 @@ private bool SimulatesServiceStoredChatHistory /// The base class sets with the raw session parameter /// (which may be null) and restores it after each yield in streaming scenarios. After /// resolves or creates a session, we update the - /// context so the decorator always has a valid session. + /// context so the 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. /// @@ -939,19 +939,19 @@ private static void EnsureRunContextHasSession(ChatClientAgentSession safeSessio /// /// Checks for potential misconfiguration when using a custom chat client stack and logs warnings. /// - 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(); + var persistingClient = this.ChatClient.GetService(); if (persistingClient is null && this._logger.IsEnabled(LogLevel.Warning)) { var loggingAgentName = this.GetLoggingAgentName(); @@ -998,7 +998,7 @@ private void WarnOnMissingServiceStoredSimulatingClient() /// /// /// This method is used by both the agent (during ) and by - /// to load history before each service call. + /// to load history before each service call. /// internal async Task> LoadChatHistoryAsync( ChatClientAgentSession session, diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentLogMessages.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentLogMessages.cs index 5899ef5cb2..dde1d97ba4 100644 --- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentLogMessages.cs +++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentLogMessages.cs @@ -72,12 +72,12 @@ public static partial void LogAgentChatClientHistoryProviderConflict( /// /// Logs a warning when is - /// and is , - /// but no is found in the custom chat client stack. + /// and is , + /// but no is found in the custom chat client stack. /// [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 @@ public static partial void LogAgentChatClientMissingPersistingClient( /// [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, diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentOptions.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentOptions.cs index e3d4326ae6..e7340cec19 100644 --- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentOptions.cs +++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentOptions.cs @@ -92,38 +92,56 @@ public sealed class ChatClientAgentOptions public bool ThrowOnChatHistoryProviderConflict { get; set; } = true; /// - /// Gets or sets a value indicating whether the should simulate - /// service-stored chat history behavior using its configured . + /// Gets or sets a value indicating whether the should persist + /// chat history after each individual service call within the + /// loop, rather than at the end of the full agent run. /// /// /// - /// When set to , a decorator is - /// injected between the and the leaf - /// in the chat client pipeline. This decorator takes full ownership of the chat history lifecycle: - /// it loads history from the before each service call and persists - /// new messages after each service call. It also returns a sentinel - /// on the response, causing the to treat the conversation - /// as service-managed — clearing accumulated history and not injecting duplicate - /// during approval-response processing. - /// - /// - /// 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 , a + /// decorator becomes active in the chat client pipeline. It handles two complementary scenarios: /// + /// + /// + /// Framework-managed chat history + /// + /// The decorator loads history from the before each service call + /// and persists new request and response messages after each call. It returns a sentinel + /// on the response, causing the + /// to treat the conversation as service-managed — clearing + /// accumulated history between iterations and not injecting duplicate + /// during approval-response processing. + /// + /// + /// + /// AI Service-stored chat history + /// + /// When the service manages its own chat history (returning a real ), + /// the decorator updates 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 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 ensures that the + /// latest ID is always captured, even mid-run. + /// Enabling this option ensures consistent per-service-call behavior across all service types. + /// + /// + /// /// /// When set to (the default), the handles - /// chat history persistence at the end of the full agent run via the - /// pipeline. + /// chat history persistence at the end of the full agent run via the if using + /// framework-managed chat history. For AI service-stored chat history, the + /// updates happen only at the end of the run. /// /// /// When setting the setting to and - /// to , ensure that your custom chat client stack includes a - /// to enable per-service-call persistence. - /// If no is provided, and you are not storing chat history via other means, + /// to , ensure that your custom chat client stack includes a + /// to enable per-service-call persistence. + /// If no 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 - /// manually via the + /// When using a custom chat client stack, you can add a + /// manually via the /// extension method. /// /// @@ -131,7 +149,7 @@ public sealed class ChatClientAgentOptions /// Default is . /// [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] - public bool SimulateServiceStoredChatHistory { get; set; } + public bool RequirePerServiceCallChatHistoryPersistence { get; set; } /// /// Creates a new instance of with the same values as this instance. @@ -149,6 +167,6 @@ public ChatClientAgentOptions Clone() ClearOnChatHistoryProviderConflict = this.ClearOnChatHistoryProviderConflict, WarnOnChatHistoryProviderConflict = this.WarnOnChatHistoryProviderConflict, ThrowOnChatHistoryProviderConflict = this.ThrowOnChatHistoryProviderConflict, - SimulateServiceStoredChatHistory = this.SimulateServiceStoredChatHistory, + RequirePerServiceCallChatHistoryPersistence = this.RequirePerServiceCallChatHistoryPersistence, }; } diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientBuilderExtensions.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientBuilderExtensions.cs index 68e1307eeb..5cf87aa950 100644 --- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientBuilderExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientBuilderExtensions.cs @@ -86,21 +86,21 @@ public static ChatClientAgent BuildAIAgent( services: services); /// - /// Adds a to the chat client pipeline. + /// Adds a to the chat client pipeline. /// /// /// /// This decorator should be positioned between the and the leaf - /// 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 - /// on the response. + /// in the pipeline. It persists chat history after each individual service call + /// and updates the session per call for both framework-managed + /// and service-stored chat history scenarios. /// /// /// This extension method is intended for use with custom chat client stacks when /// is . /// When is (the default), - /// the automatically injects this decorator when - /// is . + /// the automatically includes this decorator in the pipeline and activates it when + /// is . /// /// /// This decorator only works within the context of a running and will throw an @@ -110,8 +110,8 @@ public static ChatClientAgent BuildAIAgent( /// The to add the decorator to. /// The for chaining. [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)); } } diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientExtensions.cs index 5251314766..b2c48ec572 100644 --- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientExtensions.cs @@ -63,16 +63,16 @@ internal static IChatClient WithDefaultAgentMiddleware(this IChatClient chatClie }); } - // 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); diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ServiceStoredSimulatingChatClient.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/PerServiceCallChatHistoryPersistingChatClient.cs similarity index 84% rename from dotnet/src/Microsoft.Agents.AI/ChatClient/ServiceStoredSimulatingChatClient.cs rename to dotnet/src/Microsoft.Agents.AI/ChatClient/PerServiceCallChatHistoryPersistingChatClient.cs index f7dafa303d..200baf7b94 100644 --- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ServiceStoredSimulatingChatClient.cs +++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/PerServiceCallChatHistoryPersistingChatClient.cs @@ -11,23 +11,39 @@ namespace Microsoft.Agents.AI; /// -/// A delegating chat client that simulates service-stored chat history behavior using -/// framework-managed instances. +/// A delegating chat client that persists chat history and updates session state after each +/// individual service call within the loop. /// /// /// /// This decorator is intended to operate between the and the leaf -/// in a pipeline. +/// in a pipeline. It is activated when +/// is . /// /// -/// Before each service call, it loads chat history from the agent's -/// 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 -/// on the response so that the -/// treats the conversation as service-managed — -/// clearing accumulated history between iterations and not injecting duplicate -/// during approval-response processing. +/// When active, it handles two complementary scenarios: /// +/// +/// +/// Framework-managed chat history +/// +/// Before each service call, the decorator loads history from the agent's +/// and prepends it to the request messages. After each successful call, it persists new messages to +/// the provider and returns a sentinel so that +/// treats the conversation as service-managed — clearing +/// accumulated history between iterations and not injecting duplicate +/// during approval-response processing. +/// +/// +/// +/// Service-stored chat history +/// +/// When the underlying service manages its own chat history (real ), +/// the decorator updates after each service call so +/// that intermediate ConversationId changes are captured immediately rather than only at the end of the run. +/// +/// +/// /// /// This chat client must be used within the context of a running . It retrieves the /// current agent and session from , which is set automatically when an agent's @@ -38,7 +54,7 @@ namespace Microsoft.Agents.AI; /// available or if the agent is not a . /// /// -internal sealed class ServiceStoredSimulatingChatClient : DelegatingChatClient +internal sealed class PerServiceCallChatHistoryPersistingChatClient : DelegatingChatClient { /// /// A sentinel value returned on to signal @@ -59,10 +75,10 @@ internal sealed class ServiceStoredSimulatingChatClient : DelegatingChatClient internal const string LocalHistoryConversationId = "_agent_local_chat_history"; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The underlying chat client that will handle the core operations. - public ServiceStoredSimulatingChatClient(IChatClient innerClient) + public PerServiceCallChatHistoryPersistingChatClient(IChatClient innerClient) : base(innerClient) { } @@ -237,18 +253,18 @@ private static (ChatClientAgent Agent, ChatClientAgentSession Session) GetRequir { 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() ?? 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"}'."); } diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentTestHelper.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentTestHelper.cs index b3296a1306..d7ab9b2808 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentTestHelper.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentTestHelper.cs @@ -14,7 +14,7 @@ namespace Microsoft.Agents.AI.UnitTests; /// /// Shared test helper for integration tests that verify -/// end-to-end behavior with and +/// end-to-end behavior with and /// . /// internal static class ChatClientAgentTestHelper diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_ApprovalsTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_ApprovalsTests.cs index f3550359dc..36bd2c70dd 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_ApprovalsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_ApprovalsTests.cs @@ -9,7 +9,7 @@ namespace Microsoft.Agents.AI.UnitTests; /// /// Contains unit tests that verify the end-to-end approval flow behavior of the -/// class with , +/// class with , /// ensuring that chat history is correctly persisted across multi-turn approval interactions. /// public class ChatClientAgent_ApprovalsTests @@ -48,7 +48,7 @@ public async Task RunAsync_ApprovalRequired_PerServiceCallPersistence_PersistsCo agentOptions: new() { ChatOptions = new() { Tools = [approvalTool] }, - SimulateServiceStoredChatHistory = true, + RequirePerServiceCallChatHistoryPersistence = true, }, callIndex: callIndex, capturedInputs: capturedInputs); @@ -260,7 +260,7 @@ public async Task RunAsync_ApprovalRejected_PersistsRejectionInHistoryAsync() agentOptions: new() { ChatOptions = new() { Tools = [approvalTool] }, - SimulateServiceStoredChatHistory = true, + RequirePerServiceCallChatHistoryPersistence = true, }, callIndex: callIndex, capturedInputs: capturedInputs); diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_ChatHistoryManagementTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_ChatHistoryManagementTests.cs index 83bdc2b2a2..410ee4edda 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_ChatHistoryManagementTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_ChatHistoryManagementTests.cs @@ -520,7 +520,7 @@ await ChatClientAgentTestHelper.RunAsync( agentOptions: new() { ChatOptions = new() { Instructions = "Be helpful" }, - SimulateServiceStoredChatHistory = true, + RequirePerServiceCallChatHistoryPersistence = true, }, expectedServiceCallCount: 1, expectedHistory: @@ -554,7 +554,7 @@ await ChatClientAgentTestHelper.RunAsync( agentOptions: new() { ChatOptions = new() { Tools = [tool] }, - SimulateServiceStoredChatHistory = true, + RequirePerServiceCallChatHistoryPersistence = true, }, expectedServiceCallCount: 2, expectedHistory: diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ServiceStoredSimulatingChatClientTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/PerServiceCallChatHistoryPersistingChatClientTests.cs similarity index 94% rename from dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ServiceStoredSimulatingChatClientTests.cs rename to dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/PerServiceCallChatHistoryPersistingChatClientTests.cs index ea346dade9..8613d37747 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ServiceStoredSimulatingChatClientTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/PerServiceCallChatHistoryPersistingChatClientTests.cs @@ -13,15 +13,15 @@ namespace Microsoft.Agents.AI.UnitTests; /// -/// Contains unit tests for the decorator, +/// Contains unit tests for the decorator, /// verifying that it persists messages via the after each /// individual service call by default, or marks messages for end-of-run persistence when the -/// option is enabled. +/// option is enabled. /// -public class ServiceStoredSimulatingChatClientTests +public class PerServiceCallChatHistoryPersistingChatClientTests { /// - /// Verifies that by default (SimulateServiceStoredChatHistory is false), + /// Verifies that by default (RequirePerServiceCallChatHistoryPersistence is false), /// the ChatHistoryProvider receives messages after a successful non-streaming call. /// [Fact] @@ -50,7 +50,7 @@ public async Task RunAsync_PersistsMessagesPerServiceCall_ByDefaultAsync() ChatClientAgent agent = new(mockService.Object, options: new() { ChatHistoryProvider = mockChatHistoryProvider.Object, - SimulateServiceStoredChatHistory = true, + RequirePerServiceCallChatHistoryPersistence = true, }); // Act @@ -97,7 +97,7 @@ public async Task RunAsync_PersistsMessagesAtEndOfRun_WhenOptionEnabledAsync() ChatClientAgent agent = new(mockService.Object, options: new() { ChatHistoryProvider = mockChatHistoryProvider.Object, - SimulateServiceStoredChatHistory = true, + RequirePerServiceCallChatHistoryPersistence = true, }); // Act @@ -145,7 +145,7 @@ public async Task RunAsync_NotifiesProviderOfFailure_WhenPerServiceCallPersisten ChatClientAgent agent = new(mockService.Object, options: new() { ChatHistoryProvider = mockChatHistoryProvider.Object, - SimulateServiceStoredChatHistory = true, + RequirePerServiceCallChatHistoryPersistence = true, }); // Act @@ -163,7 +163,7 @@ public async Task RunAsync_NotifiesProviderOfFailure_WhenPerServiceCallPersisten } /// - /// Verifies that the decorator is NOT injected by default (SimulateServiceStoredChatHistory is false). + /// Verifies that the decorator is NOT injected by default (RequirePerServiceCallChatHistoryPersistence is false). /// [Fact] public void ChatClient_DoesNotContainDecorator_ByDefault() @@ -175,15 +175,15 @@ public void ChatClient_DoesNotContainDecorator_ByDefault() ChatClientAgent agent = new(mockService.Object, options: new()); // Assert - var decorator = agent.ChatClient.GetService(); + var decorator = agent.ChatClient.GetService(); Assert.Null(decorator); } /// - /// Verifies that the decorator is injected when SimulateServiceStoredChatHistory is true. + /// Verifies that the decorator is injected when RequirePerServiceCallChatHistoryPersistence is true. /// [Fact] - public void ChatClient_ContainsDecorator_WhenSimulateServiceStoredChatHistory() + public void ChatClient_ContainsDecorator_WhenRequirePerServiceCallChatHistoryPersistence() { // Arrange Mock mockService = new(); @@ -191,11 +191,11 @@ public void ChatClient_ContainsDecorator_WhenSimulateServiceStoredChatHistory() // Act ChatClientAgent agent = new(mockService.Object, options: new() { - SimulateServiceStoredChatHistory = true, + RequirePerServiceCallChatHistoryPersistence = true, }); // Assert - var decorator = agent.ChatClient.GetService(); + var decorator = agent.ChatClient.GetService(); Assert.NotNull(decorator); } @@ -215,27 +215,27 @@ public void ChatClient_DoesNotContainDecorator_WhenUseProvidedChatClientAsIs() }); // Assert - var decorator = agent.ChatClient.GetService(); + var decorator = agent.ChatClient.GetService(); Assert.Null(decorator); } /// - /// Verifies that the SimulateServiceStoredChatHistory option is included in Clone(). + /// Verifies that the RequirePerServiceCallChatHistoryPersistence option is included in Clone(). /// [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); } /// @@ -289,7 +289,7 @@ public async Task RunAsync_PersistsPerServiceCall_DuringFunctionInvocationLoopAs { ChatOptions = new() { Tools = [tool] }, ChatHistoryProvider = mockChatHistoryProvider.Object, - SimulateServiceStoredChatHistory = true, + RequirePerServiceCallChatHistoryPersistence = true, }, services: new ServiceCollection().BuildServiceProvider()); // Act @@ -358,7 +358,7 @@ public async Task RunStreamingAsync_PersistsMessagesPerServiceCall_ByDefaultAsyn ChatClientAgent agent = new(mockService.Object, options: new() { ChatHistoryProvider = mockChatHistoryProvider.Object, - SimulateServiceStoredChatHistory = true, + RequirePerServiceCallChatHistoryPersistence = true, }); // Act @@ -407,7 +407,7 @@ public async Task RunAsync_NotifiesAIContextProviders_ByDefaultAsync() ChatClientAgent agent = new(mockService.Object, options: new() { AIContextProviders = [mockContextProvider.Object], - SimulateServiceStoredChatHistory = true, + RequirePerServiceCallChatHistoryPersistence = true, }); // Act @@ -454,7 +454,7 @@ public async Task RunAsync_NotifiesAIContextProvidersOfFailure_ByDefaultAsync() ChatClientAgent agent = new(mockService.Object, options: new() { AIContextProviders = [mockContextProvider.Object], - SimulateServiceStoredChatHistory = true, + RequirePerServiceCallChatHistoryPersistence = true, }); // Act @@ -513,7 +513,7 @@ public async Task RunAsync_NotifiesBothProviders_ByDefaultAsync() { ChatHistoryProvider = mockChatHistoryProvider.Object, AIContextProviders = [mockContextProvider.Object], - SimulateServiceStoredChatHistory = true, + RequirePerServiceCallChatHistoryPersistence = true, }); // Act @@ -587,7 +587,7 @@ public async Task RunAsync_DoesNotReNotifyResponseMessagesAsRequestMessages_Duri { ChatOptions = new() { Tools = [tool] }, ChatHistoryProvider = mockChatHistoryProvider.Object, - SimulateServiceStoredChatHistory = true, + RequirePerServiceCallChatHistoryPersistence = true, }, services: new ServiceCollection().BuildServiceProvider()); // Act @@ -652,7 +652,7 @@ public async Task RunAsync_DeduplicatesRequestMessages_OnFailureDuringFicLoopAsy { ChatOptions = new() { Tools = [tool] }, ChatHistoryProvider = mockChatHistoryProvider.Object, - SimulateServiceStoredChatHistory = true, + RequirePerServiceCallChatHistoryPersistence = true, }, services: new ServiceCollection().BuildServiceProvider()); // Act @@ -720,8 +720,8 @@ private static async IAsyncEnumerable CreateAsyncEnumerableA /// /// Verifies that when per-service-call persistence is active and no real conversation ID exists, - /// sets the - /// sentinel on the chat options and strips it before + /// sets the + /// sentinel on the chat options and strips it before /// forwarding to the inner client. /// [Fact] @@ -741,7 +741,7 @@ public async Task RunAsync_SetsAndStripsSentinelConversationId_WhenPerServiceCal ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test" }, - SimulateServiceStoredChatHistory = true, + RequirePerServiceCallChatHistoryPersistence = true, }); // Act @@ -773,7 +773,7 @@ public async Task RunAsync_DoesNotSetSentinel_WhenEndOfRunPersistenceEnabledAsyn ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test" }, - SimulateServiceStoredChatHistory = true, + RequirePerServiceCallChatHistoryPersistence = true, }); // Act @@ -808,7 +808,7 @@ public async Task RunAsync_DoesNotSetSentinel_WhenRealConversationIdExistsAsync( ChatClientAgent agent = new(mockService.Object, options: new() { - SimulateServiceStoredChatHistory = true, + RequirePerServiceCallChatHistoryPersistence = true, }); // Create a session with a real conversation ID. @@ -842,7 +842,7 @@ public async Task RunStreamingAsync_SetsAndStripsSentinelConversationId_WhenPerS ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test" }, - SimulateServiceStoredChatHistory = true, + RequirePerServiceCallChatHistoryPersistence = true, }); // Act @@ -862,7 +862,7 @@ public async Task RunStreamingAsync_SetsAndStripsSentinelConversationId_WhenPerS /// skip provider resolution in the agent (the decorator handles it). /// [Fact] - public async Task RunAsync_SetsSentinelOnSession_WhenSimulateServiceStoredChatHistoryActiveAsync() + public async Task RunAsync_SetsSentinelOnSession_WhenRequirePerServiceCallChatHistoryPersistenceActiveAsync() { // Arrange Mock mockService = new(); @@ -875,7 +875,7 @@ public async Task RunAsync_SetsSentinelOnSession_WhenSimulateServiceStoredChatHi ChatClientAgent agent = new(mockService.Object, options: new() { - SimulateServiceStoredChatHistory = true, + RequirePerServiceCallChatHistoryPersistence = true, }); // Act @@ -883,7 +883,7 @@ public async Task RunAsync_SetsSentinelOnSession_WhenSimulateServiceStoredChatHi 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); } /// @@ -924,7 +924,7 @@ public async Task RunAsync_Throws_WhenServiceReturnsRealConversationIdWithChatHi 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 async Task RunAsync_NotifiesProvidersAndUpdatesSession_WhenRequestHasReal ChatClientAgent agent = new(mockService.Object, options: new() { - SimulateServiceStoredChatHistory = true, + RequirePerServiceCallChatHistoryPersistence = true, AIContextProviders = [mockContextProvider.Object], }); @@ -1025,7 +1025,7 @@ public async Task RunAsync_NotifiesProvidersOfFailure_WhenRequestHasRealConversa ChatClientAgent agent = new(mockService.Object, options: new() { - SimulateServiceStoredChatHistory = true, + RequirePerServiceCallChatHistoryPersistence = true, AIContextProviders = [mockContextProvider.Object], }); @@ -1077,7 +1077,7 @@ public async Task RunStreamingAsync_NotifiesProvidersAndUpdatesSession_WhenReque ChatClientAgent agent = new(mockService.Object, options: new() { - SimulateServiceStoredChatHistory = true, + RequirePerServiceCallChatHistoryPersistence = true, AIContextProviders = [mockContextProvider.Object], }); @@ -1137,7 +1137,7 @@ public async Task RunAsync_NotifiesProvidersAndUpdatesSession_WhenServiceReturns // 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 async Task RunStreamingAsync_NotifiesProvidersAndUpdatesSession_WhenServi // 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 async Task RunAsync_SkipsSimulation_WhenAllowBackgroundResponsesAsync() ChatClientAgent agent = new(mockService.Object, options: new() { ChatHistoryProvider = mockChatHistoryProvider.Object, - SimulateServiceStoredChatHistory = true, + RequirePerServiceCallChatHistoryPersistence = true, }); // Act @@ -1270,7 +1270,7 @@ await agent.RunAsync( 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); } /// @@ -1291,7 +1291,7 @@ public async Task RunStreamingAsync_SkipsSimulation_WhenAllowBackgroundResponses ChatClientAgent agent = new(mockService.Object, options: new() { - SimulateServiceStoredChatHistory = true, + RequirePerServiceCallChatHistoryPersistence = true, }); // Act @@ -1309,6 +1309,6 @@ public async Task RunStreamingAsync_SkipsSimulation_WhenAllowBackgroundResponses Assert.NotEmpty(updates); // Assert — session should NOT have the sentinel - Assert.NotEqual(ServiceStoredSimulatingChatClient.LocalHistoryConversationId, session!.ConversationId); + Assert.NotEqual(PerServiceCallChatHistoryPersistingChatClient.LocalHistoryConversationId, session!.ConversationId); } }