179 lines
5.7 KiB
Go
179 lines
5.7 KiB
Go
package agents
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"g.fc.run/misc/byre/internal/api"
|
|
"g.fc.run/misc/byre/internal/git"
|
|
"g.fc.run/misc/byre/internal/mcp"
|
|
"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}, base, actor)
|
|
}
|
|
|
|
func (m *Manager) StartTodo(ctx context.Context, project api.Project, creator api.User, title, model, actor string) (api.Task, error) {
|
|
if creator.GitName == "" || creator.GitEmail == "" {
|
|
return api.Task{}, ErrNoGitIdentity
|
|
}
|
|
|
|
src, found, err := git.ReadFile(project.RepoPath, project.DefaultBranch, todo.File)
|
|
if err != nil {
|
|
return api.Task{}, err
|
|
}
|
|
if !found {
|
|
return api.Task{}, fmt.Errorf("%w: %s has no %s", store.ErrNotFound, project.DefaultBranch, todo.File)
|
|
}
|
|
item, found := todo.Find(todo.Parse(src), title)
|
|
if !found {
|
|
return api.Task{}, 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 api.Task{}, err
|
|
}
|
|
for _, t := range tasks {
|
|
if t.Status == api.TaskStatusActive && t.TodoItem != nil && *t.TodoItem == title {
|
|
return api.Task{}, fmt.Errorf("%w: task %s is already working on %q", ErrConflict, t.Id, title)
|
|
}
|
|
}
|
|
|
|
task, err := m.createTask(ctx, project, api.Task{Title: item.Title, CreatedBy: creator.Login, TodoItem: &item.Title}, project.DefaultBranch, actor)
|
|
if err != nil {
|
|
return api.Task{}, err
|
|
}
|
|
|
|
_, err = m.create(ctx, task, creator, todoPrompt(item), model, actor, nil)
|
|
if err != nil {
|
|
return api.Task{}, fmt.Errorf("task %s created, but its agent did not start: %w", task.Id, err)
|
|
}
|
|
if actor == creator.Login {
|
|
m.setStartedTodo(creator.Login, true)
|
|
}
|
|
return task, nil
|
|
}
|
|
|
|
func todoPrompt(item api.TodoItem) string {
|
|
return 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))
|
|
}
|
|
|
|
func (m *Manager) createTask(ctx context.Context, project api.Project, t api.Task, 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, actor)
|
|
if err != nil {
|
|
return api.Task{}, err
|
|
}
|
|
return m.store.GetTask(ctx, t.Id)
|
|
}
|
|
|
|
type createTaskArgs struct {
|
|
Title string `json:"title"`
|
|
Prompt string `json:"prompt"`
|
|
Base string `json:"base"`
|
|
Model string `json:"model"`
|
|
}
|
|
|
|
func (m *Manager) createTaskTool(l *live) mcp.Tool {
|
|
return mcp.Tool{
|
|
Name: "create_task",
|
|
Description: "Create a new byre task in this project, with its own branch and worktree, and start an agent on it with prompt as its first message. " +
|
|
"Use it to run independent pieces of work in parallel. The new agent sees nothing of your context, so the prompt must be a complete brief. " +
|
|
"base picks the starting commit: default_branch starts from the project's default branch; this_task starts from this task's HEAD, " +
|
|
"which includes your committed work but not uncommitted edits (byre commits at the end of each turn). " +
|
|
"Returns at once with the new task's id; the new agent runs on its own.",
|
|
InputSchema: json.RawMessage(`{
|
|
"type": "object",
|
|
"properties": {
|
|
"title": {"type": "string", "description": "Short task title, shown in the task list."},
|
|
"prompt": {"type": "string", "description": "The new agent's first message: a complete, self-contained brief."},
|
|
"base": {"type": "string", "enum": ["default_branch", "this_task"]},
|
|
"model": {"type": "string", "description": "Model for the new agent. Defaults to this session's model."}
|
|
},
|
|
"required": ["title", "prompt", "base"]
|
|
}`),
|
|
Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
|
|
return m.createTaskFromAgent(ctx, l, raw)
|
|
},
|
|
}
|
|
}
|
|
|
|
func (m *Manager) createTaskFromAgent(ctx context.Context, l *live, raw json.RawMessage) (string, error) {
|
|
args := createTaskArgs{}
|
|
err := json.Unmarshal(raw, &args)
|
|
if err != nil {
|
|
return "", fmt.Errorf("malformed create_task call: %w", err)
|
|
}
|
|
|
|
if strings.TrimSpace(args.Title) == "" || strings.TrimSpace(args.Prompt) == "" {
|
|
return "", errors.New("title and prompt are required")
|
|
}
|
|
|
|
sess, err := m.store.GetSession(ctx, l.id)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
project, err := m.store.GetProject(ctx, l.projectID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
creator, err := m.store.GetUser(ctx, sess.CreatedBy)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
base := ""
|
|
switch args.Base {
|
|
case "default_branch":
|
|
base = project.DefaultBranch
|
|
case "this_task":
|
|
base, err = git.Head(l.dir)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
default:
|
|
return "", fmt.Errorf("base must be default_branch or this_task, not %q", args.Base)
|
|
}
|
|
|
|
model := args.Model
|
|
if model == "" {
|
|
model = sess.Model
|
|
}
|
|
|
|
task, err := m.CreateTask(ctx, project, args.Title, creator.Login, base, creator.Login)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
_, err = m.create(ctx, task, creator, args.Prompt, model, agentActor, nil)
|
|
if err != nil {
|
|
return "", fmt.Errorf("task %s created, but its agent did not start: %w", task.Id, err)
|
|
}
|
|
|
|
return fmt.Sprintf("Task %s %q created from %s and its agent started.", task.Id, task.Title, task.BaseRef), nil
|
|
}
|