2026-06-22 00:08:31 +08:00
package validator
import (
"fmt"
"strings"
"agent-desk/internal/ai/workflow/dsl"
"agent-desk/internal/ai/workflow/registry"
)
type Error struct {
Field string `json:"field"`
Message string `json:"message"`
}
type Result struct {
Valid bool `json:"valid"`
Errors [] Error `json:"errors"`
}
func ValidateDefinition ( def dsl . Definition , reg * registry . Registry ) Result {
if reg == nil {
reg = registry . DefaultRegistry ()
}
v := definitionValidator {
def : def ,
registry : reg ,
nodesByID : make ( map [ string ] dsl . Node , len ( def . Nodes )),
outgoing : make ( map [ string ][] string ),
incoming : make ( map [ string ][] string ),
startNodeIDs : make ([] string , 0 , 1 ),
endNodeIDs : make ([] string , 0 , 1 ),
}
v . validate ()
return Result {
Valid : len ( v . errors ) == 0 ,
Errors : v . errors ,
}
}
type definitionValidator struct {
def dsl . Definition
registry * registry . Registry
nodesByID map [ string ] dsl . Node
outgoing map [ string ][] string
incoming map [ string ][] string
startNodeIDs [] string
endNodeIDs [] string
errors [] Error
}
func ( v * definitionValidator ) validate () {
v . validateNodes ()
v . validateEdges ()
v . validateEntry ()
v . validateReachability ()
v . validateConfirmationGuards ()
2026-06-22 18:33:39 +08:00
v . validateVariableMappings ()
2026-06-23 09:34:08 +08:00
v . validateConditions ()
2026-06-22 00:08:31 +08:00
}
func ( v * definitionValidator ) validateNodes () {
for index , node := range v . def . Nodes {
node . ID = strings . TrimSpace ( node . ID )
node . Type = strings . TrimSpace ( node . Type )
field := fmt . Sprintf ( "nodes[%d]" , index )
if node . ID == "" {
v . addError ( field + ".id" , "node id is required" )
continue
}
if _ , exists := v . nodesByID [ node . ID ]; exists {
v . addError ( field + ".id" , "duplicate node id: " + node . ID )
continue
}
v . nodesByID [ node . ID ] = node
if node . Type == "" {
v . addError ( field + ".type" , "node type is required" )
continue
}
if _ , ok := v . registry . Get ( node . Type ); ! ok {
v . addError ( field + ".type" , "unknown node type: " + node . Type )
continue
}
switch node . Type {
case registry . NodeTypeStart :
v . startNodeIDs = append ( v . startNodeIDs , node . ID )
case registry . NodeTypeEnd :
v . endNodeIDs = append ( v . endNodeIDs , node . ID )
}
}
if len ( v . startNodeIDs ) != 1 {
v . addError ( "nodes" , "workflow must contain exactly one start node" )
}
if len ( v . endNodeIDs ) == 0 {
v . addError ( "nodes" , "workflow must contain at least one end node" )
}
}
func ( v * definitionValidator ) validateEdges () {
seen := make ( map [ string ] struct {}, len ( v . def . Edges ))
for index , edge := range v . def . Edges {
edge . ID = strings . TrimSpace ( edge . ID )
edge . Source = strings . TrimSpace ( edge . Source )
edge . Target = strings . TrimSpace ( edge . Target )
field := fmt . Sprintf ( "edges[%d]" , index )
if edge . ID == "" {
v . addError ( field + ".id" , "edge id is required" )
} else if _ , exists := seen [ edge . ID ]; exists {
v . addError ( field + ".id" , "duplicate edge id: " + edge . ID )
}
seen [ edge . ID ] = struct {}{}
if edge . Source == "" {
v . addError ( field + ".source" , "edge source is required" )
} else if _ , ok := v . nodesByID [ edge . Source ]; ! ok {
v . addError ( field + ".source" , "edge source node does not exist: " + edge . Source )
}
if edge . Target == "" {
v . addError ( field + ".target" , "edge target is required" )
} else if _ , ok := v . nodesByID [ edge . Target ]; ! ok {
v . addError ( field + ".target" , "edge target node does not exist: " + edge . Target )
}
if edge . Source != "" && edge . Target != "" {
v . outgoing [ edge . Source ] = append ( v . outgoing [ edge . Source ], edge . Target )
v . incoming [ edge . Target ] = append ( v . incoming [ edge . Target ], edge . Source )
}
}
}
func ( v * definitionValidator ) validateEntry () {
entryNodeID := strings . TrimSpace ( v . def . EntryNodeID )
if entryNodeID == "" {
v . addError ( "entryNodeId" , "entry node id is required" )
return
}
entry , ok := v . nodesByID [ entryNodeID ]
if ! ok {
v . addError ( "entryNodeId" , "entry node does not exist: " + entryNodeID )
return
}
if entry . Type != registry . NodeTypeStart {
v . addError ( "entryNodeId" , "entry node must be the start node" )
}
}
func ( v * definitionValidator ) validateReachability () {
entryNodeID := strings . TrimSpace ( v . def . EntryNodeID )
if entryNodeID == "" {
return
}
if _ , ok := v . nodesByID [ entryNodeID ]; ! ok {
return
}
reachable := make ( map [ string ] struct {}, len ( v . nodesByID ))
queue := [] string { entryNodeID }
for len ( queue ) > 0 {
current := queue [ 0 ]
queue = queue [ 1 :]
if _ , exists := reachable [ current ]; exists {
continue
}
reachable [ current ] = struct {}{}
for _ , target := range v . outgoing [ current ] {
if _ , exists := reachable [ target ]; ! exists {
queue = append ( queue , target )
}
}
}
for id := range v . nodesByID {
if _ , ok := reachable [ id ]; ! ok {
v . addError ( "nodes" , "node is not reachable from entry node: " + id )
}
}
}
func ( v * definitionValidator ) validateConfirmationGuards () {
for id , node := range v . nodesByID {
spec , ok := v . registry . Get ( node . Type )
if ! ok || ! spec . RequiresConfirmationPredecessor {
continue
}
if ! v . hasConfirmationPredecessor ( id , make ( map [ string ] struct {})) {
v . addError ( "nodes." + id , node . Type + " requires human_confirm before execution" )
}
2026-06-24 14:19:41 +08:00
v . validateConfirmedInput ( id , node )
}
}
func ( v * definitionValidator ) validateConfirmedInput ( nodeID string , node dsl . Node ) {
selector , ok := node . Inputs [ "confirmed" ]
if ! ok || strings . TrimSpace ( selector . NodeID ) == "" || strings . TrimSpace ( selector . Field ) == "" {
return
}
sourceNodeID := strings . TrimSpace ( selector . NodeID )
sourceNode , ok := v . nodesByID [ sourceNodeID ]
if ! ok {
return
}
if sourceNode . Type != registry . NodeTypeHumanConfirm || strings . TrimSpace ( selector . Field ) != "confirmed" {
v . addError ( "nodes." + nodeID + ".inputs.confirmed" , "confirmed input must come from human_confirm.confirmed" )
2026-06-22 00:08:31 +08:00
}
}
2026-06-22 18:33:39 +08:00
func ( v * definitionValidator ) validateVariableMappings () {
for id , node := range v . nodesByID {
spec , ok := v . registry . Get ( node . Type )
if ! ok {
continue
}
for _ , input := range spec . InputSchema {
if ! input . Required {
continue
}
selector , ok := node . Inputs [ input . Name ]
if ! ok || strings . TrimSpace ( selector . NodeID ) == "" || strings . TrimSpace ( selector . Field ) == "" {
v . addError ( "nodes." + id + ".inputs." + input . Name , "required input mapping is missing: " + input . Name )
continue
}
v . validateInputSelector ( id , input , selector )
}
for inputName , selector := range node . Inputs {
if strings . TrimSpace ( selector . NodeID ) == "" || strings . TrimSpace ( selector . Field ) == "" {
v . addError ( "nodes." + id + ".inputs." + inputName , "input mapping source is required" )
continue
}
if _ , ok := findInputSpec ( spec . InputSchema , inputName ); ok {
continue
}
sourceNode , sourceOK := v . nodesByID [ strings . TrimSpace ( selector . NodeID )]
if ! sourceOK {
v . addError ( "nodes." + id + ".inputs." + inputName , "input source node does not exist: " + selector . NodeID )
continue
}
sourceSpec , sourceSpecOK := v . registry . Get ( sourceNode . Type )
if ! sourceSpecOK {
continue
}
if _ , ok := findOutputSpec ( sourceSpec . OutputSchema , selector . Field ); ! ok {
v . addError ( "nodes." + id + ".inputs." + inputName , "input source field does not exist: " + selector . NodeID + "." + selector . Field )
}
}
}
}
func ( v * definitionValidator ) validateInputSelector ( nodeID string , input registry . VariableSpec , selector dsl . VariableSelector ) {
sourceNodeID := strings . TrimSpace ( selector . NodeID )
sourceField := strings . TrimSpace ( selector . Field )
sourceNode , ok := v . nodesByID [ sourceNodeID ]
if ! ok {
v . addError ( "nodes." + nodeID + ".inputs." + input . Name , "input source node does not exist: " + sourceNodeID )
return
}
if ! v . hasPath ( sourceNodeID , nodeID , make ( map [ string ] struct {})) {
v . addError ( "nodes." + nodeID + ".inputs." + input . Name , "input source node is not available before current node: " + sourceNodeID )
return
}
sourceSpec , ok := v . registry . Get ( sourceNode . Type )
if ! ok {
return
}
output , ok := findOutputSpec ( sourceSpec . OutputSchema , sourceField )
if ! ok {
v . addError ( "nodes." + nodeID + ".inputs." + input . Name , "input source field does not exist: " + sourceNodeID + "." + sourceField )
return
}
if ! variableTypesCompatible ( input . Type , output . Type ) {
v . addError ( "nodes." + nodeID + ".inputs." + input . Name , fmt . Sprintf ( "input type mismatch: %s expects %s but %s.%s is %s" , input . Name , input . Type , sourceNodeID , sourceField , output . Type ))
}
}
2026-06-23 09:34:08 +08:00
func ( v * definitionValidator ) validateConditions () {
conditionalSources := make ( map [ string ] bool )
defaultSources := make ( map [ string ] bool )
for index , edge := range v . def . Edges {
field := fmt . Sprintf ( "edges[%d].condition" , index )
sourceID := strings . TrimSpace ( edge . Source )
if edge . Condition == nil {
if sourceID != "" {
defaultSources [ sourceID ] = true
}
continue
}
if sourceID != "" {
conditionalSources [ sourceID ] = true
}
v . validateCondition ( field , sourceID , edge . Condition )
}
for sourceID := range conditionalSources {
if ! defaultSources [ sourceID ] {
v . addError ( "edges." + sourceID , "conditional branch must include a default edge" )
}
}
}
func ( v * definitionValidator ) validateCondition ( field string , sourceNodeID string , condition * dsl . Condition ) {
if condition == nil {
return
}
operator := strings . TrimSpace ( condition . Operator )
if operator == "" && strings . TrimSpace ( condition . Expression ) != "" {
v . addError ( field + ".expression" , "free-form condition expressions are not supported" )
return
}
if ! isSupportedConditionOperator ( operator ) {
v . addError ( field + ".operator" , "unsupported condition operator: " + operator )
return
}
if condition . Left == nil {
v . addError ( field + ".left" , "condition left variable is required" )
return
}
sourceSelectorNodeID := strings . TrimSpace ( condition . Left . NodeID )
sourceField := strings . TrimSpace ( condition . Left . Field )
if sourceSelectorNodeID == "" || sourceField == "" {
v . addError ( field + ".left" , "condition left variable is required" )
return
}
sourceNode , ok := v . nodesByID [ sourceSelectorNodeID ]
if ! ok {
v . addError ( field + ".left" , "condition source node does not exist: " + sourceSelectorNodeID )
return
}
if sourceNodeID != "" && ! v . hasPath ( sourceSelectorNodeID , sourceNodeID , make ( map [ string ] struct {})) && sourceSelectorNodeID != sourceNodeID {
v . addError ( field + ".left" , "condition source node is not available before branch: " + sourceSelectorNodeID )
return
}
sourceSpec , ok := v . registry . Get ( sourceNode . Type )
if ! ok {
return
}
if _ , ok := findOutputSpec ( sourceSpec . OutputSchema , sourceField ); ! ok {
v . addError ( field + ".left" , "condition source field does not exist: " + sourceSelectorNodeID + "." + sourceField )
}
}
func isSupportedConditionOperator ( operator string ) bool {
switch strings . TrimSpace ( operator ) {
case "eq" , "equals" , "neq" , "not_equals" , "contains" , "exists" , "not_exists" , "truthy" , "is_true" , "falsy" , "is_false" , "gt" , "gte" , "lt" , "lte" :
return true
default :
return false
}
}
2026-06-22 18:33:39 +08:00
func ( v * definitionValidator ) hasPath ( sourceID string , targetID string , visiting map [ string ] struct {}) bool {
if sourceID == targetID {
return false
}
if _ , seen := visiting [ sourceID ]; seen {
return false
}
visiting [ sourceID ] = struct {}{}
for _ , next := range v . outgoing [ sourceID ] {
if next == targetID {
return true
}
if v . hasPath ( next , targetID , visiting ) {
return true
}
}
return false
}
func findInputSpec ( items [] registry . VariableSpec , name string ) ( registry . VariableSpec , bool ) {
name = strings . TrimSpace ( name )
for _ , item := range items {
if item . Name == name {
return item , true
}
}
return registry . VariableSpec {}, false
}
func findOutputSpec ( items [] registry . VariableSpec , name string ) ( registry . VariableSpec , bool ) {
name = strings . TrimSpace ( name )
for _ , item := range items {
if item . Name == name {
return item , true
}
}
return registry . VariableSpec {}, false
}
func variableTypesCompatible ( input registry . VariableType , output registry . VariableType ) bool {
return input == registry . VariableTypeAny || output == registry . VariableTypeAny || input == output
}
2026-06-22 00:08:31 +08:00
func ( v * definitionValidator ) hasConfirmationPredecessor ( nodeID string , visiting map [ string ] struct {}) bool {
if _ , seen := visiting [ nodeID ]; seen {
return false
}
visiting [ nodeID ] = struct {}{}
for _ , source := range v . incoming [ nodeID ] {
node , ok := v . nodesByID [ source ]
if ! ok {
continue
}
if node . Type == registry . NodeTypeHumanConfirm {
return true
}
if v . hasConfirmationPredecessor ( source , visiting ) {
return true
}
}
return false
}
func ( v * definitionValidator ) addError ( field string , message string ) {
v . errors = append ( v . errors , Error { Field : field , Message : message })
}