From 8ec17c5e3ac8160343ff6ed523a2ac3f0f3a5a17 Mon Sep 17 00:00:00 2001 From: Benjamin Michaelis Date: Thu, 7 May 2026 22:55:41 -0700 Subject: [PATCH 1/2] Move system prompt to Instructions to prevent accumulation across turns When using previous_response_id, the system prompt was being added as a ResponseItem.CreateSystemMessageItem in every turn's input list. Since OpenAI already holds the full conversation server-side, this caused the system prompt to accumulate in context on each turn and each tool-call leg. Fix: Set the system prompt via ResponseCreationOptions.Instructions in CreateResponseOptionsAsync instead. Per the Azure OpenAI API docs, Instructions is stateless across turns when using previous_response_id, so it is applied fresh each turn without accumulating. Both GetChatCompletionCore and GetChatCompletionStream now always pass only [ResponseItem.CreateUserMessageItem(prompt)] as input items. CreateResponseOptionsAsync is no longer static since it accesses _Options. --- .../Services/AIChatService.cs | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/EssentialCSharp.Chat.Shared/Services/AIChatService.cs b/EssentialCSharp.Chat.Shared/Services/AIChatService.cs index 8dfab8c4..356bf5e7 100644 --- a/EssentialCSharp.Chat.Shared/Services/AIChatService.cs +++ b/EssentialCSharp.Chat.Shared/Services/AIChatService.cs @@ -54,9 +54,9 @@ public AIChatService(IOptions options, AISearchService searchService, bool enableContextualSearch = false, CancellationToken cancellationToken = default) { - var responseOptions = await CreateResponseOptionsAsync(previousResponseId, tools, reasoningEffortLevel, mcpClient: mcpClient, cancellationToken: cancellationToken); + var responseOptions = await CreateResponseOptionsAsync(systemPrompt, previousResponseId, tools, reasoningEffortLevel, mcpClient: mcpClient, cancellationToken: cancellationToken); var enrichedPrompt = await EnrichPromptWithContext(prompt, enableContextualSearch, cancellationToken); - return await GetChatCompletionCore(enrichedPrompt, responseOptions, systemPrompt, cancellationToken); + return await GetChatCompletionCore(enrichedPrompt, responseOptions, cancellationToken); } /// @@ -82,17 +82,12 @@ public AIChatService(IOptions options, AISearchService searchService, bool enableContextualSearch = false, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default) { - var responseOptions = await CreateResponseOptionsAsync(previousResponseId, tools, reasoningEffortLevel, mcpClient: mcpClient, cancellationToken: cancellationToken); + var responseOptions = await CreateResponseOptionsAsync(systemPrompt, previousResponseId, tools, reasoningEffortLevel, mcpClient: mcpClient, cancellationToken: cancellationToken); var enrichedPrompt = await EnrichPromptWithContext(prompt, enableContextualSearch, cancellationToken); - // Construct the user input with system context if provided - var systemContext = !string.IsNullOrWhiteSpace(systemPrompt) ? systemPrompt : _Options.SystemPrompt; - // Create the streaming response using the Responses API #pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - List responseItems = systemContext is not null - ? [ResponseItem.CreateSystemMessageItem(systemContext), ResponseItem.CreateUserMessageItem(enrichedPrompt)] - : [ResponseItem.CreateUserMessageItem(enrichedPrompt)]; + List responseItems = [ResponseItem.CreateUserMessageItem(enrichedPrompt)]; #pragma warning restore OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. var streamingUpdates = _ResponseClient.CreateResponseStreamingAsync( responseItems, @@ -254,7 +249,8 @@ private async Task EnrichPromptWithContext(string prompt, bool enableCon /// Creates response options with optional features /// #pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - private static async Task CreateResponseOptionsAsync( + private async Task CreateResponseOptionsAsync( + string? systemPrompt = null, string? previousResponseId = null, IEnumerable? tools = null, ResponseReasoningEffortLevel? reasoningEffortLevel = null, @@ -265,6 +261,14 @@ private static async Task CreateResponseOptionsAsync( var options = new ResponseCreationOptions(); #pragma warning restore OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + // Set the system prompt via Instructions — this is stateless across turns when using previous_response_id, + // preventing accumulation of system messages in the conversation context. + var resolvedSystemPrompt = !string.IsNullOrWhiteSpace(systemPrompt) ? systemPrompt : _Options.SystemPrompt; + if (resolvedSystemPrompt is not null) + { + options.Instructions = resolvedSystemPrompt; + } + // Add conversation context if available if (!string.IsNullOrEmpty(previousResponseId)) { @@ -312,20 +316,13 @@ private static async Task CreateResponseOptionsAsync( #pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. ResponseCreationOptions responseOptions, #pragma warning restore OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - string? systemPrompt = null, CancellationToken cancellationToken = default) { - // Construct the user input with system context if provided - var systemContext = !string.IsNullOrWhiteSpace(systemPrompt) ? systemPrompt : _Options.SystemPrompt; - - // Create the streaming response using the Responses API + // Create the response using the Responses API #pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - List responseItems = systemContext is not null - ? [ResponseItem.CreateSystemMessageItem(systemContext), ResponseItem.CreateUserMessageItem(prompt)] - : [ResponseItem.CreateUserMessageItem(prompt)]; + List responseItems = [ResponseItem.CreateUserMessageItem(prompt)]; #pragma warning restore OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - // Create the response using the Responses API var response = await _ResponseClient.CreateResponseAsync( responseItems, options: responseOptions, From 12b0886dce1f71773847a606ebda941c47aa7a7f Mon Sep 17 00:00:00 2001 From: Benjamin Michaelis Date: Fri, 8 May 2026 19:13:06 -0700 Subject: [PATCH 2/2] Guard Instructions with IsNullOrWhiteSpace to avoid sending empty string --- EssentialCSharp.Chat.Shared/Services/AIChatService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EssentialCSharp.Chat.Shared/Services/AIChatService.cs b/EssentialCSharp.Chat.Shared/Services/AIChatService.cs index 356bf5e7..a9a29e74 100644 --- a/EssentialCSharp.Chat.Shared/Services/AIChatService.cs +++ b/EssentialCSharp.Chat.Shared/Services/AIChatService.cs @@ -264,7 +264,7 @@ private async Task CreateResponseOptionsAsync( // Set the system prompt via Instructions — this is stateless across turns when using previous_response_id, // preventing accumulation of system messages in the conversation context. var resolvedSystemPrompt = !string.IsNullOrWhiteSpace(systemPrompt) ? systemPrompt : _Options.SystemPrompt; - if (resolvedSystemPrompt is not null) + if (!string.IsNullOrWhiteSpace(resolvedSystemPrompt)) { options.Instructions = resolvedSystemPrompt; }