From 4bdb10784f7b0efc3e4b096375f6b33fe561bb70 Mon Sep 17 00:00:00 2001 From: Shyju Krishnankutty Date: Thu, 2 Jul 2026 13:17:03 -0700 Subject: [PATCH 1/5] Fix WorkflowOutputEvent streaming deserialization using renamed property The WorkflowOutputEvent.SourceId property was renamed to ExecutorId, but DeserializeEventByType still looked for "sourceId" in the JSON payload. This caused a KeyNotFoundException when WatchStreamAsync encountered a WorkflowOutputEvent during streaming. Update the manual deserialization to read "executorId" instead. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Workflows/DurableStreamingWorkflowRun.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableStreamingWorkflowRun.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableStreamingWorkflowRun.cs index d05d01b4549..f71d03857f6 100644 --- a/dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableStreamingWorkflowRun.cs +++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableStreamingWorkflowRun.cs @@ -425,9 +425,9 @@ private static bool TryParseWorkflowResult(string? serializedOutput, [NotNullWhe } // WorkflowOutputEvent - string sourceId = root.GetProperty("sourceId").GetString() ?? string.Empty; + string outputExecutorId = root.GetProperty("executorId").GetString() ?? string.Empty; object? outputData = GetDataProperty(root); - return new WorkflowOutputEvent(outputData!, sourceId); + return new WorkflowOutputEvent(outputData!, outputExecutorId); } return JsonSerializer.Deserialize(json, eventType, DurableSerialization.Options) as WorkflowEvent; From b22cb16f590de8debbdcd9b3c5a10bebbb709591 Mon Sep 17 00:00:00 2001 From: Shyju Krishnankutty Date: Thu, 2 Jul 2026 14:02:41 -0700 Subject: [PATCH 2/5] Add WorkflowOutputEvent streaming round-trip test Covers the serialize-deserialize path through DeserializeEventByType for WorkflowOutputEvent, which was missing and allowed the sourceId to executorId rename regression to go undetected. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../DurableStreamingWorkflowRunTests.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/dotnet/tests/Microsoft.Agents.AI.DurableTask.UnitTests/Workflows/DurableStreamingWorkflowRunTests.cs b/dotnet/tests/Microsoft.Agents.AI.DurableTask.UnitTests/Workflows/DurableStreamingWorkflowRunTests.cs index 404ed3496d6..be825381ebd 100644 --- a/dotnet/tests/Microsoft.Agents.AI.DurableTask.UnitTests/Workflows/DurableStreamingWorkflowRunTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.DurableTask.UnitTests/Workflows/DurableStreamingWorkflowRunTests.cs @@ -233,6 +233,35 @@ public async Task WatchStreamAsync_CompletedWithEventsInOutput_YieldsEventsAndCo Assert.Equal("result", completedResult.Result); } + [Fact] + public async Task WatchStreamAsync_WorkflowOutputEvent_RoundTripsCorrectlyAsync() + { + // Arrange + WorkflowOutputEvent outputEvent = new("test-data", "executor-1"); + string serializedEvent = SerializeEvent(outputEvent); + string serializedOutput = SerializeWorkflowResult("done", [serializedEvent]); + + Mock mockClient = new("test"); + mockClient.Setup(c => c.GetInstanceAsync(InstanceId, true, It.IsAny())) + .ReturnsAsync(CreateMetadata(OrchestrationRuntimeStatus.Completed, serializedOutput: serializedOutput)); + + DurableStreamingWorkflowRun run = new(mockClient.Object, InstanceId, CreateTestWorkflow()); + + // Act + List events = []; + await foreach (WorkflowEvent evt in run.WatchStreamAsync()) + { + events.Add(evt); + } + + // Assert + Assert.Equal(2, events.Count); + WorkflowOutputEvent result = Assert.IsType(events[0]); + Assert.Equal("executor-1", result.ExecutorId); + Assert.Equal("test-data", result.Data?.ToString()); + Assert.Empty(result.Tags); + } + [Fact] public async Task WatchStreamAsync_CompletedWithoutWrapper_YieldsFailedEventAsync() { From 4111a976e4f7c6ff197753ec0153531a97bd15d8 Mon Sep 17 00:00:00 2001 From: Shyju Krishnankutty Date: Thu, 2 Jul 2026 14:16:00 -0700 Subject: [PATCH 3/5] Address PR review: TryGetProperty fallback and CHANGELOG entry Use TryGetProperty with sourceId fallback for backward compatibility with older persisted streams. Add CHANGELOG entry per repo convention. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Microsoft.Agents.AI.DurableTask/CHANGELOG.md | 1 + .../Workflows/DurableStreamingWorkflowRun.cs | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/CHANGELOG.md b/dotnet/src/Microsoft.Agents.AI.DurableTask/CHANGELOG.md index 2264d994280..28c2415b2b4 100644 --- a/dotnet/src/Microsoft.Agents.AI.DurableTask/CHANGELOG.md +++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/CHANGELOG.md @@ -2,6 +2,7 @@ ## [Unreleased] +- Fixed `WorkflowOutputEvent` streaming deserialization to read `executorId` instead of the renamed `sourceId` property, with fallback for backward compatibility. - Fix issue with resuming checkpoint after package version upgrade ([#6670](https://github.com/microsoft/agent-framework/pull/6670)) - Bind MCP threadId to the current agent and guard cross-agent session dispatch ([#6531](https://github.com/microsoft/agent-framework/pull/6531)) - Added support for durable workflows ([#4436](https://github.com/microsoft/agent-framework/pull/4436)) diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableStreamingWorkflowRun.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableStreamingWorkflowRun.cs index f71d03857f6..72de08c0771 100644 --- a/dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableStreamingWorkflowRun.cs +++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableStreamingWorkflowRun.cs @@ -425,7 +425,11 @@ private static bool TryParseWorkflowResult(string? serializedOutput, [NotNullWhe } // WorkflowOutputEvent - string outputExecutorId = root.GetProperty("executorId").GetString() ?? string.Empty; + string outputExecutorId = root.TryGetProperty("executorId", out JsonElement execIdElem) + ? execIdElem.GetString() ?? string.Empty + : root.TryGetProperty("sourceId", out JsonElement srcIdElem) + ? srcIdElem.GetString() ?? string.Empty + : string.Empty; object? outputData = GetDataProperty(root); return new WorkflowOutputEvent(outputData!, outputExecutorId); } From c07580eb06d6f80aab8cb55972a93d518e23bf5c Mon Sep 17 00:00:00 2001 From: Shyju Krishnankutty Date: Thu, 2 Jul 2026 14:37:35 -0700 Subject: [PATCH 4/5] Address PR review: add legacy sourceId test and CHANGELOG PR link Add test covering the TryGetProperty fallback path for legacy sourceId payloads. Add PR link to CHANGELOG entry to match repo convention. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../CHANGELOG.md | 2 +- .../DurableStreamingWorkflowRunTests.cs | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/CHANGELOG.md b/dotnet/src/Microsoft.Agents.AI.DurableTask/CHANGELOG.md index 28c2415b2b4..b23d2425bf2 100644 --- a/dotnet/src/Microsoft.Agents.AI.DurableTask/CHANGELOG.md +++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/CHANGELOG.md @@ -2,7 +2,7 @@ ## [Unreleased] -- Fixed `WorkflowOutputEvent` streaming deserialization to read `executorId` instead of the renamed `sourceId` property, with fallback for backward compatibility. +- Fixed `WorkflowOutputEvent` streaming deserialization to read `executorId` instead of the renamed `sourceId` property, with fallback for backward compatibility ([#6896](https://github.com/microsoft/agent-framework/pull/6896)) - Fix issue with resuming checkpoint after package version upgrade ([#6670](https://github.com/microsoft/agent-framework/pull/6670)) - Bind MCP threadId to the current agent and guard cross-agent session dispatch ([#6531](https://github.com/microsoft/agent-framework/pull/6531)) - Added support for durable workflows ([#4436](https://github.com/microsoft/agent-framework/pull/4436)) diff --git a/dotnet/tests/Microsoft.Agents.AI.DurableTask.UnitTests/Workflows/DurableStreamingWorkflowRunTests.cs b/dotnet/tests/Microsoft.Agents.AI.DurableTask.UnitTests/Workflows/DurableStreamingWorkflowRunTests.cs index be825381ebd..a8f5a2d0227 100644 --- a/dotnet/tests/Microsoft.Agents.AI.DurableTask.UnitTests/Workflows/DurableStreamingWorkflowRunTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.DurableTask.UnitTests/Workflows/DurableStreamingWorkflowRunTests.cs @@ -262,6 +262,37 @@ public async Task WatchStreamAsync_WorkflowOutputEvent_RoundTripsCorrectlyAsync( Assert.Empty(result.Tags); } + [Fact] + public async Task WatchStreamAsync_WorkflowOutputEvent_LegacySourceId_RoundTripsCorrectlyAsync() + { + // Arrange — simulate a legacy payload that uses "sourceId" instead of "executorId" + const string legacyEventJson = """{"sourceId":"legacy-executor","data":"legacy-data"}"""; + string typeName = typeof(WorkflowOutputEvent).AssemblyQualifiedName!; + string serializedEvent = JsonSerializer.Serialize( + new { typeName, data = legacyEventJson }, + DurableSerialization.Options); + string serializedOutput = SerializeWorkflowResult("done", [serializedEvent]); + + Mock mockClient = new("test"); + mockClient.Setup(c => c.GetInstanceAsync(InstanceId, true, It.IsAny())) + .ReturnsAsync(CreateMetadata(OrchestrationRuntimeStatus.Completed, serializedOutput: serializedOutput)); + + DurableStreamingWorkflowRun run = new(mockClient.Object, InstanceId, CreateTestWorkflow()); + + // Act + List events = []; + await foreach (WorkflowEvent evt in run.WatchStreamAsync()) + { + events.Add(evt); + } + + // Assert + Assert.Equal(2, events.Count); + WorkflowOutputEvent result = Assert.IsType(events[0]); + Assert.Equal("legacy-executor", result.ExecutorId); + Assert.Equal("legacy-data", result.Data?.ToString()); + } + [Fact] public async Task WatchStreamAsync_CompletedWithoutWrapper_YieldsFailedEventAsync() { From 6db88585a7fc68aa16550117a68484ffa7facca7 Mon Sep 17 00:00:00 2001 From: Shyju Krishnankutty Date: Thu, 2 Jul 2026 14:44:30 -0700 Subject: [PATCH 5/5] Fix const naming to PascalCase for dotnet format Rename legacyEventJson to LegacyEventJson to satisfy IDE1006 naming rule requiring constants to use PascalCase. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Workflows/DurableStreamingWorkflowRunTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/tests/Microsoft.Agents.AI.DurableTask.UnitTests/Workflows/DurableStreamingWorkflowRunTests.cs b/dotnet/tests/Microsoft.Agents.AI.DurableTask.UnitTests/Workflows/DurableStreamingWorkflowRunTests.cs index a8f5a2d0227..33277f2ff22 100644 --- a/dotnet/tests/Microsoft.Agents.AI.DurableTask.UnitTests/Workflows/DurableStreamingWorkflowRunTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.DurableTask.UnitTests/Workflows/DurableStreamingWorkflowRunTests.cs @@ -266,10 +266,10 @@ public async Task WatchStreamAsync_WorkflowOutputEvent_RoundTripsCorrectlyAsync( public async Task WatchStreamAsync_WorkflowOutputEvent_LegacySourceId_RoundTripsCorrectlyAsync() { // Arrange — simulate a legacy payload that uses "sourceId" instead of "executorId" - const string legacyEventJson = """{"sourceId":"legacy-executor","data":"legacy-data"}"""; + const string LegacyEventJson = """{"sourceId":"legacy-executor","data":"legacy-data"}"""; string typeName = typeof(WorkflowOutputEvent).AssemblyQualifiedName!; string serializedEvent = JsonSerializer.Serialize( - new { typeName, data = legacyEventJson }, + new { typeName, data = LegacyEventJson }, DurableSerialization.Options); string serializedOutput = SerializeWorkflowResult("done", [serializedEvent]);