Give each workflow run a parent task with no agent

This commit is contained in:
Ian Gulliver
2026-10-21 09:02:09 -07:00
parent d062d7ccae
commit a130b9556d
23 changed files with 661 additions and 474 deletions
+84 -39
View File
@@ -7,6 +7,7 @@ import (
"fmt"
"log/slog"
"strings"
"time"
"g.fc.run/ai/byre/internal/api"
"g.fc.run/ai/byre/internal/git"
@@ -68,24 +69,31 @@ func (m *Manager) TaskWorkflow(ctx context.Context, taskID string) (api.TaskWork
}
func (m *Manager) WorkflowRun(ctx context.Context, taskID string) (api.WorkflowRun, error) {
frozen, err := m.store.TaskWorkflow(ctx, taskID)
if err != nil {
return api.WorkflowRun{}, err
}
task, err := m.store.GetTask(ctx, taskID)
if err != nil {
return api.WorkflowRun{}, err
}
tasks, err := m.store.RunTasks(ctx, *task.WorkflowRun)
if task.Workflow == nil {
return api.WorkflowRun{}, fmt.Errorf("%w: task %s isn't a workflow run", store.ErrNotFound, taskID)
}
frozen, err := m.store.TaskWorkflow(ctx, taskID)
if err != nil {
return api.WorkflowRun{}, err
}
events, err := m.store.RunEvents(ctx, *task.WorkflowRun)
tasks, err := m.store.RunTasks(ctx, taskID)
if err != nil {
return api.WorkflowRun{}, err
}
events, err := m.store.RunEvents(ctx, taskID)
if err != nil {
return api.WorkflowRun{}, err
}
finished, err := m.store.WorkflowFinished(ctx, taskID)
if err != nil {
return api.WorkflowRun{}, err
}
run := api.WorkflowRun{Name: frozen.Name, Run: *task.WorkflowRun, Title: frozen.Title, Brief: frozen.Brief, Definition: frozen.Definition, Stages: []api.WorkflowRunStage{}}
run := api.WorkflowRun{Name: frozen.Name, Title: frozen.Title, Brief: frozen.Brief, Definition: frozen.Definition, Finished: finished, Stages: []api.WorkflowRunStage{}}
after := map[string][]string{}
discardedBy := map[string]string{}
for _, ev := range events {
@@ -100,8 +108,6 @@ func (m *Manager) WorkflowRun(ctx context.Context, taskID string) (api.WorkflowR
after[*ev.TaskId] = ids
case "task.discarded":
discardedBy[*ev.TaskId] = ev.Actor
case "task.workflow_finished":
run.Finished = true
}
}
@@ -232,33 +238,60 @@ func (m *Manager) StartWorkflow(ctx context.Context, project api.Project, creato
frozen.Title = title
frozen.Audit = audit
run := ids.New(ids.WorkflowRun)
tasks := []api.Task{}
run, err := m.createRun(ctx, project, creator, frozen, todoItem, actor)
if err != nil {
return nil, err
}
tasks := []api.Task{run}
for _, stage := range workflow.Starts(frozen.Definition) {
task, err := m.startStage(ctx, project, creator, frozen, run, stage, base, todoItem, nil, actor)
task, err := m.startStage(ctx, project, creator, frozen, run, stage, base, nil, actor)
if task.Id != "" {
tasks = append(tasks, task)
}
if err != nil {
return tasks, fmt.Errorf("workflow %q: stage %q did not start: %w", name, stage, err)
if err == nil {
continue
}
if len(tasks) == 1 {
_, derr := m.store.DiscardTask(ctx, run.Id, actor)
if derr != nil {
slog.Error("discard workflow run with no stage tasks", "task", run.Id, "err", derr)
}
}
return tasks, fmt.Errorf("workflow %q: stage %q did not start: %w", name, stage, err)
}
return tasks, nil
}
func (m *Manager) startStage(ctx context.Context, project api.Project, creator api.User, frozen store.FrozenWorkflow, run, stage, base string, todoItem *string, parents []parentStage, actor string) (api.Task, error) {
func (m *Manager) createRun(ctx context.Context, project api.Project, creator api.User, frozen store.FrozenWorkflow, todoItem *string, actor string) (api.Task, error) {
t := api.Task{
Title: fmt.Sprintf("%s: %s", frozen.Title, stage),
MergeTitle: frozen.Title,
CreatedBy: creator.Login,
TodoItem: todoItem,
WorkflowRun: &run,
WorkflowStage: &stage,
Id: ids.New(ids.Task),
ProjectId: project.Id,
Title: frozen.Title,
MergeTitle: frozen.Title,
Status: api.TaskStatusActive,
CreatedBy: creator.Login,
CreatedAt: time.Now().UTC(),
TodoItem: todoItem,
}
if frozen.Audit != "" {
t.Audit = &frozen.Audit
}
task, err := m.createTask(ctx, project, t, &frozen, base, actor)
err := m.store.CreateTask(ctx, t, &frozen, actor)
if err != nil {
return api.Task{}, err
}
return m.store.GetTask(ctx, t.Id)
}
func (m *Manager) startStage(ctx context.Context, project api.Project, creator api.User, frozen store.FrozenWorkflow, run api.Task, stage, base string, parents []parentStage, actor string) (api.Task, error) {
t := api.Task{
Title: fmt.Sprintf("%s: %s", frozen.Title, stage),
MergeTitle: frozen.Title,
CreatedBy: creator.Login,
WorkflowStage: &stage,
ParentTaskId: &run.Id,
}
task, err := m.createTask(ctx, project, t, base, actor)
if err != nil {
return api.Task{}, err
}
@@ -280,7 +313,7 @@ func (m *Manager) beginStage(ctx context.Context, task api.Task, project api.Pro
ProjectId: &task.ProjectId,
TaskId: &task.Id,
Actor: actor,
Payload: map[string]any{"id": task.Id, "workflow": frozen.Name, "run": *task.WorkflowRun, "stage": stage, "after": after},
Payload: map[string]any{"id": task.Id, "workflow": frozen.Name, "run": *task.ParentTaskId, "stage": stage, "after": after},
})
if err != nil {
return api.AgentSession{}, err
@@ -401,16 +434,20 @@ func (m *Manager) nextStage(ctx context.Context, sessionID string) ([]string, er
m.workflowMu.Lock()
defer m.workflowMu.Unlock()
run, err := m.store.GetTask(ctx, *task.ParentTaskId)
if err != nil {
return nil, err
}
next := workflow.Next(frozen.Definition, *task.WorkflowStage)
if len(next) == 0 {
finished, err := m.store.WorkflowFinished(ctx, *task.WorkflowRun)
finished, err := m.store.WorkflowFinished(ctx, run.Id)
if err != nil {
return nil, err
}
if finished {
return nil, m.store.ClearStage(ctx, sess.Id)
}
tasks, err := m.store.RunTasks(ctx, *task.WorkflowRun)
tasks, err := m.store.RunTasks(ctx, run.Id)
if err != nil {
return nil, err
}
@@ -436,12 +473,12 @@ func (m *Manager) nextStage(ctx context.Context, sessionID string) ([]string, er
if err != nil {
return nil, err
}
return nil, m.store.FinishWorkflow(ctx, task, sess.Id, frozen.Name, output, kept)
return nil, m.store.FinishWorkflow(ctx, run, sess.Id, frozen.Name, output, kept)
}
started := []string{}
for _, n := range next {
ids, err := m.advanceTo(ctx, task, frozen, n)
ids, err := m.advanceTo(ctx, run, frozen, n)
if err != nil {
return started, err
}
@@ -450,8 +487,8 @@ func (m *Manager) nextStage(ctx context.Context, sessionID string) ([]string, er
return started, m.store.ClearStage(ctx, sess.Id)
}
func (m *Manager) advanceTo(ctx context.Context, from api.Task, frozen store.FrozenWorkflow, stage string) ([]string, error) {
tasks, err := m.store.RunTasks(ctx, *from.WorkflowRun)
func (m *Manager) advanceTo(ctx context.Context, run api.Task, frozen store.FrozenWorkflow, stage string) ([]string, error) {
tasks, err := m.store.RunTasks(ctx, run.Id)
if err != nil {
return nil, err
}
@@ -463,11 +500,11 @@ func (m *Manager) advanceTo(ctx context.Context, from api.Task, frozen store.Fro
}
parents = append(parents, p)
}
project, err := m.store.GetProject(ctx, from.ProjectId)
project, err := m.store.GetProject(ctx, run.ProjectId)
if err != nil {
return nil, err
}
creator, err := m.store.GetUser(ctx, from.CreatedBy)
creator, err := m.store.GetUser(ctx, run.CreatedBy)
if err != nil {
return nil, err
}
@@ -488,7 +525,7 @@ func (m *Manager) advanceTo(ctx context.Context, from api.Task, frozen store.Fro
default:
base = project.DefaultBranch
}
task, err := m.startStage(ctx, project, creator, frozen, *from.WorkflowRun, stage, base, from.TodoItem, parents, "byre")
task, err := m.startStage(ctx, project, creator, frozen, run, stage, base, parents, "byre")
if err != nil {
return nil, err
}
@@ -604,21 +641,25 @@ func (m *Manager) discardStages(ctx context.Context, tasks []api.Task, keep map[
}
func (m *Manager) workflowStatus(ctx context.Context, task api.Task) (string, error) {
finished, err := m.store.WorkflowFinished(ctx, *task.WorkflowRun)
if err != nil {
return "", err
if task.Workflow != nil {
finished, err := m.store.WorkflowFinished(ctx, task.Id)
if err != nil {
return "", err
}
status := fmt.Sprintf("run of workflow %q, with no agent, worktree or branch; its stage tasks are its subtasks", *task.Workflow)
if finished {
status += "; workflow finished"
}
return status, nil
}
output, _, err := m.store.StageResult(ctx, task.Id)
if err != nil {
return "", err
}
status := fmt.Sprintf("stage %q", *task.WorkflowStage)
status := fmt.Sprintf("stage %q of workflow run %s", *task.WorkflowStage, *task.ParentTaskId)
if output != "" {
status += " (finished)"
}
if finished {
status += ", workflow finished"
}
return status, nil
}
@@ -673,6 +714,10 @@ func (m *Manager) finishStageTool(l *live) mcp.Tool {
func describeTasks(tasks []api.Task) string {
parts := []string{}
for _, t := range tasks {
if t.Workflow != nil {
parts = append(parts, fmt.Sprintf("run task %s %q", t.Id, t.Title))
continue
}
parts = append(parts, fmt.Sprintf("task %s %q from %s", t.Id, t.Title, t.BaseRef))
}
return strings.Join(parts, ", ")