diff --git a/dotnet/src/Connectors/Connectors.Amazon.UnitTests/Core/BedrockModelUtilitiesTests.cs b/dotnet/src/Connectors/Connectors.Amazon.UnitTests/Core/BedrockModelUtilitiesTests.cs new file mode 100644 index 000000000000..4e00a010f7d5 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Amazon.UnitTests/Core/BedrockModelUtilitiesTests.cs @@ -0,0 +1,380 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using Amazon.BedrockRuntime; +using Amazon.BedrockRuntime.Model; +using Microsoft.SemanticKernel.ChatCompletion; +using Xunit; + +namespace Microsoft.SemanticKernel.Connectors.Amazon.UnitTests; + +/// +/// Unit tests for BedrockModelUtilities, specifically the BuildMessageList method +/// and its handling of tool call / tool result merging. +/// +public sealed class BedrockModelUtilitiesTests +{ + /// + /// Verifies that simple text messages are converted correctly. + /// + [Fact] + public void BuildMessageListShouldConvertSimpleTextMessages() + { + // Arrange + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Hello"); + chatHistory.AddAssistantMessage("Hi there"); + chatHistory.AddUserMessage("How are you?"); + + // Act + var messages = Core.BedrockModelUtilities.BuildMessageList(chatHistory); + + // Assert + Assert.Equal(3, messages.Count); + Assert.Equal(ConversationRole.User, messages[0].Role); + Assert.Equal("Hello", messages[0].Content[0].Text); + Assert.Equal(ConversationRole.Assistant, messages[1].Role); + Assert.Equal("Hi there", messages[1].Content[0].Text); + Assert.Equal(ConversationRole.User, messages[2].Role); + Assert.Equal("How are you?", messages[2].Content[0].Text); + } + + /// + /// Verifies that system messages are excluded from the message list. + /// + [Fact] + public void BuildMessageListShouldExcludeSystemMessages() + { + // Arrange + var chatHistory = new ChatHistory(); + chatHistory.AddSystemMessage("You are an AI assistant"); + chatHistory.AddUserMessage("Hello"); + + // Act + var messages = Core.BedrockModelUtilities.BuildMessageList(chatHistory); + + // Assert + Assert.Single(messages); + Assert.Equal(ConversationRole.User, messages[0].Role); + Assert.Equal("Hello", messages[0].Content[0].Text); + } + + /// + /// Verifies that consecutive assistant messages with FunctionCallContent are merged into a single message. + /// This is the core scenario from issue #13647. + /// + [Fact] + public void BuildMessageListShouldMergeConsecutiveAssistantToolCalls() + { + // Arrange + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Get feeds and distribution list"); + + // First assistant message with a tool call + var assistantMessage1 = new ChatMessageContent(AuthorRole.Assistant, (string?)null); + assistantMessage1.Items.Add(new FunctionCallContent( + functionName: "GetFeeds", + pluginName: "Feeds", + id: "tooluse_G64hibpFmRqXEcAYwOfP5s")); + chatHistory.Add(assistantMessage1); + + // Second assistant message with another tool call (parallel tool invocation) + var assistantMessage2 = new ChatMessageContent(AuthorRole.Assistant, (string?)null); + assistantMessage2.Items.Add(new FunctionCallContent( + functionName: "GetTeamsDistributionList", + pluginName: "DistributionList", + id: "tooluse_sMmlRWbbCGd0lnhiLjvk8H", + arguments: new KernelArguments { { "distributionListName", "developers" } })); + chatHistory.Add(assistantMessage2); + + // Act + var messages = Core.BedrockModelUtilities.BuildMessageList(chatHistory); + + // Assert + Assert.Equal(2, messages.Count); // user + single merged assistant + + // The assistant message should have been merged + var assistantMsg = messages[1]; + Assert.Equal(ConversationRole.Assistant, assistantMsg.Role); + Assert.Equal(2, assistantMsg.Content.Count); + + // First tool use + Assert.NotNull(assistantMsg.Content[0].ToolUse); + Assert.Equal("tooluse_G64hibpFmRqXEcAYwOfP5s", assistantMsg.Content[0].ToolUse.ToolUseId); + Assert.Equal("Feeds-GetFeeds", assistantMsg.Content[0].ToolUse.Name); + + // Second tool use + Assert.NotNull(assistantMsg.Content[1].ToolUse); + Assert.Equal("tooluse_sMmlRWbbCGd0lnhiLjvk8H", assistantMsg.Content[1].ToolUse.ToolUseId); + Assert.Equal("DistributionList-GetTeamsDistributionList", assistantMsg.Content[1].ToolUse.Name); + } + + /// + /// Verifies that consecutive tool result messages are merged into a single user message. + /// This is part of the scenario from issue #13647. + /// + [Fact] + public void BuildMessageListShouldMergeConsecutiveToolResults() + { + // Arrange + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Get feeds and distribution list"); + + // Assistant with tool calls (single message with multiple items) + var assistantMessage = new ChatMessageContent(AuthorRole.Assistant, (string?)null); + assistantMessage.Items.Add(new FunctionCallContent( + functionName: "GetFeeds", + pluginName: "Feeds", + id: "tooluse_G64hibpFmRqXEcAYwOfP5s")); + assistantMessage.Items.Add(new FunctionCallContent( + functionName: "GetTeamsDistributionList", + pluginName: "DistributionList", + id: "tooluse_sMmlRWbbCGd0lnhiLjvk8H", + arguments: new KernelArguments { { "distributionListName", "developers" } })); + chatHistory.Add(assistantMessage); + + // First tool result + var toolResult1 = new ChatMessageContent(AuthorRole.Tool, (string?)null); + toolResult1.Items.Add(new FunctionResultContent( + functionName: "GetFeeds", + pluginName: "Feeds", + callId: "tooluse_G64hibpFmRqXEcAYwOfP5s", + result: "Dataset DataReference: memory://8df2be02a23c4ebfb77e1170dea6c4b4. Dataset size: 78 items.")); + chatHistory.Add(toolResult1); + + // Second tool result + var toolResult2 = new ChatMessageContent(AuthorRole.Tool, (string?)null); + toolResult2.Items.Add(new FunctionResultContent( + functionName: "GetTeamsDistributionList", + pluginName: "DistributionList", + callId: "tooluse_sMmlRWbbCGd0lnhiLjvk8H", + result: "19:791df3464b4f4c3fac8429041e8e2540@thread.v2")); + chatHistory.Add(toolResult2); + + // Act + var messages = Core.BedrockModelUtilities.BuildMessageList(chatHistory); + + // Assert + Assert.Equal(3, messages.Count); // user + assistant + single merged tool results (as user) + + // Tool results should be merged into a single user message + var toolResultMsg = messages[2]; + Assert.Equal(ConversationRole.User, toolResultMsg.Role); // Tool role maps to User in Bedrock + Assert.Equal(2, toolResultMsg.Content.Count); + + // First tool result + Assert.NotNull(toolResultMsg.Content[0].ToolResult); + Assert.Equal("tooluse_G64hibpFmRqXEcAYwOfP5s", toolResultMsg.Content[0].ToolResult.ToolUseId); + Assert.Equal("Dataset DataReference: memory://8df2be02a23c4ebfb77e1170dea6c4b4. Dataset size: 78 items.", + toolResultMsg.Content[0].ToolResult.Content[0].Text); + + // Second tool result + Assert.NotNull(toolResultMsg.Content[1].ToolResult); + Assert.Equal("tooluse_sMmlRWbbCGd0lnhiLjvk8H", toolResultMsg.Content[1].ToolResult.ToolUseId); + Assert.Equal("19:791df3464b4f4c3fac8429041e8e2540@thread.v2", + toolResultMsg.Content[1].ToolResult.Content[0].Text); + } + + /// + /// Full round-trip test matching the exact scenario from issue #13647: + /// separate assistant tool-call messages + separate tool-result messages. + /// After merging, the Bedrock message list should have properly coalesced messages. + /// + [Fact] + public void BuildMessageListShouldProduceValidBedrockMessagesForParallelToolCalls() + { + // Arrange: Reproduce the exact ChatHistory from the issue + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Audit the feeds and get the developers distribution list"); + + // Two separate assistant messages with tool calls (as SK creates them) + var assistantMsg1 = new ChatMessageContent(AuthorRole.Assistant, (string?)null); + assistantMsg1.Items.Add(new FunctionCallContent( + functionName: "GetFeeds", + pluginName: "Feeds", + id: "tooluse_G64hibpFmRqXEcAYwOfP5s")); + chatHistory.Add(assistantMsg1); + + var assistantMsg2 = new ChatMessageContent(AuthorRole.Assistant, (string?)null); + assistantMsg2.Items.Add(new FunctionCallContent( + functionName: "GetTeamsDistributionList", + pluginName: "DistributionList", + id: "tooluse_sMmlRWbbCGd0lnhiLjvk8H", + arguments: new KernelArguments { { "distributionListName", "developers" } })); + chatHistory.Add(assistantMsg2); + + // Two separate tool result messages + var toolResult1 = new ChatMessageContent(AuthorRole.Tool, (string?)null); + toolResult1.Items.Add(new FunctionResultContent( + functionName: "GetFeeds", + pluginName: "Feeds", + callId: "tooluse_G64hibpFmRqXEcAYwOfP5s", + result: "Dataset DataReference: memory://8df2be02a23c4ebfb77e1170dea6c4b4. Dataset size: 78 items.")); + chatHistory.Add(toolResult1); + + var toolResult2 = new ChatMessageContent(AuthorRole.Tool, (string?)null); + toolResult2.Items.Add(new FunctionResultContent( + functionName: "GetTeamsDistributionList", + pluginName: "DistributionList", + callId: "tooluse_sMmlRWbbCGd0lnhiLjvk8H", + result: "19:791df3464b4f4c3fac8429041e8e2540@thread.v2")); + chatHistory.Add(toolResult2); + + // Act + var messages = Core.BedrockModelUtilities.BuildMessageList(chatHistory); + + // Assert: Should produce exactly 3 messages: user, assistant (merged), user/tool-results (merged) + Assert.Equal(3, messages.Count); + + // Message 1: User prompt + Assert.Equal(ConversationRole.User, messages[0].Role); + Assert.Single(messages[0].Content); + Assert.Equal("Audit the feeds and get the developers distribution list", messages[0].Content[0].Text); + + // Message 2: Merged assistant with both tool use blocks + Assert.Equal(ConversationRole.Assistant, messages[1].Role); + Assert.Equal(2, messages[1].Content.Count); + Assert.NotNull(messages[1].Content[0].ToolUse); + Assert.NotNull(messages[1].Content[1].ToolUse); + + // Verify tool use IDs are preserved correctly + var toolUseIds = messages[1].Content.Select(c => c.ToolUse.ToolUseId).ToList(); + Assert.Contains("tooluse_G64hibpFmRqXEcAYwOfP5s", toolUseIds); + Assert.Contains("tooluse_sMmlRWbbCGd0lnhiLjvk8H", toolUseIds); + + // Message 3: Merged tool results (as user role) + Assert.Equal(ConversationRole.User, messages[2].Role); + Assert.Equal(2, messages[2].Content.Count); + Assert.NotNull(messages[2].Content[0].ToolResult); + Assert.NotNull(messages[2].Content[1].ToolResult); + + // Verify tool result IDs match the tool use IDs + var toolResultIds = messages[2].Content.Select(c => c.ToolResult.ToolUseId).ToList(); + Assert.Contains("tooluse_G64hibpFmRqXEcAYwOfP5s", toolResultIds); + Assert.Contains("tooluse_sMmlRWbbCGd0lnhiLjvk8H", toolResultIds); + } + + /// + /// Verifies that Tool role maps to User in the Bedrock conversation role. + /// + [Fact] + public void MapAuthorRoleToConversationRoleShouldMapToolToUser() + { + // Act + var result = Core.BedrockModelUtilities.MapAuthorRoleToConversationRole(AuthorRole.Tool); + + // Assert + Assert.Equal(ConversationRole.User, result); + } + + /// + /// Verifies that FunctionCallContent without a plugin name uses just the function name. + /// + [Fact] + public void BuildMessageListShouldUseOnlyFunctionNameWhenNoPluginName() + { + // Arrange + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Do something"); + + var assistantMessage = new ChatMessageContent(AuthorRole.Assistant, (string?)null); + assistantMessage.Items.Add(new FunctionCallContent( + functionName: "MyFunction", + id: "tool_123")); + chatHistory.Add(assistantMessage); + + // Act + var messages = Core.BedrockModelUtilities.BuildMessageList(chatHistory); + + // Assert + Assert.Equal(2, messages.Count); + Assert.NotNull(messages[1].Content[0].ToolUse); + Assert.Equal("MyFunction", messages[1].Content[0].ToolUse.Name); + } + + /// + /// Verifies that tool call arguments are properly converted to Document. + /// + [Fact] + public void BuildMessageListShouldConvertFunctionCallArguments() + { + // Arrange + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Search for something"); + + var assistantMessage = new ChatMessageContent(AuthorRole.Assistant, (string?)null); + assistantMessage.Items.Add(new FunctionCallContent( + functionName: "Search", + pluginName: "Web", + id: "tool_456", + arguments: new KernelArguments + { + { "query", "semantic kernel" }, + { "maxResults", 10 } + })); + chatHistory.Add(assistantMessage); + + // Act + var messages = Core.BedrockModelUtilities.BuildMessageList(chatHistory); + + // Assert + var toolUse = messages[1].Content[0].ToolUse; + Assert.NotNull(toolUse); + Assert.Equal("Web-Search", toolUse.Name); + Assert.True(toolUse.Input.IsDictionary()); + var inputDict = toolUse.Input.AsDictionary(); + Assert.Equal("semantic kernel", inputDict["query"].AsString()); + Assert.Equal(10, inputDict["maxResults"].AsInt()); + } + + /// + /// Verifies that non-consecutive same-role messages are NOT merged. + /// + [Fact] + public void BuildMessageListShouldNotMergeNonConsecutiveSameRoleMessages() + { + // Arrange + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Hello"); + chatHistory.AddAssistantMessage("Hi"); + chatHistory.AddUserMessage("How are you?"); + + // Act + var messages = Core.BedrockModelUtilities.BuildMessageList(chatHistory); + + // Assert: All three messages should be separate + Assert.Equal(3, messages.Count); + } + + /// + /// Verifies that mixed text and tool content in a single assistant message works. + /// + [Fact] + public void BuildMessageListShouldHandleAssistantMessageWithTextAndToolCall() + { + // Arrange + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("What's the weather?"); + + var assistantMessage = new ChatMessageContent(AuthorRole.Assistant, (string?)null); + assistantMessage.Items.Add(new TextContent("Let me check the weather for you.")); + assistantMessage.Items.Add(new FunctionCallContent( + functionName: "GetWeather", + pluginName: "Weather", + id: "tool_789", + arguments: new KernelArguments { { "city", "Seattle" } })); + chatHistory.Add(assistantMessage); + + // Act + var messages = Core.BedrockModelUtilities.BuildMessageList(chatHistory); + + // Assert + Assert.Equal(2, messages.Count); + var assistantMsg = messages[1]; + Assert.Equal(2, assistantMsg.Content.Count); + Assert.Equal("Let me check the weather for you.", assistantMsg.Content[0].Text); + Assert.NotNull(assistantMsg.Content[1].ToolUse); + Assert.Equal("Weather-GetWeather", assistantMsg.Content[1].ToolUse.Name); + } +} diff --git a/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/BedrockModelUtilities.cs b/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/BedrockModelUtilities.cs index dac71cd8e142..4a884dfe02c1 100644 --- a/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/BedrockModelUtilities.cs +++ b/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/BedrockModelUtilities.cs @@ -5,6 +5,7 @@ using System.Linq; using Amazon.BedrockRuntime; using Amazon.BedrockRuntime.Model; +using Amazon.Runtime.Documents; using Microsoft.SemanticKernel.ChatCompletion; namespace Microsoft.SemanticKernel.Connectors.Amazon.Core; @@ -32,6 +33,12 @@ internal static ConversationRole MapAuthorRoleToConversationRole(AuthorRole role return ConversationRole.Assistant; } + // Tool role maps to User in Bedrock (tool results are sent as user messages). + if (role == AuthorRole.Tool) + { + return ConversationRole.User; + } + throw new ArgumentOutOfRangeException($"Invalid role: {role}"); } @@ -50,27 +57,159 @@ internal static List GetSystemMessages(ChatHistory chatHisto /// /// Creates the list of user and assistant messages for the Converse Request from the Chat History. + /// Handles FunctionCallContent (tool use) and FunctionResultContent (tool result) items, + /// and merges consecutive messages with the same Bedrock role into a single message. + /// This is required because Bedrock expects all tool_use blocks from one assistant turn + /// in a single message and all tool_result blocks in a single user message. /// /// The ChatHistory object to be building the message list from. /// The list of messages for the converse request. - /// Thrown if invalid last message in chat history. + /// Thrown if the chat history is empty. internal static List BuildMessageList(ChatHistory chatHistory) { - // Check that the text from the latest message in the chat history is not empty. Verify.NotNullOrEmpty(chatHistory); - string? text = chatHistory[chatHistory.Count - 1].Content; - if (string.IsNullOrWhiteSpace(text)) + + var messages = new List(); + + foreach (var chatMessage in chatHistory) { - throw new ArgumentException("Last message in chat history was null or whitespace."); + if (chatMessage.Role == AuthorRole.System) + { + continue; + } + + var bedrockRole = MapAuthorRoleToConversationRole(chatMessage.Role); + var contentBlocks = BuildContentBlocks(chatMessage); + + if (contentBlocks.Count == 0) + { + continue; + } + + // Merge with the previous message if it has the same role. + // This handles consecutive assistant tool-call messages and consecutive tool-result messages. + if (messages.Count > 0 && messages[messages.Count - 1].Role == bedrockRole) + { + messages[messages.Count - 1].Content.AddRange(contentBlocks); + } + else + { + messages.Add(new Message + { + Role = bedrockRole, + Content = contentBlocks + }); + } } - return chatHistory - .Where(m => m.Role != AuthorRole.System) - .Select(m => new Message + + return messages; + } + + /// + /// Builds the list of Bedrock ContentBlock objects from a ChatMessageContent, + /// handling text, FunctionCallContent (ToolUse), and FunctionResultContent (ToolResult). + /// + /// The chat message to convert. + /// A list of ContentBlock objects. + private static List BuildContentBlocks(ChatMessageContent chatMessage) + { + var contentBlocks = new List(); + + foreach (var item in chatMessage.Items) + { + switch (item) { - Role = MapAuthorRoleToConversationRole(m.Role), - Content = [new() { Text = m.Content }] - }) - .ToList(); + case FunctionCallContent functionCall: + contentBlocks.Add(new ContentBlock + { + ToolUse = new ToolUseBlock + { + ToolUseId = functionCall.Id, + Name = functionCall.PluginName is not null + ? $"{functionCall.PluginName}-{functionCall.FunctionName}" + : functionCall.FunctionName, + Input = ConvertArgumentsToDocument(functionCall.Arguments) + } + }); + break; + + case FunctionResultContent functionResult: + contentBlocks.Add(new ContentBlock + { + ToolResult = new ToolResultBlock + { + ToolUseId = functionResult.CallId, + Content = [new ToolResultContentBlock { Text = functionResult.Result?.ToString() ?? string.Empty }] + } + }); + break; + + case TextContent textContent: + if (!string.IsNullOrEmpty(textContent.Text)) + { + contentBlocks.Add(new ContentBlock { Text = textContent.Text }); + } + break; + + default: + // For other content types, fall back to using ToString. + var text = item.ToString(); + if (!string.IsNullOrEmpty(text)) + { + contentBlocks.Add(new ContentBlock { Text = text }); + } + break; + } + } + + // If no items were processed but there's text content on the message itself, use that. + if (contentBlocks.Count == 0 && !string.IsNullOrEmpty(chatMessage.Content)) + { + contentBlocks.Add(new ContentBlock { Text = chatMessage.Content }); + } + + return contentBlocks; + } + + /// + /// Converts KernelArguments to an Amazon.Runtime.Documents.Document + /// for use as ToolUseBlock input. + /// + /// The arguments to convert. + /// A Document representing the arguments as a JSON-like structure. + private static Document ConvertArgumentsToDocument(KernelArguments? arguments) + { + if (arguments == null || arguments.Count == 0) + { + return new Document(new Dictionary()); + } + + var dict = new Dictionary(); + foreach (var kvp in arguments) + { + dict[kvp.Key] = ConvertValueToDocument(kvp.Value); + } + + return new Document(dict); + } + + /// + /// Converts a single value to a Document, handling common types. + /// + private static Document ConvertValueToDocument(object? value) + { + return value switch + { + null => new Document(), + string s => new Document(s), + bool b => new Document(b), + int i => new Document(i), + long l => new Document(l), + float f => new Document(f), + double d => new Document(d), + decimal dec => new Document((double)dec), + _ => new Document(value.ToString() ?? string.Empty) + }; } /// diff --git a/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/Clients/BedrockChatCompletionClient.cs b/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/Clients/BedrockChatCompletionClient.cs index fde1f2c0af3d..b8eb8d8047bd 100644 --- a/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/Clients/BedrockChatCompletionClient.cs +++ b/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/Clients/BedrockChatCompletionClient.cs @@ -144,11 +144,97 @@ private static ChatMessageContentItemCollection CreateChatMessageContentItemColl var itemCollection = new ChatMessageContentItemCollection(); foreach (var contentBlock in contentBlocks) { - itemCollection.Add(new TextContent(contentBlock.Text)); + if (contentBlock.ToolUse != null) + { + var toolUse = contentBlock.ToolUse; + KernelArguments? arguments = null; + if (toolUse.Input.IsDictionary()) + { + arguments = ParseDocumentToArguments(toolUse.Input); + } + + // Parse plugin name and function name from the tool name. + // The Bedrock connector uses "PluginName-FunctionName" format. + string? pluginName = null; + string functionName = toolUse.Name ?? string.Empty; + var separatorIndex = functionName.IndexOf('-'); + if (separatorIndex > 0) + { + pluginName = functionName.Substring(0, separatorIndex); + functionName = functionName.Substring(separatorIndex + 1); + } + + itemCollection.Add(new FunctionCallContent( + functionName: functionName, + pluginName: pluginName, + id: toolUse.ToolUseId, + arguments: arguments)); + } + else if (!string.IsNullOrEmpty(contentBlock.Text)) + { + itemCollection.Add(new TextContent(contentBlock.Text)); + } } return itemCollection; } + /// + /// Parses an Amazon.Runtime.Documents.Document to KernelArguments. + /// + private static KernelArguments? ParseDocumentToArguments(global::Amazon.Runtime.Documents.Document document) + { + if (!document.IsDictionary()) + { + return null; + } + + var arguments = new KernelArguments(); + foreach (var kvp in document.AsDictionary()) + { + arguments[kvp.Key] = DocumentToObject(kvp.Value); + } + + return arguments; + } + + /// + /// Converts a Document to a plain object for use in KernelArguments. + /// + private static object? DocumentToObject(global::Amazon.Runtime.Documents.Document doc) + { + if (doc.IsNull()) + { + return null; + } + + if (doc.IsString()) + { + return doc.AsString(); + } + + if (doc.IsBool()) + { + return doc.AsBool(); + } + + if (doc.IsInt()) + { + return doc.AsInt(); + } + + if (doc.IsLong()) + { + return doc.AsLong(); + } + + if (doc.IsDouble()) + { + return doc.AsDouble(); + } + + return doc.ToString(); + } + internal async IAsyncEnumerable StreamChatMessageAsync( ChatHistory chatHistory, PromptExecutionSettings? executionSettings = null,