package workflow import ( "errors" "fmt" "strings" "unicode" "unicode/utf8" "g.fc.run/misc/byre/internal/api" ) var ErrInvalid = errors.New("invalid workflow") const maxNameRunes = 100 func VendorOf(model string) api.AgentSessionVendor { if strings.HasPrefix(model, "gpt-") { return api.Codex } return api.Claude } func CleanName(name string) (string, error) { name = strings.TrimSpace(name) problem := nameProblem(name) if problem != "" { return "", fmt.Errorf("%w: workflow name %s", ErrInvalid, problem) } return name, nil } func nameProblem(name string) string { switch { case strings.TrimSpace(name) == "": return "is empty" case strings.TrimSpace(name) != name: return fmt.Sprintf("%q has leading or trailing spaces", name) case utf8.RuneCountInString(name) > maxNameRunes: return fmt.Sprintf("%q is longer than %d characters", name, maxNameRunes) case strings.ContainsFunc(name, unicode.IsControl): return fmt.Sprintf("%q isn't a single line", name) } return "" } func Validate(def api.WorkflowDefinition) error { problems := []string{} if len(def.Stages) == 0 { problems = append(problems, "it has no stages") } seen := map[string]bool{} for i, s := range def.Stages { where := fmt.Sprintf("stage %d (%q)", i+1, s.Name) p := nameProblem(s.Name) if p != "" { problems = append(problems, fmt.Sprintf("%s: name %s", where, p)) } if seen[s.Name] { problems = append(problems, fmt.Sprintf("%s: another stage has the same name", where)) } seen[s.Name] = true if strings.TrimSpace(s.Model) == "" { problems = append(problems, fmt.Sprintf("%s: model is required", where)) } if strings.TrimSpace(s.Prompt) == "" { problems = append(problems, fmt.Sprintf("%s: prompt is empty", where)) } } if len(problems) > 0 { return fmt.Errorf("%w: %s", ErrInvalid, strings.Join(problems, "; ")) } return nil }