Run reviews and nudges on the state-driven coordinator
This commit is contained in:
+24
-104
@@ -8,7 +8,6 @@ import (
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"g.fc.run/ai/byre/internal/api"
|
||||
"g.fc.run/ai/byre/internal/mcp"
|
||||
"g.fc.run/ai/byre/internal/store"
|
||||
"g.fc.run/ai/byre/internal/workflow"
|
||||
@@ -142,121 +141,42 @@ func (m *Manager) cancelReadyTool(l *live) mcp.Tool {
|
||||
}
|
||||
}
|
||||
|
||||
func readyState(s api.TaskState) bool {
|
||||
return s == api.TaskStateIdle || s == api.TaskStateFinished || s == api.TaskStateReadyToMerge || s == api.TaskStateDiscardSuggested
|
||||
}
|
||||
|
||||
func (m *Manager) promptReady(taskID string, from any) {
|
||||
func (m *Manager) promptReady(p store.ReadyPrompt) {
|
||||
ctx := context.Background()
|
||||
err := m.tryPromptReady(ctx, taskID, from == string(api.TaskStateReviewing))
|
||||
err := m.sendReadyPrompt(ctx, p)
|
||||
if err != nil {
|
||||
slog.Error("prompt ready for merge", "task", taskID, "err", err)
|
||||
slog.Info("ready for merge prompt not sent", "task", p.TaskID, "snapshot", p.Snapshot, "session", p.SessionID, "err", err)
|
||||
return
|
||||
}
|
||||
slog.Info("ready for merge prompted", "task", p.TaskID, "snapshot", p.Snapshot, "session", p.SessionID)
|
||||
}
|
||||
|
||||
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 || !readyState(task.State) {
|
||||
return nil
|
||||
}
|
||||
stagePrompt := ""
|
||||
if task.WorkflowStage != nil {
|
||||
frozen, err := m.store.TaskWorkflow(ctx, taskID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
output, _, err := m.store.StageResult(ctx, taskID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if output != "" && len(workflow.Next(frozen.Definition, *task.WorkflowStage)) > 0 {
|
||||
return nil
|
||||
}
|
||||
finished, err := m.store.WorkflowFinished(ctx, *task.ParentTaskId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if output == "" && !finished {
|
||||
stagePrompt = finishStagePrompt(frozen, *task.WorkflowStage)
|
||||
}
|
||||
}
|
||||
sess, err = m.store.GetSession(ctx, sess.Id)
|
||||
if err != nil || sess.State == api.AgentSessionStateRunning || m.interrupted(sess.Id) {
|
||||
return err
|
||||
}
|
||||
latest, err := m.store.LastReviewable(ctx, taskID)
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reviews, err := m.store.SnapshotReviews(ctx, taskID, latest.N)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(reviews) == 0 {
|
||||
project, err := m.store.GetProject(ctx, task.ProjectId)
|
||||
if err != nil || project.ReviewWorkflow != "" {
|
||||
return err
|
||||
}
|
||||
}
|
||||
settled, err := m.reviewsSettled(ctx, reviews)
|
||||
if err != nil || !settled {
|
||||
return err
|
||||
}
|
||||
for _, r := range reviews {
|
||||
if r.State == api.ReviewStateFailed && task.Background {
|
||||
m.auditError(ctx, task, fmt.Sprintf("Review %q of snapshot %d failed: %s. The audit can't go on without you.", r.Stage, latest.N, r.Error))
|
||||
return nil
|
||||
}
|
||||
if r.State != api.ReviewStateDone && r.State != api.ReviewStateSkipped {
|
||||
return nil
|
||||
}
|
||||
if fromReview && r.Result != nil {
|
||||
findings, _ := (*r.Result)["findings"].([]any)
|
||||
if len(findings) > 0 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
due, err := m.store.ReadyPromptDue(ctx, taskID)
|
||||
if err != nil || !due {
|
||||
return err
|
||||
}
|
||||
open, err := m.store.ListInbox(ctx, store.InboxFilter{TaskID: taskID, State: api.Open})
|
||||
if err != nil || len(open) > 0 {
|
||||
return err
|
||||
}
|
||||
func (m *Manager) sendReadyPrompt(ctx context.Context, p store.ReadyPrompt) error {
|
||||
prompt := readyPrompt
|
||||
if len(reviews) == 1 && reviews[0].State == api.ReviewStateSkipped {
|
||||
switch p.Kind {
|
||||
case store.ReadyPromptUnchanged:
|
||||
prompt = readyPromptUnchanged
|
||||
case store.ReadyPromptFinishStage:
|
||||
task, err := m.store.GetTask(ctx, p.TaskID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
frozen, err := m.store.TaskWorkflow(ctx, p.TaskID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
prompt = finishStagePrompt(frozen, *task.WorkflowStage)
|
||||
}
|
||||
if stagePrompt != "" {
|
||||
prompt = stagePrompt
|
||||
}
|
||||
_, err = m.Send(ctx, sess.Id, "byre", prompt, "", "", "")
|
||||
|
||||
unlock := m.lockDelivery(p.SessionID)
|
||||
defer unlock()
|
||||
_, err := m.Send(ctx, p.SessionID, "byre", prompt, "", "", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
seq, err := m.store.LastTaskSeq(ctx, taskID)
|
||||
seq, err := m.store.LastTaskSeq(ctx, p.TaskID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = m.store.SetReadyPromptSeq(ctx, taskID, seq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
slog.Info("ready for merge prompted", "task", taskID, "snapshot", latest.N, "session", sess.Id)
|
||||
return nil
|
||||
return m.store.SetReadyPromptSeq(ctx, p.TaskID, seq)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user