From 4bdb10784f7b0efc3e4b096375f6b33fe561bb70 Mon Sep 17 00:00:00 2001 From: Shyju Krishnankutty Date: Thu, 2 Jul 2026 13:17:03 -0700 Subject: [PATCH 1/6] 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/6] 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/6] 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/6] 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/6] 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]); From 3d447af377dd410efbe59033ba26c737a2701bb0 Mon Sep 17 00:00:00 2001 From: Shyju Krishnankutty Date: Tue, 7 Jul 2026 11:06:53 -0700 Subject: [PATCH 6/6] Throw JsonException when neither executorId nor sourceId is present When both keys are missing the payload is genuinely malformed. Throwing JsonException is consistent with the caller's existing catch handler (lines 395-397) which returns null for unparsable events, rather than silently producing a WorkflowOutputEvent with an empty ExecutorId. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Workflows/DurableStreamingWorkflowRun.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableStreamingWorkflowRun.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableStreamingWorkflowRun.cs index 72de08c0771..e0b36643ab2 100644 --- a/dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableStreamingWorkflowRun.cs +++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableStreamingWorkflowRun.cs @@ -429,7 +429,7 @@ private static bool TryParseWorkflowResult(string? serializedOutput, [NotNullWhe ? execIdElem.GetString() ?? string.Empty : root.TryGetProperty("sourceId", out JsonElement srcIdElem) ? srcIdElem.GetString() ?? string.Empty - : string.Empty; + : throw new JsonException("WorkflowOutputEvent is missing required 'executorId' (or legacy 'sourceId') property."); object? outputData = GetDataProperty(root); return new WorkflowOutputEvent(outputData!, outputExecutorId); }