Run each workflow stage as its own task, with fan-out and fan-in

This commit is contained in:
Ian Gulliver
2026-10-15 22:53:54 -07:00
parent f7a0ffcd4c
commit 2180a7e824
15 changed files with 739 additions and 408 deletions
+13 -12
View File
@@ -156,7 +156,8 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
tool("create_task",
"Create a task with its own branch and worktree, and start an agent on it with prompt as its first message. "+
"The new agent sees nothing else, so the prompt must be a complete brief. It starts from the project's default branch, "+
"or from from_task_id's committed HEAD. With workflow, the task runs that workflow's stages in order instead, each in a fresh session on the stage's model; "+
"or from from_task_id's committed HEAD. With workflow, it runs that workflow instead: each stage becomes its own task, titled \"<title>: <stage>\", on the stage's model, "+
"and this call creates the tasks of the stages that start from the brief (the others follow as the stages they come after finish); "+
"prompt is then optional and is added to every stage's first message as its brief, and model can't be given.",
`{"type": "object", "properties": {
"project_id": {"type": "string"},
@@ -206,16 +207,16 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
return "", err
}
}
task, err := m.CreateTask(ctx, project, args.Title, user.Login, base, leadActor(user.Login), args.Workflow, args.Prompt)
if err != nil {
return "", err
}
if args.Workflow != "" {
err = m.startWorkflow(ctx, task, user, leadActor(user.Login))
tasks, err := m.StartWorkflow(ctx, project, user, args.Title, base, leadActor(user.Login), args.Workflow, args.Prompt, nil)
if err != nil {
return "", err
}
return fmt.Sprintf("Task %s %q created from %s and its workflow %q started.", task.Id, task.Title, task.BaseRef, args.Workflow), nil
return fmt.Sprintf("Workflow %q started: %s.", args.Workflow, describeTasks(tasks)), nil
}
task, err := m.CreateTask(ctx, project, args.Title, user.Login, base, leadActor(user.Login))
if err != nil {
return "", err
}
_, err = m.create(ctx, task, user, args.Prompt, m.modelOr(args.Model, l), leadActor(user.Login), nil)
if err != nil {
@@ -226,7 +227,7 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
tool("start_todo",
"Start a task on an item of a project's TODO.md (its title, exactly as list_tasks' project or the file on the default branch has it). "+
"With workflow, the task runs that workflow's stages with the item as their brief, and model can't be given.",
"With workflow, it runs that workflow with the item as its brief, each stage as its own task, and model can't be given.",
`{"type": "object", "properties": {
"project_id": {"type": "string"},
"title": {"type": "string"},
@@ -251,11 +252,11 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
if err != nil {
return "", err
}
task, err := m.StartTodo(ctx, project, user, args.Title, m.modelOr(args.Model, l), leadActor(user.Login), args.Workflow)
tasks, err := m.StartTodo(ctx, project, user, args.Title, m.modelOr(args.Model, l), leadActor(user.Login), args.Workflow)
if err != nil {
return "", err
}
return fmt.Sprintf("Task %s %q started.", task.Id, task.Title), nil
return fmt.Sprintf("Started %s.", describeTasks(tasks)), nil
}),
tool("interrupt", "Interrupt the turn a task's agent is running.", taskOnly,
@@ -581,7 +582,7 @@ func (m *Manager) listTasksText(ctx context.Context) (string, error) {
fmt.Fprintf(b, "; forked from %s at snapshot %d", *t.ForkedFrom, *t.ForkedAt)
}
if t.Workflow != nil {
stage, err := m.workflowStatus(ctx, t.Id)
stage, err := m.workflowStatus(ctx, t)
if err != nil {
return "", err
}
@@ -700,7 +701,7 @@ func transcriptLine(ev api.Event, root string) string {
case "task.merged", "task.discarded":
return fmt.Sprintf("%s by %s", ev.Kind, ev.Actor)
case "task.workflow_stage":
return fmt.Sprintf("started stage %v/%v of workflow %q: %s", p["stage"], p["stages"], text("workflow"), text("name"))
return fmt.Sprintf("started stage %q of workflow %q", fmt.Sprint(p["stage"]), text("workflow"))
case "task.workflow_finished":
return fmt.Sprintf("finished workflow %q: %s", text("workflow"), clip(text("output"), 300))
case "review.finished":