Files
byre/internal/agents/compact.go
T

138 lines
4.7 KiB
Go

package agents
import (
"context"
"errors"
"fmt"
"log/slog"
"strings"
"g.fc.run/ai/byre/internal/api"
"g.fc.run/ai/byre/internal/store"
)
const handoffPrompt = `Write a handoff for a fresh agent that will continue this task with none of your context. ` +
`Cover: the goal; decisions made and why; the current state (what is done, what is in progress, what is verified); ` +
`dead ends tried; open questions; and the files that matter. Be complete: the next agent sees nothing else. ` +
`Your final message is the handoff, and nothing else.`
const handoffPreamble = "You are continuing this task from a previous agent session, which ended so the work could go on with a fresh context. Its handoff follows.\n\n"
const leadHandoffPreamble = "You are continuing as the human's lead agent from a previous session, which ended so you could go on with a fresh context. Its handoff follows. Signals that arrived meanwhile come next.\n\n"
const leadHandoffPrompt = `Write a handoff for a fresh lead agent that will take over from you with none of your context. ` +
`Cover: the human's standing instructions and preferences; what each task you are tracking is doing and what you are waiting on from it; ` +
`questions you have surfaced or answered and why; what you last pointed the human's attention at; and anything you promised to do or check. ` +
`Be complete: the next agent sees nothing else. Your final message is the handoff, and nothing else.`
func (m *Manager) Compact(ctx context.Context, sessionID, model, effort string) (api.AgentSession, error) {
sess, err := m.store.GetSession(ctx, sessionID)
if err != nil {
return api.AgentSession{}, err
}
task, err := m.taskFor(ctx, sess)
if err != nil {
return api.AgentSession{}, err
}
unlock := m.lockDelivery(sessionID)
if m.isPaused(sessionID) {
unlock()
return api.AgentSession{}, fmt.Errorf("%w: session %s is paused; resume it first", ErrConflict, sessionID)
}
err = m.startRunning(ctx, sess, "byre")
unlock()
if err != nil {
return api.AgentSession{}, err
}
return m.launchCompaction(ctx, sess, task, model, effort)
}
type modelChoice struct {
model, effort string
}
func (m *Manager) launchCompaction(ctx context.Context, sess api.AgentSession, task api.Task, model, effort string) (api.AgentSession, error) {
prompt := handoffPrompt
if sess.Owner != "" {
prompt = leadHandoffPrompt
}
m.mu.Lock()
m.handoffModel[sess.Id] = modelChoice{model, effort}
m.mu.Unlock()
return m.launch(ctx, sess, task, "byre", Prompt{Text: prompt})
}
func (m *Manager) continuationModel(sess api.AgentSession) modelChoice {
m.mu.Lock()
c := m.handoffModel[sess.Id]
delete(m.handoffModel, sess.Id)
m.mu.Unlock()
if c.model == "" {
return modelChoice{sess.Model, sess.Effort}
}
return c
}
func isHandoffPrompt(actor, text string) bool {
return actor == "byre" && (text == handoffPrompt || text == leadHandoffPrompt)
}
func isContinuation(actor, text string) bool {
return actor == "byre" && (strings.HasPrefix(text, handoffPreamble) || strings.HasPrefix(text, leadHandoffPreamble))
}
func (m *Manager) continueFromHandoff(sessionID, handoff string) {
ctx := context.Background()
m.waitResumed(sessionID)
err := m.handoff(ctx, sessionID, handoff)
if err == nil {
return
}
slog.Error("continue from handoff", "session", sessionID, "err", err)
err = m.store.TransitionSession(ctx, sessionID, api.AgentSessionStateRunning, api.AgentSessionStateIdle, "byre")
if err != nil && !errors.Is(err, store.ErrStateChanged) {
slog.Error("end failed handoff", "session", sessionID, "err", err)
}
}
func (m *Manager) handoff(ctx context.Context, sessionID, handoff string) error {
sess, err := m.store.GetSession(ctx, sessionID)
if err != nil {
return err
}
creator, err := m.store.GetUser(ctx, sess.CreatedBy)
if err != nil {
return err
}
model := m.continuationModel(sess)
if sess.Owner != "" {
err = m.retire(ctx, sess, "byre")
if err != nil {
return err
}
_, err = m.createLead(ctx, creator, "byre", Prompt{Text: leadHandoffPreamble + handoff, Model: model.model, Effort: model.effort})
return err
}
if !creator.HasGitIdentity() {
return ErrNoGitIdentity
}
task, err := m.store.GetTask(ctx, sess.TaskId)
if err != nil {
return err
}
next := sessionFor(task, creator, sess.Member, model.model, model.effort)
next.State = api.AgentSessionStateRunning
err = m.store.ReplaceSession(ctx, sess.Id, next, task.ProjectId, "byre")
if errors.Is(err, store.ErrStateChanged) {
return transition(ctx, m.store, sess.Id, api.AgentSessionStateRunning, api.AgentSessionStateIdle, "byre")
}
if err != nil {
return err
}
m.stopLive(sess.Id)
_, err = m.launch(ctx, next, task, "byre", Prompt{Text: handoffPreamble + handoff})
return err
}