semantic-kernel icon indicating copy to clipboard operation
semantic-kernel copied to clipboard

.Net: Bug: ChatHistory function call ordering is broken in streaming mode

Open bjornhandersson opened this issue 2 months ago • 4 comments

When using GetStreamingChatMessageContentsAsync with function calls, the results are added to ChatHistory in order of completion rather than order of execution causing error when submitting history in subsequence "chats" to bedrock.

Expected toolResult blocks at messages.7.content for the following Ids: tooluse_DiChMHwyRF2Ww0ib_XXXXX, but found: tooluse_IH0P-g2aTgi_ufQ0OXXXXX

To Reproduce See code in comment below

Expected behavior Bedrock tool use and tool result should maintain the order in the history.

Platform

  • Language: C#
  • AI model: Bedrock - claude sonnet 4.5
  • Versions
    • Microsoft.SemanticKernel: 1.65
    • Microsoft.SemanticKernel.Connectors.Amazon: 1.65-alpha

bjornhandersson avatar Oct 11 '25 15:10 bjornhandersson

Reproduce:

using System.ComponentModel;
using System.Text;
using System.Text.Json;
using Amazon;
using Amazon.BedrockAgent;
using Amazon.BedrockAgentRuntime;
using Amazon.BedrockRuntime;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;

var builder = Kernel.CreateBuilder();
builder.AddBedrockChatClient(modelId: "us.anthropic.claude-sonnet-4-5-20250929-v1:0");

builder
    .Services.AddSingleton<AmazonBedrockAgentClient>()
    .AddSingleton<AmazonBedrockAgentRuntimeClient>()
    .AddTransient(s => s.GetRequiredService<IChatClient>().AsChatCompletionService());

builder.Plugins.AddFromType<GreetingsPlugin>();
builder.Plugins.AddFromType<RandomNamePlugin>();

var kernel = builder.Build();

var executionSettings = new PromptExecutionSettings
{
    FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(
        autoInvoke: false,
        options: new FunctionChoiceBehaviorOptions
        {
            AllowParallelCalls = false,
            AllowConcurrentInvocation = false,
        }
    ),
};

var chatService = kernel.GetRequiredService<IChatCompletionService>();
var history = new ChatHistory();
history.AddUserMessage("Greet 5 random persons");

try
{
    while (true)
    {
        var responses = chatService.GetStreamingChatMessageContentsAsync(
            history,
            executionSettings,
            kernel,
            CancellationToken.None
        );

        var assistantMessage = new StringBuilder();
        await foreach (var response in responses)
        {
            if (response.Content != null)
            {
                Console.Write(response.Content);
                assistantMessage.Append(response.Content);
            }
        }

        var assistantMessageStr = assistantMessage.ToString();
        if (string.IsNullOrEmpty(assistantMessageStr))
        {
            history.AddUserMessage(assistantMessageStr);
        }

        history.AddUserMessage("Greet 5 more");
    }
}
catch (Exception ex)
{
    Console.WriteLine(
        JsonSerializer.Serialize(history, new JsonSerializerOptions { WriteIndented = true })
    );
}

public class GreetingsPlugin
{
    [KernelFunction, Description("Greet a person by name")]
    public string Greet(string name)
    {
        return $"Hello, {name}!";
    }
}

public class RandomNamePlugin
{
    private static readonly string[] Names =
    {
        "Alice",
        "Bob",
        "Charlie",
        "Diana",
        "Ethan",
        "Fiona",
        "George",
        "Hannah",
        "Ian",
        "Julia",
    };

    [KernelFunction, Description("Get a random name")]
    public string GetRandomName()
    {
        var random = new Random();
        var name = Names[random.Next(Names.Length)];
        Console.WriteLine($"Tool called: GetRandomName() -> {name}");
        return name;
    }
}

Serialized history from above code:

[
  {
    "Role": {
      "Label": "user"
    },
    "Items": [
      {
        "$type": "TextContent",
        "Text": "Greet 5 random person s"
      }
    ]
  },
  {
    "Role": {
      "Label": "assistant"
    },
    "Items": [
      {
        "$type": "FunctionCallContent",
        "Id": "tooluse_t-7l3xEeTUacH0QC0jcmng",
        "FunctionName": "RandomNamePlugin_GetRandomName"
      }
    ],
    "Metadata": {
      "Usage": null
    }
  },
  {
    "Role": {
      "Label": "assistant"
    },
    "Items": [
      {
        "$type": "FunctionCallContent",
        "Id": "tooluse_nUOe7NH7Q3ynXiAOhWZNmg",
        "FunctionName": "RandomNamePlugin_GetRandomName"
      }
    ],
    "Metadata": {
      "Usage": null
    }
  },
  {
    "Role": {
      "Label": "assistant"
    },
    "Items": [
      {
        "$type": "FunctionCallContent",
        "Id": "tooluse_Lj3yYrwJSVi_mA9fQ-qhyQ",
        "FunctionName": "RandomNamePlugin_GetRandomName"
      }
    ],
    "Metadata": {
      "Usage": null
    }
  },
  {
    "Role": {
      "Label": "assistant"
    },
    "Items": [
      {
        "$type": "FunctionCallContent",
        "Id": "tooluse_3DZKKlTGSSegGYI9QgyOFg",
        "FunctionName": "RandomNamePlugin_GetRandomName"
      }
    ],
    "Metadata": {
      "Usage": null
    }
  },
  {
    "Role": {
      "Label": "assistant"
    },
    "Items": [
      {
        "$type": "FunctionCallContent",
        "Id": "tooluse_9_DauDhvR92JNivoBOweYA",
        "FunctionName": "RandomNamePlugin_GetRandomName"
      }
    ],
    "Metadata": {
      "Usage": null
    }
  },
  {
    "Role": {
      "Label": "user"
    },
    "Items": [
      {
        "$type": "TextContent",
        "Text": "Greet 5 more"
      }
    ]
  }
]

bjornhandersson avatar Oct 11 '25 16:10 bjornhandersson

@bjornhandersson Thanks for creating this issue. Are you interested in creating a fix?

markwallace-microsoft avatar Oct 17 '25 14:10 markwallace-microsoft

@bjornhandersson Thanks for creating this issue. Are you interested in creating a fix?

Sure! Made a PR from my fork #13260

bjornhandersson avatar Oct 17 '25 21:10 bjornhandersson

@bjornhandersson, I was wondering whether you actually need IChatCompletionService? I'm asking because you are getting an IChatClient and converting it to IChatCompletionService. For new code we typically recommend that folks just use the 'IChatClient' interface directly. It is similar to IChatCompletionService, but has broader support and will have more investment going forward. E.g. Semantic Kernel Agents are built on top of IChatCompletionService, but the new Microsoft Agent Framework is being built on top of 'IChatClient'.

The two abstractions do have different input and output types, but they are broadly similar. Here is some documentation on using IChatClient: https://learn.microsoft.com/en-us/dotnet/ai/quickstarts/build-chat-app?pivots=openai

westey-m avatar Oct 23 '25 10:10 westey-m