Add Merge & continue and Other… to ready cards
This commit is contained in:
@@ -37,7 +37,7 @@ func (m *Manager) Compact(ctx context.Context, sessionID, model string) (api.Age
|
||||
if sess.Owner != "" {
|
||||
prompt = leadHandoffPrompt
|
||||
}
|
||||
err = transition(ctx, m.store, sess.Id, api.AgentSessionStateIdle, api.AgentSessionStateRunning, "byre")
|
||||
err = m.startRunning(ctx, sess, task.Id, "byre")
|
||||
if err != nil {
|
||||
return api.AgentSession{}, err
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ type Manager struct {
|
||||
finishing map[string]bool
|
||||
handoffModel map[string]string
|
||||
mergeTurns map[string]mergeTurn
|
||||
landing map[string]landing
|
||||
|
||||
deliverMu sync.Mutex
|
||||
deliverLocks map[string]*sessionLock
|
||||
@@ -158,6 +159,7 @@ func New(st *store.Store, worktreeRoot, leadRoot, handsRoot, codexHome, antigrav
|
||||
finishing: map[string]bool{},
|
||||
handoffModel: map[string]string{},
|
||||
mergeTurns: map[string]mergeTurn{},
|
||||
landing: map[string]landing{},
|
||||
signals: make(chan []api.Event, 1024),
|
||||
snaps: map[string]*snapState{},
|
||||
merges: map[string]*projectMerge{},
|
||||
@@ -537,13 +539,32 @@ func (m *Manager) Usage(ctx context.Context) (Usage, error) {
|
||||
}
|
||||
|
||||
func (m *Manager) turn(ctx context.Context, sess api.AgentSession, task api.Task, actor, text string, images []string) (api.AgentSession, error) {
|
||||
err := transition(ctx, m.store, sess.Id, api.AgentSessionStateIdle, api.AgentSessionStateRunning, actor)
|
||||
err := m.startRunning(ctx, sess, task.Id, actor)
|
||||
if err != nil {
|
||||
return api.AgentSession{}, err
|
||||
}
|
||||
return m.launch(ctx, sess, task, actor, text, images)
|
||||
}
|
||||
|
||||
func (m *Manager) startRunning(ctx context.Context, sess api.AgentSession, taskID, actor string) error {
|
||||
held := fmt.Errorf("%w: held while this task merges and continues", ErrConflict)
|
||||
if m.heldForLanding(taskID, sess.Id) {
|
||||
return held
|
||||
}
|
||||
err := transition(ctx, m.store, sess.Id, api.AgentSessionStateIdle, api.AgentSessionStateRunning, actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if m.heldForLanding(taskID, sess.Id) {
|
||||
err = transition(ctx, m.store, sess.Id, api.AgentSessionStateRunning, api.AgentSessionStateIdle, actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return held
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) launch(ctx context.Context, sess api.AgentSession, task api.Task, actor, text string, images []string) (api.AgentSession, error) {
|
||||
l, err := m.ensureLive(ctx, sess, task)
|
||||
if err == nil {
|
||||
@@ -1070,6 +1091,8 @@ func (m *Manager) turnEnded(ctx context.Context, l *live, res endResult) error {
|
||||
return m.reviewTurnEnded(ctx, l, res)
|
||||
}
|
||||
|
||||
defer m.endLanding(l.taskID, l.id)
|
||||
|
||||
m.mu.Lock()
|
||||
interrupted := l.interrupted
|
||||
rateLimited := l.rateLimited
|
||||
@@ -1091,11 +1114,11 @@ func (m *Manager) turnEnded(ctx context.Context, l *live, res endResult) error {
|
||||
m.endMergeTurn(l.id, false)
|
||||
}
|
||||
}()
|
||||
if !finished {
|
||||
err := git.AbortMerge(l.dir)
|
||||
if err != nil {
|
||||
slog.Error("abort unfinished merge", "session", l.id, "err", err)
|
||||
}
|
||||
}
|
||||
if (owner || m.landingBy(l.taskID, l.id) != "") && !finished {
|
||||
err := git.AbortMerge(l.dir)
|
||||
if err != nil {
|
||||
slog.Error("abort unfinished merge", "session", l.id, "err", err)
|
||||
}
|
||||
}
|
||||
unlock := func() {}
|
||||
|
||||
@@ -5,10 +5,12 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"g.fc.run/ai/byre/internal/api"
|
||||
"g.fc.run/ai/byre/internal/git"
|
||||
"g.fc.run/ai/byre/internal/mcp"
|
||||
"g.fc.run/ai/byre/internal/store"
|
||||
)
|
||||
|
||||
type mergeToMainArgs struct {
|
||||
@@ -52,7 +54,11 @@ func (m *Manager) mergeToMain(ctx context.Context, l *live, raw json.RawMessage)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
user, err := m.store.GetUser(ctx, sess.CreatedBy)
|
||||
login := sess.CreatedBy
|
||||
if by := m.landingBy(l.taskID, l.id); by != "" {
|
||||
login = by
|
||||
}
|
||||
user, err := m.store.GetUser(ctx, login)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -168,3 +174,116 @@ func (m *Manager) pushToMain(ctx context.Context, l *live, task api.Task, projec
|
||||
}
|
||||
return commit, nil
|
||||
}
|
||||
|
||||
func landPrompt(target, title string) string {
|
||||
return fmt.Sprintf("The human asked to merge this task's work so far into %[1]s and keep the task open (Merge & continue); byre holds the task's other agents until this turn ends. "+
|
||||
"First merge %[1]s in with `git merge --no-ff --no-commit %[1]s`, resolve any conflicts keeping both sides' intent, regenerate generated files instead of hand-merging them, and check that the build and tests pass. "+
|
||||
"If resolving the conflicts took substantive changes rather than combining both sides as written, stop there: end your turn with a description of those changes, so the review covers them before anything lands. "+
|
||||
"Otherwise call merge_to_main with title %[2]q, which commits the merge and lands the work on %[1]s, then end your turn with one line.", target, title)
|
||||
}
|
||||
|
||||
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 == "" {
|
||||
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)
|
||||
}
|
||||
sess, found, err := m.liveSession(ctx, task.Id)
|
||||
if err != nil {
|
||||
return api.Task{}, err
|
||||
}
|
||||
if !found {
|
||||
return api.Task{}, fmt.Errorf("%w: task %s has no agent", ErrConflict, task.Id)
|
||||
}
|
||||
|
||||
unlock := m.lockDelivery(sess.Id)
|
||||
defer unlock()
|
||||
|
||||
m.mu.Lock()
|
||||
holder := m.landing[task.Id]
|
||||
if holder.session == "" {
|
||||
m.landing[task.Id] = landing{session: sess.Id, login: user.Login}
|
||||
}
|
||||
m.mu.Unlock()
|
||||
if holder.session != "" {
|
||||
return api.Task{}, fmt.Errorf("%w: task %s is already merging and continuing", ErrConflict, task.Id)
|
||||
}
|
||||
sessions, err := m.store.ListSessions(ctx, task.Id)
|
||||
if err == nil {
|
||||
for _, s := range sessions {
|
||||
if s.Role == api.Implementer && s.State == api.AgentSessionStateRunning {
|
||||
err = fmt.Errorf("%w: an agent on this task is mid-turn; merge when they're all idle", ErrConflict)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
m.clearMergeAfterReview(ctx, task.Id)
|
||||
_, err = m.Send(ctx, sess.Id, "byre", landPrompt(project.DefaultBranch, title), "")
|
||||
}
|
||||
if err != nil {
|
||||
m.endLanding(task.Id, sess.Id)
|
||||
return api.Task{}, err
|
||||
}
|
||||
|
||||
err = m.store.ClearReady(ctx, task.Id, user.Login)
|
||||
if err != nil && !errors.Is(err, store.ErrNotFound) {
|
||||
return api.Task{}, err
|
||||
}
|
||||
return m.store.GetTask(ctx, task.Id)
|
||||
}
|
||||
|
||||
type landing struct {
|
||||
session string
|
||||
login string
|
||||
}
|
||||
|
||||
func (m *Manager) heldForLanding(taskID, sessionID string) bool {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
holder := m.landing[taskID].session
|
||||
return holder != "" && holder != sessionID
|
||||
}
|
||||
|
||||
func (m *Manager) landingBy(taskID, sessionID string) string {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.landing[taskID].session != sessionID {
|
||||
return ""
|
||||
}
|
||||
return m.landing[taskID].login
|
||||
}
|
||||
|
||||
func (m *Manager) endLanding(taskID, sessionID string) {
|
||||
m.mu.Lock()
|
||||
held := m.landing[taskID].session == sessionID
|
||||
if held {
|
||||
delete(m.landing, taskID)
|
||||
}
|
||||
m.mu.Unlock()
|
||||
if held {
|
||||
go m.releaseTeam(taskID, sessionID)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) releaseTeam(taskID, landingID string) {
|
||||
ctx := context.Background()
|
||||
sessions, err := m.store.ListSessions(ctx, taskID)
|
||||
if err != nil {
|
||||
slog.Error("list sessions after merge and continue", "task", taskID, "err", err)
|
||||
return
|
||||
}
|
||||
for _, s := range sessions {
|
||||
if s.Id == landingID || s.Role != api.Implementer || s.State != api.AgentSessionStateIdle || m.interrupted(s.Id) {
|
||||
continue
|
||||
}
|
||||
err = m.Deliver(ctx, s.Id)
|
||||
if err != nil {
|
||||
slog.Error("deliver after merge and continue", "session", s.Id, "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ func (m *Manager) Message(ctx context.Context, sessionID, actor, text, model str
|
||||
return api.AgentSession{}, false, err
|
||||
}
|
||||
}
|
||||
held := sess.State == api.AgentSessionStateIdle && sess.Role == api.Implementer && automatedActor(actor) && m.interrupted(sess.Id)
|
||||
held := sess.State == api.AgentSessionStateIdle && sess.Role == api.Implementer && (automatedActor(actor) && m.interrupted(sess.Id) || m.heldForLanding(sess.TaskId, sess.Id))
|
||||
if !held && (sess.State != api.AgentSessionStateRunning || sess.Role != api.Implementer) {
|
||||
sess, err = m.Send(ctx, sessionID, actor, text, model, images...)
|
||||
return sess, false, err
|
||||
|
||||
@@ -26,6 +26,7 @@ const readyPromptUnchanged = "Your turn has ended, and this task's branch has no
|
||||
|
||||
type readyArgs struct {
|
||||
Overview string `json:"overview"`
|
||||
Merge string `json:"merge"`
|
||||
}
|
||||
|
||||
func (m *Manager) readyForMergeTool(l *live) mcp.Tool {
|
||||
@@ -33,12 +34,13 @@ func (m *Manager) readyForMergeTool(l *live) mcp.Tool {
|
||||
Name: "ready_for_merge",
|
||||
Description: "Tell the human this task is ready to merge, with an overview of the whole task: all of the branch's work, not just what changed since your last call or the last discussion. " +
|
||||
"Write it for someone jumping between tasks who hasn't followed this one: a summary of what the change does, the tradeoffs and compromises made, what was not done, and follow-up work for later. " +
|
||||
"byre shows it in the task's transcript and passes it to the human's lead agent. " +
|
||||
"byre shows it in the task's transcript, with buttons to merge and complete the task or merge and continue it, and passes it to the human's lead agent. " +
|
||||
"Call it only once the review of your most recent changes has come back: byre reviews the work after each turn that changes files and messages you when that review finishes, so never call it in the same turn as a change, or while a review of your latest work is pending.",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"overview": {"type": "string", "description": "Markdown, with sections for the summary, tradeoffs and compromises, not done, and future TODOs; leave out a section with nothing in it."}
|
||||
"overview": {"type": "string", "description": "Markdown, with sections for the summary, tradeoffs and compromises, not done, and future TODOs; leave out a section with nothing in it."},
|
||||
"merge": {"type": "string", "enum": ["complete", "continue"], "description": "The button byre highlights: complete (the default) lands the work and closes the task; continue lands the work so far and keeps the task open, for when you or the human mean to do more on it."}
|
||||
},
|
||||
"required": ["overview"]
|
||||
}`),
|
||||
@@ -52,7 +54,13 @@ func (m *Manager) readyForMergeTool(l *live) mcp.Tool {
|
||||
if overview == "" {
|
||||
return "", fmt.Errorf("%w: overview must not be empty", ErrInvalid)
|
||||
}
|
||||
err = m.store.MarkReady(ctx, l.taskID, overview, agentActor)
|
||||
if args.Merge == "" {
|
||||
args.Merge = "complete"
|
||||
}
|
||||
if args.Merge != "complete" && args.Merge != "continue" {
|
||||
return "", fmt.Errorf("%w: merge must be complete or continue", ErrInvalid)
|
||||
}
|
||||
err = m.store.MarkReady(ctx, l.taskID, overview, args.Merge, agentActor)
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
return "", fmt.Errorf("%w: this task is closed", ErrConflict)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user