126 lines
4.5 KiB
Go
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)
|
|
}
|