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"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/user"
|
"os/user"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
@ -60,9 +61,12 @@ func startAsync(ctx context.Context, fn func()) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// runWorkflow executes a workflow script in the background.
|
// 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() {
|
startAsync(s.Ctx, func() {
|
||||||
scriptPath := util.CfgDir() + "/workflows/" + workflow
|
workflowDir := util.CfgDir() + "/workflows/"
|
||||||
|
scriptPath := workflowDir + workflow
|
||||||
if _, err := os.Stat(scriptPath); err != nil {
|
if _, err := os.Stat(scriptPath); err != nil {
|
||||||
s.SetGoalStatus("error: workflow not found: " + workflow)
|
s.SetGoalStatus("error: workflow not found: " + workflow)
|
||||||
return
|
return
|
||||||
|
|
@ -74,6 +78,24 @@ func runWorkflow(s *session.Session, workflow string) {
|
||||||
"OLLIE_SESSION_NAME="+s.Name(),
|
"OLLIE_SESSION_NAME="+s.Name(),
|
||||||
"OLLIE_CWD="+cwd,
|
"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
|
cmd.Dir = cwd
|
||||||
out, err := cmd.CombinedOutput()
|
out, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -146,19 +168,52 @@ func buildTreeSpec(cfg *Config) virtfs.FsNodeDecl {
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
virtfs.FileNode("workflows", 0444,
|
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) {
|
virtfs.Read(func() ([]byte, error) {
|
||||||
dir := util.CfgDir() + "/workflows"
|
dir := util.CfgDir() + "/workflows"
|
||||||
entries, err := os.ReadDir(dir)
|
entries, err := os.ReadDir(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
var sb strings.Builder
|
// Collect executable workflow scripts (no extension).
|
||||||
|
workflows := make(map[string][]string)
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
if e.IsDir() {
|
if e.IsDir() {
|
||||||
continue
|
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')
|
sb.WriteByte('\n')
|
||||||
}
|
}
|
||||||
return []byte(sb.String()), nil
|
return []byte(sb.String()), nil
|
||||||
|
|
@ -256,7 +311,7 @@ func buildTreeSpec(cfg *Config) virtfs.FsNodeDecl {
|
||||||
virtfs.Doc("Create session"),
|
virtfs.Doc("Create session"),
|
||||||
virtfs.Rdwr(func(_ context.Context, data []byte) ([]byte, error) {
|
virtfs.Rdwr(func(_ context.Context, data []byte) ([]byte, error) {
|
||||||
args := strings.Fields(string(data))
|
args := strings.Fields(string(data))
|
||||||
name, remote, workflow, cwd := "", "", "", ""
|
name, remote, workflow, variant, cwd := "", "", "", "", ""
|
||||||
yolo := false
|
yolo := false
|
||||||
for _, arg := range args {
|
for _, arg := range args {
|
||||||
if k, v, ok := strings.Cut(arg, "="); ok {
|
if k, v, ok := strings.Cut(arg, "="); ok {
|
||||||
|
|
@ -267,6 +322,8 @@ func buildTreeSpec(cfg *Config) virtfs.FsNodeDecl {
|
||||||
remote = v
|
remote = v
|
||||||
case "workflow":
|
case "workflow":
|
||||||
workflow = v
|
workflow = v
|
||||||
|
case "variant":
|
||||||
|
variant = v
|
||||||
case "cwd":
|
case "cwd":
|
||||||
cwd = v
|
cwd = v
|
||||||
case "yolo":
|
case "yolo":
|
||||||
|
|
@ -285,6 +342,9 @@ func buildTreeSpec(cfg *Config) virtfs.FsNodeDecl {
|
||||||
if workflow != "" {
|
if workflow != "" {
|
||||||
sess.SetWorkflow(workflow)
|
sess.SetWorkflow(workflow)
|
||||||
}
|
}
|
||||||
|
if variant != "" {
|
||||||
|
sess.SetVariant(variant)
|
||||||
|
}
|
||||||
return []byte(sess.Name() + "\n"), nil
|
return []byte(sess.Name() + "\n"), nil
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|
@ -375,7 +435,7 @@ func buildSessionChildren(
|
||||||
s.SetGoal(input)
|
s.SetGoal(input)
|
||||||
// Trigger workflow only if not already running.
|
// Trigger workflow only if not already running.
|
||||||
if status == "" || status == "complete" || status == "blocked" || strings.HasPrefix(status, "error") {
|
if status == "" || status == "complete" || status == "blocked" || strings.HasPrefix(status, "error") {
|
||||||
runWorkflow(s, s.Workflow())
|
runWorkflow(s, s.Workflow(), s.Variant())
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}),
|
}),
|
||||||
|
|
@ -454,10 +514,14 @@ func buildSessionChildren(
|
||||||
},
|
},
|
||||||
"run": func(args []string) ([]byte, error) {
|
"run": func(args []string) ([]byte, error) {
|
||||||
workflow := s.Workflow()
|
workflow := s.Workflow()
|
||||||
|
variant := s.Variant()
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
workflow = args[0]
|
workflow = args[0]
|
||||||
}
|
}
|
||||||
runWorkflow(s, workflow)
|
if len(args) > 1 {
|
||||||
|
variant = args[1]
|
||||||
|
}
|
||||||
|
runWorkflow(s, workflow, variant)
|
||||||
return []byte("ok\n"), nil
|
return []byte("ok\n"), nil
|
||||||
},
|
},
|
||||||
}, data)
|
}, data)
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ type PersistedSession struct {
|
||||||
Yolo bool `json:"yolo,omitempty"`
|
Yolo bool `json:"yolo,omitempty"`
|
||||||
Paused bool `json:"paused,omitempty"`
|
Paused bool `json:"paused,omitempty"`
|
||||||
Workflow string `json:"workflow,omitempty"`
|
Workflow string `json:"workflow,omitempty"`
|
||||||
|
Variant string `json:"variant,omitempty"`
|
||||||
Goal string `json:"goal,omitempty"`
|
Goal string `json:"goal,omitempty"`
|
||||||
GoalStatus string `json:"goalStatus,omitempty"`
|
GoalStatus string `json:"goalStatus,omitempty"`
|
||||||
Agents []PersistedAgent `json:"agents"`
|
Agents []PersistedAgent `json:"agents"`
|
||||||
|
|
@ -75,6 +76,7 @@ func PersistSession(name string) error {
|
||||||
Yolo: sess.Yolo,
|
Yolo: sess.Yolo,
|
||||||
Paused: sess.IsPaused(),
|
Paused: sess.IsPaused(),
|
||||||
Workflow: sess.Workflow(),
|
Workflow: sess.Workflow(),
|
||||||
|
Variant: sess.Variant(),
|
||||||
Goal: goalText,
|
Goal: goalText,
|
||||||
GoalStatus: goalStatus,
|
GoalStatus: goalStatus,
|
||||||
}
|
}
|
||||||
|
|
@ -206,6 +208,9 @@ func restoreMultiAgentSession(ps *PersistedSession) (*RestoredSession, error) {
|
||||||
if ps.Workflow != "" {
|
if ps.Workflow != "" {
|
||||||
sess.SetWorkflow(ps.Workflow)
|
sess.SetWorkflow(ps.Workflow)
|
||||||
}
|
}
|
||||||
|
if ps.Variant != "" {
|
||||||
|
sess.SetVariant(ps.Variant)
|
||||||
|
}
|
||||||
if ps.Goal != "" {
|
if ps.Goal != "" {
|
||||||
sess.SetGoal(ps.Goal)
|
sess.SetGoal(ps.Goal)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ type Session struct {
|
||||||
goalSignalMu sync.Mutex
|
goalSignalMu sync.Mutex
|
||||||
goalSignalCh chan struct{}
|
goalSignalCh chan struct{}
|
||||||
workflow string // workflow profile name (default: "conductor")
|
workflow string // workflow profile name (default: "conductor")
|
||||||
|
variant string // workflow variant (empty = "default")
|
||||||
|
|
||||||
// Autosave
|
// Autosave
|
||||||
saveMu sync.Mutex
|
saveMu sync.Mutex
|
||||||
|
|
@ -274,6 +275,21 @@ func (s *Session) SetWorkflow(w string) {
|
||||||
s.mu.Unlock()
|
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.
|
// FindAgent returns the agent matching the given name or ID, or nil.
|
||||||
func (s *Session) FindAgent(nameOrID string) *agent.Agent {
|
func (s *Session) FindAgent(nameOrID string) *agent.Agent {
|
||||||
for _, ag := range s.agents {
|
for _, ag := range s.agents {
|
||||||
|
|
|
||||||
|
|
@ -8,17 +8,19 @@ set -euo pipefail
|
||||||
SESSION_NAME="$OLLIE_SESSION_NAME"
|
SESSION_NAME="$OLLIE_SESSION_NAME"
|
||||||
CWD="$OLLIE_CWD"
|
CWD="$OLLIE_CWD"
|
||||||
MAX_ROUNDS=${OLLIE_REFINE_MAX:-3}
|
MAX_ROUNDS=${OLLIE_REFINE_MAX:-3}
|
||||||
|
AUTHOR_PROFILE=${AUTHOR_PROFILE:-author}
|
||||||
|
REVIEWER_PROFILE=${REVIEWER_PROFILE:-reviewer}
|
||||||
|
|
||||||
# Set status to running.
|
# Set status to running.
|
||||||
echo "running" | ollie-9p write "session/$SESSION_NAME/goalstatus"
|
echo "running" | ollie-9p write "session/$SESSION_NAME/goalstatus"
|
||||||
|
|
||||||
# Create the author agent.
|
# 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")
|
| ollie-9p rdwr "session/$SESSION_NAME/agent/new")
|
||||||
AUTHOR=$(echo "$AUTHOR" | tr -d '\n')
|
AUTHOR=$(echo "$AUTHOR" | tr -d '\n')
|
||||||
|
|
||||||
# Create the reviewer agent.
|
# 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")
|
| ollie-9p rdwr "session/$SESSION_NAME/agent/new")
|
||||||
REVIEWER=$(echo "$REVIEWER" | tr -d '\n')
|
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