mirror of
https://github.com/open-gitagent/langship.sh.git
synced 2026-08-03 07:21:04 +02:00
feat(web): initialize Next.js project with Tailwind CSS and TypeScript setup
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user