experiment: add simple way to toggle experimental features
diff --git a/cmd/sketch/main.go b/cmd/sketch/main.go
index d66b995..86f57da 100644
--- a/cmd/sketch/main.go
+++ b/cmd/sketch/main.go
@@ -15,6 +15,7 @@
"strings"
"time"
+ "sketch.dev/experiment"
"sketch.dev/llm"
"sketch.dev/llm/gem"
"sketch.dev/llm/oai"
@@ -74,6 +75,15 @@
return fmt.Errorf("only -model=claude is supported in safe mode right now, use -unsafe -skaband-addr=''")
}
+ if err := flagArgs.experimentFlag.Process(); err != nil {
+ fmt.Fprintf(os.Stderr, "error parsing experimental flags: %v\n", err)
+ os.Exit(1)
+ }
+ if experiment.Enabled("list") {
+ experiment.Fprint(os.Stdout)
+ os.Exit(0)
+ }
+
// Add a global "session_id" to all logs using this context.
// A "session" is a single full run of the agent.
ctx := skribe.ContextWithAttr(context.Background(), slog.String("session_id", flagArgs.sessionID))
@@ -146,6 +156,7 @@
initialCommit string
gitUsername string
gitEmail string
+ experimentFlag experiment.Flag
sessionID string
record bool
noCleanup bool
@@ -192,6 +203,7 @@
flag.StringVar(&flags.outsideOS, "outside-os", "", "(internal) OS on the outside system")
flag.StringVar(&flags.outsideWorkingDir, "outside-working-dir", "", "(internal) working dir on the outside system")
flag.StringVar(&flags.sketchBinaryLinux, "sketch-binary-linux", "", "(development) path to a pre-built sketch binary for linux")
+ flag.Var(&flags.experimentFlag, "x", "enable experimental features (comma-separated list or repeat flag; use 'list' to show all)")
flag.Parse()
return flags
diff --git a/experiment/experiment.go b/experiment/experiment.go
new file mode 100644
index 0000000..1e57f8d
--- /dev/null
+++ b/experiment/experiment.go
@@ -0,0 +1,108 @@
+// Package experiment provides support for experimental features.
+package experiment
+
+import (
+ "fmt"
+ "io"
+ "strings"
+ "sync"
+)
+
+// Experiment represents an experimental feature.
+// Experiments are global.
+type Experiment struct {
+ Name string // The name of the experiment used in -x flag
+ Description string // A short description of what the experiment does
+ Enabled bool // Whether the experiment is enabled
+}
+
+var (
+ mu sync.Mutex
+ experiments = []Experiment{
+ {
+ Name: "list",
+ Description: "List all available experiments and exit",
+ },
+ {
+ Name: "all",
+ Description: "Enable all experiments",
+ },
+ }
+ byName = map[string]*Experiment{}
+)
+
+func Enabled(name string) bool {
+ mu.Lock()
+ defer mu.Unlock()
+ return byName[name].Enabled
+}
+
+func init() {
+ for _, e := range experiments {
+ byName[e.Name] = &e
+ }
+}
+
+func (e Experiment) String() string {
+ return fmt.Sprintf("\t%-15s %s\n", e.Name, e.Description)
+}
+
+// Fprint writes a list of all available experiments to w.
+func Fprint(w io.Writer) {
+ mu.Lock()
+ defer mu.Unlock()
+
+ fmt.Fprintln(w, "Available experiments:")
+ for _, e := range experiments {
+ fmt.Fprintln(w, e)
+ }
+}
+
+// Flag is a custom flag type that allows for comma-separated
+// values and can be used multiple times.
+type Flag struct {
+ Value string
+}
+
+// String returns the string representation of the flag value.
+func (f *Flag) String() string {
+ return f.Value
+}
+
+// Set adds a value to the flag.
+func (f *Flag) Set(value string) error {
+ f.Value = f.Value + "," + value // quadratic, doesn't matter, tiny N
+ return nil
+}
+
+// Get returns the flag values.
+func (f *Flag) Get() any {
+ return f.Value
+}
+
+// Process handles all flag values, enabling the appropriate experiments.
+func (f *Flag) Process() error {
+ mu.Lock()
+ defer mu.Unlock()
+
+ for name := range strings.SplitSeq(f.Value, ",") {
+ name = strings.TrimSpace(name)
+ if name == "" {
+ continue
+ }
+ e, ok := byName[name]
+ if !ok {
+ return fmt.Errorf("unknown experiment: %q", name)
+ }
+ e.Enabled = true
+ }
+ if byName["all"].Enabled {
+ for _, e := range experiments {
+ if e.Name == "list" {
+ continue
+ }
+ e.Enabled = true
+ }
+ }
+ return nil
+}