From 3c2932c8fb375f858b19df3791a1daddbd656af7 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Fri, 23 Oct 2026 13:05:14 -0700 Subject: [PATCH] Simplify code found by simplicity audit of d6be98f --- internal/agents/afterreview.go | 2 +- internal/agents/audit.go | 2 +- internal/agents/codex.go | 5 +- internal/agents/compact.go | 8 ++-- internal/agents/fork.go | 12 ++--- internal/agents/inbox.go | 2 +- internal/agents/lead.go | 34 +++++++------- internal/agents/leadtools.go | 14 ++---- internal/agents/manager.go | 71 +++++++++++++++------------- internal/agents/mergein.go | 12 ++--- internal/agents/mergemain.go | 25 ++++++---- internal/agents/queue.go | 12 ++--- internal/agents/ready.go | 2 +- internal/agents/review.go | 4 +- internal/agents/schedule.go | 4 +- internal/agents/subtasks.go | 6 +-- internal/agents/summary.go | 2 +- internal/agents/tasks.go | 14 +++--- internal/agents/team.go | 6 +-- internal/agents/terminal.go | 2 +- internal/agents/workflows.go | 6 +-- internal/api/user.go | 5 ++ internal/server/lead.go | 3 +- internal/server/server.go | 84 ++++++++++++---------------------- internal/store/store.go | 62 +++++++------------------ internal/store/workflow.go | 6 +-- 26 files changed, 180 insertions(+), 225 deletions(-) create mode 100644 internal/api/user.go diff --git a/internal/agents/afterreview.go b/internal/agents/afterreview.go index 59adfe8..3042525 100644 --- a/internal/agents/afterreview.go +++ b/internal/agents/afterreview.go @@ -25,7 +25,7 @@ func (m *Manager) MergeAfterReview(ctx context.Context, task api.Task, user api. return api.Task{}, fmt.Errorf("%w: task %s is not a subtask", ErrInvalid, task.Id) } if target == api.MergeTargetMain { - if user.GitName == "" || user.GitEmail == "" { + if !user.HasGitIdentity() { return api.Task{}, ErrNoGitIdentity } title, err = cleanMergeTitle(title) diff --git a/internal/agents/audit.go b/internal/agents/audit.go index bdd96a0..9c10995 100644 --- a/internal/agents/audit.go +++ b/internal/agents/audit.go @@ -25,7 +25,7 @@ func (m *Manager) SetAuditWorkflow(ctx context.Context, projectID, name, actor s if err != nil { return api.Project{}, err } - if creator.GitName == "" || creator.GitEmail == "" { + if !creator.HasGitIdentity() { return api.Project{}, fmt.Errorf("%w: audits run as the project's creator %s, who has no git identity", ErrInvalid, creator.Login) } } diff --git a/internal/agents/codex.go b/internal/agents/codex.go index e00c16f..bca7c96 100644 --- a/internal/agents/codex.go +++ b/internal/agents/codex.go @@ -218,10 +218,7 @@ func (m *Manager) codexAfterTool(l *live) hookcmd.Func { } func imageAlt(prompt string) string { - alt := strings.NewReplacer("[", "", "]", "", "\n", " ").Replace(strings.TrimSpace(prompt)) - if r := []rune(alt); len(r) > 120 { - alt = string(r[:120]) + "…" - } + alt := clip(strings.NewReplacer("[", "", "]", "", "\n", " ").Replace(strings.TrimSpace(prompt)), 120) if alt == "" { return "generated image" } diff --git a/internal/agents/compact.go b/internal/agents/compact.go index 1598359..b6f4d5a 100644 --- a/internal/agents/compact.go +++ b/internal/agents/compact.go @@ -59,7 +59,7 @@ func (m *Manager) launchCompaction(ctx context.Context, sess api.AgentSession, t m.mu.Lock() m.handoffModel[sess.Id] = modelChoice{model, effort} m.mu.Unlock() - return m.launch(ctx, sess, task, "byre", prompt, "", "", nil) + return m.launch(ctx, sess, task, "byre", Prompt{Text: prompt}) } func (m *Manager) continuationModel(sess api.AgentSession) modelChoice { @@ -111,11 +111,11 @@ func (m *Manager) handoff(ctx context.Context, sessionID, handoff string) error if err != nil { return err } - _, err = m.createLead(ctx, creator, leadHandoffPreamble+handoff, model.model, model.effort, "byre", "", "", nil) + _, err = m.createLead(ctx, creator, "byre", Prompt{Text: leadHandoffPreamble + handoff, Model: model.model, Effort: model.effort}) return err } - if creator.GitName == "" || creator.GitEmail == "" { + if !creator.HasGitIdentity() { return ErrNoGitIdentity } task, err := m.store.GetTask(ctx, sess.TaskId) @@ -132,6 +132,6 @@ func (m *Manager) handoff(ctx context.Context, sessionID, handoff string) error return err } m.stopLive(sess.Id) - _, err = m.launch(ctx, next, task, "byre", handoffPreamble+handoff, "", "", nil) + _, err = m.launch(ctx, next, task, "byre", Prompt{Text: handoffPreamble + handoff}) return err } diff --git a/internal/agents/fork.go b/internal/agents/fork.go index fb5694e..3eca841 100644 --- a/internal/agents/fork.go +++ b/internal/agents/fork.go @@ -81,7 +81,7 @@ func (m *Manager) forkTool(l *live) mcp.Tool { if err != nil { return "", err } - _, _, err = m.Message(ctx, sessionID, agentActor, args.Prompt, "", "", "", "") + _, _, err = m.Message(ctx, sessionID, agentActor, Prompt{Text: args.Prompt}) if err != nil { return "", fmt.Errorf("task %s forked, but its agent did not start: %w", t.Id, err) } @@ -153,7 +153,7 @@ func (m *Manager) AnswerAgain(ctx context.Context, creator api.User, create api. } func (m *Manager) fork(ctx context.Context, src api.Task, project api.Project, n int, creator api.User, title string) (api.Task, string, error) { - if creator.GitName == "" || creator.GitEmail == "" { + if !creator.HasGitIdentity() { return api.Task{}, "", ErrNoGitIdentity } if src.Status != api.TaskStatusActive { @@ -192,8 +192,8 @@ func (m *Manager) fork(ctx context.Context, src api.Task, project api.Project, n }, snap.CommitSha, creator, srcSess, at) } -func (m *Manager) Continue(ctx context.Context, srcSess api.AgentSession, src api.Task, project api.Project, creator api.User, text, model, effort, sendID, client string, images ...string) (api.Task, error) { - if creator.GitName == "" || creator.GitEmail == "" { +func (m *Manager) Continue(ctx context.Context, srcSess api.AgentSession, src api.Task, project api.Project, creator api.User, p Prompt) (api.Task, error) { + if !creator.HasGitIdentity() { return api.Task{}, ErrNoGitIdentity } if src.Status == api.TaskStatusActive { @@ -209,7 +209,7 @@ func (m *Manager) Continue(ctx context.Context, srcSess api.AgentSession, src ap unlock := m.lockDelivery("continue:" + srcSess.Id) defer unlock() ctx = context.WithoutCancel(ctx) - prior, sent, err := m.priorSend(ctx, sendID) + prior, sent, err := m.priorSend(ctx, p.SendID) if err != nil { return api.Task{}, err } @@ -235,7 +235,7 @@ func (m *Manager) Continue(ctx context.Context, srcSess api.AgentSession, src ap if err != nil { return api.Task{}, err } - _, _, err = m.Message(ctx, sessionID, creator.Login, text, model, effort, sendID, client, images...) + _, _, err = m.Message(ctx, sessionID, creator.Login, p) if err != nil { return api.Task{}, fmt.Errorf("task %s created, but its agent did not start: %w", t.Id, err) } diff --git a/internal/agents/inbox.go b/internal/agents/inbox.go index 1706eb8..ca3c3b1 100644 --- a/internal/agents/inbox.go +++ b/internal/agents/inbox.go @@ -358,7 +358,7 @@ func (m *Manager) Deliver(ctx context.Context, sessionID string) error { for _, p := range pending { images = append(images, p.Response.Images...) } - after, err := m.turn(ctx, sess, task, pending[0].Response.User, deliveryMessage(pending), "", "", images) + after, err := m.turn(ctx, sess, task, pending[0].Response.User, Prompt{Text: deliveryMessage(pending), Images: images}) if errors.Is(err, ErrConflict) { slog.Info("deliver held: session changed state", "session", sessionID, "err", err, "responses", ids) return nil diff --git a/internal/agents/lead.go b/internal/agents/lead.go index c18708c..dd10ca5 100644 --- a/internal/agents/lead.go +++ b/internal/agents/lead.go @@ -100,7 +100,7 @@ func (m *Manager) activeLead(ctx context.Context, owner string) (api.AgentSessio return api.AgentSession{}, false, nil } -func (m *Manager) createLead(ctx context.Context, owner api.User, prompt, model, effort, actor, sendID, client string, images []string) (api.AgentSession, error) { +func (m *Manager) createLead(ctx context.Context, owner api.User, actor string, p Prompt) (api.AgentSession, error) { _, found, err := m.activeLead(ctx, owner.Login) if err != nil { return api.AgentSession{}, err @@ -110,13 +110,13 @@ func (m *Manager) createLead(ctx context.Context, owner api.User, prompt, model, } id := ids.New(ids.Session) - vendor := vendorOf(model) + vendor := vendorOf(p.Model) sess := api.AgentSession{ Id: id, Owner: owner.Login, Vendor: vendor, - Model: model, - Effort: effort, + Model: p.Model, + Effort: p.Effort, VendorSessionId: initialVendorSessionID(vendor), State: api.AgentSessionStateIdle, Role: api.Lead, @@ -127,10 +127,10 @@ func (m *Manager) createLead(ctx context.Context, owner api.User, prompt, model, if err != nil { return api.AgentSession{}, err } - return m.turn(ctx, sess, api.Task{}, actor, prompt, sendID, client, images) + return m.turn(ctx, sess, api.Task{}, actor, p) } -func (m *Manager) MessageLead(ctx context.Context, user api.User, text, model, effort, sendID, client string, images []string) (api.AgentSession, error) { +func (m *Manager) MessageLead(ctx context.Context, user api.User, p Prompt) (api.AgentSession, error) { m.setStartedTodo(user.Login, false) m.leadMu.Lock() @@ -140,7 +140,7 @@ func (m *Manager) MessageLead(ctx context.Context, user api.User, text, model, e if err != nil { return api.AgentSession{}, err } - prior, sent, err := m.priorSend(ctx, sendID) + prior, sent, err := m.priorSend(ctx, p.SendID) if err != nil { return api.AgentSession{}, err } @@ -151,21 +151,21 @@ func (m *Manager) MessageLead(ctx context.Context, user api.User, text, model, e return prior, nil } if !found { - return m.createLead(ctx, user, text, model, effort, user.Login, sendID, client, images) + return m.createLead(ctx, user, user.Login, p) } if sess.State == api.AgentSessionStateIdle { - s, err := m.Send(ctx, sess.Id, user.Login, text, model, effort, sendID, client, images...) + s, err := m.Send(ctx, sess.Id, user.Login, p) if !errors.Is(err, ErrConflict) { return s, err } } - sess, err = m.changeModel(ctx, sess, model, effort, user.Login) + sess, err = m.changeModel(ctx, sess, p.Model, p.Effort, user.Login) if err != nil { return api.AgentSession{}, err } - err = m.addLeadInput(ctx, store.LeadInput{Owner: user.Login, Kind: store.LeadMessage, Actor: user.Login, Text: text, Images: images, SendID: sendID, Client: client}) + err = m.addLeadInput(ctx, store.LeadInput{Owner: user.Login, Kind: store.LeadMessage, Actor: user.Login, Text: p.Text, Images: p.Images, SendID: p.SendID, Client: p.Client}) if err != nil { return api.AgentSession{}, err } @@ -218,7 +218,7 @@ func (m *Manager) tryDeliverLead(ctx context.Context, owner string) error { } text += "\n\n" + state } - _, err = m.Send(ctx, sess.Id, actor, text, "", "", sendID, client, images...) + _, err = m.Send(ctx, sess.Id, actor, Prompt{Text: text, SendID: sendID, Client: client, Images: images}) if errors.Is(err, ErrConflict) { return nil } @@ -705,15 +705,13 @@ func (m *Manager) itemText(ctx context.Context, item api.InboxItem) string { } func (m *Manager) answeredText(ctx context.Context, item api.InboxItem) string { - m.mu.Lock() - byLead := m.leadAnswered[item.Id] - delete(m.leadAnswered, item.Id) - m.mu.Unlock() - - if byLead || item.State != api.Answered || len(item.Responses) == 0 { + if item.State != api.Answered || len(item.Responses) == 0 { return "" } r := item.Responses[len(item.Responses)-1] + if isLeadActor(r.User) { + return "" + } answer := "" if r.Options != nil { answer = strings.Join(*r.Options, "; ") diff --git a/internal/agents/leadtools.go b/internal/agents/leadtools.go index 7d0844a..ad35969 100644 --- a/internal/agents/leadtools.go +++ b/internal/agents/leadtools.go @@ -134,14 +134,8 @@ func (m *Manager) leadTools(l *live) []mcp.Tool { if err != nil { return "", err } - m.mu.Lock() - m.leadAnswered[c.ItemId] = true - m.mu.Unlock() _, err = m.Respond(ctx, leadActor(user.Login), []api.InboxResponseCreate{c}) if err != nil { - m.mu.Lock() - delete(m.leadAnswered, c.ItemId) - m.mu.Unlock() return "", err } return fmt.Sprintf("Responded to %s. The agent gets it once it is idle and has no other question waiting.", c.ItemId), nil @@ -196,13 +190,13 @@ func (m *Manager) leadTools(l *live) []mcp.Tool { return fmt.Sprintf("Posted to %s's board; every agent of its team gets it.", task.Id), nil } if !found { - _, err = m.create(ctx, task, user, "", args.Text, args.Model, args.Effort, leadActor(user.Login), "", "", nil) + _, err = m.create(ctx, task, user, "", leadActor(user.Login), Prompt{Text: args.Text, Model: args.Model, Effort: args.Effort}) if err != nil { return "", err } return fmt.Sprintf("Started an agent on %s with your message.", task.Id), nil } - _, queued, err := m.Message(ctx, sess.Id, leadActor(user.Login), args.Text, args.Model, args.Effort, "", "") + _, queued, err := m.Message(ctx, sess.Id, leadActor(user.Login), Prompt{Text: args.Text, Model: args.Model, Effort: args.Effort}) if err != nil { return "", err } @@ -268,7 +262,7 @@ func (m *Manager) leadTools(l *live) []mcp.Tool { return "", err } } - if user.GitName == "" || user.GitEmail == "" { + if !user.HasGitIdentity() { return "", ErrNoGitIdentity } project, err := m.store.GetProject(ctx, args.ProjectID) @@ -305,7 +299,7 @@ func (m *Manager) leadTools(l *live) []mcp.Tool { return fmt.Sprintf("Task %s %q created from %s and its team of %d agents started.", task.Id, task.Title, task.BaseRef, len(args.Agents)), nil } model, effort := m.modelOr(args.Model, args.Effort, l) - _, err = m.create(ctx, task, user, "", args.Prompt, model, effort, leadActor(user.Login), "", "", nil) + _, err = m.create(ctx, task, user, "", leadActor(user.Login), Prompt{Text: args.Prompt, Model: model, Effort: effort}) if err != nil { return "", fmt.Errorf("task %s created, but its agent did not start: %w", task.Id, err) } diff --git a/internal/agents/manager.go b/internal/agents/manager.go index 9c8424b..ad801a2 100644 --- a/internal/agents/manager.go +++ b/internal/agents/manager.go @@ -56,7 +56,6 @@ type Manager struct { closing bool rateLimits map[api.AgentSessionVendor]api.Usage usageCommands map[api.AgentSessionVendor]string - leadAnswered map[string]bool paneFocus map[string]api.PaneFocus paneReported map[string]time.Time paneMoved map[string]time.Time @@ -163,7 +162,6 @@ func New(st *store.Store, worktreeRoot, leadRoot, handsRoot, codexHome, antigrav rateLimits: map[api.AgentSessionVendor]api.Usage{}, usageCommands: map[api.AgentSessionVendor]string{}, usageMu: map[api.AgentSessionVendor]*sync.Mutex{api.Claude: {}, api.Codex: {}, api.Antigravity: {}}, - leadAnswered: map[string]bool{}, paneFocus: map[string]api.PaneFocus{}, paneReported: map[string]time.Time{}, paneMoved: map[string]time.Time{}, @@ -194,12 +192,21 @@ func (m *Manager) taskFor(ctx context.Context, sess api.AgentSession) (api.Task, return m.store.GetTask(ctx, sess.TaskId) } -func (m *Manager) Create(ctx context.Context, task api.Task, creator api.User, prompt, model, effort, sendID, client string, images ...string) (api.AgentSession, error) { +type Prompt struct { + Text string + Model string + Effort string + SendID string + Client string + Images []string +} + +func (m *Manager) Create(ctx context.Context, task api.Task, creator api.User, p Prompt) (api.AgentSession, error) { unlock := m.lockDelivery("create:" + task.Id) defer unlock() ctx = context.WithoutCancel(ctx) - prior, sent, err := m.priorSend(ctx, sendID) + prior, sent, err := m.priorSend(ctx, p.SendID) if err != nil || sent { return prior, err } @@ -209,13 +216,13 @@ func (m *Manager) Create(ctx context.Context, task api.Task, creator api.User, p } if found { slog.Info("first message to an unused session", "session", unused, "task", task.Id) - return m.Send(ctx, unused, creator.Login, prompt, model, effort, sendID, client, images...) + return m.Send(ctx, unused, creator.Login, p) } - return m.create(ctx, task, creator, "", prompt, model, effort, creator.Login, sendID, client, images) + return m.create(ctx, task, creator, "", creator.Login, p) } -func (m *Manager) create(ctx context.Context, task api.Task, creator api.User, member, prompt, model, effort, actor, sendID, client string, images []string) (api.AgentSession, error) { - if creator.GitName == "" || creator.GitEmail == "" { +func (m *Manager) create(ctx context.Context, task api.Task, creator api.User, member, actor string, p Prompt) (api.AgentSession, error) { + if !creator.HasGitIdentity() { return api.AgentSession{}, ErrNoGitIdentity } @@ -223,11 +230,11 @@ func (m *Manager) create(ctx context.Context, task api.Task, creator api.User, m return api.AgentSession{}, fmt.Errorf("%w: task %s is %s", ErrConflict, task.Id, task.Status) } - sess, err := m.newSession(ctx, task, creator, member, model, effort) + sess, err := m.newSession(ctx, task, creator, member, p.Model, p.Effort) if err != nil { return api.AgentSession{}, err } - return m.turn(ctx, sess, task, actor, prompt, sendID, client, images) + return m.turn(ctx, sess, task, actor, p) } func (m *Manager) newSession(ctx context.Context, task api.Task, creator api.User, member, model, effort string) (api.AgentSession, error) { @@ -267,7 +274,7 @@ func sessionFor(task api.Task, creator api.User, member, model, effort string) a } } -func (m *Manager) Send(ctx context.Context, sessionID, actor, text, model, effort, sendID, client string, images ...string) (api.AgentSession, error) { +func (m *Manager) Send(ctx context.Context, sessionID, actor string, p Prompt) (api.AgentSession, error) { sess, err := m.store.GetSession(ctx, sessionID) if err != nil { return api.AgentSession{}, err @@ -277,7 +284,7 @@ func (m *Manager) Send(ctx context.Context, sessionID, actor, text, model, effor return api.AgentSession{}, fmt.Errorf("%w: session %s is %s", ErrConflict, sess.Id, sess.State) } - sess, err = m.changeModel(ctx, sess, model, effort, actor) + sess, err = m.changeModel(ctx, sess, p.Model, p.Effort, actor) if err != nil { return api.AgentSession{}, err } @@ -287,7 +294,7 @@ func (m *Manager) Send(ctx context.Context, sessionID, actor, text, model, effor return api.AgentSession{}, err } - return m.turn(ctx, sess, task, actor, text, sendID, client, images) + return m.turn(ctx, sess, task, actor, p) } func (m *Manager) Interrupt(ctx context.Context, sessionID string) (api.AgentSession, error) { @@ -600,7 +607,7 @@ func (m *Manager) Usage(ctx context.Context) (Usage, error) { return u, nil } -func (m *Manager) turn(ctx context.Context, sess api.AgentSession, task api.Task, actor, text, sendID, client string, images []string) (api.AgentSession, error) { +func (m *Manager) turn(ctx context.Context, sess api.AgentSession, task api.Task, actor string, p Prompt) (api.AgentSession, error) { if m.isPaused(sess.Id) { return api.AgentSession{}, fmt.Errorf("%w: session %s is paused; resume it first", ErrConflict, sess.Id) } @@ -608,20 +615,20 @@ func (m *Manager) turn(ctx context.Context, sess api.AgentSession, task api.Task if err != nil { return api.AgentSession{}, err } - return m.launch(ctx, sess, task, actor, text, sendID, client, images) + return m.launch(ctx, sess, task, actor, p) } func (m *Manager) startRunning(ctx context.Context, sess api.AgentSession, actor string) error { return transition(ctx, m.store, sess.Id, api.AgentSessionStateIdle, api.AgentSessionStateRunning, actor) } -func (m *Manager) launch(ctx context.Context, sess api.AgentSession, task api.Task, actor, text, sendID, client string, images []string) (api.AgentSession, error) { - imgs, compacting, err := m.recordMessage(ctx, sess, task, actor, text, sendID, client, images) +func (m *Manager) launch(ctx context.Context, sess api.AgentSession, task api.Task, actor string, p Prompt) (api.AgentSession, error) { + imgs, compacting, err := m.recordMessage(ctx, sess, task, actor, p) if err != nil { m.failStart(ctx, sess.Id, actor, err) return api.AgentSession{}, err } - err = m.deliver(ctx, sess, task, actor, client, text, imgs, compacting) + err = m.deliver(ctx, sess, task, actor, p.Client, p.Text, imgs, compacting) if err != nil { return api.AgentSession{}, err } @@ -721,7 +728,7 @@ func transition(ctx context.Context, st *store.Store, id string, from, to api.Ag } func (m *Manager) message(ctx context.Context, sess api.AgentSession, task api.Task, l *live, actor, text string, images []string) error { - imgs, compacting, err := m.recordMessage(ctx, sess, task, actor, text, "", "", images) + imgs, compacting, err := m.recordMessage(ctx, sess, task, actor, Prompt{Text: text, Images: images}) if err != nil { return err } @@ -737,17 +744,17 @@ func (m *Manager) send(ctx context.Context, l *live, text string, imgs []agentin return m.store.MarkSessionDelivered(ctx, l.id) } -func (m *Manager) recordMessage(ctx context.Context, sess api.AgentSession, task api.Task, actor, text, sendID, client string, images []string) ([]agentinput.Image, bool, error) { +func (m *Manager) recordMessage(ctx context.Context, sess api.AgentSession, task api.Task, actor string, p Prompt) ([]agentinput.Image, bool, error) { implementer := sess.Role == api.Implementer - if implementer && isMergeInPrompt(actor, text) { + if implementer && isMergeInPrompt(actor, p.Text) { m.ownMerge(sess.Id, task.Id, task.WorktreePath) } - imgs, err := m.readImages(ctx, images) + imgs, err := m.readImages(ctx, p.Images) if err != nil { return nil, false, err } - payload := map[string]any{"text": text} + payload := map[string]any{"text": p.Text} if implementer { head, err := git.Head(ctx, task.WorktreePath) if err != nil { @@ -755,24 +762,24 @@ func (m *Manager) recordMessage(ctx context.Context, sess api.AgentSession, task } payload["head"] = head } - if len(images) > 0 { - payload["images"] = images + if len(p.Images) > 0 { + payload["images"] = p.Images } - compacting := isHandoffPrompt(actor, text) + compacting := isHandoffPrompt(actor, p.Text) if compacting { payload["handoff"] = true } - if isContinuation(actor, text) { + if isContinuation(actor, p.Text) { payload["continuation"] = true } - if isFindingsMessage(actor, text) { + if isFindingsMessage(actor, p.Text) { payload["findings"] = true } - if sendID != "" { - payload["send_id"] = sendID + if p.SendID != "" { + payload["send_id"] = p.SendID } - if client != "" { - payload["client"] = client + if p.Client != "" { + payload["client"] = p.Client } err = m.store.RecordTurnMessage(ctx, sess.Id, newEvent(sess.Id, sess.Owner, task.ProjectId, task.Id, actor, "user.message", payload)) if err != nil { diff --git a/internal/agents/mergein.go b/internal/agents/mergein.go index 4b5df62..948d582 100644 --- a/internal/agents/mergein.go +++ b/internal/agents/mergein.go @@ -49,7 +49,7 @@ func (m *Manager) headStartMerge(ctx context.Context, task api.Task, user api.Us } prompt := mergeInPrompt(target) + fmt.Sprintf(" This task's Merge is held while another task merges into %s; this turn gets a head start, and byre merges %s in again when the held merge starts. If you stop for substantive changes, call stop_merge first, so byre drops the held merge and nothing is pushed before the review.", target, target) go func() { - _, err := m.launch(context.Background(), sess, task, "byre", prompt, "", "", nil) + _, err := m.launch(context.Background(), sess, task, "byre", Prompt{Text: prompt}) if err != nil { slog.Error("start held merge head start", "task", task.Id, "err", err) } @@ -62,13 +62,13 @@ func (m *Manager) sendMergeIn(ctx context.Context, task api.Task, user api.User, return api.AgentSession{}, err } if found { - return m.Send(ctx, sess.Id, "byre", prompt, "", "", "", "") + return m.Send(ctx, sess.Id, "byre", Prompt{Text: prompt}) } - return m.create(ctx, task, user, sess.Member, prompt, "", "", "byre", "", "", nil) + return m.create(ctx, task, user, sess.Member, "byre", Prompt{Text: prompt}) } func (m *Manager) Merge(ctx context.Context, task api.Task, project api.Project, user api.User, title string) (api.Task, error) { - if user.GitName == "" || user.GitEmail == "" { + if !user.HasGitIdentity() { return api.Task{}, ErrNoGitIdentity } if task.MergeQueuedAt != nil { @@ -140,7 +140,7 @@ func (m *Manager) beginClaimedMerge(ctx context.Context, c store.ClaimedMerge) e if err != nil { return err } - if user.GitName == "" || user.GitEmail == "" { + if !user.HasGitIdentity() { return ErrNoGitIdentity } sess, err := m.leadSession(ctx, task, user) @@ -184,7 +184,7 @@ func (m *Manager) launchMerge(sess api.AgentSession, task api.Task, project api. } func (m *Manager) deliverMerge(ctx context.Context, sess api.AgentSession, task api.Task, project api.Project, prompt string) error { - _, compacting, err := m.recordMessage(ctx, sess, task, "byre", prompt, "", "", nil) + _, compacting, err := m.recordMessage(ctx, sess, task, "byre", Prompt{Text: prompt}) if err != nil { return err } diff --git a/internal/agents/mergemain.go b/internal/agents/mergemain.go index b71253a..29f2be0 100644 --- a/internal/agents/mergemain.go +++ b/internal/agents/mergemain.go @@ -36,6 +36,13 @@ func (m *Manager) mergeToMainTool(l *live) mcp.Tool { } } +func checkLandable(task api.Task) error { + if task.Status != api.TaskStatusActive || task.Merging || task.MergeQueuedAt != nil || task.MergingIntoParent || task.Discarding { + return fmt.Errorf("%w: task %s is %s", ErrConflict, task.Id, task.State) + } + return nil +} + func (m *Manager) mergeToMain(ctx context.Context, l *live, raw json.RawMessage) (string, error) { args := mergeToMainArgs{} err := json.Unmarshal(raw, &args) @@ -66,7 +73,7 @@ func (m *Manager) mergeToMain(ctx context.Context, l *live, raw json.RawMessage) if err != nil { return "", err } - if user.GitName == "" || user.GitEmail == "" { + if !user.HasGitIdentity() { return "", ErrNoGitIdentity } @@ -77,8 +84,9 @@ func (m *Manager) mergeToMain(ctx context.Context, l *live, raw json.RawMessage) if err != nil { return "", err } - if task.Status != api.TaskStatusActive || task.Merging || task.MergeQueuedAt != nil || task.MergingIntoParent || task.Discarding { - return "", fmt.Errorf("%w: task %s is %s", ErrConflict, task.Id, task.State) + err = checkLandable(task) + if err != nil { + return "", err } project, err := m.store.GetProject(ctx, task.ProjectId) if err != nil { @@ -153,7 +161,7 @@ func (m *Manager) pushToMain(ctx context.Context, l *live, task api.Task, projec if err != nil { return "", err } - err = git.Merge(ctx, l.dir, commit, fmt.Sprintf("Merge %s from %s", commit[:7], project.DefaultBranch), l.gitName, l.gitEmail) + err = git.Merge(ctx, l.dir, commit, fmt.Sprintf("Merge %s from %s", short(commit), project.DefaultBranch), l.gitName, l.gitEmail) if err != nil { return "", fmt.Errorf("pushed %s to %s, but merging it back into this branch failed: %w", commit, pushed, err) } @@ -176,15 +184,16 @@ func landPrompt(target, title string) string { } func (m *Manager) MergeToMain(ctx context.Context, task api.Task, project api.Project, user api.User, title string) (api.Task, error) { - if user.GitName == "" || user.GitEmail == "" { + if !user.HasGitIdentity() { return api.Task{}, ErrNoGitIdentity } title, err := cleanMergeTitle(title) if err != nil { return api.Task{}, err } - if task.Merging || task.MergeQueuedAt != nil || task.MergingIntoParent || task.Discarding { - return api.Task{}, fmt.Errorf("%w: task %s is %s", ErrConflict, task.Id, task.State) + err = checkLandable(task) + if err != nil { + return api.Task{}, err } sess, found, err := m.liveSession(ctx, task.Id) if err != nil { @@ -205,7 +214,7 @@ func (m *Manager) MergeToMain(ctx context.Context, task api.Task, project api.Pr return api.Task{}, err } m.clearMergeAfterReview(ctx, task.Id) - _, err = m.launch(ctx, sess, task, "byre", landPrompt(project.DefaultBranch, title), "", "", nil) + _, err = m.launch(ctx, sess, task, "byre", Prompt{Text: landPrompt(project.DefaultBranch, title)}) if err != nil { return api.Task{}, err } diff --git a/internal/agents/queue.go b/internal/agents/queue.go index 3761026..2cd3eff 100644 --- a/internal/agents/queue.go +++ b/internal/agents/queue.go @@ -39,11 +39,11 @@ func (m *Manager) lockDelivery(sessionID string) func() { } } -func (m *Manager) Message(ctx context.Context, sessionID, actor, text, model, effort, sendID, client string, images ...string) (api.AgentSession, bool, error) { +func (m *Manager) Message(ctx context.Context, sessionID, actor string, p Prompt) (api.AgentSession, bool, error) { unlock := m.lockDelivery(sessionID) defer unlock() - prior, sent, err := m.priorSend(ctx, sendID) + prior, sent, err := m.priorSend(ctx, p.SendID) if err != nil || sent { return prior, false, err } @@ -64,11 +64,11 @@ func (m *Manager) Message(ctx context.Context, sessionID, actor, text, model, ef } held := sess.State == api.AgentSessionStateIdle && sess.Role == api.Implementer && (automatedActor(actor) && m.interrupted(sess.Id) || landing || m.isPaused(sess.Id)) if !held && (sess.State != api.AgentSessionStateRunning || sess.Role != api.Implementer) { - sess, err = m.Send(ctx, sessionID, actor, text, model, effort, sendID, client, images...) + sess, err = m.Send(ctx, sessionID, actor, p) return sess, false, err } - sess, err = m.changeModel(ctx, sess, model, effort, actor) + sess, err = m.changeModel(ctx, sess, p.Model, p.Effort, actor) if err != nil { return api.AgentSession{}, false, err } @@ -77,7 +77,7 @@ func (m *Manager) Message(ctx context.Context, sessionID, actor, text, model, ef if err != nil { return api.AgentSession{}, false, err } - err = m.store.AddTaskInput(ctx, store.TaskInput{TaskID: sess.TaskId, Member: sess.Member, Actor: actor, Text: text, Images: images, SendID: sendID, Client: client}, task.ProjectId, sess.Id) + err = m.store.AddTaskInput(ctx, store.TaskInput{TaskID: sess.TaskId, Member: sess.Member, Actor: actor, Text: p.Text, Images: p.Images, SendID: p.SendID, Client: p.Client}, task.ProjectId, sess.Id) if err != nil { return api.AgentSession{}, false, err } @@ -157,7 +157,7 @@ func (m *Manager) deliverQueued(ctx context.Context, sess api.AgentSession) (boo } in := inputs[0] - _, err = m.Send(ctx, sess.Id, in.Actor, in.Text, "", "", in.SendID, in.Client, in.Images...) + _, err = m.Send(ctx, sess.Id, in.Actor, Prompt{Text: in.Text, SendID: in.SendID, Client: in.Client, Images: in.Images}) if errors.Is(err, ErrConflict) { slog.Info("queued message held: session changed state", "session", sess.Id, "task", task.Id, "err", err) return true, nil diff --git a/internal/agents/ready.go b/internal/agents/ready.go index aa6c833..5d6f095 100644 --- a/internal/agents/ready.go +++ b/internal/agents/ready.go @@ -212,7 +212,7 @@ func (m *Manager) sendReadyPrompt(ctx context.Context, p store.ReadyPrompt) erro unlock := m.lockDelivery(p.SessionID) defer unlock() - _, err := m.Send(ctx, p.SessionID, "byre", prompt, "", "", "", "") + _, err := m.Send(ctx, p.SessionID, "byre", Prompt{Text: prompt}) if err != nil { return err } diff --git a/internal/agents/review.go b/internal/agents/review.go index 1038949..84fbca7 100644 --- a/internal/agents/review.go +++ b/internal/agents/review.go @@ -220,7 +220,7 @@ func (m *Manager) launchReviewer(ctx context.Context, task api.Task, project api err = os.MkdirAll(m.reviewMediaDir(project.Id, r.Id), 0o755) } if err == nil { - _, err = m.launch(ctx, rv.Session, task, "byre", prompt, "", "", nil) + _, err = m.launch(ctx, rv.Session, task, "byre", Prompt{Text: prompt}) } if err != nil { slog.Error("start reviewer", "review", r.Id, "task", task.Id, "stage", r.Stage, "err", err) @@ -383,7 +383,7 @@ func (m *Manager) deliverFindings(ctx context.Context, ref store.SnapshotRef, re slog.Info("review findings not delivered, task has no live agent", "task", ref.TaskID, "snapshot", ref.N) return } - _, _, err = m.Message(ctx, s.Id, "byre", text, "", "", "", "") + _, _, err = m.Message(ctx, s.Id, "byre", Prompt{Text: text}) if err != nil { slog.Error("deliver review findings", "task", ref.TaskID, "snapshot", ref.N, "session", s.Id, "err", err) return diff --git a/internal/agents/schedule.go b/internal/agents/schedule.go index d3c17f3..8549b94 100644 --- a/internal/agents/schedule.go +++ b/internal/agents/schedule.go @@ -39,7 +39,7 @@ func (m *Manager) SaveSchedule(ctx context.Context, projectID, scheduleID string if err != nil { return api.Schedule{}, err } - if user.GitName == "" || user.GitEmail == "" { + if !user.HasGitIdentity() { return api.Schedule{}, ErrNoGitIdentity } project, err := m.store.GetProject(ctx, projectID) @@ -105,7 +105,7 @@ func (m *Manager) startScheduled(ctx context.Context, sc api.Schedule) error { if err != nil { return err } - if user.GitName == "" || user.GitEmail == "" { + if !user.HasGitIdentity() { return ErrNoGitIdentity } _, err = m.StartWorkflow(ctx, project, user, sc.Title, project.DefaultBranch, "byre", sc.Workflow, sc.Prompt, nil, "") diff --git a/internal/agents/subtasks.go b/internal/agents/subtasks.go index e9ec56f..ab8024e 100644 --- a/internal/agents/subtasks.go +++ b/internal/agents/subtasks.go @@ -76,7 +76,7 @@ func (m *Manager) subtaskTools(l *live) []mcp.Tool { return "", err } model, effort := m.modelOr(args.Model, args.Effort, l) - _, err = m.create(ctx, task, creator, "", args.Prompt, model, effort, agentActor, "", "", nil) + _, err = m.create(ctx, task, creator, "", agentActor, Prompt{Text: args.Prompt, Model: model, Effort: effort}) if err != nil { return "", fmt.Errorf("subtask %s created, but its agent did not start: %w", task.Id, err) } @@ -297,10 +297,10 @@ func (m *Manager) messageTask(ctx context.Context, task api.Task, user api.User, return err } if !found { - _, err = m.create(ctx, task, user, sess.Member, text, "", "", actor, "", "", nil) + _, err = m.create(ctx, task, user, sess.Member, actor, Prompt{Text: text}) return err } - _, _, err = m.Message(ctx, sess.Id, actor, text, "", "", "", "") + _, _, err = m.Message(ctx, sess.Id, actor, Prompt{Text: text}) return err } diff --git a/internal/agents/summary.go b/internal/agents/summary.go index 1edc8bf..f54516e 100644 --- a/internal/agents/summary.go +++ b/internal/agents/summary.go @@ -199,7 +199,7 @@ func (m *Manager) beginDiscard(ctx context.Context, taskID string) error { slog.Info("discard turn started", "task", taskID) go func() { ctx := context.Background() - _, err := m.launch(ctx, sess, task, "byre", discardPrompt, "", "", nil) + _, err := m.launch(ctx, sess, task, "byre", Prompt{Text: discardPrompt}) if err == nil { return } diff --git a/internal/agents/tasks.go b/internal/agents/tasks.go index 1c99557..b8ccc10 100644 --- a/internal/agents/tasks.go +++ b/internal/agents/tasks.go @@ -19,28 +19,28 @@ func (m *Manager) CreateTask(ctx context.Context, project api.Project, title, cr return m.createTask(ctx, project, api.Task{Title: title, CreatedBy: createdBy}, base, actor) } -func (m *Manager) StartTask(ctx context.Context, project api.Project, creator api.User, title, prompt, model, effort, client string, images []string) (api.Task, error) { - if creator.GitName == "" || creator.GitEmail == "" { +func (m *Manager) StartTask(ctx context.Context, project api.Project, creator api.User, title string, p Prompt) (api.Task, error) { + if !creator.HasGitIdentity() { return api.Task{}, ErrNoGitIdentity } task, err := m.CreateTask(ctx, project, title, creator.Login, project.DefaultBranch, creator.Login) if err != nil { return api.Task{}, err } - sess, err := m.newSession(ctx, task, creator, "", model, effort) + sess, err := m.newSession(ctx, task, creator, "", p.Model, p.Effort) if err == nil { err = m.startRunning(ctx, sess, creator.Login) } if err != nil { return api.Task{}, fmt.Errorf("task %s created, but its agent did not start: %w", task.Id, err) } - imgs, compacting, err := m.recordMessage(ctx, sess, task, creator.Login, prompt, "", client, images) + imgs, compacting, err := m.recordMessage(ctx, sess, task, creator.Login, p) if err != nil { m.failStart(ctx, sess.Id, creator.Login, err) return api.Task{}, fmt.Errorf("task %s created, but its agent did not start: %w", task.Id, err) } go func() { - err := m.deliver(context.Background(), sess, task, creator.Login, client, prompt, imgs, compacting) + err := m.deliver(context.Background(), sess, task, creator.Login, p.Client, p.Text, imgs, compacting) if err != nil { slog.Error("start task agent", "task", task.Id, "err", err) } @@ -49,7 +49,7 @@ func (m *Manager) StartTask(ctx context.Context, project api.Project, creator ap } func (m *Manager) StartTodo(ctx context.Context, project api.Project, creator api.User, title, model, effort, actor, workflowName string, discuss bool) ([]api.Task, error) { - if creator.GitName == "" || creator.GitEmail == "" { + if !creator.HasGitIdentity() { return nil, ErrNoGitIdentity } src, found, err := git.ReadFile(ctx, project.RepoPath, project.DefaultBranch, todo.File) @@ -85,7 +85,7 @@ func (m *Manager) StartTodo(ctx context.Context, project api.Project, creator ap if err != nil { return nil, err } - _, err = m.create(ctx, task, creator, "", todoPrompt(item, discuss), model, effort, actor, "", "", nil) + _, err = m.create(ctx, task, creator, "", actor, Prompt{Text: todoPrompt(item, discuss), Model: model, Effort: effort}) if err != nil { return nil, fmt.Errorf("task %s created, but its agent did not start: %w", task.Id, err) } diff --git a/internal/agents/team.go b/internal/agents/team.go index afa91b1..e8acb9e 100644 --- a/internal/agents/team.go +++ b/internal/agents/team.go @@ -82,7 +82,7 @@ func (m *Manager) startTeam(ctx context.Context, task api.Task, creator api.User if err != nil { return err } - if creator.GitName == "" || creator.GitEmail == "" { + if !creator.HasGitIdentity() { return ErrNoGitIdentity } sessions := []api.AgentSession{} @@ -95,7 +95,7 @@ func (m *Manager) startTeam(ctx context.Context, task api.Task, creator api.User } failed := []string{} for i, sess := range sessions { - _, err = m.turn(ctx, sess, task, actor, teamPrompt(shared, members[i]), "", "", nil) + _, err = m.turn(ctx, sess, task, actor, Prompt{Text: teamPrompt(shared, members[i])}) if err != nil { failed = append(failed, fmt.Sprintf("%q: %v", members[i].Name, err)) } @@ -189,7 +189,7 @@ func (m *Manager) post(ctx context.Context, task api.Task, from poster, text str func (m *Manager) deliverPost(ctx context.Context, task api.Task, s api.AgentSession, msg string) { started, err := m.store.SessionStarted(ctx, s.Id) if err == nil && started { - _, _, err = m.Message(ctx, s.Id, boardActor, msg, "", "", "", "") + _, _, err = m.Message(ctx, s.Id, boardActor, Prompt{Text: msg}) } if err == nil && started { return diff --git a/internal/agents/terminal.go b/internal/agents/terminal.go index 6fc5ecc..2fcdf5f 100644 --- a/internal/agents/terminal.go +++ b/internal/agents/terminal.go @@ -199,7 +199,7 @@ func (m *Manager) terminalTypeTool(l *live) mcp.Tool { func (m *Manager) wakeOnPrompt(l *live) func(string, []byte) { return func(exit string, out []byte) { text := fmt.Sprintf("The terminal you last typed into is back at its prompt: the command there finished with exit status %s (130 means it was canceled with Ctrl-C). The terminal's output since your last terminal_type reply:\n\n%s", exit, terminal.Text(out)) - _, _, err := m.Message(context.Background(), l.id, "byre", text, "", "", "", "") + _, _, err := m.Message(context.Background(), l.id, "byre", Prompt{Text: text}) if err != nil { slog.Error("wake agent on terminal prompt", "session", l.id, "task", l.taskID, "err", err) return diff --git a/internal/agents/workflows.go b/internal/agents/workflows.go index 22656aa..b256c97 100644 --- a/internal/agents/workflows.go +++ b/internal/agents/workflows.go @@ -244,7 +244,7 @@ type parentStage struct { } func (m *Manager) StartWorkflow(ctx context.Context, project api.Project, creator api.User, title, base, actor, name, brief string, todoItem *string, audit string) ([]api.Task, error) { - if creator.GitName == "" || creator.GitEmail == "" { + if !creator.HasGitIdentity() { return nil, ErrNoGitIdentity } frozen, err := m.store.FreezeWorkflow(ctx, name) @@ -338,7 +338,7 @@ func (m *Manager) beginStage(ctx context.Context, task api.Task, project api.Pro s := workflow.Get(frozen.Definition, stage) message := stageMessage(frozen, stage, parents, project.DefaultBranch) if s.Agents == nil { - return m.create(ctx, task, creator, "", message, s.Model, s.Effort, "byre", "", "", nil) + return m.create(ctx, task, creator, "", "byre", Prompt{Text: message, Model: s.Model, Effort: s.Effort}) } err = m.startTeam(ctx, task, creator, message, *s.Agents, "byre") if err != nil { @@ -612,7 +612,7 @@ func (m *Manager) resumeStage(ctx context.Context, task api.Task, project api.Pr } message = teamPrompt(message, mem) } - _, err = m.turn(ctx, sess, task, "byre", message, "", "", nil) + _, err = m.turn(ctx, sess, task, "byre", Prompt{Text: message}) if err != nil { return ids, err } diff --git a/internal/api/user.go b/internal/api/user.go new file mode 100644 index 0000000..63377e1 --- /dev/null +++ b/internal/api/user.go @@ -0,0 +1,5 @@ +package api + +func (u User) HasGitIdentity() bool { + return u.GitName != "" && u.GitEmail != "" +} diff --git a/internal/server/lead.go b/internal/server/lead.go index 4666edd..70c5f28 100644 --- a/internal/server/lead.go +++ b/internal/server/lead.go @@ -6,6 +6,7 @@ import ( "net/http" "strings" + "g.fc.run/ai/byre/internal/agents" "g.fc.run/ai/byre/internal/api" ) @@ -57,7 +58,7 @@ func (s *Server) MessageLead(w http.ResponseWriter, r *http.Request) { return } - sess, err := s.agents.MessageLead(r.Context(), u, req.Text, deref(req.Model), deref(req.Effort), deref(req.SendId), r.Header.Get("Byre-Client"), images) + sess, err := s.agents.MessageLead(r.Context(), u, agents.Prompt{Text: req.Text, Model: deref(req.Model), Effort: deref(req.Effort), SendID: deref(req.SendId), Client: r.Header.Get("Byre-Client"), Images: images}) if err != nil { writeAgentError(w, err) return diff --git a/internal/server/server.go b/internal/server/server.go index c932f96..ade4280 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -450,7 +450,7 @@ func (s *Server) CreateTask(w http.ResponseWriter, r *http.Request, projectID st if !ok { return } - t, err := s.agents.StartTask(r.Context(), p, u, title, prompt, deref(req.Model), deref(req.Effort), r.Header.Get("Byre-Client"), images) + t, err := s.agents.StartTask(r.Context(), p, u, title, agents.Prompt{Text: prompt, Model: deref(req.Model), Effort: deref(req.Effort), Client: r.Header.Get("Byre-Client"), Images: images}) if err != nil { writeAgentError(w, err) return @@ -528,64 +528,40 @@ func (s *Server) MergeTask(w http.ResponseWriter, r *http.Request, taskID string return } - var err error - if req.Target != nil && !req.Target.Valid() { - writeError(w, http.StatusBadRequest, fmt.Errorf("unknown target %q", *req.Target)) - return - } - if req.KeepOpen != nil && *req.KeepOpen { - if (req.Target != nil && *req.Target != api.MergeTargetMain) || (req.AfterReview != nil && *req.AfterReview) { - writeError(w, http.StatusBadRequest, errors.New("keep_open is only for target main, without after_review")) + target := api.MergeTargetMain + if req.Target != nil { + if !req.Target.Valid() { + writeError(w, http.StatusBadRequest, fmt.Errorf("unknown target %q", *req.Target)) return } + target = *req.Target + } + afterReview := deref(req.AfterReview) + keepOpen := deref(req.KeepOpen) + if keepOpen && (target != api.MergeTargetMain || afterReview) { + writeError(w, http.StatusBadRequest, errors.New("keep_open is only for target main, without after_review")) + return + } + title := "" + if target == api.MergeTargetMain { if req.MergeTitle == nil { writeError(w, http.StatusBadRequest, errors.New("merge_title is required to merge into the default branch")) return } - t, err = s.agents.MergeToMain(r.Context(), t, p, u, *req.MergeTitle) - if err != nil { - writeAgentError(w, err) - return - } - writeJSON(w, http.StatusAccepted, t) - return - } - if req.AfterReview != nil && *req.AfterReview { - target := api.MergeTargetMain - if req.Target != nil { - target = *req.Target - } - title := "" - if target == api.MergeTargetMain { - if req.MergeTitle == nil { - writeError(w, http.StatusBadRequest, errors.New("merge_title is required to merge into the default branch")) - return - } - title = *req.MergeTitle - } - t, err = s.agents.MergeAfterReview(r.Context(), t, u, target, title) - if err != nil { - writeAgentError(w, err) - return - } - writeJSON(w, http.StatusAccepted, t) - return - } - if req.Target != nil && *req.Target == api.MergeTargetParent { - t, err = s.agents.MergeIntoParent(r.Context(), t, u) - if err != nil { - writeAgentError(w, err) - return - } - writeJSON(w, http.StatusAccepted, t) - return - } - if req.MergeTitle == nil { - writeError(w, http.StatusBadRequest, errors.New("merge_title is required to merge into the default branch")) - return + title = *req.MergeTitle } - t, err = s.agents.Merge(r.Context(), t, p, u, *req.MergeTitle) + var err error + switch { + case keepOpen: + t, err = s.agents.MergeToMain(r.Context(), t, p, u, title) + case afterReview: + t, err = s.agents.MergeAfterReview(r.Context(), t, u, target, title) + case target == api.MergeTargetParent: + t, err = s.agents.MergeIntoParent(r.Context(), t, u) + default: + t, err = s.agents.Merge(r.Context(), t, p, u, title) + } if err != nil { writeAgentError(w, err) return @@ -890,7 +866,7 @@ func (s *Server) ContinueSession(w http.ResponseWriter, r *http.Request, session return } - task, err := s.agents.Continue(r.Context(), src, t, p, u, req.Text, deref(req.Model), deref(req.Effort), deref(req.SendId), r.Header.Get("Byre-Client"), images...) + task, err := s.agents.Continue(r.Context(), src, t, p, u, agents.Prompt{Text: req.Text, Model: deref(req.Model), Effort: deref(req.Effort), SendID: deref(req.SendId), Client: r.Header.Get("Byre-Client"), Images: images}) if err != nil { writeAgentError(w, err) return @@ -938,7 +914,7 @@ func (s *Server) CreateSession(w http.ResponseWriter, r *http.Request, taskID st return } - sess, err := s.agents.Create(r.Context(), t, u, req.Prompt, deref(req.Model), deref(req.Effort), deref(req.SendId), r.Header.Get("Byre-Client"), images...) + sess, err := s.agents.Create(r.Context(), t, u, agents.Prompt{Text: req.Prompt, Model: deref(req.Model), Effort: deref(req.Effort), SendID: deref(req.SendId), Client: r.Header.Get("Byre-Client"), Images: images}) if err != nil { writeAgentError(w, err) return @@ -971,7 +947,7 @@ func (s *Server) SendMessage(w http.ResponseWriter, r *http.Request, sessionID s return } - sess, _, err := s.agents.Message(r.Context(), sessionID, actor(r), req.Text, deref(req.Model), deref(req.Effort), deref(req.SendId), r.Header.Get("Byre-Client"), images...) + sess, _, err := s.agents.Message(r.Context(), sessionID, actor(r), agents.Prompt{Text: req.Text, Model: deref(req.Model), Effort: deref(req.Effort), SendID: deref(req.SendId), Client: r.Header.Get("Byre-Client"), Images: images}) if err != nil { writeAgentError(w, err) return diff --git a/internal/store/store.go b/internal/store/store.go index d6cfc8b..32f9d6c 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -943,42 +943,38 @@ func participants(ctx context.Context, q querier, taskID string) ([]api.Particip const eventColumns = `seq, kind, project_id, task_id, agent_session_id, owner, actor, payload, created_at` -func (s *Store) EventsAfter(ctx context.Context, viewer string, after int64, limit int) ([]api.Event, error) { - rows, err := s.db.QueryContext(ctx, ` - SELECT `+eventColumns+` - FROM events WHERE seq > ? AND (owner IS NULL OR owner = ?) ORDER BY seq LIMIT ? - `, after, viewer, limit) +func (s *Store) queryEvents(ctx context.Context, query string, args ...any) ([]api.Event, error) { + rows, err := s.db.QueryContext(ctx, query, args...) if err != nil { return nil, err } return scanEvents(rows) } +func (s *Store) EventsAfter(ctx context.Context, viewer string, after int64, limit int) ([]api.Event, error) { + return s.queryEvents(ctx, ` + SELECT `+eventColumns+` + FROM events WHERE seq > ? AND (owner IS NULL OR owner = ?) ORDER BY seq LIMIT ? + `, after, viewer, limit) +} + func (s *Store) TaskEvents(ctx context.Context, taskID string, after int64, limit int) ([]api.Event, error) { - rows, err := s.db.QueryContext(ctx, ` + return s.queryEvents(ctx, ` SELECT `+eventColumns+` FROM events WHERE task_id = ? AND seq > ? ORDER BY seq LIMIT ? `, taskID, after, limit) - if err != nil { - return nil, err - } - return scanEvents(rows) } // ForkEvents returns only the payload fields needed to find a safe conversation boundary. // Tool results can contain large captures that would otherwise be decoded just to discard them. func (s *Store) ForkEvents(ctx context.Context, taskID, sessionID string) ([]api.Event, error) { - rows, err := s.db.QueryContext(ctx, ` + return s.queryEvents(ctx, ` SELECT seq, kind, project_id, task_id, agent_session_id, owner, actor, json_object('vendor_uuid', json_extract(payload, '$.vendor_uuid'), 'id', json_extract(payload, '$.id'), 'tool_use_id', json_extract(payload, '$.tool_use_id')), created_at FROM events WHERE task_id = ? AND agent_session_id = ? ORDER BY seq `, taskID, sessionID) - if err != nil { - return nil, err - } - return scanEvents(rows) } func (s *Store) TranscriptEvents(ctx context.Context, taskID string, after int64, limit int) ([]api.Event, error) { @@ -1007,11 +1003,7 @@ func (s *Store) TranscriptEvents(ctx context.Context, taskID string, after int64 id = from } // Merge the indexed ranges in sequence order instead of sorting the remaining history for every page. - rows, err := s.db.QueryContext(ctx, query+" ORDER BY seq LIMIT ?", append(args, limit)...) - if err != nil { - return nil, err - } - return scanEvents(rows) + return s.queryEvents(ctx, query+" ORDER BY seq LIMIT ?", append(args, limit)...) } func (s *Store) FirstPrompt(ctx context.Context, taskID string) (string, error) { @@ -1024,14 +1016,10 @@ func (s *Store) FirstPrompt(ctx context.Context, taskID string) (string, error) } func (s *Store) LastMessage(ctx context.Context, sessionID string) (api.Event, bool, error) { - rows, err := s.db.QueryContext(ctx, ` + evs, err := s.queryEvents(ctx, ` SELECT `+eventColumns+` FROM events WHERE agent_session_id = ? AND kind = 'user.message' ORDER BY seq DESC LIMIT 1 `, sessionID) - if err != nil { - return api.Event{}, false, err - } - evs, err := scanEvents(rows) if err != nil || len(evs) == 0 { return api.Event{}, false, err } @@ -1039,24 +1027,16 @@ func (s *Store) LastMessage(ctx context.Context, sessionID string) (api.Event, b } func (s *Store) BoardPosts(ctx context.Context, taskID string, after int64) ([]api.Event, error) { - rows, err := s.db.QueryContext(ctx, ` + return s.queryEvents(ctx, ` SELECT `+eventColumns+` FROM events WHERE task_id = ? AND kind = 'board.posted' AND seq > ? ORDER BY seq `, taskID, after) - if err != nil { - return nil, err - } - return scanEvents(rows) } func (s *Store) TaskEventsTail(ctx context.Context, taskID string, limit int) ([]api.Event, error) { - rows, err := s.db.QueryContext(ctx, ` + return s.queryEvents(ctx, ` SELECT * FROM (SELECT `+eventColumns+` FROM events WHERE task_id = ? ORDER BY seq DESC LIMIT ?) ORDER BY seq `, taskID, limit) - if err != nil { - return nil, err - } - return scanEvents(rows) } type Handoff struct { @@ -1116,14 +1096,10 @@ func sessionStopped(ctx context.Context, q querier, taskID, sessionID string) (b } func (s *Store) LeadEvents(ctx context.Context, owner string, after int64, limit int) ([]api.Event, error) { - rows, err := s.db.QueryContext(ctx, ` + return s.queryEvents(ctx, ` SELECT `+eventColumns+` FROM events WHERE owner = ? AND agent_session_id IS NOT NULL AND seq > ? ORDER BY seq LIMIT ? `, owner, after, limit) - if err != nil { - return nil, err - } - return scanEvents(rows) } func (s *Store) LeadEventsPage(ctx context.Context, owner string, before int64, turns int) (api.EventPage, error) { @@ -1139,17 +1115,13 @@ func (s *Store) LeadEventsPage(ctx context.Context, owner string, before int64, return api.EventPage{}, err } - rows, err := s.db.QueryContext(ctx, ` + events, err := s.queryEvents(ctx, ` SELECT `+eventColumns+` FROM events WHERE owner = ? AND agent_session_id IS NOT NULL AND seq > ? AND seq < ? ORDER BY seq `, owner, from, before) if err != nil { return api.EventPage{}, err } - events, err := scanEvents(rows) - if err != nil { - return api.EventPage{}, err - } return api.EventPage{Events: events, More: more}, nil } diff --git a/internal/store/workflow.go b/internal/store/workflow.go index a7c4240..358bedd 100644 --- a/internal/store/workflow.go +++ b/internal/store/workflow.go @@ -260,15 +260,11 @@ func (s *Store) RunTasks(ctx context.Context, run string) ([]api.Task, error) { } func (s *Store) RunEvents(ctx context.Context, run string) ([]api.Event, error) { - rows, err := s.db.QueryContext(ctx, ` + return s.queryEvents(ctx, ` SELECT `+eventColumns+` FROM events WHERE task_id IN (SELECT id FROM tasks WHERE parent_task_id = ?1 AND workflow_stage != '') AND kind IN ('task.workflow_stage', 'task.discarded') ORDER BY seq `, run) - if err != nil { - return nil, err - } - return scanEvents(rows) } func (s *Store) RecordStage(ctx context.Context, taskID, output, commit string) error {