blob: 465b3634341513bf37d2f7315c56799f40a6d7d9 [file] [log] [blame]
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +00001// Package experiment provides support for experimental features.
2package experiment
3
4import (
5 "fmt"
6 "io"
Josh Bleecher Snyder6f446fa2025-05-05 14:51:49 -07007 "log/slog"
Josh Bleecher Snyder436655e2025-05-14 10:20:25 -07008 "os"
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +00009 "strings"
10 "sync"
11)
12
13// Experiment represents an experimental feature.
14// Experiments are global.
15type Experiment struct {
16 Name string // The name of the experiment used in -x flag
17 Description string // A short description of what the experiment does
18 Enabled bool // Whether the experiment is enabled
19}
20
21var (
22 mu sync.Mutex
23 experiments = []Experiment{
24 {
25 Name: "list",
26 Description: "List all available experiments and exit",
27 },
28 {
29 Name: "all",
30 Description: "Enable all experiments",
31 },
Josh Bleecher Snyder503b5e32025-05-05 13:30:55 -070032 {
Josh Bleecher Snydere2518e52025-04-29 11:13:40 -070033 Name: "llm_review",
34 Description: "Add an LLM step to the codereview tool",
35 },
Josh Bleecher Snydera997be62025-05-07 22:52:46 +000036 {
37 Name: "memory",
38 Description: "Enable memory subsystem (dear_llm.md)",
39 },
Josh Bleecher Snyder31785ae2025-05-06 01:50:58 +000040 {
41 Name: "kb",
42 Description: "Enable knowledge_base tool",
43 },
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +000044 }
45 byName = map[string]*Experiment{}
46)
47
48func Enabled(name string) bool {
49 mu.Lock()
50 defer mu.Unlock()
Josh Bleecher Snyder6f446fa2025-05-05 14:51:49 -070051 e, ok := byName[name]
52 if !ok {
53 slog.Error("unknown experiment", "name", name)
54 return false
55 }
56 return e.Enabled
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +000057}
58
59func init() {
Josh Bleecher Snydere01ea0e2025-05-06 02:03:26 +000060 for i := range experiments {
61 e := &experiments[i]
62 byName[e.Name] = e
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +000063 }
64}
65
66func (e Experiment) String() string {
67 return fmt.Sprintf("\t%-15s %s\n", e.Name, e.Description)
68}
69
70// Fprint writes a list of all available experiments to w.
71func Fprint(w io.Writer) {
72 mu.Lock()
73 defer mu.Unlock()
74
75 fmt.Fprintln(w, "Available experiments:")
76 for _, e := range experiments {
77 fmt.Fprintln(w, e)
78 }
79}
80
81// Flag is a custom flag type that allows for comma-separated
82// values and can be used multiple times.
83type Flag struct {
84 Value string
Josh Bleecher Snyder436655e2025-05-14 10:20:25 -070085 set bool // whether the flag has been set explicitly
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +000086}
87
88// String returns the string representation of the flag value.
89func (f *Flag) String() string {
Josh Bleecher Snyder436655e2025-05-14 10:20:25 -070090 return f.Get().(string)
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +000091}
92
93// Set adds a value to the flag.
94func (f *Flag) Set(value string) error {
Josh Bleecher Snyder085f74c2025-05-06 01:58:25 +000095 if f.Value == "" {
96 f.Value = value
97 } else {
98 f.Value = f.Value + "," + value // quadratic, doesn't matter, tiny N
99 }
Josh Bleecher Snyder436655e2025-05-14 10:20:25 -0700100 f.set = true
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +0000101 return nil
102}
103
104// Get returns the flag values.
105func (f *Flag) Get() any {
Josh Bleecher Snyder436655e2025-05-14 10:20:25 -0700106 if f.set {
107 return f.Value
108 }
109 return os.Getenv("SKETCH_EXPERIMENT")
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +0000110}
111
112// Process handles all flag values, enabling the appropriate experiments.
113func (f *Flag) Process() error {
114 mu.Lock()
115 defer mu.Unlock()
Josh Bleecher Snyder436655e2025-05-14 10:20:25 -0700116 v := f.String()
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +0000117
Josh Bleecher Snyder436655e2025-05-14 10:20:25 -0700118 for name := range strings.SplitSeq(v, ",") {
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +0000119 name = strings.TrimSpace(name)
120 if name == "" {
121 continue
122 }
123 e, ok := byName[name]
124 if !ok {
125 return fmt.Errorf("unknown experiment: %q", name)
126 }
127 e.Enabled = true
128 }
129 if byName["all"].Enabled {
Josh Bleecher Snyderd42577f2025-05-05 17:59:00 -0700130 for i := range experiments {
131 e := &experiments[i]
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +0000132 if e.Name == "list" {
133 continue
134 }
135 e.Enabled = true
136 }
137 }
138 return nil
139}