126 lines
4.0 KiB
Go
126 lines
4.0 KiB
Go
package agents
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"encoding/json"
|
||
|
|
"errors"
|
||
|
|
"fmt"
|
||
|
|
"log/slog"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"g.fc.run/ai/byre/internal/api"
|
||
|
|
"g.fc.run/ai/byre/internal/mcp"
|
||
|
|
"g.fc.run/ai/byre/internal/store"
|
||
|
|
)
|
||
|
|
|
||
|
|
const readyPrompt = "Your turn has ended and the review of your latest work has finished. If the task is ready to merge, call ready_for_merge with an overview of the whole task, then end your turn with one line. " +
|
||
|
|
"If it isn't (work remains that you mean to do, or you're waiting on the human), don't call it: say what's left in one line and end your turn. Don't change any files this turn."
|
||
|
|
|
||
|
|
type readyArgs struct {
|
||
|
|
Overview string `json:"overview"`
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *Manager) readyForMergeTool(l *live) mcp.Tool {
|
||
|
|
return 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.",
|
||
|
|
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."}
|
||
|
|
},
|
||
|
|
"required": ["overview"]
|
||
|
|
}`),
|
||
|
|
Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
|
||
|
|
args := readyArgs{}
|
||
|
|
err := json.Unmarshal(raw, &args)
|
||
|
|
if err != nil {
|
||
|
|
return "", fmt.Errorf("malformed ready_for_merge call: %w", err)
|
||
|
|
}
|
||
|
|
overview := strings.TrimSpace(args.Overview)
|
||
|
|
if overview == "" {
|
||
|
|
return "", fmt.Errorf("%w: overview must not be empty", ErrInvalid)
|
||
|
|
}
|
||
|
|
err = m.store.MarkReady(ctx, l.taskID, overview, agentActor)
|
||
|
|
if errors.Is(err, store.ErrNotFound) {
|
||
|
|
return "", fmt.Errorf("%w: this task is closed", ErrConflict)
|
||
|
|
}
|
||
|
|
if err != nil {
|
||
|
|
return "", err
|
||
|
|
}
|
||
|
|
return "Recorded: the human sees this task as ready to merge, with your overview.", nil
|
||
|
|
},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *Manager) promptReady(taskID string, from any) {
|
||
|
|
ctx := context.Background()
|
||
|
|
err := m.tryPromptReady(ctx, taskID, from == string(api.TaskStateReviewing))
|
||
|
|
if err != nil {
|
||
|
|
slog.Error("prompt ready for merge", "task", taskID, "err", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *Manager) tryPromptReady(ctx context.Context, taskID string, fromReview bool) error {
|
||
|
|
sess, found, err := m.liveSession(ctx, taskID)
|
||
|
|
if err != nil || !found {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
unlock := m.lockDelivery(sess.Id)
|
||
|
|
defer unlock()
|
||
|
|
|
||
|
|
task, err := m.store.GetTask(ctx, taskID)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if task.Status != api.TaskStatusActive || task.State != api.TaskStateIdle || task.WorkflowStage != nil || task.MergeAfterReview {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
sess, err = m.store.GetSession(ctx, sess.Id)
|
||
|
|
if err != nil || sess.State == api.AgentSessionStateRunning {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
latest, err := m.store.LastSnapshotNot(ctx, taskID, api.SnapshotTriggerAnswer)
|
||
|
|
if errors.Is(err, store.ErrNotFound) {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
reviews, err := m.store.ListReviews(ctx, taskID)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
var review *api.Review
|
||
|
|
for _, r := range reviews {
|
||
|
|
if r.SnapshotN == latest.N {
|
||
|
|
review = &r
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if review == nil || (review.State != api.ReviewStateDone && review.State != api.ReviewStateSkipped) {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
if fromReview && review.State == api.ReviewStateDone && review.Result != nil {
|
||
|
|
findings, _ := (*review.Result)["findings"].([]any)
|
||
|
|
if len(findings) > 0 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
}
|
||
|
|
prompted, err := m.store.ReadyPromptN(ctx, taskID)
|
||
|
|
if err != nil || prompted >= latest.N {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
_, err = m.Send(ctx, sess.Id, "byre", readyPrompt, "")
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
err = m.store.SetReadyPromptN(ctx, taskID, latest.N)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
slog.Info("ready for merge prompted", "task", taskID, "snapshot", latest.N, "session", sess.Id)
|
||
|
|
return nil
|
||
|
|
}
|