From b43a5061cf5de6907a5108c5757859b4e4a732fb Mon Sep 17 00:00:00 2001 From: ognjenkatic Date: Thu, 27 Aug 2026 11:00:13 +0200 Subject: [PATCH] Rename the reader's TryRead to async ReadOrNullAsync with a nullable result Restores the async signature; the Try shape forced the method synchronous since async methods cannot have out parameters. Co-Authored-By: Claude Fable 5 --- .../Util/FailedTaskStructuredErrorReader.cs | 27 +++++++--------- .../FailedTaskStructuredErrorReaderTests.cs | 32 ++++++++----------- 2 files changed, 24 insertions(+), 35 deletions(-) diff --git a/src/ConductorSharp.Engine/Util/FailedTaskStructuredErrorReader.cs b/src/ConductorSharp.Engine/Util/FailedTaskStructuredErrorReader.cs index d7af8cc..442287b 100644 --- a/src/ConductorSharp.Engine/Util/FailedTaskStructuredErrorReader.cs +++ b/src/ConductorSharp.Engine/Util/FailedTaskStructuredErrorReader.cs @@ -1,3 +1,4 @@ +#nullable enable using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -28,26 +29,20 @@ public FailedTaskStructuredErrorReader(IWorkflowService workflowService) /// /// Reads the structured error declared by the deepest failed task of , - /// following the Try pattern: returns false (with a null ) when - /// the execution has no failed task or the failed task declared nothing. Synchronous — an out - /// parameter rules out async — so the underlying Conductor call blocks the calling thread. + /// or null when the execution has no failed task or the failed task declared nothing. /// - public bool TryRead(string workflowId, out StructuredError error, CancellationToken cancellationToken) + public async Task ReadOrNullAsync(string? workflowId, CancellationToken cancellationToken) { - var failed = FindDeepestFailedTaskAsync(workflowId, cancellationToken).GetAwaiter().GetResult(); + var failed = await FindDeepestFailedTaskAsync(workflowId, cancellationToken); if (failed?.OutputData != null && StructuredErrorSerializer.TryDeserialize(failed.OutputData, out var structured)) - { - error = structured; - return true; - } + return structured; - error = null; - return false; + return null; } /// - /// Like , but always yields an error: a failure that declared nothing is + /// Like , but never returns null: a failure that declared nothing is /// classified as with as the /// sanitized reason, so raw internals never cross a boundary by default. The returned /// always carries the most specific diagnostic available: the declared @@ -64,9 +59,9 @@ public bool TryRead(string workflowId, out StructuredError error, CancellationTo /// inside the diagnostic message, never as the sanitized reason. /// public async Task ReadOrFallbackAsync( - string workflowId, + string? workflowId, string genericReason, - string fallbackReason, + string? fallbackReason, CancellationToken cancellationToken ) { @@ -98,7 +93,7 @@ CancellationToken cancellationToken /// so any failed sub-workflow is descended into first (walking from the last), and FORK/JOIN aggregators /// are skipped when picking a leaf. Returns null when the execution has no failed task. /// - public async Task FindDeepestFailedTaskAsync(string workflowId, CancellationToken cancellationToken) + public async Task FindDeepestFailedTaskAsync(string? workflowId, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(workflowId)) return null; @@ -124,7 +119,7 @@ CancellationToken cancellationToken return failedTasks.LastOrDefault(t => t.TaskType is not ("JOIN" or "FORK")) ?? failedTasks[^1]; } - private static string BuildDiagnosticMessage(ConductorSharp.Client.Generated.Task task, string fallbackReason) + private static string BuildDiagnosticMessage(ConductorSharp.Client.Generated.Task? task, string? fallbackReason) { if (task == null) return fallbackReason ?? "No failed task found."; diff --git a/test/ConductorSharp.Engine.Tests/Unit/FailedTaskStructuredErrorReaderTests.cs b/test/ConductorSharp.Engine.Tests/Unit/FailedTaskStructuredErrorReaderTests.cs index 97cb24e..e0f3f96 100644 --- a/test/ConductorSharp.Engine.Tests/Unit/FailedTaskStructuredErrorReaderTests.cs +++ b/test/ConductorSharp.Engine.Tests/Unit/FailedTaskStructuredErrorReaderTests.cs @@ -159,42 +159,36 @@ private static GeneratedTask FailedTask( private static Workflow Execution(params GeneratedTask[] tasks) => new() { Tasks = tasks }; [Fact] - public void TryRead_returns_the_declared_structured_error() + public async Task ReadOrNull_returns_the_declared_structured_error() { var output = StructuredErrorSerializer.Serialize(new StructuredError { Code = "RESOURCE_UNAVAILABLE", Reason = "No port available" }); var reader = Reader(new() { ["wf"] = Execution(FailedTask(outputData: output)) }); - var found = reader.TryRead("wf", out var error, CancellationToken.None); + var error = await reader.ReadOrNullAsync("wf", CancellationToken.None); - Assert.True(found); + Assert.NotNull(error); Assert.Equal("RESOURCE_UNAVAILABLE", error.Code); Assert.Equal("No port available", error.Reason); } [Fact] - public void TryRead_returns_false_when_the_failed_task_declared_nothing() + public async Task ReadOrNull_returns_null_when_the_failed_task_declared_nothing() { var reader = Reader(new() { ["wf"] = Execution(FailedTask(reason: "raw internals")) }); - var found = reader.TryRead("wf", out var error, CancellationToken.None); - - Assert.False(found); - Assert.Null(error); + Assert.Null(await reader.ReadOrNullAsync("wf", CancellationToken.None)); } [Fact] - public void TryRead_returns_false_when_nothing_failed() + public async Task ReadOrNull_returns_null_when_nothing_failed() { var reader = Reader(new() { ["wf"] = Execution() }); - var found = reader.TryRead("wf", out var error, CancellationToken.None); - - Assert.False(found); - Assert.Null(error); + Assert.Null(await reader.ReadOrNullAsync("wf", CancellationToken.None)); } [Fact] - public void Descends_into_the_failed_sub_workflow_instead_of_stopping_on_the_join() + public async Task Descends_into_the_failed_sub_workflow_instead_of_stopping_on_the_join() { // Parent: a failed SUB_WORKFLOW and the aggregating JOIN that failed after it. The declared error // lives on the leaf task inside the child execution. @@ -207,23 +201,23 @@ public void Descends_into_the_failed_sub_workflow_instead_of_stopping_on_the_joi } ); - var found = reader.TryRead("parent", out var error, CancellationToken.None); + var error = await reader.ReadOrNullAsync("parent", CancellationToken.None); - Assert.True(found); + Assert.NotNull(error); Assert.Equal("LEAF", error.Code); } [Fact] - public void Skips_fork_and_join_aggregators_when_picking_the_leaf() + public async Task Skips_fork_and_join_aggregators_when_picking_the_leaf() { var output = StructuredErrorSerializer.Serialize(new StructuredError { Code = "SIMPLE_LEAF", Reason = "r" }); var reader = Reader( new() { ["wf"] = Execution(FailedTask(taskType: "FORK"), FailedTask(outputData: output), FailedTask(taskType: "JOIN")) } ); - var found = reader.TryRead("wf", out var error, CancellationToken.None); + var error = await reader.ReadOrNullAsync("wf", CancellationToken.None); - Assert.True(found); + Assert.NotNull(error); Assert.Equal("SIMPLE_LEAF", error.Code); }