Files
langship.sh/pkg/engine/dag.go
T

124 lines
3.0 KiB
Go

package engine
import (
"fmt"
"sort"
"github.com/lyzrai/flow/pkg/models"
)
// Edge represents a directed connection between nodes in the DAG.
type Edge struct {
Target string
SourceOutputIndex int
TargetInputIndex int
}
// DAG is the directed acyclic graph built from a workflow's nodes and connections.
type DAG struct {
Nodes map[string]models.NodeDef
Adjacency map[string][]Edge
InEdges map[string][]Edge
InDegree map[string]int
Connections []models.ConnectionDef
}
// BuildDAG constructs a DAG from a WorkflowDefinition.
func BuildDAG(wf *models.WorkflowDefinition) (*DAG, error) {
dag := &DAG{
Nodes: make(map[string]models.NodeDef, len(wf.Nodes)),
Adjacency: make(map[string][]Edge),
InEdges: make(map[string][]Edge),
InDegree: make(map[string]int),
Connections: wf.Connections,
}
for _, node := range wf.Nodes {
if _, exists := dag.Nodes[node.Name]; exists {
return nil, fmt.Errorf("duplicate node name %q", node.Name)
}
dag.Nodes[node.Name] = node
dag.InDegree[node.Name] = 0
}
for _, conn := range wf.Connections {
edge := Edge{
Target: conn.TargetNode,
SourceOutputIndex: conn.SourceOutputIndex,
TargetInputIndex: conn.TargetInputIndex,
}
dag.Adjacency[conn.SourceNode] = append(dag.Adjacency[conn.SourceNode], edge)
inEdge := Edge{
Target: conn.SourceNode,
SourceOutputIndex: conn.SourceOutputIndex,
TargetInputIndex: conn.TargetInputIndex,
}
dag.InEdges[conn.TargetNode] = append(dag.InEdges[conn.TargetNode], inEdge)
dag.InDegree[conn.TargetNode]++
}
return dag, nil
}
// TopologicalSort returns nodes in execution order using Kahn's algorithm.
// Returns an error if a cycle is detected.
func (d *DAG) TopologicalSort() ([]string, error) {
inDegree := make(map[string]int, len(d.InDegree))
for k, v := range d.InDegree {
inDegree[k] = v
}
var queue []string
for name, deg := range inDegree {
if deg == 0 {
queue = append(queue, name)
}
}
sort.Strings(queue)
var order []string
for len(queue) > 0 {
current := queue[0]
queue = queue[1:]
order = append(order, current)
for _, edge := range d.Adjacency[current] {
inDegree[edge.Target]--
if inDegree[edge.Target] == 0 {
queue = append(queue, edge.Target)
}
}
}
if len(order) != len(d.Nodes) {
return nil, fmt.Errorf("cycle detected in workflow graph: sorted %d of %d nodes", len(order), len(d.Nodes))
}
return order, nil
}
// HasApprovalNode returns true if any node in the workflow is a waitForApproval node.
// Used by the runner to gate workflows that need durable HITL.
func HasApprovalNode(wf *models.WorkflowDefinition) bool {
for _, node := range wf.Nodes {
if node.Type == "flow-nodes-base.waitForApproval" {
return true
}
}
return false
}
// GetStartNodes returns nodes with no incoming connections.
func (d *DAG) GetStartNodes() []string {
var starts []string
for name, deg := range d.InDegree {
if deg == 0 {
starts = append(starts, name)
}
}
return starts
}