Add team tasks: several agents in one worktree sharing a board

This commit is contained in:
Ian Gulliver
2026-10-18 08:48:57 -07:00
parent 7f62df19d2
commit 6bcde13b2f
31 changed files with 1392 additions and 439 deletions
+91 -31
View File
@@ -266,7 +266,17 @@ func (m *Manager) beginStage(ctx context.Context, task api.Task, project api.Pro
if err != nil {
return api.AgentSession{}, err
}
return m.create(ctx, task, creator, stageMessage(frozen, stage, parents, project.DefaultBranch), workflow.Get(frozen.Definition, stage).Model, "byre", nil)
s := workflow.Get(frozen.Definition, stage)
message := stageMessage(frozen, stage, parents, project.DefaultBranch)
if s.Agents == nil {
return m.create(ctx, task, creator, "", message, s.Model, "byre", nil)
}
err = m.startTeam(ctx, task, creator, message, *s.Agents, "byre")
if err != nil {
return api.AgentSession{}, err
}
sess, _, err := m.liveSession(ctx, task.Id)
return sess, err
}
func stageMessage(frozen store.FrozenWorkflow, stage string, parents []parentStage, defaultBranch string) string {
@@ -288,6 +298,18 @@ func stageMessage(frozen store.FrozenWorkflow, stage string, parents []parentSta
return b.String()
}
func (m *Manager) recordStage(ctx context.Context, l *live) error {
output, err := m.store.StageOutput(ctx, l.id)
if err != nil {
return err
}
head, err := git.Head(l.dir)
if err != nil {
return err
}
return m.store.RecordStage(ctx, l.taskID, output, head)
}
func (m *Manager) advanceStage(l *live) {
ctx := context.Background()
_, err := m.nextStage(ctx, l.id)
@@ -371,43 +393,41 @@ func (m *Manager) nextStage(ctx context.Context, sessionID string) ([]string, er
started := []string{}
for _, n := range next {
id, err := m.advanceTo(ctx, task, frozen, n)
ids, err := m.advanceTo(ctx, task, frozen, n)
if err != nil {
return started, err
}
if id != "" {
started = append(started, id)
}
started = append(started, ids...)
}
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) {
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)
if err != nil {
return "", err
return nil, err
}
parents := []parentStage{}
for _, a := range workflow.After(frozen.Definition, stage) {
p, found, err := m.finishedStage(ctx, tasks, a)
if err != nil || !found {
return "", err
return nil, err
}
parents = append(parents, p)
}
project, err := m.store.GetProject(ctx, from.ProjectId)
if err != nil {
return "", err
return nil, err
}
creator, err := m.store.GetUser(ctx, from.CreatedBy)
if err != nil {
return "", err
return nil, err
}
existing, found := latestStage(tasks, stage)
if found {
if existing.Status != api.TaskStatusActive || existing.Discarding {
return "", nil
return nil, nil
}
return m.resumeStage(ctx, existing, project, creator, frozen, parents)
}
@@ -418,13 +438,21 @@ func (m *Manager) advanceTo(ctx context.Context, from api.Task, frozen store.Fro
}
task, err := m.startStage(ctx, project, creator, frozen, *from.WorkflowRun, stage, base, from.TodoItem, parents, "byre")
if err != nil {
return "", err
return nil, err
}
sessions, err := m.store.ListSessions(ctx, task.Id)
if err != nil || len(sessions) == 0 {
return "", err
return m.sessionIDs(ctx, task.Id)
}
func (m *Manager) sessionIDs(ctx context.Context, taskID string) ([]string, error) {
sessions, err := m.store.ListSessions(ctx, taskID)
if err != nil {
return nil, err
}
return sessions[0].Id, nil
ids := []string{}
for _, s := range sessions {
ids = append(ids, s.Id)
}
return ids, nil
}
func latestStage(tasks []api.Task, stage string) (api.Task, bool) {
@@ -452,28 +480,58 @@ func (m *Manager) finishedStage(ctx context.Context, tasks []api.Task, stage str
return parentStage{}, false, nil
}
func (m *Manager) resumeStage(ctx context.Context, task api.Task, project api.Project, creator api.User, frozen store.FrozenWorkflow, parents []parentStage) (string, error) {
func (m *Manager) resumeStage(ctx context.Context, task api.Task, project api.Project, creator api.User, frozen store.FrozenWorkflow, parents []parentStage) ([]string, error) {
sessions, err := m.store.ListSessions(ctx, task.Id)
if err != nil {
return "", err
return nil, err
}
if len(sessions) == 0 {
sess, err := m.beginStage(ctx, task, project, creator, frozen, parents, "byre")
return sess.Id, err
}
sess := sessions[0]
started, err := m.store.SessionStarted(ctx, sess.Id)
if err != nil || started || sess.State == api.AgentSessionStateCompleted {
return "", err
}
if sess.State == api.AgentSessionStateRunning {
err = m.store.SetSessionState(ctx, sess.Id, api.AgentSessionStateIdle, "byre", nil)
_, err := m.beginStage(ctx, task, project, creator, frozen, parents, "byre")
if err != nil {
return "", err
return nil, err
}
return m.sessionIDs(ctx, task.Id)
}
s := workflow.Get(frozen.Definition, *task.WorkflowStage)
ids := []string{}
for _, sess := range sessions {
started, err := m.store.SessionStarted(ctx, sess.Id)
if err != nil {
return ids, err
}
if started || sess.State == api.AgentSessionStateCompleted {
continue
}
if sess.State == api.AgentSessionStateRunning {
err = m.store.SetSessionState(ctx, sess.Id, api.AgentSessionStateIdle, "byre", nil)
if err != nil {
return ids, err
}
}
message := stageMessage(frozen, *task.WorkflowStage, parents, project.DefaultBranch)
if s.Agents != nil {
mem, err := teamMember(*s.Agents, sess.Member)
if err != nil {
return ids, err
}
message = teamPrompt(message, mem)
}
_, err = m.turn(ctx, sess, task, "byre", message, nil)
if err != nil {
return ids, err
}
ids = append(ids, sess.Id)
}
return ids, nil
}
func teamMember(members []api.TeamMember, name string) (api.TeamMember, error) {
for _, mem := range members {
if mem.Name == name {
return mem, nil
}
}
_, err = m.turn(ctx, sess, task, "byre", stageMessage(frozen, *task.WorkflowStage, parents, project.DefaultBranch), nil)
return sess.Id, err
return api.TeamMember{}, fmt.Errorf("the stage's frozen workflow has no agent %q", name)
}
func (m *Manager) discardStages(ctx context.Context, final api.Task) error {
@@ -595,6 +653,8 @@ const workflowFileHelp = "A workflow's file is YAML: an optional description, th
"base (parent, the default: branch from the commit its one after stage finished at, or for a stage with none, from where the workflow was started; main: branch from the default branch; " +
"refused on a stage with more than one after stage, which always branches from the default branch and is told the branches it can merge), " +
"model (which also picks the vendor: gpt-* models run on Codex, every other model on Claude Code) and prompt (the stage agent's instructions). " +
"In place of model, agents (a list of at least two, each with name, model and prompt) runs the stage as a team: those agents work in the stage's one task and worktree and talk through a shared board; " +
"each gets the stage's prompt and then its own; the first agent finishes the stage. " +
"Every stage runs as its own task. Exactly one stage is final (no other stage comes after it), and cycles are refused. " +
"The workflow's name is not in the file."