2026-10-14 19:35:34 -07:00
package agents
import (
"context"
2026-10-16 10:08:08 -07:00
"errors"
2026-10-14 19:35:34 -07:00
"log/slog"
2026-10-15 11:16:50 -07:00
"strings"
2026-10-14 19:35:34 -07:00
2026-10-18 15:32:07 -07:00
"g.fc.run/ai/byre/internal/api"
"g.fc.run/ai/byre/internal/store"
2026-10-14 19:35:34 -07:00
)
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"
2026-10-15 09:40:30 -07:00
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.`
2026-10-18 00:43:00 -07:00
func ( m * Manager ) Compact ( ctx context . Context , sessionID , model string ) ( api . AgentSession , error ) {
2026-10-15 09:40:30 -07:00
sess , err := m . store . GetSession ( ctx , sessionID )
if err != nil {
return api . AgentSession {}, err
}
2026-10-18 00:43:00 -07:00
task , err := m . taskFor ( ctx , sess )
if err != nil {
return api . AgentSession {}, err
}
2026-10-21 13:59:37 -07:00
err = m . startRunning ( ctx , sess , "byre" )
if err != nil {
return api . AgentSession {}, err
}
return m . launchCompaction ( ctx , sess , task , model )
}
func ( m * Manager ) launchCompaction ( ctx context . Context , sess api . AgentSession , task api . Task , model string ) ( api . AgentSession , error ) {
2026-10-18 00:43:00 -07:00
prompt := handoffPrompt
2026-10-15 09:40:30 -07:00
if sess . Owner != "" {
2026-10-18 00:43:00 -07:00
prompt = leadHandoffPrompt
}
m . mu . Lock ()
2026-10-21 13:59:37 -07:00
m . handoffModel [ sess . Id ] = model
2026-10-18 00:43:00 -07:00
m . mu . Unlock ()
2026-10-20 23:15:19 -07:00
return m . launch ( ctx , sess , task , "byre" , prompt , "" , "" , nil )
2026-10-18 00:43:00 -07:00
}
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
2026-10-15 09:40:30 -07:00
}
2026-10-18 00:43:00 -07:00
return model
2026-10-14 19:35:34 -07:00
}
2026-10-15 09:40:30 -07:00
func isHandoffPrompt ( actor , text string ) bool {
return actor == "byre" && ( text == handoffPrompt || text == leadHandoffPrompt )
}
2026-10-15 11:16:50 -07:00
func isContinuation ( actor , text string ) bool {
return actor == "byre" && ( strings . HasPrefix ( text , handoffPreamble ) || strings . HasPrefix ( text , leadHandoffPreamble ))
}
2026-10-14 19:35:34 -07:00
func ( m * Manager ) continueFromHandoff ( sessionID , handoff string ) {
ctx := context . Background ()
err := m . handoff ( ctx , sessionID , handoff )
2026-10-21 11:28:07 -07:00
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 )
2026-10-14 19:35:34 -07:00
}
}
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
}
2026-10-18 00:43:00 -07:00
model := m . continuationModel ( sess )
2026-10-14 19:35:34 -07:00
2026-10-16 10:08:08 -07:00
if sess . Owner != "" {
err = m . retire ( ctx , sess , "byre" )
if err != nil {
return err
}
2026-10-20 23:15:19 -07:00
_ , err = m . createLead ( ctx , creator , leadHandoffPreamble + handoff , model , "byre" , "" , "" , nil )
2026-10-14 19:35:34 -07:00
return err
}
2026-10-21 11:28:07 -07:00
if creator . GitName == "" || creator . GitEmail == "" {
return ErrNoGitIdentity
}
task , err := m . store . GetTask ( ctx , sess . TaskId )
2026-10-16 10:08:08 -07:00
if err != nil {
2026-10-15 09:40:30 -07:00
return err
}
2026-10-21 11:28:07 -07:00
next := sessionFor ( task , creator , sess . Member , model )
next . State = api . AgentSessionStateRunning
err = m . store . ReplaceSession ( ctx , sess . Id , next , task . ProjectId , "byre" )
if errors . Is ( err , store . ErrStateChanged ) {
2026-10-21 11:53:37 -07:00
return transition ( ctx , m . store , sess . Id , api . AgentSessionStateRunning , api . AgentSessionStateIdle , "byre" )
2026-10-16 10:08:08 -07:00
}
if err != nil {
return err
}
m . stopLive ( sess . Id )
2026-10-21 11:28:07 -07:00
_ , err = m . launch ( ctx , next , task , "byre" , handoffPreamble + handoff , "" , "" , nil )
2026-10-14 19:35:34 -07:00
return err
}