From edf234aa4a4c31d2b8e9d68b19beb25e1087711f Mon Sep 17 00:00:00 2001 From: Shyju Krishnankutty Date: Mon, 20 Apr 2026 08:28:40 -0700 Subject: [PATCH] Address PR review feedback. - Return 404 Not Found when no orchestration with the given ID exists - Return 200 OK for failed workflows (the HTTP operation succeeded; the workflow outcome is conveyed via the response body) - Rename 'status' to 'workflowStatus' in WorkflowRunResponse to avoid inconsistency with AgentRunSuccessResponse which uses integer status - Add optional 'error' field (omitted from JSON when null) to WorkflowRunResponse for failed workflow details --- .../01_SequentialWorkflow/README.md | 2 +- .../BuiltInFunctions.cs | 37 ++++++++++++++----- .../WorkflowSamplesValidation.cs | 24 ++++++++++++ 3 files changed, 52 insertions(+), 11 deletions(-) diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/README.md b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/README.md index 4e2a98dc7a..4f455b3dec 100644 --- a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/README.md +++ b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/README.md @@ -107,7 +107,7 @@ curl -X POST http://localhost:7071/api/workflows/CancelOrder/run \ ```json { "runId": "abc123def456", - "status": "Completed", + "workflowStatus": "Completed", "result": "Cancellation email sent for order 12345 to jerry@example.com." } ``` diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs index 0717c989d7..376f2fa2ca 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs @@ -445,15 +445,28 @@ internal static class BuiltInFunctions if (metadata is null) { - return await CreateErrorResponseAsync(req, context, HttpStatusCode.InternalServerError, - $"Workflow orchestration '{instanceId}' returned no metadata.", acceptsJson); + return await CreateErrorResponseAsync(req, context, HttpStatusCode.NotFound, + $"No workflow orchestration with ID '{instanceId}' was found.", acceptsJson); } if (metadata.RuntimeStatus is OrchestrationRuntimeStatus.Failed) { string errorMessage = metadata.FailureDetails?.ErrorMessage ?? "Unknown error"; - return await CreateErrorResponseAsync(req, context, HttpStatusCode.InternalServerError, - $"Workflow orchestration '{instanceId}' failed: {errorMessage}", acceptsJson); + HttpResponseData failedResponse = req.CreateResponse(HttpStatusCode.OK); + + if (acceptsJson) + { + await failedResponse.WriteAsJsonAsync( + new WorkflowRunResponse(instanceId, metadata.RuntimeStatus.ToString(), Result: null, Error: errorMessage), + context.CancellationToken); + } + else + { + failedResponse.Headers.Add("Content-Type", "text/plain"); + await failedResponse.WriteStringAsync(errorMessage, context.CancellationToken); + } + + return failedResponse; } if (metadata.RuntimeStatus is not OrchestrationRuntimeStatus.Completed) @@ -490,7 +503,9 @@ internal static class BuiltInFunctions } } - await response.WriteAsJsonAsync(new WorkflowRunSuccessResponse(instanceId, metadata.RuntimeStatus.ToString(), resultElement), context.CancellationToken); + await response.WriteAsJsonAsync( + new WorkflowRunResponse(instanceId, metadata.RuntimeStatus.ToString(), resultElement), + context.CancellationToken); } else { @@ -687,15 +702,17 @@ internal static class BuiltInFunctions [property: JsonPropertyName("response")] JsonElement Response); /// - /// Represents a successful workflow run response when waiting for completion. + /// Represents a workflow run response when waiting for completion. /// /// The orchestration run ID. - /// The orchestration runtime status. + /// The orchestration runtime status (e.g., "Completed", "Failed"). /// The workflow result as a JSON element so POCOs serialize as nested objects rather than escaped strings. - private sealed record WorkflowRunSuccessResponse( + /// An optional error message when the workflow has failed. + private sealed record WorkflowRunResponse( [property: JsonPropertyName("runId")] string RunId, - [property: JsonPropertyName("status")] string Status, - [property: JsonPropertyName("result")] JsonElement? Result); + [property: JsonPropertyName("workflowStatus")] string WorkflowStatus, + [property: JsonPropertyName("result")] JsonElement? Result, + [property: JsonPropertyName("error"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] string? Error = null); /// /// A service provider that combines the original service provider with an additional DurableTaskClient instance. diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/WorkflowSamplesValidation.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/WorkflowSamplesValidation.cs index 435627a169..2eba009c67 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/WorkflowSamplesValidation.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/WorkflowSamplesValidation.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Reflection; using System.Text; +using System.Text.Json; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using ModelContextProtocol.Client; @@ -141,6 +142,29 @@ public sealed class WorkflowSamplesValidation(ITestOutputHelper outputHelper) : // The response should contain the workflow result (not just "started for CancelOrder") Assert.DoesNotContain("Workflow orchestration started", waitResponseText); Assert.Contains("55555", waitResponseText); + + // Test the wait-for-response with Accept: application/json header + this._outputHelper.WriteLine("Starting CancelOrder workflow with x-ms-wait-for-response and Accept: application/json..."); + + using HttpRequestMessage jsonWaitRequest = new(HttpMethod.Post, cancelOrderUri); + jsonWaitRequest.Content = new StringContent("77777", Encoding.UTF8, "text/plain"); + jsonWaitRequest.Headers.Add("x-ms-wait-for-response", "true"); + jsonWaitRequest.Headers.Add("Accept", "application/json"); + + using CancellationTokenSource jsonWaitCts = new(s_orchestrationTimeout); + using HttpResponseMessage jsonWaitResponse = await s_sharedHttpClient.SendAsync(jsonWaitRequest, jsonWaitCts.Token); + + Assert.True(jsonWaitResponse.IsSuccessStatusCode, $"CancelOrder JSON wait-for-response request failed with status: {jsonWaitResponse.StatusCode}"); + string jsonWaitResponseText = await jsonWaitResponse.Content.ReadAsStringAsync(); + this._outputHelper.WriteLine($"CancelOrder JSON wait-for-response result: {jsonWaitResponseText}"); + + using JsonDocument jsonDoc = JsonDocument.Parse(jsonWaitResponseText); + JsonElement root = jsonDoc.RootElement; + Assert.True(root.TryGetProperty("runId", out _), "JSON response missing 'runId' property"); + Assert.True(root.TryGetProperty("workflowStatus", out JsonElement statusEl), "JSON response missing 'workflowStatus' property"); + Assert.Equal("Completed", statusEl.GetString()); + Assert.True(root.TryGetProperty("result", out JsonElement resultEl), "JSON response missing 'result' property"); + Assert.Contains("77777", resultEl.GetString()); }); }