Add team tasks: several agents in one worktree sharing a board
This commit is contained in:
@@ -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."
|
||||
|
||||
|
||||
Reference in New Issue
Block a user