Files
byre/internal/agents/tasks.go
T
2026-10-14 20:07:26 -07:00

129 lines
3.8 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"
)
func (m *Manager) CreateTask(ctx context.Context, project api.Project, title, createdBy, base string) (api.Task, error) {
id := store.NewID()
t := api.Task{
Id: id,
ProjectId: project.Id,
Title: title,
Branch: "byre/" + id,
WorktreePath: filepath.Join(m.worktreeRoot, project.Id, id),
Status: api.TaskStatusActive,
CreatedBy: createdBy,
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)
if err != nil {
return api.Task{}, err
}
return t, nil
}
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)
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
}