diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/OpenAIHostingJsonUtilities.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/OpenAIHostingJsonUtilities.cs index f77143c583..60c6c916fd 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/OpenAIHostingJsonUtilities.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/OpenAIHostingJsonUtilities.cs @@ -147,6 +147,7 @@ private static JsonSerializerOptions CreateDefaultOptions() [JsonSerializable(typeof(ItemContentOutputText))] [JsonSerializable(typeof(ItemContentOutputAudio))] [JsonSerializable(typeof(ItemContentRefusal))] +[JsonSerializable(typeof(ItemContentFunctionApprovalResponse))] [JsonSerializable(typeof(TextConfiguration))] [JsonSerializable(typeof(ResponseTextFormatConfiguration))] [JsonSerializable(typeof(ResponseTextFormatConfigurationText))] diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/ItemContentConverter.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/ItemContentConverter.cs index 2476ce2fbd..9ab96b4d86 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/ItemContentConverter.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/ItemContentConverter.cs @@ -70,6 +70,10 @@ private static string MediaTypeToAudioFormat(string mediaType) => ItemContentOutputAudio outputAudio => new DataContent(outputAudio.Data, "audio/*"), + // Function approval response - preserve raw representation for downstream processing + ItemContentFunctionApprovalResponse approvalResponse => + new TextContent($"[Function approval response: request_id={approvalResponse.RequestId}, approved={approvalResponse.Approved}]"), + _ => null }; diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ItemResource.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ItemResource.cs index 289bafbc43..ae199d73a0 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ItemResource.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ItemResource.cs @@ -266,6 +266,7 @@ internal enum FunctionToolCallOutputItemResourceStatus [JsonDerivedType(typeof(ItemContentOutputText), "output_text")] [JsonDerivedType(typeof(ItemContentOutputAudio), "output_audio")] [JsonDerivedType(typeof(ItemContentRefusal), "refusal")] +[JsonDerivedType(typeof(ItemContentFunctionApprovalResponse), "function_approval_response")] internal abstract class ItemContent { /// @@ -443,6 +444,29 @@ internal sealed class ItemContentRefusal : ItemContent public required string Refusal { get; init; } } +/// +/// A function approval response content item. +/// Used by DevUI for human-in-the-loop tool approval responses. +/// +internal sealed class ItemContentFunctionApprovalResponse : ItemContent +{ + /// + [JsonIgnore] + public override string Type => "function_approval_response"; + + /// + /// The unique identifier of the approval request being responded to. + /// + [JsonPropertyName("request_id")] + public required string RequestId { get; init; } + + /// + /// Whether the request was approved. + /// + [JsonPropertyName("approved")] + public bool Approved { get; init; } +} + // Additional ItemResource types from TypeSpec /// diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ForeachExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ForeachExecutor.cs index e6ab5e49a9..e88cd25ef6 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ForeachExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ForeachExecutor.cs @@ -49,7 +49,7 @@ public ForeachExecutor(Foreach model, WorkflowFormulaState state) EvaluationResult expressionResult = this.Evaluator.GetValue(this.Model.Items); if (expressionResult.Value is TableDataValue tableValue) { - this._values = [.. tableValue.Values.Select(value => value.Properties.Values.First().ToFormula())]; + this._values = [.. tableValue.Values.Select(value => value.ToFormula())]; } else { diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ForeachExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ForeachExecutorTest.cs index 63d6e15bb8..0215aed659 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ForeachExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ForeachExecutorTest.cs @@ -291,6 +291,38 @@ public async Task ForeachRestoreWithNoSavedStateAsync() Assert.False(executor.HasValue); } + /// + /// Foreach over a table with multi-field records must preserve all fields in the loop value variable. + /// Regression test for GH-6183. + /// + [Fact] + public async Task ForeachPreservesMultiFieldRecordsAsync() + { + // Arrange + this.SetVariableState("CurrentValue"); + TableDataValue tableValue = DataValue.TableFromRecords( + DataValue.RecordFromFields( + new KeyValuePair("name", new StringDataValue("Alice")), + new KeyValuePair("role", new StringDataValue("Engineer"))), + DataValue.RecordFromFields( + new KeyValuePair("name", new StringDataValue("Bob")), + new KeyValuePair("role", new StringDataValue("Designer")))); + + Foreach model = this.CreateModel( + displayName: nameof(ForeachPreservesMultiFieldRecordsAsync), + items: ValueExpression.Literal(tableValue), + valueName: "CurrentValue", + indexName: null); + + ForeachExecutor action = new(model, this.State); + + // Act — execute the initialisation then take the first iteration. + await this.ExecuteAsync(action, ForeachExecutor.Steps.Next(action.Id), action.TakeNextAsync); + + // Assert — the value must be present (all fields preserved, not collapsed to first field). + Assert.True(action.HasValue); + } + /// /// Checkpoint/restore around a foreach over an empty source must roundtrip cleanly /// (zero-length PortableValue[] snapshot).