Implement workflow variants system
- runWorkflow accepts a variant parameter; sources {workflow}-{variant}.conf
as env vars before exec'ing the script
- Session stores variant; persisted and restored
- session/new accepts variant= parameter
- 'run' ctl command accepts optional variant as second arg
- workflows file now lists variants: name<TAB>default,variant1,...
- review workflow reads AUTHOR_PROFILE/REVIEWER_PROFILE env vars
- Add review-code.conf and review-writing.conf example variants
This commit is contained in:
parent
59f729f591
commit
5fdd80c26c
|
|
@ -10,6 +10,7 @@ import (
|
|||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
|
@ -60,9 +61,12 @@ func startAsync(ctx context.Context, fn func()) bool {
|
|||
}
|
||||
|
||||
// runWorkflow executes a workflow script in the background.
|
||||
func runWorkflow(s *session.Session, workflow string) {
|
||||
// If variant is non-empty and not "default", the matching conf sidecar is
|
||||
// sourced as environment variables before the script runs.
|
||||
func runWorkflow(s *session.Session, workflow, variant string) {
|
||||
startAsync(s.Ctx, func() {
|
||||
scriptPath := util.CfgDir() + "/workflows/" + workflow
|
||||
workflowDir := util.CfgDir() + "/workflows/"
|
||||
scriptPath := workflowDir + workflow
|
||||
if _, err := os.Stat(scriptPath); err != nil {
|
||||
s.SetGoalStatus("error: workflow not found: " + workflow)
|
||||
return
|
||||
|
|
@ -74,6 +78,24 @@ func runWorkflow(s *session.Session, workflow string) {
|
|||
"OLLIE_SESSION_NAME="+s.Name(),
|
||||
"OLLIE_CWD="+cwd,
|
||||
)
|
||||
// Source variant conf if specified.
|
||||
if variant != "" && variant != "default" {
|
||||
confPath := workflowDir + workflow + "-" + variant + ".conf"
|
||||
confData, err := os.ReadFile(confPath)
|
||||
if err != nil {
|
||||
s.SetGoalStatus("error: variant conf not found: " + workflow + "-" + variant + ".conf")
|
||||
return
|
||||
}
|
||||
for _, line := range strings.Split(string(confData), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || line[0] == '#' {
|
||||
continue
|
||||
}
|
||||
if _, _, ok := strings.Cut(line, "="); ok {
|
||||
cmd.Env = append(cmd.Env, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
cmd.Dir = cwd
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
|
|
@ -146,19 +168,52 @@ func buildTreeSpec(cfg *Config) virtfs.FsNodeDecl {
|
|||
}),
|
||||
),
|
||||
virtfs.FileNode("workflows", 0444,
|
||||
virtfs.Doc("Available workflows, one per line"),
|
||||
virtfs.Doc("Available workflows with variants. Format: name\\tdefault,variant1,variant2"),
|
||||
virtfs.Read(func() ([]byte, error) {
|
||||
dir := util.CfgDir() + "/workflows"
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
var sb strings.Builder
|
||||
// Collect executable workflow scripts (no extension).
|
||||
workflows := make(map[string][]string)
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
sb.WriteString(e.Name())
|
||||
name := e.Name()
|
||||
if strings.Contains(name, ".") {
|
||||
continue // skip .conf files
|
||||
}
|
||||
workflows[name] = []string{"default"}
|
||||
}
|
||||
// Find variant confs: {name}-{variant}.conf
|
||||
for _, e := range entries {
|
||||
name := e.Name()
|
||||
if !strings.HasSuffix(name, ".conf") {
|
||||
continue
|
||||
}
|
||||
base := strings.TrimSuffix(name, ".conf")
|
||||
// Find the longest workflow name that is a prefix.
|
||||
for wf := range workflows {
|
||||
if strings.HasPrefix(base, wf+"-") {
|
||||
variant := strings.TrimPrefix(base, wf+"-")
|
||||
if variant != "" {
|
||||
workflows[wf] = append(workflows[wf], variant)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var sb strings.Builder
|
||||
names := make([]string, 0, len(workflows))
|
||||
for wf := range workflows {
|
||||
names = append(names, wf)
|
||||
}
|
||||
sort.Strings(names)
|
||||
for _, wf := range names {
|
||||
sb.WriteString(wf)
|
||||
sb.WriteByte('\t')
|
||||
sb.WriteString(strings.Join(workflows[wf], ","))
|
||||
sb.WriteByte('\n')
|
||||
}
|
||||
return []byte(sb.String()), nil
|
||||
|
|
@ -256,7 +311,7 @@ func buildTreeSpec(cfg *Config) virtfs.FsNodeDecl {
|
|||
virtfs.Doc("Create session"),
|
||||
virtfs.Rdwr(func(_ context.Context, data []byte) ([]byte, error) {
|
||||
args := strings.Fields(string(data))
|
||||
name, remote, workflow, cwd := "", "", "", ""
|
||||
name, remote, workflow, variant, cwd := "", "", "", "", ""
|
||||
yolo := false
|
||||
for _, arg := range args {
|
||||
if k, v, ok := strings.Cut(arg, "="); ok {
|
||||
|
|
@ -267,6 +322,8 @@ func buildTreeSpec(cfg *Config) virtfs.FsNodeDecl {
|
|||
remote = v
|
||||
case "workflow":
|
||||
workflow = v
|
||||
case "variant":
|
||||
variant = v
|
||||
case "cwd":
|
||||
cwd = v
|
||||
case "yolo":
|
||||
|
|
@ -285,6 +342,9 @@ func buildTreeSpec(cfg *Config) virtfs.FsNodeDecl {
|
|||
if workflow != "" {
|
||||
sess.SetWorkflow(workflow)
|
||||
}
|
||||
if variant != "" {
|
||||
sess.SetVariant(variant)
|
||||
}
|
||||
return []byte(sess.Name() + "\n"), nil
|
||||
}),
|
||||
),
|
||||
|
|
@ -375,7 +435,7 @@ func buildSessionChildren(
|
|||
s.SetGoal(input)
|
||||
// Trigger workflow only if not already running.
|
||||
if status == "" || status == "complete" || status == "blocked" || strings.HasPrefix(status, "error") {
|
||||
runWorkflow(s, s.Workflow())
|
||||
runWorkflow(s, s.Workflow(), s.Variant())
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
|
|
@ -454,10 +514,14 @@ func buildSessionChildren(
|
|||
},
|
||||
"run": func(args []string) ([]byte, error) {
|
||||
workflow := s.Workflow()
|
||||
variant := s.Variant()
|
||||
if len(args) > 0 {
|
||||
workflow = args[0]
|
||||
}
|
||||
runWorkflow(s, workflow)
|
||||
if len(args) > 1 {
|
||||
variant = args[1]
|
||||
}
|
||||
runWorkflow(s, workflow, variant)
|
||||
return []byte("ok\n"), nil
|
||||
},
|
||||
}, data)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ type PersistedSession struct {
|
|||
Yolo bool `json:"yolo,omitempty"`
|
||||
Paused bool `json:"paused,omitempty"`
|
||||
Workflow string `json:"workflow,omitempty"`
|
||||
Variant string `json:"variant,omitempty"`
|
||||
Goal string `json:"goal,omitempty"`
|
||||
GoalStatus string `json:"goalStatus,omitempty"`
|
||||
Agents []PersistedAgent `json:"agents"`
|
||||
|
|
@ -75,6 +76,7 @@ func PersistSession(name string) error {
|
|||
Yolo: sess.Yolo,
|
||||
Paused: sess.IsPaused(),
|
||||
Workflow: sess.Workflow(),
|
||||
Variant: sess.Variant(),
|
||||
Goal: goalText,
|
||||
GoalStatus: goalStatus,
|
||||
}
|
||||
|
|
@ -206,6 +208,9 @@ func restoreMultiAgentSession(ps *PersistedSession) (*RestoredSession, error) {
|
|||
if ps.Workflow != "" {
|
||||
sess.SetWorkflow(ps.Workflow)
|
||||
}
|
||||
if ps.Variant != "" {
|
||||
sess.SetVariant(ps.Variant)
|
||||
}
|
||||
if ps.Goal != "" {
|
||||
sess.SetGoal(ps.Goal)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ type Session struct {
|
|||
goalSignalMu sync.Mutex
|
||||
goalSignalCh chan struct{}
|
||||
workflow string // workflow profile name (default: "conductor")
|
||||
variant string // workflow variant (empty = "default")
|
||||
|
||||
// Autosave
|
||||
saveMu sync.Mutex
|
||||
|
|
@ -274,6 +275,21 @@ func (s *Session) SetWorkflow(w string) {
|
|||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// Variant returns the workflow variant name (empty means "default").
|
||||
func (s *Session) Variant() string {
|
||||
s.mu.RLock()
|
||||
v := s.variant
|
||||
s.mu.RUnlock()
|
||||
return v
|
||||
}
|
||||
|
||||
// SetVariant sets the workflow variant name.
|
||||
func (s *Session) SetVariant(v string) {
|
||||
s.mu.Lock()
|
||||
s.variant = v
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// FindAgent returns the agent matching the given name or ID, or nil.
|
||||
func (s *Session) FindAgent(nameOrID string) *agent.Agent {
|
||||
for _, ag := range s.agents {
|
||||
|
|
|
|||
|
|
@ -8,17 +8,19 @@ set -euo pipefail
|
|||
SESSION_NAME="$OLLIE_SESSION_NAME"
|
||||
CWD="$OLLIE_CWD"
|
||||
MAX_ROUNDS=${OLLIE_REFINE_MAX:-3}
|
||||
AUTHOR_PROFILE=${AUTHOR_PROFILE:-author}
|
||||
REVIEWER_PROFILE=${REVIEWER_PROFILE:-reviewer}
|
||||
|
||||
# Set status to running.
|
||||
echo "running" | ollie-9p write "session/$SESSION_NAME/goalstatus"
|
||||
|
||||
# Create the author agent.
|
||||
AUTHOR=$(printf 'profile=author\ncwd=%s\nname=author\n' "$CWD" \
|
||||
AUTHOR=$(printf 'profile=%s\ncwd=%s\nname=author\n' "$AUTHOR_PROFILE" "$CWD" \
|
||||
| ollie-9p rdwr "session/$SESSION_NAME/agent/new")
|
||||
AUTHOR=$(echo "$AUTHOR" | tr -d '\n')
|
||||
|
||||
# Create the reviewer agent.
|
||||
REVIEWER=$(printf 'profile=reviewer\ncwd=%s\nname=reviewer\n' "$CWD" \
|
||||
REVIEWER=$(printf 'profile=%s\ncwd=%s\nname=reviewer\n' "$REVIEWER_PROFILE" "$CWD" \
|
||||
| ollie-9p rdwr "session/$SESSION_NAME/agent/new")
|
||||
REVIEWER=$(echo "$REVIEWER" | tr -d '\n')
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
# Variant: code
|
||||
# Optimized for code review cycles — more iterations, code-focused profiles.
|
||||
OLLIE_REFINE_MAX=5
|
||||
AUTHOR_PROFILE=author
|
||||
REVIEWER_PROFILE=reviewer
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# Variant: writing
|
||||
# Optimized for document drafting — fewer iterations, writing-focused profiles.
|
||||
OLLIE_REFINE_MAX=3
|
||||
AUTHOR_PROFILE=author
|
||||
REVIEWER_PROFILE=reviewer
|
||||
Loading…
Reference in New Issue