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);
}