122 lines
3.9 KiB
Go
122 lines
3.9 KiB
Go
package agents
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"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 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
|
|
}
|
|
prompt := handoffPrompt
|
|
if sess.Owner != "" {
|
|
prompt = leadHandoffPrompt
|
|
}
|
|
err = transition(ctx, m.store, sess.Id, api.AgentSessionStateIdle, api.AgentSessionStateRunning, "byre")
|
|
if err != nil {
|
|
return api.AgentSession{}, err
|
|
}
|
|
m.mu.Lock()
|
|
m.handoffModel[sessionID] = model
|
|
m.mu.Unlock()
|
|
return m.launch(ctx, sess, task, "byre", prompt, nil)
|
|
}
|
|
|
|
func (m *Manager) continuationModel(sess api.AgentSession) string {
|
|
m.mu.Lock()
|
|
model := m.handoffModel[sess.Id]
|
|
delete(m.handoffModel, sess.Id)
|
|
m.mu.Unlock()
|
|
if model == "" {
|
|
return sess.Model
|
|
}
|
|
return model
|
|
}
|
|
|
|
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()
|
|
err := m.handoff(ctx, sessionID, handoff)
|
|
if err != nil {
|
|
slog.Error("continue from 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, leadHandoffPreamble+handoff, model, "byre", nil)
|
|
return err
|
|
}
|
|
|
|
discard, err := m.store.GetDiscard(ctx, sess.TaskId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if discard.State != store.DiscardNone {
|
|
m.startDiscard(sess.TaskId)
|
|
return nil
|
|
}
|
|
err = transition(ctx, m.store, sess.Id, api.AgentSessionStateIdle, api.AgentSessionStateCompleted, "byre")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
m.stopLive(sess.Id)
|
|
|
|
task, err := m.store.GetTask(ctx, sess.TaskId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = m.create(ctx, task, creator, sess.Member, handoffPreamble+handoff, model, "byre", nil)
|
|
if errors.Is(err, ErrConflict) {
|
|
m.startDiscard(task.Id)
|
|
return nil
|
|
}
|
|
return err
|
|
}
|