From e36b00ff1da7430517aacbdd587c72cd12ce9b4c Mon Sep 17 00:00:00 2001 From: David Kaya Date: Wed, 8 Apr 2026 19:15:58 +0200 Subject: [PATCH] fix: make sub-workflow resolution tolerant of missing references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract TryResolveSubWorkflowDefinition that returns null instead of throwing when a referenced workflow is missing from the library. Use the tolerant variant in all graph-walking helpers that build agent indexes, collect agent nodes, and find sub-workflow nodes. These operations are used for activity UI enrichment and agent identity resolution — they should degrade gracefully rather than crash the turn. The throwing ResolveSubWorkflowDefinition is now only used by WorkflowRunner.CreateNodeRoute, which is the actual execution path and correctly fails with a clear error if a reference is truly missing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Services/AgentIdentityResolver.cs | 7 ++++- .../Services/WorkflowDefinitionExtensions.cs | 26 +++++++++++++++---- .../AgentIdentityResolverTests.cs | 17 ++++++++++++ 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/sidecar/src/Aryx.AgentHost/Services/AgentIdentityResolver.cs b/sidecar/src/Aryx.AgentHost/Services/AgentIdentityResolver.cs index bb2f4b7..2d9277b 100644 --- a/sidecar/src/Aryx.AgentHost/Services/AgentIdentityResolver.cs +++ b/sidecar/src/Aryx.AgentHost/Services/AgentIdentityResolver.cs @@ -350,7 +350,12 @@ internal static class AgentIdentityResolver continue; } - WorkflowDefinitionDto subWorkflow = node.ResolveSubWorkflowDefinition(workflowLibrary); + WorkflowDefinitionDto? subWorkflow = node.TryResolveSubWorkflowDefinition(workflowLibrary); + if (subWorkflow is null) + { + continue; + } + CollectAgentSubworkflowContexts( subWorkflow, workflowLibrary, diff --git a/sidecar/src/Aryx.AgentHost/Services/WorkflowDefinitionExtensions.cs b/sidecar/src/Aryx.AgentHost/Services/WorkflowDefinitionExtensions.cs index ca732b3..4cb2d0c 100644 --- a/sidecar/src/Aryx.AgentHost/Services/WorkflowDefinitionExtensions.cs +++ b/sidecar/src/Aryx.AgentHost/Services/WorkflowDefinitionExtensions.cs @@ -66,6 +66,15 @@ internal static class WorkflowDefinitionExtensions public static WorkflowDefinitionDto ResolveSubWorkflowDefinition( this WorkflowNodeDto node, IReadOnlyDictionary? workflowLibrary) + { + return node.TryResolveSubWorkflowDefinition(workflowLibrary) + ?? throw new InvalidOperationException( + $"Sub-workflow node \"{node.Id}\" references unknown workflow \"{node.Config.WorkflowId}\"."); + } + + public static WorkflowDefinitionDto? TryResolveSubWorkflowDefinition( + this WorkflowNodeDto node, + IReadOnlyDictionary? workflowLibrary) { ArgumentNullException.ThrowIfNull(node); @@ -81,8 +90,7 @@ internal static class WorkflowDefinitionExtensions return workflow; } - throw new InvalidOperationException( - $"Sub-workflow node \"{node.Id}\" references unknown workflow \"{node.Config.WorkflowId}\"."); + return null; } public static bool IsOrchestrationMode(this WorkflowDefinitionDto workflow, string mode) @@ -190,8 +198,11 @@ internal static class WorkflowDefinitionExtensions continue; } - WorkflowDefinitionDto subWorkflow = node.ResolveSubWorkflowDefinition(workflowLibrary); - CollectAgentNodes(subWorkflow, workflowLibrary, agentNodes, visitedWorkflowIds, visitedAnonymousWorkflows); + WorkflowDefinitionDto? subWorkflow = node.TryResolveSubWorkflowDefinition(workflowLibrary); + if (subWorkflow is not null) + { + CollectAgentNodes(subWorkflow, workflowLibrary, agentNodes, visitedWorkflowIds, visitedAnonymousWorkflows); + } } } @@ -227,7 +238,12 @@ internal static class WorkflowDefinitionExtensions return node; } - WorkflowDefinitionDto subWorkflow = node.ResolveSubWorkflowDefinition(workflowLibrary); + WorkflowDefinitionDto? subWorkflow = node.TryResolveSubWorkflowDefinition(workflowLibrary); + if (subWorkflow is null) + { + continue; + } + WorkflowNodeDto? match = FindSubWorkflowNode( subWorkflow, nodeId, diff --git a/sidecar/tests/Aryx.AgentHost.Tests/AgentIdentityResolverTests.cs b/sidecar/tests/Aryx.AgentHost.Tests/AgentIdentityResolverTests.cs index ef40495..cf8ef13 100644 --- a/sidecar/tests/Aryx.AgentHost.Tests/AgentIdentityResolverTests.cs +++ b/sidecar/tests/Aryx.AgentHost.Tests/AgentIdentityResolverTests.cs @@ -174,6 +174,23 @@ public sealed class AgentIdentityResolverTests Assert.Equal("Inner Review", subworkflow.SubworkflowName); } + [Fact] + public void BuildAgentSubworkflowIndex_SkipsUnresolvableSubWorkflowReferences() + { + WorkflowDefinitionDto workflow = CreateWorkflow( + "parent-workflow", + [ + CreateAgent("agent-top-level", "Top Level"), + CreateSubworkflow("subworkflow-missing", "Missing Pipeline", workflowId: "nonexistent-workflow"), + ], + orchestrationMode: "concurrent"); + + IReadOnlyDictionary index = + AgentIdentityResolver.BuildAgentSubworkflowIndex(workflow); + + Assert.Empty(index); + } + private static WorkflowDefinitionDto CreateWorkflow( IReadOnlyList nodes, string orchestrationMode = "concurrent")