diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Foundry/AzureAgentProvider.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Foundry/AzureAgentProvider.cs index 98e8b7f53f..0a673e2aa5 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Foundry/AzureAgentProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Foundry/AzureAgentProvider.cs @@ -4,7 +4,6 @@ using System; using System.ClientModel.Primitives; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Linq; using System.Net.Http; using System.Runtime.CompilerServices; using System.Text.Json.Nodes; @@ -70,7 +69,14 @@ public sealed class AzureAgentProvider(Uri projectEndpoint, TokenCredential proj include: null, cancellationToken).ConfigureAwait(false); - return newItems.AsChatMessages().Single(); + ChatMessage[] createdMessages = [.. newItems.AsChatMessages()]; + if (createdMessages.Length != 1) + { + throw new InvalidOperationException( + $"Expected exactly one chat message from created conversation item in conversation '{conversationId}', but got {createdMessages.Length}."); + } + + return createdMessages[0]; IEnumerable GetResponseItems() { @@ -208,7 +214,14 @@ public sealed class AzureAgentProvider(Uri projectEndpoint, TokenCredential proj { AgentResponseItem responseItem = await this.GetConversationClient().GetProjectConversationItemAsync(conversationId, messageId, include: null, cancellationToken).ConfigureAwait(false); ResponseItem[] items = [responseItem.AsResponseResultItem()]; - return items.AsChatMessages().Single(); + ChatMessage[] messages = [.. items.AsChatMessages()]; + if (messages.Length != 1) + { + throw new InvalidOperationException( + $"Expected exactly one chat message for message '{messageId}' in conversation '{conversationId}', but got {messages.Length}."); + } + + return messages[0]; } /// diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs index 86efa6ec43..322c460ee3 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs @@ -49,7 +49,11 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, ResponseA public async ValueTask ResumeAsync(IWorkflowContext context, ExternalInputResponse response, CancellationToken cancellationToken) { - await context.SetLastMessageAsync(response.Messages.Last()).ConfigureAwait(false); + ChatMessage? lastMessage = response.Messages.LastOrDefault(); + if (lastMessage is not null) + { + await context.SetLastMessageAsync(lastMessage).ConfigureAwait(false); + } await this.InvokeAgentAsync(context, response.Messages, cancellationToken).ConfigureAwait(false); } @@ -85,15 +89,19 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, ResponseA await this.AssignAsync(this.AgentOutput?.Messages?.Path, agentResponse.Messages.ToTable(), context).ConfigureAwait(false); // Attempt to parse the last message as JSON and assign to the response object variable. - try + string? lastMessageText = agentResponse.Messages.LastOrDefault()?.Text; + if (!string.IsNullOrEmpty(lastMessageText)) { - JsonDocument jsonDocument = JsonDocument.Parse(agentResponse.Messages.Last().Text); - Dictionary objectProperties = jsonDocument.ParseRecord(VariableType.RecordType); - await this.AssignAsync(this.AgentOutput?.ResponseObject?.Path, objectProperties.ToFormula(), context).ConfigureAwait(false); - } - catch - { - // Not valid json, skip assignment. + try + { + using JsonDocument jsonDocument = JsonDocument.Parse(lastMessageText); + Dictionary objectProperties = jsonDocument.ParseRecord(VariableType.RecordType); + await this.AssignAsync(this.AgentOutput?.ResponseObject?.Path, objectProperties.ToFormula(), context).ConfigureAwait(false); + } + catch (JsonException) + { + // Not valid json, skip assignment. + } } if (this.Model.Input?.ExternalLoop?.When is not null) diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/QuestionExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/QuestionExecutor.cs index 31cb82353e..32ce93c2af 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/QuestionExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/QuestionExecutor.cs @@ -122,10 +122,13 @@ internal sealed class QuestionExecutor(Question model, ResponseAgentProvider age string? workflowConversationId = context.GetWorkflowConversation(); if (workflowConversationId is not null) { - // Input message always defined if values has been extracted. - ChatMessage input = response.Messages.Last(); - await agentProvider.CreateMessageAsync(workflowConversationId, input, cancellationToken).ConfigureAwait(false); - await context.SetLastMessageAsync(input).ConfigureAwait(false); + // Input message expected to be defined when values have been extracted, but guard defensively. + ChatMessage? input = response.Messages.LastOrDefault(); + if (input is not null) + { + await agentProvider.CreateMessageAsync(workflowConversationId, input, cancellationToken).ConfigureAwait(false); + await context.SetLastMessageAsync(input).ConfigureAwait(false); + } } } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/RequestExternalInputExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/RequestExternalInputExecutor.cs index 239b178415..172348b37e 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/RequestExternalInputExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/RequestExternalInputExecutor.cs @@ -45,7 +45,11 @@ internal sealed class RequestExternalInputExecutor(RequestExternalInput model, R await agentProvider.CreateMessageAsync(workflowConversationId, inputMessage, cancellationToken).ConfigureAwait(false); } } - await context.SetLastMessageAsync(response.Messages.Last()).ConfigureAwait(false); + ChatMessage? lastMessage = response.Messages.LastOrDefault(); + if (lastMessage is not null) + { + await context.SetLastMessageAsync(lastMessage).ConfigureAwait(false); + } await this.AssignAsync(this.Model.Variable?.Path, response.Messages.ToFormula(), context).ConfigureAwait(false); await context.RaiseCompletionEventAsync(this.Model, cancellationToken).ConfigureAwait(false); diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/RequestExternalInputExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/RequestExternalInputExecutorTest.cs index 8eda895b15..bc1bc10284 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/RequestExternalInputExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/RequestExternalInputExecutorTest.cs @@ -85,6 +85,29 @@ public sealed class RequestExternalInputExecutorTest(ITestOutputHelper output) : expectMessagesCreated: true); } + [Fact] + public async Task CaptureResponseWithEmptyMessagesAsync() + { + await this.CaptureResponseTestAsync( + displayName: nameof(CaptureResponseWithEmptyMessagesAsync), + variableName: "TestVariable", + messageCount: 0); + } + + [Fact] + public async Task CaptureResponseWithEmptyMessagesAndWorkflowConversationAsync() + { + // Arrange + this.State.Set(SystemScope.Names.ConversationId, FormulaValue.New("WorkflowConversationId"), VariableScopeNames.System); + + // Act & Assert + await this.CaptureResponseTestAsync( + displayName: nameof(CaptureResponseWithEmptyMessagesAndWorkflowConversationAsync), + variableName: "TestVariable", + messageCount: 0, + expectMessagesCreated: false); + } + private async Task ExecuteTestAsync( string displayName, string variableName)