feat(workflows): add conditional edges and loop validation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-04-05 18:57:33 +02:00
co-authored by Copilot
parent f011311514
commit 8f6830dca1
8 changed files with 1404 additions and 7 deletions
@@ -0,0 +1,80 @@
using Aryx.AgentHost.Contracts;
using Aryx.AgentHost.Services;
namespace Aryx.AgentHost.Tests;
public sealed class WorkflowConditionEvaluatorTests
{
[Fact]
public void Evaluate_PropertyCondition_MatchesPayload()
{
EdgeConditionDto condition = new()
{
Type = "property",
Combinator = "and",
Rules =
[
new WorkflowConditionRuleDto
{
PropertyPath = "Role",
Operator = "equals",
Value = "user",
},
],
};
bool matched = WorkflowConditionEvaluator.Evaluate(condition, new TestPayload("user", 1, "hello"));
Assert.True(matched);
}
[Fact]
public void Evaluate_ExpressionCondition_MatchesPayload()
{
EdgeConditionDto condition = new()
{
Type = "expression",
Expression = "Iteration < 3 && Role == \"user\"",
};
bool matched = WorkflowConditionEvaluator.Evaluate(condition, new TestPayload("user", 2, "hello"));
Assert.True(matched);
}
[Fact]
public void Compile_LoopCondition_StopsAfterMaxIterations()
{
WorkflowEdgeDto edge = new()
{
Id = "edge-loop",
Source = "agent",
Target = "agent",
Kind = "direct",
IsLoop = true,
MaxIterations = 2,
Condition = new EdgeConditionDto
{
Type = "property",
Rules =
[
new WorkflowConditionRuleDto
{
PropertyPath = "Iteration",
Operator = "lt",
Value = "10",
},
],
},
};
Func<object?, bool>? compiled = WorkflowConditionEvaluator.Compile(edge);
Assert.NotNull(compiled);
Assert.True(compiled!(new TestPayload("user", 1, "hello")));
Assert.True(compiled(new TestPayload("user", 2, "hello")));
Assert.False(compiled(new TestPayload("user", 3, "hello")));
}
private sealed record TestPayload(string Role, int Iteration, string Content);
}
@@ -0,0 +1,187 @@
using Aryx.AgentHost.Contracts;
using Aryx.AgentHost.Services;
namespace Aryx.AgentHost.Tests;
public sealed class WorkflowValidatorTests
{
private readonly WorkflowValidator _validator = new();
[Fact]
public void Validate_RejectsInvalidConditionOperator()
{
WorkflowDefinitionDto workflow = CreateWorkflow();
workflow = new WorkflowDefinitionDto
{
Id = workflow.Id,
Name = workflow.Name,
Settings = workflow.Settings,
Graph = new WorkflowGraphDto
{
Nodes = workflow.Graph.Nodes,
Edges =
[
new WorkflowEdgeDto
{
Id = "edge-start-agent",
Source = "start",
Target = "agent",
Kind = "direct",
Condition = new EdgeConditionDto
{
Type = "property",
Rules =
[
new WorkflowConditionRuleDto
{
PropertyPath = "Role",
Operator = "bad-op",
Value = "user",
},
],
},
},
workflow.Graph.Edges[1],
],
},
};
IReadOnlyList<WorkflowValidationIssueDto> issues = _validator.Validate(workflow);
Assert.Contains(issues, issue => issue.Field == "graph.edges.condition.rules.operator");
}
[Fact]
public void Validate_RejectsLoopWithoutMetadata()
{
WorkflowDefinitionDto workflow = CreateWorkflow();
workflow = new WorkflowDefinitionDto
{
Id = workflow.Id,
Name = workflow.Name,
Settings = workflow.Settings,
Graph = new WorkflowGraphDto
{
Nodes = workflow.Graph.Nodes,
Edges =
[
.. workflow.Graph.Edges,
new WorkflowEdgeDto
{
Id = "edge-loop",
Source = "agent",
Target = "agent",
Kind = "direct",
},
],
},
};
IReadOnlyList<WorkflowValidationIssueDto> issues = _validator.Validate(workflow);
Assert.Contains(issues, issue => issue.Field == "graph.edges.isLoop");
Assert.Contains(issues, issue => issue.Field == "graph.edges.condition");
Assert.Contains(issues, issue => issue.Field == "graph.edges.maxIterations");
}
[Fact]
public void Validate_AcceptsLoopWithExitPathConditionAndCap()
{
WorkflowDefinitionDto workflow = CreateWorkflow();
workflow = new WorkflowDefinitionDto
{
Id = workflow.Id,
Name = workflow.Name,
Settings = workflow.Settings,
Graph = new WorkflowGraphDto
{
Nodes = workflow.Graph.Nodes,
Edges =
[
.. workflow.Graph.Edges,
new WorkflowEdgeDto
{
Id = "edge-loop",
Source = "agent",
Target = "agent",
Kind = "direct",
IsLoop = true,
MaxIterations = 3,
Condition = new EdgeConditionDto
{
Type = "expression",
Expression = "Iteration < 3",
},
},
],
},
};
IReadOnlyList<WorkflowValidationIssueDto> issues = _validator.Validate(workflow);
Assert.DoesNotContain(issues, issue => issue.Level == "error");
}
private static WorkflowDefinitionDto CreateWorkflow()
{
return new WorkflowDefinitionDto
{
Id = "workflow-1",
Name = "Loop Workflow",
Graph = new WorkflowGraphDto
{
Nodes =
[
new WorkflowNodeDto
{
Id = "start",
Kind = "start",
Label = "Start",
Config = new WorkflowNodeConfigDto { Kind = "start" },
},
new WorkflowNodeDto
{
Id = "agent",
Kind = "agent",
Label = "Agent",
Config = new WorkflowNodeConfigDto
{
Kind = "agent",
Id = "agent",
Name = "Agent",
Model = "gpt-5.4",
},
},
new WorkflowNodeDto
{
Id = "end",
Kind = "end",
Label = "End",
Config = new WorkflowNodeConfigDto { Kind = "end" },
},
],
Edges =
[
new WorkflowEdgeDto
{
Id = "edge-start-agent",
Source = "start",
Target = "agent",
Kind = "direct",
},
new WorkflowEdgeDto
{
Id = "edge-agent-end",
Source = "agent",
Target = "end",
Kind = "direct",
},
],
},
Settings = new WorkflowSettingsDto
{
Checkpointing = new WorkflowCheckpointSettingsDto(),
},
};
}
}