From 0590e3b5daef6a0c02215a7f813fca6380f5c465 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Sun, 5 Oct 2025 10:44:11 -0400 Subject: [PATCH] Fix Assistants IChatClient handling of unrelated tool calls in history The implementation is assuming that any tool results in the incoming message history mean a thread ID is required, which would be necessary if those tool results were for an active run. But that's not the case if the history just contains historical function calls/responses, as is the case in agent scenarios where the message history from one agent is passed to another. The fix is to just stop throwing based on this incorrect assumption. --- .../Microsoft.Extensions.AI.OpenAI/CHANGELOG.md | 2 ++ .../OpenAIAssistantsChatClient.cs | 12 ++++-------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md index eb7152558ad..589790cd8ee 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md @@ -2,6 +2,8 @@ ## NOT YET RELEASED +- Fixed issue with IChatClient for Assistants API where a chat history including unrelated function calls would cause an exception. + ## 9.9.1-preview.1.25474.6 - Updated to depend on OpenAI 2.5.0. diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs index ea682a11beb..20bc87dd9f3 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs @@ -80,14 +80,10 @@ public async IAsyncEnumerable GetStreamingResponseAsync( // Get the thread ID. string? threadId = options?.ConversationId ?? _defaultThreadId; - if (threadId is null && toolResults is not null) - { - Throw.ArgumentException(nameof(messages), "No thread ID was provided, but chat messages includes tool results."); - } // Get any active run ID for this thread. This is necessary in case a thread has been left with an - // active run, in which all attempts other than submitting tools will fail. We thus need to cancel - // any active run on the thread. + // active run, in which case all attempts other than submitting tools will fail. We thus need to cancel + // any active run on the thread if we're not submitting tool results to it. ThreadRun? threadRun = null; if (threadId is not null) { @@ -111,7 +107,7 @@ public async IAsyncEnumerable GetStreamingResponseAsync( ConvertFunctionResultsToToolOutput(toolResults, out List? toolOutputs) is { } toolRunId && toolRunId == threadRun.Id) { - // There's an active run and we have tool results to submit, so submit the results and continue streaming. + // There's an active run and, critically, we have tool results to submit for that exact run, so submit the results and continue streaming. // This is going to ignore any additional messages in the run options, as we are only submitting tool outputs, // but there doesn't appear to be a way to submit additional messages, and having such additional messages is rare. updates = _client.SubmitToolOutputsToRunStreamingAsync(threadRun.ThreadId, threadRun.Id, toolOutputs, cancellationToken); @@ -487,7 +483,7 @@ void AppendSystemInstructions(string? toAppend) messageContents.Add(MessageContent.FromImageUri(image.Uri)); break; - case FunctionResultContent result: + case FunctionResultContent result when chatMessage.Role == ChatRole.Tool: (functionResults ??= []).Add(result); break; }