workflows: script-based workflows with session-level CWD

- Workflows are executable scripts in data/workflows/
- New 'workflows' 9P file lists available workflows
- Goal file stores text; writing triggers workflow if status allows
- goalstatus file for status read/write, goalwait for blocking
- Session ctl accepts 'run [workflow]' command
- Session now owns CWD; agents inherit via callback
- Conductor workflow: creates agent, primes with instructions, exits
- GUI workflow combo reads from workflows, not agents
- Persistence includes goal, goalstatus, workflow, and session CWD
This commit is contained in:
Levi Neely 2026-08-17 12:00:10 +02:00
parent 743e40c2f8
commit 958d3d2ddf
12 changed files with 245 additions and 112 deletions

View File

@ -67,11 +67,12 @@ install-kf5: build-kf5 test install-data
install -Dm755 kde/dolphin/ollie-session-here $(BINDIR)/ollie-session-here
install-data:
mkdir -p $(CONFIG_DIR)/agents $(CONFIG_DIR)/prompts $(CONFIG_DIR)/skills $(CONFIG_DIR)/tools
mkdir -p $(CONFIG_DIR)/agents $(CONFIG_DIR)/prompts $(CONFIG_DIR)/skills $(CONFIG_DIR)/tools $(CONFIG_DIR)/workflows
@test -f $(CONFIG_DIR)/backends.conf || install -Dm600 data/backends.conf $(CONFIG_DIR)/backends.conf
cp -a data/agents/. $(CONFIG_DIR)/agents/
cp -a data/prompts/. $(CONFIG_DIR)/prompts/
cp -a data/skills/. $(CONFIG_DIR)/skills/
install -Dm755 data/workflows/* $(CONFIG_DIR)/workflows/
install -Dm644 cmd/toolsrv/internal/sandbox/sandbox.yaml $(CONFIG_DIR)/sandbox.yaml
install -Dm755 data/scripts/ollie-remount $(BINDIR)/ollie-remount
install -Dm755 data/scripts/logseq-cli $(BINDIR)/logseq-cli

View File

@ -39,7 +39,7 @@ type Agent struct {
// Execution state — owned by the agent, protected by stateMu.
state string // "idle", "thinking", "calling: <tool>"
reply string // last assistant response
cwd string // working directory for tool execution
getCwd func() string // returns session working directory
id string // agent identity (unique principal)
parentID string // immutable ID of the agent that spawned this agent
depth int // sub-agent depth (0=top-level, 1=sub-agent, 2=sub-sub-agent)
@ -357,23 +357,20 @@ func (ag *Agent) SetToolServer(newToolServer func() *toolclient.ToolsrvConn, con
}
}
// CWD returns the agent's working directory.
// Cwd returns the agent's working directory (from session).
func (ag *Agent) Cwd() string {
ag.stateMu.RLock()
c := ag.cwd
ag.stateMu.RUnlock()
return c
}
// effectiveCwd returns the agent's cwd, falling back to os.Getwd() if empty.
func (ag *Agent) effectiveCwd() string {
if c := ag.Cwd(); c != "" {
return c
if ag.getCwd != nil {
return ag.getCwd()
}
wd, _ := os.Getwd()
return wd
}
// SetGetCwd sets the CWD getter callback.
func (ag *Agent) SetGetCwd(fn func() string) {
ag.getCwd = fn
}
// IsRunning returns true if the agent has an active turn in progress.
func (ag *Agent) IsRunning() bool {
return ag.currentAction.Load() != nil
@ -439,7 +436,7 @@ func (ag *Agent) SwitchProfile(name string) error {
disp.SetAgentID(ag.id)
}
env := []string{"OLLIE_SESSION_ID=" + ag.sessionID, "OLLIE_UNAME=" + ag.id}
rt := BuildRuntime(cfg, disp, ag.cwd, env, ag.systemPrompt, ag.envBlock)
rt := BuildRuntime(cfg, disp, ag.Cwd(), env, ag.systemPrompt, ag.envBlock)
if cfg.Backend != "" {
newBe, err := ag.newBackend(cfg.Backend)
if err != nil {
@ -542,11 +539,9 @@ func (ag *Agent) Close() {
}
}
// SetCWD updates the agent's working directory, preamble environment section, and dispatcher.
// SetCWD updates the preamble environment section and toolserver CWD.
// Note: This updates local state only. To change session CWD, use session.SetCwd().
func (ag *Agent) SetCWD(dir string) {
ag.stateMu.Lock()
ag.cwd = dir
ag.stateMu.Unlock()
isGitRepo := util.IsGitRepo(dir)
ag.envBlock = EnvironmentBlock(dir, runtime.GOOS, isGitRepo, "")
ag.runtime.Preamble.Set(SectionEnv, ag.envBlock)
@ -737,7 +732,7 @@ type AgentParams struct {
History *History
Runtime *Runtime
AgentsDir string
Cwd string // working directory for tool execution
GetCwd func() string // returns session working directory
SystemPrompt string
EnvBlock string
NewToolServer func() *toolclient.ToolsrvConn
@ -760,7 +755,7 @@ func NewAgent(cfg AgentParams) *Agent {
agentsDir: cfg.AgentsDir,
id: cfg.ID,
parentID: cfg.ParentID,
cwd: cfg.Cwd,
getCwd: cfg.GetCwd,
systemPrompt: cfg.SystemPrompt,
envBlock: cfg.EnvBlock,
newToolServer: cfg.NewToolServer,

View File

@ -361,7 +361,7 @@ func (ag *Agent) autoWarnLimit(ctx context.Context) int {
func (ag *Agent) spawnContext(ctx context.Context) string {
var parts []string
// Inject AGENTS.md from the working directory if it exists.
if cwd := ag.effectiveCwd(); cwd != "" {
if cwd := ag.Cwd(); cwd != "" {
if data, err := os.ReadFile(cwd + "/AGENTS.md"); err == nil && len(data) > 0 {
parts = append(parts, string(data))
}

View File

@ -8,6 +8,7 @@ import (
"encoding/json"
"fmt"
"os"
"os/exec"
"os/user"
"strconv"
"strings"
@ -39,6 +40,34 @@ const (
var treeSpecUID, treeSpecGID string
// runWorkflow executes a workflow script in the background.
func runWorkflow(s *session.Session, workflow string) {
go func() {
scriptPath := util.CfgDir() + "/workflows/" + workflow
if _, err := os.Stat(scriptPath); err != nil {
s.SetGoalStatus("error: workflow not found: " + workflow)
return
}
cwd := s.Cwd()
cmd := exec.CommandContext(s.Ctx, scriptPath)
cmd.Env = append(os.Environ(),
"OLLIE_SESSION_ID="+s.ID,
"OLLIE_SESSION_NAME="+s.Name(),
"OLLIE_CWD="+cwd,
)
cmd.Dir = cwd
out, err := cmd.CombinedOutput()
if err != nil {
msg := strings.TrimSpace(string(out))
if msg == "" {
msg = err.Error()
}
s.SetGoalStatus("error: " + msg)
}
// Script exited — don't auto-set complete; the conductor agent handles that.
}()
}
// buildTreeSpec constructs the full 9P namespace.
func buildTreeSpec(cfg *Config) virtfs.FsNodeDecl {
mc := cfg.ModelCache
@ -97,6 +126,25 @@ func buildTreeSpec(cfg *Config) virtfs.FsNodeDecl {
return []byte(sb.String()), nil
}),
),
virtfs.FileNode("workflows", 0444,
virtfs.Doc("Available workflows, one per line"),
virtfs.Read(func() ([]byte, error) {
dir := util.CfgDir() + "/workflows"
entries, err := os.ReadDir(dir)
if err != nil {
return nil, nil
}
var sb strings.Builder
for _, e := range entries {
if e.IsDir() {
continue
}
sb.WriteString(e.Name())
sb.WriteByte('\n')
}
return []byte(sb.String()), nil
}),
),
virtfs.FileNode("ctl", 0666,
virtfs.Doc("Server control. Write: 'invalidate', 'kill'"),
virtfs.Rdwr(func(_ context.Context, data []byte) ([]byte, error) {
@ -189,7 +237,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 := "", "", ""
name, remote, workflow, cwd := "", "", "", ""
for _, arg := range args {
if k, v, ok := strings.Cut(arg, "="); ok {
switch k {
@ -199,13 +247,19 @@ func buildTreeSpec(cfg *Config) virtfs.FsNodeDecl {
remote = v
case "workflow":
workflow = v
case "cwd":
cwd = v
}
}
}
if cwd == "" {
cwd, _ = os.Getwd()
}
sess, err := session.CreateEmpty(name, remote)
if err != nil {
return nil, err
}
sess.SetCwd(cwd)
if workflow != "" {
sess.SetWorkflow(workflow)
}
@ -280,20 +334,14 @@ func buildSessionChildren(
}),
),
virtfs.FileNode("goal", 0660,
virtfs.Doc("Session goal. Write to start a workflow; read for status."),
virtfs.Doc("Session goal text. Writing triggers the workflow if not already running."),
virtfs.GID("agent"),
virtfs.Read(func() ([]byte, error) {
text, status, agentID := s.Goal()
text, _ := s.Goal()
if text == "" {
return nil, nil
}
var sb strings.Builder
fmt.Fprintf(&sb, "status=%s\n", status)
if agentID != "" {
fmt.Fprintf(&sb, "agent=%s\n", agentID)
}
fmt.Fprintf(&sb, "text=%s\n", text)
return []byte(sb.String()), nil
return []byte(text + "\n"), nil
}),
virtfs.Write(func(data []byte) error {
input := strings.TrimSpace(string(data))
@ -301,42 +349,32 @@ func buildSessionChildren(
s.ClearGoal()
return nil
}
// Status write-back from the conductor.
if strings.HasPrefix(input, "status=") {
status := strings.TrimPrefix(strings.SplitN(input, "\n", 2)[0], "status=")
s.SetGoalStatus(status, "")
return nil
_, status := s.Goal()
s.SetGoal(input)
// Trigger workflow only if not already running.
if status == "" || status == "complete" || status == "blocked" || strings.HasPrefix(status, "error") {
runWorkflow(s, s.Workflow())
}
// New goal: spawn workflow agent in background.
goalText := input
workflow := s.Workflow()
s.SetGoal(goalText)
go func() {
ag, err := session.CreateAgentWithParams(s.Name(), session.AgentParams{
Profile: workflow,
CWD: agentCwd(s),
})
if err != nil {
s.SetGoalStatus("error: "+err.Error(), "")
return
}
wireAgentEvents(s.ID, ag)
s.SetGoalStatus("running", ag.ID())
session.PublishEvent("session."+s.ID+".agent."+ag.ID()+".new", "")
ag.Submit(s.Ctx, goalText)
ag.EnsureTrailingNewline()
_, status, _ := s.Goal()
if status == "running" {
s.SetGoalStatus("complete", "")
}
}()
return nil
}),
),
virtfs.FileNode("goalstatus", 0660,
virtfs.Doc("Goal status. Read/write."),
virtfs.GID("agent"),
virtfs.Read(func() ([]byte, error) {
_, status := s.Goal()
return []byte(status + "\n"), nil
}),
virtfs.Write(func(data []byte) error {
status := strings.TrimSpace(string(data))
s.SetGoalStatus(status)
return nil
}),
),
virtfs.FileNode("goalwait", 0444,
virtfs.Doc("Blocks until goal status changes."),
virtfs.BlockOnce(func() ([]byte, string, error) {
_, status, _ := s.Goal()
_, status := s.Goal()
return []byte(status + "\n"), status, nil
}, s.GoalSignal),
),
@ -392,6 +430,14 @@ func buildSessionChildren(
}
return []byte("ok\n"), nil
},
"run": func(args []string) ([]byte, error) {
workflow := s.Workflow()
if len(args) > 0 {
workflow = args[0]
}
runWorkflow(s, workflow)
return []byte("ok\n"), nil
},
}, data)
}),
),
@ -414,6 +460,9 @@ func buildSessionChildren(
virtfs.Rdwr(func(ctx context.Context, data []byte) ([]byte, error) {
req := parseAgentNewRequest(data)
req.Params.ParentID = req.ParentID
if req.Cwd != "" {
s.SetCwd(req.Cwd)
}
if req.Prompt == "" {
// Persistent agent creation (no sub-agent mode).

View File

@ -92,6 +92,7 @@ type agentNewRequest struct {
Params session.AgentParams
Prompt string
ParentID string
Cwd string // sets session cwd if provided
ForkAt int
Timeout int // seconds; 0 = use default (600)
MaxDepth int // max sub-agent depth; 0 = use default (1)
@ -126,7 +127,7 @@ func parseAgentNewRequest(data []byte) agentNewRequest {
v = protocol.UnescapeValue(v)
switch k {
case "cwd":
r.Params.CWD = v
r.Cwd = v
case "backend":
r.Params.Backend = v
case "model":

View File

@ -17,12 +17,15 @@ import (
// PersistedSession is the on-disk format for a saved session.
type PersistedSession struct {
ID string `json:"id"`
Name string `json:"name"`
CWD string `json:"cwd"`
Remote string `json:"remote,omitempty"`
Paused bool `json:"paused,omitempty"`
Agents []PersistedAgent `json:"agents"`
ID string `json:"id"`
Name string `json:"name"`
CWD string `json:"cwd"`
Remote string `json:"remote,omitempty"`
Paused bool `json:"paused,omitempty"`
Workflow string `json:"workflow,omitempty"`
Goal string `json:"goal,omitempty"`
GoalStatus string `json:"goalStatus,omitempty"`
Agents []PersistedAgent `json:"agents"`
}
// PersistedAgent is per-agent state within a session.
@ -33,7 +36,6 @@ type PersistedAgent struct {
Profile string `json:"profile"`
Backend string `json:"backend"`
Model string `json:"model"`
CWD string `json:"cwd"`
Messages []backend.Message `json:"messages"`
// Usage tracking
TotalInputTokens int `json:"totalInputTokens,omitempty"`
@ -62,11 +64,16 @@ func PersistSession(name string) error {
return nil
}
goalText, goalStatus := sess.Goal()
ps := PersistedSession{
ID: sess.ID,
Name: sess.Name(),
Remote: sess.Remote,
Paused: sess.IsPaused(),
ID: sess.ID,
Name: sess.Name(),
CWD: sess.Cwd(),
Remote: sess.Remote,
Paused: sess.IsPaused(),
Workflow: sess.Workflow(),
Goal: goalText,
GoalStatus: goalStatus,
}
for _, ag := range agents {
@ -77,7 +84,6 @@ func PersistSession(name string) error {
Profile: ag.Profile(),
Backend: ag.BackendName(),
Model: ag.ModelName(),
CWD: ag.Cwd(),
Messages: ag.Messages(),
}
if usage := ag.Usage(); usage != nil {
@ -189,18 +195,22 @@ func restoreMultiAgentSession(ps *PersistedSession) (*RestoredSession, error) {
sess.paused = true
sess.mu.Unlock()
}
if ps.CWD != "" {
sess.SetCwd(ps.CWD)
}
if ps.Workflow != "" {
sess.SetWorkflow(ps.Workflow)
}
if ps.Goal != "" {
sess.SetGoal(ps.Goal)
}
if ps.GoalStatus != "" {
sess.SetGoalStatus(ps.GoalStatus)
}
var restoredAgents []RestoredAgent
for _, pa := range ps.Agents {
cwd := pa.CWD
if cwd == "" {
cwd = ps.CWD
}
if cwd == "" {
cwd, _ = os.Getwd()
}
// For paused sessions, create a minimal agent without tool server
if ps.Paused {
history := agent.RestoreHistoryFromMessages(pa.Messages)
@ -211,7 +221,7 @@ func restoreMultiAgentSession(ps *PersistedSession) (*RestoredSession, error) {
AgentsDir: pkgAgentsDir,
ID: pa.ID,
ParentID: pa.ParentID,
Cwd: cwd,
GetCwd: sess.Cwd,
Log: pkgSink.NewLogger("agent"),
SessionID: ps.ID,
Save: sess.saveSession,
@ -233,7 +243,6 @@ func restoreMultiAgentSession(ps *PersistedSession) (*RestoredSession, error) {
Profile: pa.Profile,
ID: pa.ID,
Name: pa.Name,
CWD: cwd,
Backend: pa.Backend,
Model: pa.Model,
Remote: ps.Remote,

View File

@ -360,7 +360,6 @@ type AgentParams struct {
Profile string
ID string // uname; if empty, generate one
Name string // display name; optional
CWD string // required
Backend string // backend name; empty = config default
Model string // model override; empty = config default
Remote string // remote target override
@ -369,13 +368,14 @@ type AgentParams struct {
}
// buildAgent constructs a fully-wired agent within a session.
// Used by both CreateAgent and restore.
// Uses the session's CWD.
func buildAgent(sess *Session, p AgentParams) (*agent.Agent, error) {
if p.ID == "" {
p.ID = NextUname()
}
if p.CWD == "" {
return nil, fmt.Errorf("cwd is required")
cwd := sess.Cwd()
if cwd == "" {
return nil, fmt.Errorf("session cwd is required")
}
cfg := agent.LoadConfig(pkgAgentsDir, p.Profile, nil)
@ -398,7 +398,7 @@ func buildAgent(sess *Session, p AgentParams) (*agent.Agent, error) {
infra, err := SetupToolServer(ToolServerConfig{
Ctx: sess.Ctx,
CWD: p.CWD,
CWD: cwd,
RemoteTarget: remote,
SessionID: sessID,
Yolo: pkgYolo,
@ -416,7 +416,7 @@ func buildAgent(sess *Session, p AgentParams) (*agent.Agent, error) {
}
// Build prompt layers
layers := BuildPromptLayers(cfg, p.CWD, sessID, p.ID, infra.Platform, infra.IsGitRepo, "")
layers := BuildPromptLayers(cfg, cwd, sessID, p.ID, infra.Platform, infra.IsGitRepo, "")
// Load tools
LoadAutoLoadTools(cfg, infra.ToolsConn, sessID, p.ID, func(f string, a ...any) {
@ -425,7 +425,7 @@ func buildAgent(sess *Session, p AgentParams) (*agent.Agent, error) {
// Build runtime
env := []string{"OLLIE_SESSION_ID=" + sessID, "OLLIE_UNAME=" + p.ID}
rt := agent.BuildRuntime(cfg, infra.ToolsConn, p.CWD, env, layers.SystemPrompt, layers.EnvBlock)
rt := agent.BuildRuntime(cfg, infra.ToolsConn, cwd, env, layers.SystemPrompt, layers.EnvBlock)
// Create backend
beName := p.Backend
@ -450,7 +450,7 @@ func buildAgent(sess *Session, p AgentParams) (*agent.Agent, error) {
Profile: p.Profile,
AgentsDir: pkgAgentsDir,
ID: p.ID,
Cwd: p.CWD,
GetCwd: sess.Cwd,
ParentID: p.ParentID,
SystemPrompt: layers.SystemPrompt,
EnvBlock: layers.EnvBlock,
@ -473,6 +473,7 @@ func buildAgent(sess *Session, p AgentParams) (*agent.Agent, error) {
func CreateAgent(sessName string, args []string) (*agent.Agent, error) {
// Parse args into AgentParams.
p := AgentParams{Profile: "default"}
var setCwd string
for _, arg := range args {
if k, v, ok := strings.Cut(arg, "="); ok {
switch k {
@ -485,12 +486,19 @@ func CreateAgent(sessName string, args []string) (*agent.Agent, error) {
case "remote":
p.Remote = v
case "cwd":
p.CWD = v
setCwd = v
}
} else if p.Profile == "default" {
p.Profile = arg
}
}
sess := Lookup(sessName)
if sess == nil {
return nil, fmt.Errorf("session not found: %s", sessName)
}
if setCwd != "" {
sess.SetCwd(setCwd)
}
return CreateAgentWithParams(sessName, p)
}
@ -506,8 +514,8 @@ func CreateAgentWithParams(sessName string, p AgentParams) (*agent.Agent, error)
p.Profile = "default"
}
if p.CWD == "" {
return nil, fmt.Errorf("cwd= is required when creating an agent")
if sess.Cwd() == "" {
return nil, fmt.Errorf("session cwd is required")
}
ag, err := buildAgent(sess, p)

View File

@ -39,14 +39,14 @@ type Session struct {
// Mutable state protected by mu
mu sync.RWMutex
name string // friendly name
cwd string // session working directory
toolsConn *toolclient.ToolsrvConn // multiplexed RPC connection
paused bool
// Goal — session-level objective. Writing triggers a workflow.
goalMu sync.RWMutex
goalText string
goalStatus string // "", "running", "complete", "blocked"
goalAgent string // ID of the conductor agent
goalStatus string // "", "running", "complete", "blocked", "error"
goalSignalMu sync.Mutex
goalSignalCh chan struct{}
workflow string // workflow profile name (default: "conductor")
@ -92,6 +92,21 @@ func (s *Session) SetName(name string) {
s.name = name
}
// Cwd returns the session working directory.
func (s *Session) Cwd() string {
s.mu.RLock()
defer s.mu.RUnlock()
return s.cwd
}
// SetCwd sets the session working directory.
func (s *Session) SetCwd(cwd string) {
s.mu.Lock()
s.cwd = cwd
s.mu.Unlock()
go s.saveSession()
}
// ToolsConn returns the tool server connection.
func (s *Session) ToolsConn() *toolclient.ToolsrvConn {
s.mu.RLock()
@ -173,31 +188,28 @@ func (s *Session) IsPaused() bool {
}
// Goal returns the current goal text and status.
func (s *Session) Goal() (text, status, agentID string) {
func (s *Session) Goal() (text, status string) {
s.goalMu.RLock()
defer s.goalMu.RUnlock()
return s.goalText, s.goalStatus, s.goalAgent
return s.goalText, s.goalStatus
}
// SetGoal sets the goal text (the objective). Called once when a goal is created.
// SetGoal sets the goal text (the objective).
func (s *Session) SetGoal(text string) {
s.goalMu.Lock()
s.goalText = text
s.goalStatus = ""
s.goalAgent = ""
s.goalMu.Unlock()
s.notifyGoal()
go s.saveSession()
}
// SetGoalStatus updates only the status and agent fields. Never touches the text.
func (s *Session) SetGoalStatus(status, agentID string) {
// SetGoalStatus updates the status field.
func (s *Session) SetGoalStatus(status string) {
s.goalMu.Lock()
s.goalStatus = status
if agentID != "" {
s.goalAgent = agentID
}
s.goalMu.Unlock()
s.notifyGoal()
go s.saveSession()
}
// ClearGoal resets the entire goal state.
@ -205,9 +217,9 @@ func (s *Session) ClearGoal() {
s.goalMu.Lock()
s.goalText = ""
s.goalStatus = ""
s.goalAgent = ""
s.goalMu.Unlock()
s.notifyGoal()
go s.saveSession()
}
func (s *Session) notifyGoal() {

32
data/workflows/conductor Executable file
View File

@ -0,0 +1,32 @@
#!/bin/bash
# Workflow: conductor
# Creates a conductor agent, primes it with instructions, and exits.
# The conductor agent handles the rest autonomously.
set -euo pipefail
SESSION_NAME="$OLLIE_SESSION_NAME"
CWD="$OLLIE_CWD"
# Set status to running.
echo "running" | ollie-9p write "session/$SESSION_NAME/goalstatus"
# Create a conductor agent.
AGENT_ID=$(printf 'profile=conductor\ncwd=%s\n' "$CWD" \
| ollie-9p rdwr "session/$SESSION_NAME/agent/new")
AGENT_ID=$(echo "$AGENT_ID" | tr -d '\n')
# Prime the conductor with instructions.
# The agent receives OLLIE_SESSION_ID in its environment.
PROMPT_PATH="session/$SESSION_NAME/agent/$AGENT_ID/prompt"
cat <<'EOF' | ollie-9p write "$PROMPT_PATH"
Read the goal from session/$OLLIE_SESSION_ID/goal.
Analyze and understand the goal. If anything is ambiguous, investigate the codebase to clarify.
Break it down into concrete, actionable steps. Rewrite the goal file with your structured plan — use a checklist format so progress can be tracked.
Delegate each step to sub-agents using subagent_spawn. As steps complete, update the goal file to mark progress.
When all steps are complete, write "complete" to session/$OLLIE_SESSION_ID/goalstatus.
If you get stuck and need human input, write "blocked" to session/$OLLIE_SESSION_ID/goalstatus.
EOF

View File

@ -101,17 +101,17 @@ Dialog {
ComboBox {
id: workflowCombo
Layout.fillWidth: true
enabled: ollie.rootAgentsLoaded
model: ollie.availableAgents
enabled: ollie.workflowsLoaded
model: ollie.availableWorkflows
currentIndex: {
var idx = ollie.availableAgents.indexOf("conductor")
var idx = ollie.availableWorkflows.indexOf("conductor")
return idx >= 0 ? idx : 0
}
}
}
Component.onCompleted: {
if (!ollie.rootAgentsLoaded) ollie.loadRootAgents()
if (!ollie.workflowsLoaded) ollie.loadWorkflows()
}
}
@ -150,7 +150,7 @@ Dialog {
errorLabel.text = ""
nameField.text = ""
remoteField.text = ""
var idx = ollie.availableAgents.indexOf("conductor")
var idx = ollie.availableWorkflows.indexOf("conductor")
workflowCombo.currentIndex = idx >= 0 ? idx : 0
}
}

View File

@ -555,6 +555,22 @@ void Ollie9pClient::loadRootAgents()
emit rootAgentsLoadedChanged();
}
void Ollie9pClient::loadWorkflows()
{
if (m_workflowsLoaded) return;
if (!m_9p || !m_9p->isConnected()) return;
QByteArray out = m_9p->read(QStringLiteral("workflows"));
QString raw = QString::fromUtf8(out).trimmed();
if (!raw.isEmpty()) {
m_availableWorkflows = raw.split('\n', Qt::SkipEmptyParts);
} else {
m_availableWorkflows = QStringList() << QStringLiteral("conductor");
}
m_workflowsLoaded = true;
emit availableWorkflowsChanged();
emit workflowsLoadedChanged();
}
QStringList Ollie9pClient::getAvailableModels(const QString &backend) const
{
if (!m_rootModelsLoaded) return {};
@ -665,6 +681,7 @@ void Ollie9pClient::ensureRootDataLoaded()
{
if (!m_rootBackendsLoaded) loadRootBackends();
if (!m_rootAgentsLoaded) loadRootAgents();
if (!m_workflowsLoaded) loadWorkflows();
if (!m_rootModelsLoaded && m_9p && m_9p->isConnected()) {
QByteArray out = m_9p->read(QStringLiteral("models"));
QString raw = QString::fromUtf8(out);

View File

@ -31,6 +31,8 @@ class Ollie9pClient : public QObject
Q_PROPERTY(QStringList availableBackends READ availableBackends NOTIFY availableBackendsChanged)
Q_PROPERTY(bool rootAgentsLoaded READ rootAgentsLoaded NOTIFY rootAgentsLoadedChanged)
Q_PROPERTY(QStringList availableAgents READ availableAgents NOTIFY availableAgentsChanged)
Q_PROPERTY(bool workflowsLoaded READ workflowsLoaded NOTIFY workflowsLoadedChanged)
Q_PROPERTY(QStringList availableWorkflows READ availableWorkflows NOTIFY availableWorkflowsChanged)
Q_PROPERTY(bool rootModelsLoaded READ rootModelsLoaded NOTIFY rootModelsLoadedChanged)
Q_PROPERTY(QString currentBackend READ currentBackend WRITE setCurrentBackend NOTIFY currentBackendChanged)
Q_PROPERTY(bool daemonConnected READ daemonConnected NOTIFY daemonConnectedChanged)
@ -49,6 +51,8 @@ public:
QStringList availableBackends() const { return m_availableBackends; }
bool rootAgentsLoaded() const { return m_rootAgentsLoaded; }
QStringList availableAgents() const { return m_availableAgents; }
bool workflowsLoaded() const { return m_workflowsLoaded; }
QStringList availableWorkflows() const { return m_availableWorkflows; }
bool rootModelsLoaded() const { return m_rootModelsLoaded; }
QString currentBackend() const { return m_currentBackend; }
bool daemonConnected() const { return m_daemonConnected; }
@ -78,6 +82,7 @@ public:
Q_INVOKABLE void switchAgent(const QString &sessionId, const QString &agentId);
Q_INVOKABLE void loadRootBackends();
Q_INVOKABLE void loadRootAgents();
Q_INVOKABLE void loadWorkflows();
Q_INVOKABLE QStringList getAvailableModels(const QString &backend) const;
Q_INVOKABLE bool createQuickSession();
Q_INVOKABLE QString createSession(const QString &name, const QString &remote, const QString &workflow = QString());
@ -98,6 +103,8 @@ signals:
void availableBackendsChanged();
void rootAgentsLoadedChanged();
void availableAgentsChanged();
void workflowsLoadedChanged();
void availableWorkflowsChanged();
void rootModelsLoadedChanged();
void currentBackendChanged();
void daemonConnectedChanged();
@ -126,6 +133,8 @@ private:
QStringList m_availableBackends;
bool m_rootAgentsLoaded = false;
QStringList m_availableAgents;
bool m_workflowsLoaded = false;
QStringList m_availableWorkflows;
bool m_rootModelsLoaded = false;
QVariantMap m_rootModels;
QString m_currentBackend;