From 61c28ebda64031dd8439aed3a05385f0cafa778c Mon Sep 17 00:00:00 2001 From: Shyju Krishnankutty Date: Thu, 16 Apr 2026 17:19:21 -0700 Subject: [PATCH 1/4] Adding support for "wait for response" when invoking workflow http endpoint. --- .../01_SequentialWorkflow/README.md | 47 +++++++ .../01_SequentialWorkflow/demo.http | 22 +++ .../BuiltInFunctions.cs | 127 +++++++++++++++--- .../CHANGELOG.md | 1 + .../WorkflowSamplesValidation.cs | 16 +++ 5 files changed, 192 insertions(+), 21 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 384fd358a7..4e2a98dc7a 100644 --- a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/README.md +++ b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/README.md @@ -65,6 +65,53 @@ Workflow orchestration started for CancelOrder. Orchestration runId: abc123def45 > > If not provided, a unique run ID is auto-generated. +### Wait for the Workflow Result + +By default, the HTTP endpoint returns `202 Accepted` immediately with the run ID. If you want to wait for the workflow to complete and get the result in the response, add the `x-ms-wait-for-response: true` header: + +Bash (Linux/macOS/WSL): + +```bash +curl -X POST http://localhost:7071/api/workflows/CancelOrder/run \ + -H "Content-Type: text/plain" \ + -H "x-ms-wait-for-response: true" \ + -d "12345" +``` + +PowerShell: + +```powershell +Invoke-RestMethod -Method Post ` + -Uri http://localhost:7071/api/workflows/CancelOrder/run ` + -ContentType text/plain ` + -Headers @{ "x-ms-wait-for-response" = "true" } ` + -Body "12345" +``` + +The response will contain the workflow result as plain text (200 OK): + +```text +Cancellation email sent for order 12345 to jerry@example.com. +``` + +To get the result as JSON, also include the `Accept: application/json` header: + +```bash +curl -X POST http://localhost:7071/api/workflows/CancelOrder/run \ + -H "Content-Type: text/plain" \ + -H "x-ms-wait-for-response: true" \ + -H "Accept: application/json" \ + -d "12345" +``` + +```json +{ + "runId": "abc123def456", + "status": "Completed", + "result": "Cancellation email sent for order 12345 to jerry@example.com." +} +``` + In the function app logs, you will see the sequential execution of each executor: ```text diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/demo.http b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/demo.http index 8366216a6c..fb9793f449 100644 --- a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/demo.http +++ b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/demo.http @@ -7,6 +7,21 @@ Content-Type: text/plain 12345 +### Cancel an order and wait for the result +POST {{authority}}/api/workflows/CancelOrder/run +Content-Type: text/plain +x-ms-wait-for-response: true + +12345 + +### Cancel an order and wait for the result (JSON response) +POST {{authority}}/api/workflows/CancelOrder/run +Content-Type: text/plain +Accept: application/json +x-ms-wait-for-response: true + +12345 + ### Cancel an order with a custom run ID POST {{authority}}/api/workflows/CancelOrder/run?runId=my-custom-id-123 Content-Type: text/plain @@ -19,6 +34,13 @@ Content-Type: text/plain 12345 +### Get order status and wait for the result +POST {{authority}}/api/workflows/OrderStatus/run +Content-Type: text/plain +x-ms-wait-for-response: true + +12345 + ### Batch cancel orders with a complex JSON input POST {{authority}}/api/workflows/BatchCancelOrders/run Content-Type: application/json diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs index e6c94347a1..9da94f8522 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs @@ -21,6 +21,8 @@ internal static class BuiltInFunctions internal const string HttpPrefix = "http-"; internal const string McpToolPrefix = "mcptool-"; + private const string WaitForResponseHeaderName = "x-ms-wait-for-response"; + internal static readonly string RunAgentHttpFunctionEntryPoint = $"{typeof(BuiltInFunctions).FullName!}.{nameof(RunAgentHttpAsync)}"; internal static readonly string RunAgentEntityFunctionEntryPoint = $"{typeof(BuiltInFunctions).FullName!}.{nameof(InvokeAgentAsync)}"; internal static readonly string RunAgentMcpToolFunctionEntryPoint = $"{typeof(BuiltInFunctions).FullName!}.{nameof(RunMcpToolAsync)}"; @@ -62,6 +64,11 @@ public static async Task RunWorkflowOrchestrationHttpTriggerAs StartOrchestrationOptions? options = instanceId is not null ? new StartOrchestrationOptions(instanceId) : null; string resolvedInstanceId = await client.ScheduleNewOrchestrationInstanceAsync(orchestrationFunctionName, orchestrationInput, options); + if (ShouldWaitForResponse(req, defaultValue: false)) + { + return await WaitForWorkflowCompletionAsync(req, client, context, resolvedInstanceId); + } + HttpResponseData response = req.CreateResponse(HttpStatusCode.Accepted); await response.WriteStringAsync($"Workflow orchestration started for {workflowName}. Orchestration runId: {resolvedInstanceId}"); return response; @@ -304,15 +311,7 @@ public static async Task RunAgentHttpAsync( } // Check if we should wait for response (default is true) - bool waitForResponse = true; - if (req.Headers.TryGetValues("x-ms-wait-for-response", out IEnumerable? waitForResponseValues)) - { - string? waitForResponseValue = waitForResponseValues.FirstOrDefault(); - if (!string.IsNullOrEmpty(waitForResponseValue) && bool.TryParse(waitForResponseValue, out bool parsedValue)) - { - waitForResponse = parsedValue; - } - } + bool waitForResponse = ShouldWaitForResponse(req, defaultValue: true); AIAgent agentProxy = client.AsDurableAgentProxy(context, agentName); @@ -428,6 +427,62 @@ await agentProxy.RunAsync( return metadata.ReadOutputAs()?.Result; } + /// + /// Waits for a workflow orchestration to complete and returns an appropriate HTTP response. + /// + private static async Task WaitForWorkflowCompletionAsync( + HttpRequestData req, + DurableTaskClient client, + FunctionContext context, + string instanceId) + { + bool acceptsJson = AcceptsJson(req); + + OrchestrationMetadata? metadata = await client.WaitForInstanceCompletionAsync( + instanceId, + getInputsAndOutputs: true, + cancellation: context.CancellationToken); + + if (metadata is null) + { + return await CreateErrorResponseAsync(req, context, HttpStatusCode.InternalServerError, + $"Workflow orchestration '{instanceId}' returned no metadata.", 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); + } + + if (metadata.RuntimeStatus is not OrchestrationRuntimeStatus.Completed) + { + return await CreateErrorResponseAsync(req, context, HttpStatusCode.InternalServerError, + $"Workflow orchestration '{instanceId}' ended with unexpected status '{metadata.RuntimeStatus}'.", acceptsJson); + } + + string? result = metadata.ReadOutputAs()?.Result; + + HttpResponseData response = req.CreateResponse(HttpStatusCode.OK); + + if (acceptsJson) + { + JsonElement? resultElement = result is not null + ? JsonDocument.Parse(result).RootElement.Clone() + : null; + + await response.WriteAsJsonAsync(new WorkflowRunSuccessResponse(instanceId, metadata.RuntimeStatus.ToString(), resultElement), context.CancellationToken); + } + else + { + response.Headers.Add("Content-Type", "text/plain"); + await response.WriteStringAsync(result ?? string.Empty, context.CancellationToken); + } + + return response; + } + /// /// Creates an error response with the specified status code and error message. /// @@ -435,18 +490,18 @@ await agentProxy.RunAsync( /// The function context. /// The HTTP status code. /// The error message. + /// Optional pre-computed value indicating whether the client accepts JSON. When , the value is determined from the request's Accept header. /// The HTTP response data containing the error. private static async Task CreateErrorResponseAsync( HttpRequestData req, FunctionContext context, HttpStatusCode statusCode, - string errorMessage) + string errorMessage, + bool? acceptsJson = null) { HttpResponseData response = req.CreateResponse(statusCode); - bool acceptsJson = req.Headers.TryGetValues("Accept", out IEnumerable? acceptValues) && - acceptValues.Contains("application/json", StringComparer.OrdinalIgnoreCase); - if (acceptsJson) + if (acceptsJson ?? AcceptsJson(req)) { ErrorResponse errorResponse = new((int)statusCode, errorMessage); await response.WriteAsJsonAsync(errorResponse, context.CancellationToken); @@ -479,10 +534,7 @@ private static async Task CreateSuccessResponseAsync( HttpResponseData response = req.CreateResponse(statusCode); response.Headers.Add("x-ms-thread-id", sessionId); - bool acceptsJson = req.Headers.TryGetValues("Accept", out IEnumerable? acceptValues) && - acceptValues.Contains("application/json", StringComparer.OrdinalIgnoreCase); - - if (acceptsJson) + if (AcceptsJson(req)) { AgentRunSuccessResponse successResponse = new((int)statusCode, sessionId, agentResponse); await response.WriteAsJsonAsync(successResponse, context.CancellationToken); @@ -511,10 +563,7 @@ private static async Task CreateAcceptedResponseAsync( HttpResponseData response = req.CreateResponse(HttpStatusCode.Accepted); response.Headers.Add("x-ms-thread-id", sessionId); - bool acceptsJson = req.Headers.TryGetValues("Accept", out IEnumerable? acceptValues) && - acceptValues.Contains("application/json", StringComparer.OrdinalIgnoreCase); - - if (acceptsJson) + if (AcceptsJson(req)) { AgentRunAcceptedResponse acceptedResponse = new((int)HttpStatusCode.Accepted, sessionId); await response.WriteAsJsonAsync(acceptedResponse, context.CancellationToken); @@ -528,6 +577,31 @@ private static async Task CreateAcceptedResponseAsync( return response; } + /// + /// Returns when the caller has requested waiting for the workflow/agent to complete, + /// as indicated by the x-ms-wait-for-response header. Falls back to + /// when the header is absent or not a valid boolean. + /// + private static bool ShouldWaitForResponse(HttpRequestData req, bool defaultValue) + { + if (req.Headers.TryGetValues(WaitForResponseHeaderName, out IEnumerable? values) && + bool.TryParse(values.FirstOrDefault(), out bool parsed)) + { + return parsed; + } + + return defaultValue; + } + + /// + /// Returns when the request includes an Accept: application/json header. + /// + private static bool AcceptsJson(HttpRequestData req) + { + return req.Headers.TryGetValues("Accept", out IEnumerable? acceptValues) && + acceptValues.Contains("application/json", StringComparer.OrdinalIgnoreCase); + } + private static string GetAgentName(FunctionContext context) { // Check if the function name starts with the HttpPrefix @@ -591,6 +665,17 @@ private sealed record WorkflowRespondRequest( [property: JsonPropertyName("eventName")] string? EventName, [property: JsonPropertyName("response")] JsonElement Response); + /// + /// Represents a successful workflow run response when waiting for completion. + /// + /// The orchestration run ID. + /// The orchestration runtime status. + /// The workflow result as a JSON element so POCOs serialize as nested objects rather than escaped strings. + private sealed record WorkflowRunSuccessResponse( + [property: JsonPropertyName("runId")] string RunId, + [property: JsonPropertyName("status")] string Status, + [property: JsonPropertyName("result")] JsonElement? Result); + /// /// A service provider that combines the original service provider with an additional DurableTaskClient instance. /// diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/CHANGELOG.md b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/CHANGELOG.md index 2c188757d5..24b02ca7a5 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/CHANGELOG.md +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/CHANGELOG.md @@ -2,6 +2,7 @@ ## [Unreleased] +- Added `x-ms-wait-for-response` header support for workflow HTTP triggers to optionally wait for completion and return the result - Added MCP tool trigger support for durable workflows ([#4768](https://github.com/microsoft/agent-framework/pull/4768)) - Added Azure Functions hosting support for durable workflows ([#4436](https://github.com/microsoft/agent-framework/pull/4436)) 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 a7f2f51156..435627a169 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/WorkflowSamplesValidation.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/WorkflowSamplesValidation.cs @@ -125,6 +125,22 @@ await this.WaitForConditionAsync( }, message: "OrderStatus workflow completed", timeout: s_orchestrationTimeout); + + // Test the CancelOrder workflow with x-ms-wait-for-response header + this._outputHelper.WriteLine("Starting CancelOrder workflow with x-ms-wait-for-response: true..."); + + using HttpRequestMessage waitRequest = new(HttpMethod.Post, cancelOrderUri); + waitRequest.Content = new StringContent("55555", Encoding.UTF8, "text/plain"); + waitRequest.Headers.Add("x-ms-wait-for-response", "true"); + using HttpResponseMessage waitResponse = await s_sharedHttpClient.SendAsync(waitRequest); + + Assert.True(waitResponse.IsSuccessStatusCode, $"CancelOrder wait-for-response request failed with status: {waitResponse.StatusCode}"); + string waitResponseText = await waitResponse.Content.ReadAsStringAsync(); + this._outputHelper.WriteLine($"CancelOrder wait-for-response result: {waitResponseText}"); + + // The response should contain the workflow result (not just "started for CancelOrder") + Assert.DoesNotContain("Workflow orchestration started", waitResponseText); + Assert.Contains("55555", waitResponseText); }); } From f8976d3f1ab4e8bea977b1fddee6392beccbc9c9 Mon Sep 17 00:00:00 2001 From: Shyju Krishnankutty Date: Thu, 16 Apr 2026 20:45:47 -0700 Subject: [PATCH 2/4] update changelog. --- .../src/Microsoft.Agents.AI.Hosting.AzureFunctions/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/CHANGELOG.md b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/CHANGELOG.md index 24b02ca7a5..f8f59c89d1 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/CHANGELOG.md +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/CHANGELOG.md @@ -2,7 +2,7 @@ ## [Unreleased] -- Added `x-ms-wait-for-response` header support for workflow HTTP triggers to optionally wait for completion and return the result +- Support returning workflow results from HTTP trigger endpoint ([#5321](https://github.com/microsoft/agent-framework/pull/5321)) - Added MCP tool trigger support for durable workflows ([#4768](https://github.com/microsoft/agent-framework/pull/4768)) - Added Azure Functions hosting support for durable workflows ([#4436](https://github.com/microsoft/agent-framework/pull/4436)) From 06f165425f4bab362f69fef9bed6e41aa2738946 Mon Sep 17 00:00:00 2001 From: Shyju Krishnankutty Date: Fri, 17 Apr 2026 10:04:48 -0700 Subject: [PATCH 3/4] PR comment fixes. --- .../BuiltInFunctions.cs | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs index 9da94f8522..0717c989d7 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs @@ -468,9 +468,27 @@ private static async Task WaitForWorkflowCompletionAsync( if (acceptsJson) { - JsonElement? resultElement = result is not null - ? JsonDocument.Parse(result).RootElement.Clone() - : null; + JsonElement? resultElement = null; + if (!string.IsNullOrEmpty(result)) + { + try + { + using JsonDocument doc = JsonDocument.Parse(result); + resultElement = doc.RootElement.Clone(); + } + catch (JsonException) + { + // Result is a plain string (not valid JSON) — serialize it as a JSON string element. + var buffer = new System.Buffers.ArrayBufferWriter(); + using (var writer = new Utf8JsonWriter(buffer)) + { + writer.WriteStringValue(result); + } + + using JsonDocument fallbackDoc = JsonDocument.Parse(buffer.WrittenMemory); + resultElement = fallbackDoc.RootElement.Clone(); + } + } await response.WriteAsJsonAsync(new WorkflowRunSuccessResponse(instanceId, metadata.RuntimeStatus.ToString(), resultElement), context.CancellationToken); } @@ -594,12 +612,15 @@ private static bool ShouldWaitForResponse(HttpRequestData req, bool defaultValue } /// - /// Returns when the request includes an Accept: application/json header. + /// Returns when the request accepts the application/json media type. /// private static bool AcceptsJson(HttpRequestData req) { return req.Headers.TryGetValues("Accept", out IEnumerable? acceptValues) && - acceptValues.Contains("application/json", StringComparer.OrdinalIgnoreCase); + acceptValues + .SelectMany(v => v.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + .Select(v => v.Split(';', 2)[0].Trim()) + .Contains("application/json", StringComparer.OrdinalIgnoreCase); } private static string GetAgentName(FunctionContext context) From edf234aa4a4c31d2b8e9d68b19beb25e1087711f Mon Sep 17 00:00:00 2001 From: Shyju Krishnankutty Date: Mon, 20 Apr 2026 08:28:40 -0700 Subject: [PATCH 4/4] 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 @@ private static async Task WaitForWorkflowCompletionAsync( 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 @@ private static async Task WaitForWorkflowCompletionAsync( } } - 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 @@ private sealed record WorkflowRespondRequest( [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 @@ await this.WaitForConditionAsync( // 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()); }); }