Run reviews and nudges on the state-driven coordinator

This commit is contained in:
Ian Gulliver
2026-10-21 12:54:34 -07:00
parent 97b71416d5
commit b4a454bab1
16 changed files with 718 additions and 567 deletions
+24 -104
View File
@@ -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)
}