mirror of
https://github.com/open-gitagent/langship.sh.git
synced 2026-08-03 07:21:04 +02:00
152 lines
4.0 KiB
Go
152 lines
4.0 KiB
Go
package engine
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"github.com/lyzrai/flow/pkg/models"
|
|
)
|
|
|
|
// terminalError is unwrapped by the runner to halt execution.
|
|
// Executors and durability layers can wrap their errors with TerminalError
|
|
// to signal "do not retry, do not forward inputs, stop the workflow".
|
|
type terminalError struct{ Err error }
|
|
|
|
func (e *terminalError) Error() string { return e.Err.Error() }
|
|
func (e *terminalError) Unwrap() error { return e.Err }
|
|
|
|
// TerminalError marks an error as non-retryable.
|
|
func TerminalError(err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
return &terminalError{Err: err}
|
|
}
|
|
|
|
// IsTerminalError returns true if err (or any wrapped error) is terminal.
|
|
func IsTerminalError(err error) bool {
|
|
var te *terminalError
|
|
return errors.As(err, &te)
|
|
}
|
|
|
|
// RunWorkflow builds the DAG and executes nodes in topological order.
|
|
func RunWorkflow(ctx context.Context, wf *models.WorkflowDefinition, triggerData []models.Item, lookup ExecutorLookup) (*models.ExecutionResult, error) {
|
|
dag, err := BuildDAG(wf)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to build DAG: %w", err)
|
|
}
|
|
|
|
order, err := dag.TopologicalSort()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to sort DAG: %w", err)
|
|
}
|
|
|
|
slog.InfoContext(ctx, "workflow_started",
|
|
slog.String("workflow", wf.Name),
|
|
slog.Int("node_count", len(order)),
|
|
)
|
|
|
|
execCtx := NewExecutionContext(wf, dag)
|
|
execCtx.Lookup = lookup
|
|
|
|
var execErrors []string
|
|
|
|
for _, nodeName := range order {
|
|
if execCtx.IsDelegated(nodeName) {
|
|
continue
|
|
}
|
|
|
|
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 non-trigger root nodes (orphaned — no path from trigger).
|
|
if node.Type != "flow-nodes-base.trigger" && len(dag.InEdges[nodeName]) == 0 {
|
|
slog.InfoContext(ctx, "node_skipped_no_trigger_path",
|
|
slog.String("node", nodeName),
|
|
slog.String("type", node.Type),
|
|
)
|
|
continue
|
|
}
|
|
|
|
// Skip nodes that have upstream connections but received no items.
|
|
if len(dag.InEdges[nodeName]) > 0 && allInputsEmpty(inputs) {
|
|
slog.InfoContext(ctx, "node_skipped_no_input",
|
|
slog.String("node", nodeName),
|
|
slog.String("type", node.Type),
|
|
)
|
|
continue
|
|
}
|
|
|
|
resolvedParams := ResolveExpressions(node.Parameters, execCtx, nodeName)
|
|
node.Parameters = resolvedParams
|
|
|
|
executorFn, err := lookup(node.Type)
|
|
if err != nil {
|
|
slog.ErrorContext(ctx, "node_skipped",
|
|
slog.String("node", nodeName),
|
|
slog.String("type", node.Type),
|
|
slog.String("error", err.Error()),
|
|
)
|
|
execErrors = append(execErrors, fmt.Sprintf("node %q: %v", nodeName, err))
|
|
forwardInputs(execCtx, nodeName, inputs)
|
|
continue
|
|
}
|
|
|
|
start := time.Now()
|
|
outputs, err := executeWithRetry(ctx, executorFn, node, inputs, execCtx)
|
|
elapsed := time.Since(start)
|
|
|
|
if err != nil {
|
|
slog.ErrorContext(ctx, "node_failed",
|
|
slog.String("node", nodeName),
|
|
slog.String("type", node.Type),
|
|
slog.String("error", err.Error()),
|
|
slog.Float64("duration_ms", float64(elapsed.Microseconds())/1000.0),
|
|
)
|
|
if IsTerminalError(err) {
|
|
return nil, fmt.Errorf("node %q: %w", nodeName, err)
|
|
}
|
|
execErrors = append(execErrors, fmt.Sprintf("node %q execution failed: %v", nodeName, err))
|
|
forwardInputs(execCtx, nodeName, inputs)
|
|
continue
|
|
}
|
|
|
|
slog.InfoContext(ctx, "node_executed",
|
|
slog.String("node", nodeName),
|
|
slog.String("type", node.Type),
|
|
slog.Float64("duration_ms", float64(elapsed.Microseconds())/1000.0),
|
|
)
|
|
|
|
for outIdx, items := range outputs {
|
|
execCtx.SetOutput(nodeName, outIdx, items)
|
|
}
|
|
}
|
|
|
|
status := "success"
|
|
if len(execErrors) > 0 {
|
|
status = "partial_error"
|
|
}
|
|
|
|
slog.InfoContext(ctx, "workflow_completed",
|
|
slog.String("workflow", wf.Name),
|
|
slog.String("status", status),
|
|
slog.Int("errors", len(execErrors)),
|
|
)
|
|
|
|
return &models.ExecutionResult{
|
|
Status: status,
|
|
Outputs: getTerminalOutputs(execCtx, dag),
|
|
NodeOutputs: execCtx.AllOutputs(),
|
|
Errors: execErrors,
|
|
}, nil
|
|
}
|