Files
byre/internal/agents/tasks.go
T

126 lines
4.5 KiB
Go

package agents
import (
"context"
"errors"
"fmt"
"log/slog"
"path/filepath"
"time"
"g.fc.run/misc/byre/internal/api"
"g.fc.run/misc/byre/internal/git"
"g.fc.run/misc/byre/internal/store"
"g.fc.run/misc/byre/internal/todo"
)
func (m *Manager) CreateTask(ctx context.Context, project api.Project, title, createdBy, base, actor string) (api.Task, error) {
return m.createTask(ctx, project, api.Task{Title: title, CreatedBy: createdBy}, nil, base, actor)
}
func (m *Manager) StartTask(ctx context.Context, project api.Project, creator api.User, title, prompt, model string) (api.Task, error) {
if creator.GitName == "" || creator.GitEmail == "" {
return api.Task{}, ErrNoGitIdentity
}
task, err := m.CreateTask(ctx, project, title, creator.Login, project.DefaultBranch, creator.Login)
if err != nil {
return api.Task{}, err
}
_, err = m.create(ctx, task, creator, "", prompt, model, creator.Login, nil)
if err != nil {
return api.Task{}, fmt.Errorf("task %s created, but its agent did not start: %w", task.Id, err)
}
return task, nil
}
func (m *Manager) StartTodo(ctx context.Context, project api.Project, creator api.User, title, model, actor, workflowName string, discuss bool) ([]api.Task, error) {
if creator.GitName == "" || creator.GitEmail == "" {
return nil, ErrNoGitIdentity
}
src, found, err := git.ReadFile(project.RepoPath, project.DefaultBranch, todo.File)
if err != nil {
return nil, err
}
if !found {
return nil, fmt.Errorf("%w: %s has no %s", store.ErrNotFound, project.DefaultBranch, todo.File)
}
item, found := todo.Find(todo.Parse(src), title)
if !found {
return nil, fmt.Errorf("%w: %s on %s has no item %q", store.ErrNotFound, todo.File, project.DefaultBranch, title)
}
tasks, err := m.store.ListTasks(ctx, project.Id)
if err != nil {
return nil, err
}
for _, t := range tasks {
if t.Status == api.TaskStatusActive && t.TodoItem != nil && *t.TodoItem == title {
return nil, fmt.Errorf("%w: task %s is already working on %q", ErrConflict, t.Id, title)
}
}
started := []api.Task{}
if workflowName != "" {
started, err = m.StartWorkflow(ctx, project, creator, item.Title, project.DefaultBranch, actor, workflowName, todoPrompt(item, discuss), &item.Title)
if err != nil {
return nil, err
}
} else {
task, err := m.createTask(ctx, project, api.Task{Title: item.Title, CreatedBy: creator.Login, TodoItem: &item.Title}, nil, project.DefaultBranch, actor)
if err != nil {
return nil, err
}
_, err = m.create(ctx, task, creator, "", todoPrompt(item, discuss), model, actor, nil)
if err != nil {
return nil, fmt.Errorf("task %s created, but its agent did not start: %w", task.Id, err)
}
started = append(started, task)
}
if actor == creator.Login {
m.setStartedTodo(creator.Login, true)
}
return started, nil
}
func todoPrompt(item api.TodoItem, discuss bool) string {
p := fmt.Sprintf("Work on this item from %[1]s at the repo root:\n\n%[2]s\n\nWhen the work is done, remove this item from %[1]s in this worktree, so the removal lands in the same merge as the work.",
todo.File, todo.Markdown(item))
if !discuss {
return p
}
return p + "\n\n" + discussPrompt
}
const discussPrompt = "The human wants to discuss this item with you before any of it is built: a conversation, not a questionnaire. Read what you need to understand it, then end your turn with your thoughts on it in your final message: how you read it, how you would approach it, and what trade-offs or doubts you see. Don't use ask_human for this; the human replies in the chat. Keep the conversation going turn by turn, and change nothing until they tell you to build it."
func (m *Manager) createTask(ctx context.Context, project api.Project, t api.Task, frozen *store.FrozenWorkflow, base, actor string) (api.Task, error) {
t.Id = store.NewID()
t.ProjectId = project.Id
if t.MergeTitle == "" {
t.MergeTitle = t.Title
}
t.Branch = "byre/" + t.Id
t.WorktreePath = filepath.Join(m.worktreeRoot, project.Id, t.Id)
t.Status = api.TaskStatusActive
t.CreatedAt = time.Now().UTC()
var err error
t.BaseRef, err = git.AddWorktree(project.RepoPath, t.WorktreePath, t.Branch, base)
if err != nil {
return api.Task{}, err
}
err = m.store.CreateTask(ctx, t, frozen, actor)
if err != nil {
rerr := git.RemoveWorktree(project.RepoPath, t.WorktreePath, t.Branch)
if rerr != nil {
slog.Error("remove worktree of task not created", "task", t.Id, "err", rerr)
}
if errors.Is(err, store.ErrParentNotOpen) {
return api.Task{}, fmt.Errorf("%w: %w", ErrConflict, err)
}
return api.Task{}, err
}
return m.store.GetTask(ctx, t.Id)
}