126 lines
4.9 KiB
Go
126 lines
4.9 KiB
Go
package agents
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"g.fc.run/ai/byre/internal/api"
|
|
"g.fc.run/ai/byre/internal/git"
|
|
"g.fc.run/ai/byre/internal/store"
|
|
"g.fc.run/ai/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, to judge whether it should be built at all and in what shape. Zoom out before going into detail. Read what you need to understand it, then give your view: what problem it solves and whether that is worth solving, whether we should do it, what the alternatives are (another approach, a smaller change, or nothing), and how you would approach it, with the trade-offs and doubts you see. Talk it over in your final messages, where the human replies in the chat, and use ask_human as often as it helps. Each question belongs to that zoomed-out discussion, such as whether to do it, which alternative, or what shape, rather than the narrow details of getting it built, and later questions can build on earlier answers. 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)
|
|
}
|