mirror of
https://github.com/open-gitagent/langship.sh.git
synced 2026-08-03 07:21:04 +02:00
125 lines
3.3 KiB
Go
125 lines
3.3 KiB
Go
package orchestrator
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
|
|
"github.com/lyzrai/flow/pkg/engine"
|
|
"github.com/lyzrai/flow/pkg/models"
|
|
)
|
|
|
|
// walkDurable executes a workflow's DAG with each node wrapped in a NodeRunner
|
|
// (typically restate.Run for journaling). Sequential topological order — the
|
|
// reactive ready-queue with futures is a v0.2 thing.
|
|
func walkDurable(
|
|
ctx context.Context,
|
|
wf *models.WorkflowDefinition,
|
|
triggerData []models.Item,
|
|
lookup engine.ExecutorLookup,
|
|
runNode NodeRunner,
|
|
) (*models.ExecutionResult, error) {
|
|
dag, err := engine.BuildDAG(wf)
|
|
if err != nil {
|
|
wrapped := fmt.Errorf("failed to build DAG: %w", err)
|
|
return &models.ExecutionResult{Status: "failed", Errors: []string{wrapped.Error()}}, wrapped
|
|
}
|
|
|
|
order, err := dag.TopologicalSort()
|
|
if err != nil {
|
|
wrapped := fmt.Errorf("failed to sort DAG: %w", err)
|
|
return &models.ExecutionResult{Status: "failed", Errors: []string{wrapped.Error()}}, wrapped
|
|
}
|
|
|
|
slog.InfoContext(ctx, "workflow_started_durable",
|
|
slog.String("workflow", wf.Name),
|
|
slog.Int("node_count", len(order)),
|
|
)
|
|
|
|
execCtx := engine.NewExecutionContext(wf, dag)
|
|
execCtx.Lookup = lookup
|
|
|
|
var execErrors []string
|
|
|
|
for _, nodeName := range order {
|
|
node := dag.Nodes[nodeName]
|
|
|
|
var inputs [][]models.Item
|
|
if node.Type == "flow-nodes-base.trigger" && triggerData != nil {
|
|
inputs = [][]models.Item{triggerData}
|
|
} else {
|
|
inputs = execCtx.GatherInputs(nodeName)
|
|
}
|
|
|
|
// Skip orphan non-trigger nodes (no path from a trigger).
|
|
if node.Type != "flow-nodes-base.trigger" && len(dag.InEdges[nodeName]) == 0 {
|
|
continue
|
|
}
|
|
|
|
// Skip nodes whose inputs were routed elsewhere (Switch/If false branch).
|
|
if len(dag.InEdges[nodeName]) > 0 && allInputsEmpty(inputs) {
|
|
continue
|
|
}
|
|
|
|
resolvedParams := engine.ResolveExpressions(node.Parameters, execCtx, nodeName)
|
|
node.Parameters = resolvedParams
|
|
|
|
executorFn, err := lookup(node.Type)
|
|
if err != nil {
|
|
slog.WarnContext(ctx, "node_skipped_unknown_type",
|
|
slog.String("node", nodeName),
|
|
slog.String("type", node.Type),
|
|
)
|
|
execErrors = append(execErrors, fmt.Sprintf("node %q: %v", nodeName, err))
|
|
continue
|
|
}
|
|
|
|
// Capture loop-locals for the closure.
|
|
nm, nd, in, ex := nodeName, node, inputs, executorFn
|
|
outputs, runErr := runNode(ctx, "node:"+nm, func(c context.Context) (map[int][]models.Item, error) {
|
|
return ex(c, nd, in, execCtx)
|
|
})
|
|
if runErr != nil {
|
|
execErrors = append(execErrors, fmt.Sprintf("node %q: %v", nm, runErr))
|
|
continue
|
|
}
|
|
for outIdx, items := range outputs {
|
|
execCtx.SetOutput(nm, outIdx, items)
|
|
}
|
|
}
|
|
|
|
status := "success"
|
|
if len(execErrors) > 0 {
|
|
status = "partial_error"
|
|
}
|
|
|
|
return &models.ExecutionResult{
|
|
Status: status,
|
|
Outputs: terminalOutputs(execCtx, dag),
|
|
NodeOutputs: execCtx.AllOutputs(),
|
|
Errors: execErrors,
|
|
}, nil
|
|
}
|
|
|
|
func allInputsEmpty(inputs [][]models.Item) bool {
|
|
for _, group := range inputs {
|
|
if len(group) > 0 {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func terminalOutputs(ctx *engine.ExecutionContext, dag *engine.DAG) map[string]map[int][]models.Item {
|
|
result := make(map[string]map[int][]models.Item)
|
|
all := ctx.AllOutputs()
|
|
for nodeName := range dag.Nodes {
|
|
if len(dag.Adjacency[nodeName]) == 0 {
|
|
if out, ok := all[nodeName]; ok {
|
|
result[nodeName] = out
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|