Skip to content

Commit 89da170

Browse files
committed
refactor: rewrite workflow system as agent team orchestration templates
Workflow is no longer a standalone execution engine that calls RunClaude() sequentially. It now serves as a quick template for creating agent teams, fully reusing the existing team infrastructure. Key changes: - Extract StartAgent/StartAllAgents from MCP handlers into agent pkg - Replace Step/WorkflowRun/ApprovalDecision types with WorkflowAgent/WorkflowTask/WorkflowRunResult - Rewrite runner as non-blocking: team_create → agent_add → task_create → StartAllAgents → return result - Add workflow_create MCP tool for programmatic workflow creation - Add --project flag to workflow run CLI command - Auto-migrate old steps-based YAML to new agents+tasks format - Delete approval.go and persistence.go (no longer needed)
1 parent b57c5d2 commit 89da170

File tree

12 files changed

+445
-549
lines changed

12 files changed

+445
-549
lines changed

internal/agent/team.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package agent
33
import (
44
"fmt"
55
"os"
6+
"os/exec"
67
"time"
78
)
89

@@ -170,3 +171,83 @@ func IsAgentAlive(teamName, agentName string) bool {
170171
}
171172
return alive
172173
}
174+
175+
// AgentStartResult holds the result of starting a single agent.
176+
type AgentStartResult struct {
177+
Name string `json:"name"`
178+
Started bool `json:"started"`
179+
PID int `json:"pid,omitempty"`
180+
Error string `json:"error,omitempty"`
181+
}
182+
183+
// StartAgent spawns an agent daemon as an independent subprocess.
184+
// Returns the PID of the spawned process.
185+
func StartAgent(teamName, agentName string) (int, error) {
186+
// Verify the agent exists
187+
if _, err := NewDaemon(teamName, agentName); err != nil {
188+
return 0, err
189+
}
190+
191+
// Check if already running
192+
if IsAgentAlive(teamName, agentName) {
193+
state, _ := GetAgentState(teamName, agentName)
194+
pid := 0
195+
if state != nil {
196+
pid = state.PID
197+
}
198+
return 0, fmt.Errorf("agent %q is already running (pid %d)", agentName, pid)
199+
}
200+
201+
exe, err := os.Executable()
202+
if err != nil {
203+
return 0, fmt.Errorf("cannot find executable: %w", err)
204+
}
205+
206+
cmd := exec.Command(exe, "agent", "run", teamName, agentName)
207+
cmd.Stdout = os.Stdout
208+
cmd.Stderr = os.Stderr
209+
if err := cmd.Start(); err != nil {
210+
return 0, fmt.Errorf("failed to start agent: %w", err)
211+
}
212+
213+
pid := cmd.Process.Pid
214+
cmd.Process.Release() // detach
215+
return pid, nil
216+
}
217+
218+
// StartAllAgents spawns all agents in a team, skipping those already running.
219+
func StartAllAgents(teamName string) ([]AgentStartResult, error) {
220+
cfg, err := GetTeam(teamName)
221+
if err != nil {
222+
return nil, err
223+
}
224+
225+
var results []AgentStartResult
226+
for _, m := range cfg.Members {
227+
r := AgentStartResult{Name: m.Name}
228+
229+
if IsAgentAlive(teamName, m.Name) {
230+
state, _ := GetAgentState(teamName, m.Name)
231+
if state != nil {
232+
r.PID = state.PID
233+
}
234+
r.Error = "already running"
235+
results = append(results, r)
236+
continue
237+
}
238+
239+
pid, err := StartAgent(teamName, m.Name)
240+
if err != nil {
241+
// StartAgent checks alive again; handle "already running" gracefully
242+
r.Error = err.Error()
243+
results = append(results, r)
244+
continue
245+
}
246+
247+
r.Started = true
248+
r.PID = pid
249+
results = append(results, r)
250+
}
251+
252+
return results, nil
253+
}

internal/commands/workflow_cobra.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ var workflowRunCmd = &cobra.Command{
2727
Run: func(cmd *cobra.Command, args []string) {
2828
dir, _ := cmd.Flags().GetString("dir")
2929
model, _ := cmd.Flags().GetString("model")
30-
RunWorkflowRun(args[0], dir, model)
30+
project, _ := cmd.Flags().GetString("project")
31+
RunWorkflowRun(args[0], dir, model, project)
3132
},
3233
}
3334

@@ -52,6 +53,7 @@ var workflowDeleteCmd = &cobra.Command{
5253
func init() {
5354
workflowRunCmd.Flags().StringP("dir", "d", "", "Working directory (default: current)")
5455
workflowRunCmd.Flags().StringP("model", "m", "", "Claude model to use")
56+
workflowRunCmd.Flags().StringP("project", "p", "", "Project name to execute in")
5557

5658
WorkflowCmd.AddCommand(workflowListCmd)
5759
WorkflowCmd.AddCommand(workflowRunCmd)

internal/commands/workflow_commands.go

Lines changed: 20 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package commands
22

33
import (
4-
"context"
54
"fmt"
65
"os"
76

@@ -32,13 +31,13 @@ func RunWorkflowList() {
3231
if wf.Description != "" {
3332
fmt.Printf(" %s\n", wf.Description)
3433
}
35-
fmt.Printf(" Steps: %d\n", len(wf.Steps))
34+
fmt.Printf(" Agents: %d Tasks: %d\n", len(wf.Agents), len(wf.Tasks))
3635
fmt.Println()
3736
}
3837
}
3938

40-
// RunWorkflowRun executes a workflow by name.
41-
func RunWorkflowRun(name, dir, model string) {
39+
// RunWorkflowRun launches a workflow as an agent team (non-blocking).
40+
func RunWorkflowRun(name, dir, model, project string) {
4241
wf, err := workflow.GetWorkflow(name)
4342
if err != nil {
4443
ui.ShowError("Workflow not found", err)
@@ -49,43 +48,27 @@ func RunWorkflowRun(name, dir, model string) {
4948
dir, _ = os.Getwd()
5049
}
5150

52-
fmt.Printf("Running workflow: %s (%d steps)\n", wf.Name, len(wf.Steps))
51+
fmt.Printf("Launching workflow: %s (%d agents, %d tasks)\n", wf.Name, len(wf.Agents), len(wf.Tasks))
5352
if wf.Description != "" {
5453
fmt.Printf(" %s\n", wf.Description)
5554
}
5655
fmt.Println()
5756

58-
ctx := context.Background()
59-
run, err := workflow.RunWorkflow(ctx, wf, workflow.RunWorkflowOptions{
57+
result, err := workflow.RunWorkflow(wf, workflow.RunWorkflowOptions{
6058
WorkDir: dir,
6159
Model: model,
60+
Project: project,
6261
})
6362
if err != nil {
64-
ui.ShowError("Workflow execution failed", err)
63+
ui.ShowError("Workflow launch failed", err)
6564
return
6665
}
6766

68-
// Print results
69-
for i, result := range run.Results {
70-
status := "✓"
71-
if result.Error != "" {
72-
status = "✗"
73-
}
74-
fmt.Printf(" %s Step %d: %s\n", status, i+1, result.StepName)
75-
if result.Error != "" {
76-
fmt.Printf(" Error: %s\n", result.Error)
77-
}
78-
if result.Cost > 0 {
79-
fmt.Printf(" Cost: $%.4f\n", result.Cost)
80-
}
81-
fmt.Println()
82-
}
83-
84-
if run.Status == "completed" {
85-
ui.ShowSuccess("Workflow completed successfully.")
86-
} else {
87-
ui.ShowError(fmt.Sprintf("Workflow %s", run.Status), nil)
88-
}
67+
ui.ShowSuccess("Workflow launched as team: %s", result.TeamName)
68+
fmt.Printf(" Agents started: %d\n", result.Agents)
69+
fmt.Printf(" Tasks created: %d\n", result.Tasks)
70+
fmt.Println()
71+
fmt.Printf("Monitor progress: codes agent status %s\n", result.TeamName)
8972
}
9073

9174
// RunWorkflowCreate creates a new workflow template file.
@@ -99,10 +82,14 @@ func RunWorkflowCreate(name string) {
9982
wf := &workflow.Workflow{
10083
Name: name,
10184
Description: "Custom workflow",
102-
Steps: []workflow.Step{
85+
Agents: []workflow.WorkflowAgent{
86+
{Name: "worker", Role: "Execute workflow tasks"},
87+
},
88+
Tasks: []workflow.WorkflowTask{
10389
{
104-
Name: "Step 1",
105-
Prompt: "Describe what this step should do",
90+
Subject: "Task 1",
91+
Assign: "worker",
92+
Prompt: "Describe what this task should do",
10693
},
10794
},
10895
}
@@ -113,7 +100,7 @@ func RunWorkflowCreate(name string) {
113100
}
114101

115102
fmt.Printf("Created workflow template: %s\n", workflow.WorkflowDir()+"/"+name+".yml")
116-
fmt.Println("Edit the YAML file to customize steps.")
103+
fmt.Println("Edit the YAML file to customize agents and tasks.")
117104
}
118105

119106
// RunWorkflowDelete removes a workflow.

internal/mcp/agent_tools.go

Lines changed: 9 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ package mcpserver
33
import (
44
"context"
55
"fmt"
6-
"os"
7-
"os/exec"
86

97
mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
108

@@ -201,38 +199,11 @@ type agentStartOutput struct {
201199
}
202200

203201
func agentStartHandler(ctx context.Context, req *mcpsdk.CallToolRequest, input agentStartInput) (*mcpsdk.CallToolResult, agentStartOutput, error) {
204-
// Verify the agent exists before spawning
205-
_, err := agent.NewDaemon(input.Team, input.Name)
202+
pid, err := agent.StartAgent(input.Team, input.Name)
206203
if err != nil {
207204
return nil, agentStartOutput{}, err
208205
}
209206

210-
// Check if the agent is already alive
211-
if agent.IsAgentAlive(input.Team, input.Name) {
212-
state, _ := agent.GetAgentState(input.Team, input.Name)
213-
pid := 0
214-
if state != nil {
215-
pid = state.PID
216-
}
217-
return nil, agentStartOutput{}, fmt.Errorf("agent %q is already running (pid %d)", input.Name, pid)
218-
}
219-
220-
// Spawn as independent subprocess so it survives MCP server restarts
221-
exe, err := os.Executable()
222-
if err != nil {
223-
return nil, agentStartOutput{}, fmt.Errorf("cannot find executable: %w", err)
224-
}
225-
226-
cmd := exec.Command(exe, "agent", "run", input.Team, input.Name)
227-
cmd.Stdout = os.Stdout
228-
cmd.Stderr = os.Stderr
229-
if err := cmd.Start(); err != nil {
230-
return nil, agentStartOutput{}, fmt.Errorf("failed to start agent: %w", err)
231-
}
232-
233-
pid := cmd.Process.Pid
234-
cmd.Process.Release() // detach
235-
236207
// Ensure background notification monitor is running
237208
ensureMonitorRunning(mcpServer)
238209

@@ -585,44 +556,19 @@ type teamStartAllOutput struct {
585556
}
586557

587558
func teamStartAllHandler(ctx context.Context, req *mcpsdk.CallToolRequest, input teamStartAllInput) (*mcpsdk.CallToolResult, teamStartAllOutput, error) {
588-
cfg, err := agent.GetTeam(input.Name)
559+
agentResults, err := agent.StartAllAgents(input.Name)
589560
if err != nil {
590561
return nil, teamStartAllOutput{}, err
591562
}
592563

593-
exe, err := os.Executable()
594-
if err != nil {
595-
return nil, teamStartAllOutput{}, fmt.Errorf("cannot find executable: %w", err)
596-
}
597-
598564
var results []teamStartAllResult
599-
for _, m := range cfg.Members {
600-
r := teamStartAllResult{Name: m.Name}
601-
602-
// Skip already alive agents
603-
if agent.IsAgentAlive(input.Name, m.Name) {
604-
state, _ := agent.GetAgentState(input.Name, m.Name)
605-
if state != nil {
606-
r.PID = state.PID
607-
}
608-
r.Error = "already running"
609-
results = append(results, r)
610-
continue
611-
}
612-
613-
cmd := exec.Command(exe, "agent", "run", input.Name, m.Name)
614-
cmd.Stdout = os.Stdout
615-
cmd.Stderr = os.Stderr
616-
if err := cmd.Start(); err != nil {
617-
r.Error = err.Error()
618-
results = append(results, r)
619-
continue
620-
}
621-
622-
r.Started = true
623-
r.PID = cmd.Process.Pid
624-
cmd.Process.Release()
625-
results = append(results, r)
565+
for _, ar := range agentResults {
566+
results = append(results, teamStartAllResult{
567+
Name: ar.Name,
568+
Started: ar.Started,
569+
PID: ar.PID,
570+
Error: ar.Error,
571+
})
626572
}
627573

628574
// Ensure background notification monitor is running

0 commit comments

Comments
 (0)