2026-10-13 22:30:39 -07:00
package agents
import (
"context"
"crypto/rand"
"encoding/json"
"errors"
"fmt"
"log/slog"
2026-10-15 09:40:30 -07:00
"net/url"
"os"
"path/filepath"
2026-10-14 19:35:34 -07:00
"strings"
2026-10-13 22:30:39 -07:00
"sync"
"time"
2026-10-18 15:32:07 -07:00
"g.fc.run/ai/byre/internal/api"
"g.fc.run/ai/byre/internal/claude"
"g.fc.run/ai/byre/internal/clients"
"g.fc.run/ai/byre/internal/git"
"g.fc.run/ai/byre/internal/mcp"
"g.fc.run/ai/byre/internal/store"
"g.fc.run/ai/byre/internal/terminal"
"g.fc.run/ai/byre/internal/workflow"
2026-10-20 13:32:56 -07:00
"tailscale.com/tsnet"
2026-10-13 22:30:39 -07:00
)
var (
ErrConflict = errors . New ( "conflict" )
2026-10-15 09:40:30 -07:00
ErrInvalid = errors . New ( "invalid" )
2026-10-13 22:30:39 -07:00
ErrNoGitIdentity = errors . New ( "set your git name and email (PUT /api/me) before starting agents" )
)
const agentActor = "agent"
const restartNotice = "byre restarted while you were mid-turn, so your last turn was cut off. Re-check the working tree (git status and the files you were editing) before continuing where you left off."
type Manager struct {
2026-10-18 16:05:50 -07:00
store * store . Store
worktreeRoot string
leadRoot string
handsRoot string
codexHome string
antigravityRoot string
status func ( context . Context ) ( api . Status , error )
addProject func ( ctx context . Context , name , repoPath , createdBy string ) ( api . Project , error )
clients * clients . Registry
terminals * terminal . Terminals
2026-10-20 13:32:56 -07:00
tailnet * tsnet . Server
2026-10-13 22:30:39 -07:00
2026-10-19 15:08:57 -07:00
mu sync . Mutex
live map [ string ] * live
starting map [ string ] * start
closing bool
rateLimits map [ api . AgentSessionVendor ] api . Usage
usageCommands map [ api . AgentSessionVendor ] string
leadAnswered map [ string ] bool
paneFocus map [ string ] api . PaneFocus
paneReported map [ string ] time . Time
paneMoved map [ string ] time . Time
humanMoved map [ string ] time . Time
moveRefused map [ string ] bool
startedTodo map [ string ] time . Time
startRefused map [ string ] bool
finishing map [ string ] bool
handoffModel map [ string ] string
mergeTurns map [ string ] mergeTurn
2026-10-20 11:01:59 -07:00
landing map [ string ] landing
2026-10-14 07:02:56 -07:00
2026-10-16 12:56:53 -07:00
deliverMu sync . Mutex
deliverLocks map [ string ] * sessionLock
leadMu sync . Mutex
signals chan [] api . Event
2026-10-14 22:35:53 -07:00
2026-10-14 22:52:02 -07:00
snapMu sync . Mutex
snaps map [ string ] * snapState
2026-10-16 14:20:16 -07:00
mergeMu sync . Mutex
merges map [ string ] * projectMerge
2026-10-15 12:11:35 -07:00
2026-10-15 16:33:17 -07:00
reviewMu sync . Mutex
2026-10-19 15:08:57 -07:00
usageMu map [ api . AgentSessionVendor ] * sync . Mutex
2026-10-15 22:53:54 -07:00
workflowMu sync . Mutex
2026-10-20 21:08:31 -07:00
auditMu sync . Mutex
2026-10-20 13:32:56 -07:00
previewMu sync . Mutex
previewListeners map [ int ] * previewListener
2026-10-14 22:35:53 -07:00
bad * badLines
watchStop chan struct {}
2026-10-13 22:30:39 -07:00
}
type live struct {
2026-10-15 14:55:58 -07:00
sess agentProcess
2026-10-14 07:02:56 -07:00
id string
2026-10-14 19:35:34 -07:00
dir string
model string
gitName string
gitEmail string
2026-10-14 09:40:12 -07:00
vendor api . AgentSessionVendor
2026-10-14 07:02:56 -07:00
taskID string
projectID string
2026-10-15 09:40:30 -07:00
owner string
2026-10-18 08:48:57 -07:00
member string
2026-10-16 10:50:29 -07:00
notes string
2026-10-15 16:33:17 -07:00
reviewID string
2026-10-15 22:53:54 -07:00
stage string
2026-10-14 07:02:56 -07:00
stopping bool
interrupted bool
2026-10-17 07:34:43 -07:00
fetching bool
2026-10-14 19:35:34 -07:00
asked bool
bounced bool
compacting bool
2026-10-15 09:40:30 -07:00
wantCompact bool
2026-10-14 09:40:12 -07:00
rateLimited map [ string ] any
2026-10-15 14:55:58 -07:00
lastText string
2026-10-17 08:21:11 -07:00
subagents map [ string ] bool
2026-10-15 11:36:51 -07:00
2026-10-14 16:03:46 -07:00
contextModel string
contextUsed int
contextWindow int
2026-10-15 22:18:55 -07:00
contextWarned bool
2026-10-13 22:30:39 -07:00
}
2026-10-16 12:56:53 -07:00
type start struct {
done chan struct {}
cancel context . CancelFunc
l * live
err error
stopping bool
}
2026-10-15 16:33:17 -07:00
func ( l * live ) implementer () bool {
return l . owner == "" && l . reviewID == ""
}
2026-10-20 13:32:56 -07:00
func New ( st * store . Store , worktreeRoot , leadRoot , handsRoot , codexHome , antigravityRoot string , status func ( context . Context ) ( api . Status , error ), addProject func ( ctx context . Context , name , repoPath , createdBy string ) ( api . Project , error ), clientRegistry * clients . Registry , terminals * terminal . Terminals , tailnet * tsnet . Server ) * Manager {
2026-10-15 09:40:30 -07:00
m := & Manager {
2026-10-20 13:32:56 -07:00
store : st ,
worktreeRoot : worktreeRoot ,
leadRoot : leadRoot ,
handsRoot : handsRoot ,
codexHome : codexHome ,
antigravityRoot : antigravityRoot ,
status : status ,
addProject : addProject ,
clients : clientRegistry ,
terminals : terminals ,
tailnet : tailnet ,
live : map [ string ] * live {},
starting : map [ string ] * start {},
deliverLocks : map [ string ] * sessionLock {},
rateLimits : map [ api . AgentSessionVendor ] api . Usage {},
usageCommands : map [ api . AgentSessionVendor ] string {},
usageMu : map [ api . AgentSessionVendor ] * sync . Mutex { api . Claude : {}, api . Codex : {}, api . Antigravity : {}},
leadAnswered : map [ string ] bool {},
paneFocus : map [ string ] api . PaneFocus {},
paneReported : map [ string ] time . Time {},
paneMoved : map [ string ] time . Time {},
humanMoved : map [ string ] time . Time {},
moveRefused : map [ string ] bool {},
startedTodo : map [ string ] time . Time {},
startRefused : map [ string ] bool {},
finishing : map [ string ] bool {},
handoffModel : map [ string ] string {},
mergeTurns : map [ string ] mergeTurn {},
landing : map [ string ] landing {},
signals : make ( chan [] api . Event , 1024 ),
snaps : map [ string ] * snapState {},
merges : map [ string ] * projectMerge {},
previewListeners : map [ int ] * previewListener {},
bad : newBadLines (),
watchStop : make ( chan struct {}),
2026-10-14 09:40:12 -07:00
}
2026-10-15 09:40:30 -07:00
go m . signalLoop ()
2026-10-19 15:08:57 -07:00
go m . loadUsage ()
2026-10-15 09:40:30 -07:00
return m
}
func ( m * Manager ) taskFor ( ctx context . Context , sess api . AgentSession ) ( api . Task , error ) {
if sess . Owner != "" {
return api . Task {}, nil
}
return m . store . GetTask ( ctx , sess . TaskId )
2026-10-13 22:30:39 -07:00
}
2026-10-20 15:22:50 -07:00
func ( m * Manager ) Create ( ctx context . Context , task api . Task , creator api . User , prompt , model , sendID string , images ... string ) ( api . AgentSession , error ) {
2026-10-20 15:47:29 -07:00
unlock := m . lockDelivery ( "create:" + task . Id )
defer unlock ()
ctx = context . WithoutCancel ( ctx )
prior , sent , err := m . priorSend ( ctx , sendID )
if err != nil || sent {
return prior , err
}
unused , found , err := m . store . UnusedSession ( ctx , task . Id , "" )
if err != nil {
return api . AgentSession {}, err
}
if found {
slog . Info ( "first message to an unused session" , "session" , unused , "task" , task . Id )
return m . Send ( ctx , unused , creator . Login , prompt , model , sendID , images ... )
}
2026-10-20 15:22:50 -07:00
return m . create ( ctx , task , creator , "" , prompt , model , creator . Login , sendID , images )
2026-10-14 19:35:34 -07:00
}
2026-10-20 15:22:50 -07:00
func ( m * Manager ) create ( ctx context . Context , task api . Task , creator api . User , member , prompt , model , actor , sendID string , images [] string ) ( api . AgentSession , error ) {
2026-10-13 22:30:39 -07:00
if creator . GitName == "" || creator . GitEmail == "" {
return api . AgentSession {}, ErrNoGitIdentity
}
if task . Status != api . TaskStatusActive {
return api . AgentSession {}, fmt . Errorf ( "%w: task %s is %s" , ErrConflict , task . Id , task . Status )
}
2026-10-18 08:48:57 -07:00
sess , err := m . newSession ( ctx , task , creator , member , model )
2026-10-16 10:08:08 -07:00
if err != nil {
return api . AgentSession {}, err
}
2026-10-20 15:22:50 -07:00
return m . turn ( ctx , sess , task , actor , prompt , sendID , images )
2026-10-16 10:08:08 -07:00
}
2026-10-18 08:48:57 -07:00
func ( m * Manager ) newSession ( ctx context . Context , task api . Task , creator api . User , member , model string ) ( api . AgentSession , error ) {
2026-10-13 22:30:39 -07:00
existing , err := m . store . ListSessions ( ctx , task . Id )
if err != nil {
return api . AgentSession {}, err
}
for _ , s := range existing {
2026-10-18 08:48:57 -07:00
if s . State != api . AgentSessionStateCompleted && s . Member == member {
2026-10-13 22:30:39 -07:00
return api . AgentSession {}, fmt . Errorf ( "%w: task %s already has session %s" , ErrConflict , task . Id , s . Id )
}
}
id := newUUID ()
2026-10-15 21:17:58 -07:00
vendor := workflow . VendorOf ( model )
2026-10-13 22:30:39 -07:00
sess := api . AgentSession {
Id : id ,
TaskId : task . Id ,
2026-10-15 14:55:58 -07:00
Vendor : vendor ,
2026-10-13 22:30:39 -07:00
Model : model ,
2026-10-15 14:55:58 -07:00
VendorSessionId : initialVendorSessionID ( vendor , id ),
2026-10-13 22:30:39 -07:00
State : api . AgentSessionStateIdle ,
Role : api . Implementer ,
2026-10-18 08:48:57 -07:00
Member : member ,
2026-10-13 22:30:39 -07:00
CreatedBy : creator . Login ,
CreatedAt : time . Now (). UTC (),
}
err = m . store . CreateSession ( ctx , sess , task . ProjectId )
2026-10-16 10:08:08 -07:00
return sess , err
2026-10-13 22:30:39 -07:00
}
2026-10-20 15:22:50 -07:00
func ( m * Manager ) Send ( ctx context . Context , sessionID , actor , text , model , sendID string , images ... string ) ( api . AgentSession , error ) {
2026-10-13 22:30:39 -07:00
sess , err := m . store . GetSession ( ctx , sessionID )
if err != nil {
return api . AgentSession {}, err
}
if sess . State != api . AgentSessionStateIdle {
return api . AgentSession {}, fmt . Errorf ( "%w: session %s is %s" , ErrConflict , sess . Id , sess . State )
}
2026-10-14 19:35:34 -07:00
if model != "" && model != sess . Model {
2026-10-15 14:55:58 -07:00
err = checkModel ( sess , model )
if err != nil {
return api . AgentSession {}, err
}
2026-10-14 19:35:34 -07:00
err = m . store . SetSessionModel ( ctx , sess . Id , model , actor )
if err != nil {
return api . AgentSession {}, err
}
sess . Model = model
}
2026-10-15 09:40:30 -07:00
task , err := m . taskFor ( ctx , sess )
2026-10-13 22:30:39 -07:00
if err != nil {
return api . AgentSession {}, err
}
2026-10-20 15:22:50 -07:00
return m . turn ( ctx , sess , task , actor , text , sendID , images )
2026-10-13 22:30:39 -07:00
}
func ( m * Manager ) Interrupt ( ctx context . Context , sessionID string ) ( api . AgentSession , error ) {
sess , err := m . store . GetSession ( ctx , sessionID )
if err != nil {
return api . AgentSession {}, err
}
m . mu . Lock ()
l := m . live [ sessionID ]
m . mu . Unlock ()
if sess . State != api . AgentSessionStateRunning || l == nil {
return api . AgentSession {}, fmt . Errorf ( "%w: session %s has no turn running" , ErrConflict , sess . Id )
}
2026-10-14 07:02:56 -07:00
m . mu . Lock ()
l . interrupted = true
2026-10-17 07:34:43 -07:00
fetching := l . fetching
2026-10-14 07:02:56 -07:00
m . mu . Unlock ()
2026-10-17 07:34:43 -07:00
if fetching {
return sess , nil
}
2026-10-13 22:30:39 -07:00
ictx , cancel := context . WithTimeout ( ctx , 10 * time . Second )
defer cancel ()
2026-10-15 14:55:58 -07:00
err = l . sess . interrupt ( ictx )
2026-10-13 22:30:39 -07:00
if err != nil {
return api . AgentSession {}, err
}
return sess , nil
}
func ( m * Manager ) StopTask ( ctx context . Context , taskID , actor string ) error {
sessions , err := m . store . ListSessions ( ctx , taskID )
if err != nil {
return err
}
for _ , sess := range sessions {
if sess . State == api . AgentSessionStateCompleted {
continue
}
2026-10-14 19:35:34 -07:00
err = m . retire ( ctx , sess , actor )
if err != nil {
return err
2026-10-13 22:30:39 -07:00
}
2026-10-14 19:35:34 -07:00
}
2026-10-13 22:30:39 -07:00
2026-10-15 14:55:58 -07:00
return nil
2026-10-14 19:35:34 -07:00
}
2026-10-13 22:30:39 -07:00
2026-10-14 19:35:34 -07:00
func ( m * Manager ) retire ( ctx context . Context , sess api . AgentSession , actor string ) error {
2026-10-16 10:08:08 -07:00
m . stopLive ( sess . Id )
return m . store . SetSessionState ( ctx , sess . Id , api . AgentSessionStateCompleted , actor , nil )
}
func ( m * Manager ) stopLive ( sessionID string ) {
2026-10-14 19:35:34 -07:00
m . mu . Lock ()
2026-10-16 10:08:08 -07:00
l := m . live [ sessionID ]
2026-10-14 19:35:34 -07:00
if l != nil {
l . stopping = true
}
2026-10-16 12:56:53 -07:00
st := m . starting [ sessionID ]
if st != nil {
st . stopping = true
}
2026-10-14 19:35:34 -07:00
m . mu . Unlock ()
2026-10-14 09:40:12 -07:00
2026-10-14 19:35:34 -07:00
if l != nil {
2026-10-15 14:55:58 -07:00
l . sess . kill ()
2026-10-13 22:30:39 -07:00
}
}
func ( m * Manager ) Recover ( ctx context . Context ) error {
2026-10-15 12:11:35 -07:00
err := m . recoverMerges ( ctx )
if err != nil {
return err
}
2026-10-16 12:46:28 -07:00
err = m . recoverFinalizing ( ctx )
if err != nil {
return err
}
2026-10-15 22:11:25 -07:00
staged , err := m . recoverStages ( ctx )
if err != nil {
return err
}
2026-10-20 13:32:56 -07:00
err = m . recoverPreviews ( ctx )
if err != nil {
return err
}
2026-10-17 08:21:11 -07:00
stale , err := m . store . SessionsWithSubagents ( ctx )
if err != nil {
return err
}
for _ , sess := range stale {
err = m . store . SetSubagents ( ctx , sess . Id , 0 , "byre" )
if err != nil {
return err
}
}
2026-10-13 22:30:39 -07:00
sessions , err := m . store . SessionsInState ( ctx , api . AgentSessionStateRunning )
if err != nil {
return err
}
2026-10-18 08:48:57 -07:00
err = m . recoverMergeTurns ( ctx , sessions )
if err != nil {
return err
}
2026-10-13 22:30:39 -07:00
for _ , sess := range sessions {
2026-10-15 22:11:25 -07:00
if staged [ sess . Id ] {
continue
}
2026-10-15 09:40:30 -07:00
task , err := m . taskFor ( ctx , sess )
2026-10-13 22:30:39 -07:00
if err != nil {
2026-10-16 12:43:54 -07:00
slog . Error ( "resume session: load task" , "session" , sess . Id , "task" , sess . TaskId , "err" , err )
continue
2026-10-13 22:30:39 -07:00
}
2026-10-20 11:49:59 -07:00
pending , err := m . store . SessionPending ( ctx , sess . Id )
if err != nil {
return err
}
if pending {
found , err := m . deliverRecorded ( ctx , sess , task )
if err != nil {
slog . Error ( "resume session: deliver its recorded message" , "session" , sess . Id , "task" , task . Id , "err" , err )
}
if found {
continue
}
}
2026-10-14 09:40:12 -07:00
l , err := m . ensureLive ( ctx , sess , task )
2026-10-13 22:30:39 -07:00
if err != nil {
2026-10-16 12:43:54 -07:00
ferr := m . failResume ( ctx , sess , task , err )
if ferr != nil {
return ferr
2026-10-15 10:16:32 -07:00
}
2026-10-13 22:30:39 -07:00
continue
}
2026-10-16 10:08:08 -07:00
notice := restartNotice
if sess . Role == api . Implementer {
discard , err := m . store . GetDiscard ( ctx , task . Id )
if err != nil {
return err
}
if discard . State == store . DiscardRunning && discard . Session == sess . Id {
notice = discardRestartPrompt
}
}
2026-10-20 11:49:59 -07:00
err = m . message ( ctx , sess , task , l , "byre" , notice , nil )
2026-10-13 22:30:39 -07:00
if err != nil {
2026-10-16 12:43:54 -07:00
ferr := m . failResume ( ctx , sess , task , err )
if ferr != nil {
return ferr
}
continue
2026-10-13 22:30:39 -07:00
}
2026-10-15 09:40:30 -07:00
slog . Info ( "resumed session" , "session" , sess . Id , "task" , task . Id , "owner" , sess . Owner )
2026-10-15 10:20:23 -07:00
m . logUndelivered ( ctx , sess . Id , "resumed with restart notice" )
}
idle , err := m . store . SessionsInState ( ctx , api . AgentSessionStateIdle )
if err != nil {
return err
}
for _ , sess := range idle {
m . logUndelivered ( ctx , sess . Id , "idle at startup" )
2026-10-13 22:30:39 -07:00
}
2026-10-15 16:33:17 -07:00
err = m . recoverReviews ( ctx )
if err != nil {
return err
}
2026-10-16 10:08:08 -07:00
err = m . recoverDiscards ( ctx )
if err != nil {
return err
}
2026-10-15 09:40:30 -07:00
go m . deliverAllLeads ()
2026-10-15 18:33:18 -07:00
go m . deliverAllQueued ( context . Background ())
2026-10-15 12:11:35 -07:00
go m . startHeldMerges ( context . Background ())
2026-10-16 22:34:14 -07:00
go m . recoverMergeAfterReviews ( context . Background ())
2026-10-20 21:08:31 -07:00
go m . auditAll ( context . Background ())
2026-10-15 14:55:58 -07:00
return nil
2026-10-13 22:30:39 -07:00
}
2026-10-16 12:43:54 -07:00
func ( m * Manager ) failResume ( ctx context . Context , sess api . AgentSession , task api . Task , err error ) error {
slog . Error ( "resume session" , "session" , sess . Id , "err" , err )
2026-10-18 08:48:57 -07:00
m . endMergeTurn ( sess . Id , true )
2026-10-16 12:43:54 -07:00
m . logUndelivered ( ctx , sess . Id , "resume failed" )
derr := m . store . DropStageOutput ( ctx , sess . Id )
if derr != nil {
return derr
}
2026-10-16 12:46:28 -07:00
next , _ , serr := m . store . EndTurn ( ctx , sess . Id , "byre" , map [ string ] any { "error" : err . Error ()}, nil , false , false )
2026-10-16 12:43:54 -07:00
if serr != nil {
return serr
}
switch {
case next == store . DiscardClosing :
go m . finishDiscard ( task . Id )
case next == store . DiscardHeld :
go m . startDiscard ( task . Id )
2026-10-18 08:48:57 -07:00
case sess . Role == api . Implementer && m . leadsTask ( ctx , task . Id , sess . Id ):
2026-10-16 12:43:54 -07:00
m . abandonMerge ( task . Id , task . ProjectId )
}
return nil
}
2026-10-15 10:20:23 -07:00
func ( m * Manager ) logUndelivered ( ctx context . Context , sessionID , reason string ) {
pending , err := m . store . Undelivered ( ctx , sessionID )
if err != nil {
slog . Error ( "list undelivered responses" , "session" , sessionID , "err" , err )
return
}
if len ( pending ) == 0 {
return
}
slog . Info ( "undelivered responses" , "session" , sessionID , "reason" , reason , "responses" , responseIDs ( pending ))
}
2026-10-13 22:30:39 -07:00
func ( m * Manager ) Close () {
2026-10-14 22:35:53 -07:00
close ( m . watchStop )
2026-10-13 22:30:39 -07:00
m . mu . Lock ()
m . closing = true
all := [] * live {}
for _ , l := range m . live {
all = append ( all , l )
}
2026-10-16 12:56:53 -07:00
pending := [] * start {}
for _ , st := range m . starting {
pending = append ( pending , st )
}
2026-10-13 22:30:39 -07:00
m . mu . Unlock ()
2026-10-16 12:56:53 -07:00
for _ , st := range pending {
st . cancel ()
}
2026-10-13 22:30:39 -07:00
for _ , l := range all {
2026-10-15 14:55:58 -07:00
l . sess . kill ()
2026-10-13 22:30:39 -07:00
}
2026-10-16 12:56:53 -07:00
for _ , st := range pending {
<- st . done
}
2026-10-15 09:40:30 -07:00
err := m . parkLeads ( context . Background ())
if err != nil {
slog . Error ( "park lead sessions" , "err" , err )
}
2026-10-13 22:30:39 -07:00
}
func ( m * Manager ) LiveCount () int {
m . mu . Lock ()
defer m . mu . Unlock ()
return len ( m . live )
}
2026-10-14 09:40:12 -07:00
type Usage struct {
2026-10-14 21:06:30 -07:00
Running [] api . TurnRef
2026-10-19 15:08:57 -07:00
RateLimits map [ string ] api . Usage
2026-10-14 09:40:12 -07:00
}
func ( m * Manager ) Usage ( ctx context . Context ) ( Usage , error ) {
2026-10-14 21:06:30 -07:00
u := Usage {
Running : [] api . TurnRef {},
2026-10-19 15:08:57 -07:00
RateLimits : map [ string ] api . Usage {},
2026-10-14 21:06:30 -07:00
}
2026-10-14 09:40:12 -07:00
2026-10-14 21:06:30 -07:00
running , err := m . store . SessionsInState ( ctx , api . AgentSessionStateRunning )
2026-10-14 09:40:12 -07:00
if err != nil {
return u , err
}
2026-10-14 21:06:30 -07:00
for _ , sess := range running {
2026-10-15 09:40:30 -07:00
if sess . Owner != "" {
continue
}
2026-10-14 21:06:30 -07:00
u . Running = append ( u . Running , api . TurnRef { SessionId : sess . Id , TaskId : sess . TaskId , Vendor : string ( sess . Vendor )})
2026-10-14 09:40:12 -07:00
}
m . mu . Lock ()
defer m . mu . Unlock ()
for vendor , info := range m . rateLimits {
u . RateLimits [ string ( vendor )] = info
}
return u , nil
}
2026-10-20 15:22:50 -07:00
func ( m * Manager ) turn ( ctx context . Context , sess api . AgentSession , task api . Task , actor , text , sendID string , images [] string ) ( api . AgentSession , error ) {
2026-10-20 11:01:59 -07:00
err := m . startRunning ( ctx , sess , task . Id , actor )
2026-10-14 09:40:12 -07:00
if err != nil {
return api . AgentSession {}, err
}
2026-10-20 15:22:50 -07:00
return m . launch ( ctx , sess , task , actor , text , sendID , images )
2026-10-16 10:08:08 -07:00
}
2026-10-14 09:40:12 -07:00
2026-10-20 11:01:59 -07:00
func ( m * Manager ) startRunning ( ctx context . Context , sess api . AgentSession , taskID , actor string ) error {
held := fmt . Errorf ( "%w: held while this task merges and continues" , ErrConflict )
if m . heldForLanding ( taskID , sess . Id ) {
return held
}
err := transition ( ctx , m . store , sess . Id , api . AgentSessionStateIdle , api . AgentSessionStateRunning , actor )
if err != nil {
return err
}
if m . heldForLanding ( taskID , sess . Id ) {
err = transition ( ctx , m . store , sess . Id , api . AgentSessionStateRunning , api . AgentSessionStateIdle , actor )
if err != nil {
return err
}
return held
}
return nil
}
2026-10-20 15:22:50 -07:00
func ( m * Manager ) launch ( ctx context . Context , sess api . AgentSession , task api . Task , actor , text , sendID string , images [] string ) ( api . AgentSession , error ) {
imgs , compacting , err := m . recordMessage ( ctx , sess , task , actor , text , sendID , images )
2026-10-20 11:49:59 -07:00
if err != nil {
m . failStart ( ctx , sess . Id , actor , err )
return api . AgentSession {}, err
}
err = m . deliver ( ctx , sess , task , actor , text , imgs , compacting )
if err != nil {
return api . AgentSession {}, err
}
return m . store . GetSession ( ctx , sess . Id )
}
func ( m * Manager ) deliver ( ctx context . Context , sess api . AgentSession , task api . Task , actor , text string , imgs [] image , compacting bool ) error {
2026-10-14 09:40:12 -07:00
l , err := m . ensureLive ( ctx , sess , task )
2026-10-14 19:35:34 -07:00
if err == nil {
err = m . syncModel ( ctx , l , sess . Model )
}
2026-10-13 22:30:39 -07:00
if err != nil {
2026-10-16 12:36:05 -07:00
m . failStart ( ctx , sess . Id , actor , err )
2026-10-20 11:49:59 -07:00
return err
2026-10-16 12:36:05 -07:00
}
2026-10-20 11:49:59 -07:00
m . beginTurn ( l , compacting )
2026-10-20 12:20:51 -07:00
err = m . send ( ctx , l , agentText ( actor , text ), imgs )
2026-10-16 12:36:05 -07:00
if err != nil {
2026-10-20 11:49:59 -07:00
m . endMergeTurn ( sess . Id , false )
return err
2026-10-13 22:30:39 -07:00
}
2026-10-20 11:49:59 -07:00
return nil
}
2026-10-13 22:30:39 -07:00
2026-10-20 11:49:59 -07:00
func ( m * Manager ) deliverRecorded ( ctx context . Context , sess api . AgentSession , task api . Task ) ( bool , error ) {
ev , found , err := m . store . LastMessage ( ctx , sess . Id )
if err != nil || ! found {
return false , err
}
text , _ := ev . Payload [ "text" ].( string )
ids := [] string {}
raw , _ := ev . Payload [ "images" ].([] any )
for _ , id := range raw {
s , _ := id .( string )
ids = append ( ids , s )
}
imgs , err := m . readImages ( ctx , ids )
2026-10-13 22:30:39 -07:00
if err != nil {
2026-10-20 11:49:59 -07:00
m . failStart ( ctx , sess . Id , ev . Actor , err )
return true , err
2026-10-13 22:30:39 -07:00
}
2026-10-20 11:49:59 -07:00
return true , m . deliver ( ctx , sess , task , ev . Actor , text , imgs , isHandoffPrompt ( ev . Actor , text ))
}
2026-10-13 22:30:39 -07:00
2026-10-20 11:49:59 -07:00
func ( m * Manager ) readImages ( ctx context . Context , ids [] string ) ([] image , error ) {
imgs := [] image {}
for _ , id := range ids {
b , data , err := m . store . ReadBlob ( ctx , id )
if err != nil {
return nil , err
}
imgs = append ( imgs , image { mediaType : b . ContentType , data : data })
}
return imgs , nil
2026-10-13 22:30:39 -07:00
}
2026-10-16 12:36:05 -07:00
func ( m * Manager ) failStart ( ctx context . Context , sessionID , actor string , err error ) {
2026-10-18 08:48:57 -07:00
m . endMergeTurn ( sessionID , false )
2026-10-16 12:36:05 -07:00
cctx , cancel := context . WithTimeout ( context . WithoutCancel ( ctx ), 10 * time . Second )
defer cancel ()
serr := m . store . SetSessionState ( cctx , sessionID , api . AgentSessionStateIdle , actor , map [ string ] any { "error" : err . Error ()})
if serr != nil {
slog . Error ( "record failed start" , "session" , sessionID , "err" , serr )
}
}
2026-10-14 19:35:34 -07:00
func ( m * Manager ) syncModel ( ctx context . Context , l * live , model string ) error {
if model == l . model {
return nil
}
cctx , cancel := context . WithTimeout ( ctx , 10 * time . Second )
defer cancel ()
2026-10-15 14:55:58 -07:00
err := l . sess . setModel ( cctx , model )
2026-10-14 19:35:34 -07:00
if err != nil {
return fmt . Errorf ( "set model %s: %w" , model , err )
}
l . model = model
return nil
}
2026-10-14 09:40:12 -07:00
func transition ( ctx context . Context , st * store . Store , id string , from , to api . AgentSessionState , actor string ) error {
err := st . TransitionSession ( ctx , id , from , to , actor )
if errors . Is ( err , store . ErrStateChanged ) {
return fmt . Errorf ( "%w: %w" , ErrConflict , err )
}
return err
}
2026-10-20 11:49:59 -07:00
func ( m * Manager ) message ( ctx context . Context , sess api . AgentSession , task api . Task , l * live , actor , text string , images [] string ) error {
2026-10-20 15:22:50 -07:00
imgs , compacting , err := m . recordMessage ( ctx , sess , task , actor , text , "" , images )
2026-10-16 12:36:05 -07:00
if err != nil {
return err
}
2026-10-20 11:49:59 -07:00
m . beginTurn ( l , compacting )
2026-10-20 12:20:51 -07:00
return m . send ( ctx , l , agentText ( actor , text ), imgs )
2026-10-16 12:36:05 -07:00
}
2026-10-20 11:49:59 -07:00
func ( m * Manager ) send ( ctx context . Context , l * live , text string , imgs [] image ) error {
err := l . sess . send ( ctx , text , imgs )
if err != nil {
return err
2026-10-18 08:48:57 -07:00
}
2026-10-20 11:49:59 -07:00
return m . store . MarkSessionDelivered ( ctx , l . id )
}
2026-10-20 15:22:50 -07:00
func ( m * Manager ) recordMessage ( ctx context . Context , sess api . AgentSession , task api . Task , actor , text , sendID string , images [] string ) ([] image , bool , error ) {
2026-10-20 11:49:59 -07:00
implementer := sess . Role == api . Implementer
if implementer && isMergeInPrompt ( actor , text ) {
m . ownMerge ( sess . Id , task . Id , task . WorktreePath )
}
imgs , err := m . readImages ( ctx , images )
if err != nil {
return nil , false , err
2026-10-14 20:07:26 -07:00
}
2026-10-15 09:40:30 -07:00
payload := map [ string ] any { "text" : text }
2026-10-20 11:49:59 -07:00
if implementer {
head , err := git . Head ( task . WorktreePath )
2026-10-15 09:40:30 -07:00
if err != nil {
2026-10-20 11:49:59 -07:00
return nil , false , err
2026-10-15 09:40:30 -07:00
}
payload [ "head" ] = head
2026-10-14 19:35:34 -07:00
}
2026-10-14 20:07:26 -07:00
if len ( images ) > 0 {
payload [ "images" ] = images
}
2026-10-15 10:30:24 -07:00
compacting := isHandoffPrompt ( actor , text )
if compacting {
payload [ "handoff" ] = true
}
2026-10-15 11:16:50 -07:00
if isContinuation ( actor , text ) {
payload [ "continuation" ] = true
}
2026-10-20 13:04:17 -07:00
if isFindingsMessage ( actor , text ) {
payload [ "findings" ] = true
}
2026-10-20 15:22:50 -07:00
if sendID != "" {
payload [ "send_id" ] = sendID
}
2026-10-20 11:49:59 -07:00
err = m . store . RecordTurnMessage ( ctx , sess . Id , newEvent ( sess . Id , sess . Owner , task . ProjectId , task . Id , actor , "user.message" , payload ))
2026-10-13 22:30:39 -07:00
if err != nil {
2026-10-20 11:49:59 -07:00
return nil , false , err
2026-10-13 22:30:39 -07:00
}
2026-10-20 11:49:59 -07:00
return imgs , compacting , nil
}
2026-10-13 22:30:39 -07:00
2026-10-20 11:49:59 -07:00
func ( m * Manager ) beginTurn ( l * live , compacting bool ) {
2026-10-14 07:02:56 -07:00
m . mu . Lock ()
l . interrupted = false
2026-10-14 19:35:34 -07:00
l . asked = false
l . bounced = false
2026-10-15 09:40:30 -07:00
l . wantCompact = false
2026-10-15 10:30:24 -07:00
l . compacting = compacting
2026-10-14 09:40:12 -07:00
l . rateLimited = nil
2026-10-15 14:55:58 -07:00
l . lastText = ""
2026-10-14 07:02:56 -07:00
m . mu . Unlock ()
2026-10-13 22:30:39 -07:00
}
2026-10-14 09:40:12 -07:00
func ( m * Manager ) ensureLive ( ctx context . Context , sess api . AgentSession , task api . Task ) ( * live , error ) {
2026-10-13 22:30:39 -07:00
m . mu . Lock ()
if m . closing {
2026-10-16 12:56:53 -07:00
m . mu . Unlock ()
2026-10-13 22:30:39 -07:00
return nil , errors . New ( "shutting down" )
}
l := m . live [ sess . Id ]
if l != nil {
2026-10-16 12:56:53 -07:00
m . mu . Unlock ()
2026-10-13 22:30:39 -07:00
return l , nil
}
2026-10-16 12:56:53 -07:00
st := m . starting [ sess . Id ]
if st != nil {
m . mu . Unlock ()
select {
case <- st . done :
return st . l , st . err
case <- ctx . Done ():
return nil , ctx . Err ()
}
}
sctx , cancel := context . WithCancel ( ctx )
st = & start { done : make ( chan struct {}), cancel : cancel }
m . starting [ sess . Id ] = st
m . mu . Unlock ()
2026-10-13 22:30:39 -07:00
2026-10-16 12:56:53 -07:00
l , err := m . startLive ( sctx , sess , task )
cancel ()
m . mu . Lock ()
delete ( m . starting , sess . Id )
closing := m . closing
stopping := st . stopping
if err == nil && ! closing {
l . stopping = stopping
m . live [ sess . Id ] = l
}
m . mu . Unlock ()
if err == nil && closing {
go l . sess . discard ()
l . sess . kill ()
l , err = nil , errors . New ( "shutting down" )
}
st . l , st . err = l , err
close ( st . done )
if err != nil {
return nil , err
}
go m . pump ( l )
if stopping {
l . sess . kill ()
}
return l , nil
}
func ( m * Manager ) startLive ( ctx context . Context , sess api . AgentSession , task api . Task ) ( * live , error ) {
2026-10-13 22:30:39 -07:00
creator , err := m . store . GetUser ( ctx , sess . CreatedBy )
if err != nil {
return nil , err
}
2026-10-16 12:56:53 -07:00
l := & live {
2026-10-14 19:35:34 -07:00
id : sess . Id ,
dir : task . WorktreePath ,
model : sess . Model ,
gitName : creator . GitName ,
gitEmail : creator . GitEmail ,
vendor : sess . Vendor ,
taskID : task . Id ,
projectID : task . ProjectId ,
2026-10-15 09:40:30 -07:00
owner : sess . Owner ,
2026-10-18 08:48:57 -07:00
member : sess . Member ,
2026-10-17 08:21:11 -07:00
subagents : map [ string ] bool {},
2026-10-14 19:35:34 -07:00
}
2026-10-13 22:30:39 -07:00
2026-10-15 11:36:51 -07:00
g , err := m . taskGuidance ( ctx , task )
if err != nil {
return nil , err
}
2026-10-20 20:22:54 -07:00
tools := append ([] mcp . Tool { m . askTool ( l ), m . cancelQuestionTool ( l ), m . setTaskTitleTool ( l ), m . setMergeTitleTool ( l ), m . stopMergeTool ( l ), m . mergeToMainTool ( l ), m . setTaskSummaryTool ( l ), m . searchClosedTool ( l ), m . exposePortTool ( l ), m . takeSnapshotTool ( l ), m . terminalReadTool ( l ), m . terminalTypeTool ( l ), m . bashTool ( l ), m . readKnowledgeTool ()}, append ( append ( m . fileTools ( l ), m . subtaskTools ( l ) ... ), m . clientTools ( l ) ... ) ... )
2026-10-18 08:48:57 -07:00
lead := true
2026-10-16 13:04:35 -07:00
if sess . Role == api . Implementer {
g += longTitleGuidance ( task )
2026-10-16 23:12:14 -07:00
notes , err := m . notesText ( ctx , task . CreatedBy , task . ProjectId )
if err != nil {
return nil , err
}
l . notes = "\n\n" + taskNotesGuidance + "\n\n" + notes
2026-10-20 08:51:32 -07:00
tools = append ( tools , m . suggestNoteTool ( l ), m . readyForMergeTool ( l ), m . suggestDiscardTool ( l ), m . cancelReadyTool ( l ), m . respondToFindingTool ( l ))
2026-10-19 21:40:24 -07:00
if sess . Vendor == api . Claude {
tools = append ( tools , m . forkTool ( l ))
}
2026-10-16 13:04:35 -07:00
}
2026-10-18 08:48:57 -07:00
if sess . Role == api . Implementer && sess . Member != "" {
sessions , err := m . store . ListSessions ( ctx , task . Id )
if err != nil {
return nil , err
}
lead = sess . Member == teamLead ( sessions )
g += teamGuidance ( sessions , sess . Member , task . WorkflowStage != nil )
tools = append ( tools , m . boardTools ( l ) ... )
}
if sess . Role == api . Implementer && task . WorkflowStage != nil && lead {
2026-10-15 22:11:25 -07:00
frozen , err := m . store . TaskWorkflow ( ctx , task . Id )
if err != nil {
return nil , err
}
2026-10-15 22:53:54 -07:00
l . stage = * task . WorkflowStage
2026-10-15 22:11:25 -07:00
g += stageGuidance ( frozen , l . stage )
tools = append ( tools , m . finishStageTool ( l ))
}
2026-10-15 14:55:58 -07:00
env := portEnv ( task )
2026-10-15 19:02:33 -07:00
stdio := [] mcp . Stdio {}
2026-10-17 22:43:38 -07:00
if sess . Role == api . Implementer {
2026-10-18 00:39:04 -07:00
media := MediaDir ( task . WorktreePath )
err = os . MkdirAll ( media , 0 o755 )
if err != nil {
return nil , err
}
g += screenshotGuidance
2026-10-18 09:20:01 -07:00
hands , err := m . handsMCP ( sess . Id , media )
if err != nil {
return nil , err
}
stdio = [] mcp . Stdio { hands , playwrightMCP ( media )}
2026-10-17 22:43:38 -07:00
}
2026-10-15 16:33:17 -07:00
if sess . Role == api . Reviewer {
r , err := m . store . ReviewBySession ( ctx , sess . Id )
if err != nil {
return nil , err
}
l . reviewID = r . Id
l . dir = m . reviewDir ( task . ProjectId , r . Id )
g = reviewGuidance
tools = [] mcp . Tool { m . submitReviewTool ( l )}
env = nil
2026-10-15 19:02:33 -07:00
stdio = [] mcp . Stdio { playwrightMCP ( m . reviewMediaDir ( task . ProjectId , r . Id ))}
2026-10-15 16:33:17 -07:00
}
2026-10-15 09:40:30 -07:00
if l . owner != "" {
l . dir = filepath . Join ( m . leadRoot , url . PathEscape ( l . owner ))
err = os . MkdirAll ( l . dir , 0 o755 )
if err != nil {
return nil , err
}
g = leadGuidance
2026-10-16 23:12:14 -07:00
notes , err := m . notesText ( ctx , l . owner , "" )
2026-10-16 10:50:29 -07:00
if err != nil {
return nil , err
}
l . notes = "\n\n" + notes
2026-10-19 15:08:57 -07:00
tools = append ( m . leadTools ( l ), m . readKnowledgeTool ())
2026-10-15 14:55:58 -07:00
env = nil
2026-10-13 22:30:39 -07:00
}
2026-10-15 14:55:58 -07:00
servers := [] * mcp . Server {{ Name : "byre" , Version : "0.0.0" , Tools : tools }}
2026-10-13 22:30:39 -07:00
sctx , cancel := context . WithTimeout ( ctx , 60 * time . Second )
defer cancel ()
2026-10-15 14:55:58 -07:00
switch l . vendor {
case api . Codex :
2026-10-15 19:02:33 -07:00
l . sess , err = m . startCodex ( sctx , sess , l , g , servers , stdio , env )
2026-10-18 16:05:50 -07:00
case api . Antigravity :
l . sess , err = m . startAntigravity ( sctx , sess , l , g , servers , stdio , env )
2026-10-15 14:55:58 -07:00
default :
2026-10-15 19:02:33 -07:00
l . sess , err = m . startClaude ( sctx , sess , l , g , servers , stdio , env )
2026-10-15 14:55:58 -07:00
}
2026-10-13 22:30:39 -07:00
if err != nil {
2026-10-15 14:55:58 -07:00
return nil , err
2026-10-13 22:30:39 -07:00
}
return l , nil
}
func ( m * Manager ) pump ( l * live ) {
ctx := context . Background ()
2026-10-15 14:55:58 -07:00
l . sess . drive ( m , l )
2026-10-13 22:30:39 -07:00
2026-10-18 09:20:01 -07:00
err := os . RemoveAll ( m . handsDir ( l . id ))
if err != nil {
slog . Error ( "remove session's byre-hands" , "session" , l . id , "err" , err )
}
2026-10-13 22:30:39 -07:00
m . mu . Lock ()
delete ( m . live , l . id )
2026-10-17 08:21:11 -07:00
closing := m . closing
quiet := closing || l . stopping
2026-10-13 22:30:39 -07:00
m . mu . Unlock ()
2026-10-17 08:21:11 -07:00
if len ( l . subagents ) > 0 && ! closing {
err := m . store . SetSubagents ( ctx , l . id , 0 , "byre" )
if err != nil {
slog . Error ( "clear exited session's subagents" , "session" , l . id , "err" , err )
}
}
2026-10-13 22:30:39 -07:00
if quiet {
2026-10-15 10:20:23 -07:00
m . logUndelivered ( ctx , l . id , "agent process ended while closing or stopping" )
2026-10-13 22:30:39 -07:00
return
}
sess , err := m . store . GetSession ( ctx , l . id )
if err != nil {
slog . Error ( "load exited session" , "session" , l . id , "err" , err )
return
}
if sess . State != api . AgentSessionStateRunning {
return
}
2026-10-15 10:20:23 -07:00
m . logUndelivered ( ctx , l . id , "agent process exited mid-turn" )
2026-10-15 22:53:54 -07:00
if l . stage != "" {
2026-10-15 22:11:25 -07:00
err = m . store . DropStageOutput ( ctx , l . id )
if err != nil {
slog . Error ( "drop stage output" , "session" , l . id , "err" , err )
}
}
2026-10-18 08:48:57 -07:00
m . endMergeTurn ( l . id , true )
2026-10-20 09:33:36 -07:00
next , _ , err := m . store . EndTurn ( ctx , l . id , agentActor , map [ string ] any { "error" : "agent process exited mid-turn" , "stopped" : true }, nil , false , false )
2026-10-13 22:30:39 -07:00
if err != nil {
slog . Error ( "record session exit" , "session" , l . id , "err" , err )
}
2026-10-15 16:33:17 -07:00
if l . reviewID != "" {
m . finishReviewer ( l , "the reviewer's process exited mid-turn" )
return
}
2026-10-16 10:08:08 -07:00
switch next {
case store . DiscardClosing :
m . finishDiscard ( l . taskID )
return
case store . DiscardHeld :
m . startDiscard ( l . taskID )
return
}
2026-10-15 09:40:30 -07:00
if l . owner == "" {
2026-10-18 08:48:57 -07:00
if m . leadsTask ( ctx , l . taskID , l . id ) {
m . abandonMerge ( l . taskID , l . projectID )
}
2026-10-18 15:06:06 -07:00
m . recordError ( l , "The agent process exited mid-turn. Send a message to resume it." )
2026-10-15 09:40:30 -07:00
}
2026-10-13 22:30:39 -07:00
}
func ( m * Manager ) handle ( ctx context . Context , l * live , ev claude . Event ) error {
switch ev . Type {
case "assistant" :
blocks , err := ev . Blocks ()
if err != nil {
return err
}
2026-10-15 11:08:53 -07:00
parent , err := ev . ParentToolUseID ()
if err != nil {
return err
}
2026-10-15 11:36:51 -07:00
uuid , err := ev . UUID ()
if err != nil {
return err
}
2026-10-13 22:30:39 -07:00
for _ , b := range blocks {
2026-10-15 11:36:51 -07:00
err = m . handleAssistantBlock ( ctx , l , b , parent , uuid )
2026-10-13 22:30:39 -07:00
if err != nil {
return err
}
}
2026-10-15 11:08:53 -07:00
if parent != "" {
return nil
}
2026-10-14 16:03:46 -07:00
usage , err := ev . ContextUsage ()
if err != nil {
return err
}
if usage . Tokens == 0 {
return nil
}
l . contextModel = usage . Model
return m . updateContext ( ctx , l , usage . Tokens , l . contextWindow )
2026-10-13 22:30:39 -07:00
case "user" :
blocks , err := ev . Blocks ()
if err != nil {
return err
}
2026-10-15 11:08:53 -07:00
parent , err := ev . ParentToolUseID ()
if err != nil {
return err
}
2026-10-15 11:36:51 -07:00
uuid , err := ev . UUID ()
if err != nil {
return err
}
2026-10-13 22:30:39 -07:00
for _ , b := range blocks {
if b . Type != "tool_result" {
continue
}
content , err := decode ( b . Content )
if err != nil {
return err
}
2026-10-15 11:08:53 -07:00
err = m . emit ( ctx , l , agentActor , "agent.tool_result" , withParent ( map [ string ] any {
2026-10-13 22:30:39 -07:00
"tool_use_id" : b . ToolUseID ,
"is_error" : b . IsError ,
"content" : content ,
2026-10-15 11:36:51 -07:00
}, parent , uuid ))
2026-10-13 22:30:39 -07:00
if err != nil {
return err
}
}
return nil
case "result" :
res , err := ev . Result ()
if err != nil {
return err
}
2026-10-16 09:16:26 -07:00
m . mu . Lock ()
interrupted := l . interrupted
m . mu . Unlock ()
if res . NumTurns == 0 && ! res . IsError && res . Result == "" && ! interrupted {
slog . Info ( "ignored result with no model turns" , "session" , l . id , "subtype" , res . Subtype , "duration_ms" , res . DurationMS )
break
}
2026-10-15 14:55:58 -07:00
return m . turnEnded ( ctx , l , endResult {
subtype : res . Subtype ,
isError : res . IsError ,
result : res . Result ,
payload : store . ToPayload ( res ),
window : res . ModelUsage [ l . contextModel ]. ContextWindow ,
})
2026-10-13 22:30:39 -07:00
2026-10-14 19:35:34 -07:00
case "stream_event" :
text , err := ev . TextDelta ()
if err != nil || text == "" {
return err
}
2026-10-15 09:40:30 -07:00
m . store . Broadcast ( m . event ( l , agentActor , "agent.delta" , map [ string ] any { "text" : text }))
2026-10-14 19:35:34 -07:00
return nil
2026-10-14 09:40:12 -07:00
case "rate_limit_event" :
info := struct {
RateLimitInfo map [ string ] any `json:"rate_limit_info"`
}{}
err := json . Unmarshal ( ev . Raw , & info )
if err != nil {
return err
}
m . mu . Lock ()
if info . RateLimitInfo [ "status" ] == "rejected" {
l . rateLimited = info . RateLimitInfo
}
m . mu . Unlock ()
2026-10-19 15:08:57 -07:00
m . setVendorUsage ( l . vendor , claudeUsage ( info . RateLimitInfo ))
2026-10-14 09:40:12 -07:00
2026-10-13 22:30:39 -07:00
case "system" :
if ev . Subtype == "init" {
return nil
}
}
raw := map [ string ] any {}
err := json . Unmarshal ( ev . Raw , & raw )
if err != nil {
return err
}
2026-10-17 08:21:11 -07:00
err = m . emit ( ctx , l , agentActor , "agent.vendor" , raw )
if err != nil {
return err
}
return m . trackSubagents ( ctx , l , raw )
}
func ( m * Manager ) trackSubagents ( ctx context . Context , l * live , raw map [ string ] any ) error {
if raw [ "type" ] != "system" {
return nil
}
id , _ := raw [ "task_id" ].( string )
n := len ( l . subagents )
switch raw [ "subtype" ] {
case "task_started" :
if raw [ "task_type" ] == "local_agent" {
l . subagents [ id ] = true
}
case "task_notification" :
delete ( l . subagents , id )
}
if len ( l . subagents ) == n {
return nil
}
return m . store . SetSubagents ( ctx , l . id , len ( l . subagents ), agentActor )
2026-10-13 22:30:39 -07:00
}
2026-10-15 14:55:58 -07:00
type endResult struct {
subtype string
isError bool
result string
payload map [ string ] any
window int
}
func ( m * Manager ) turnEnded ( ctx context . Context , l * live , res endResult ) error {
2026-10-19 15:08:57 -07:00
go m . readUsage ( l . vendor )
2026-10-15 14:55:58 -07:00
if l . owner != "" {
return m . leadTurnEnded ( ctx , l , res )
}
2026-10-15 16:33:17 -07:00
if l . reviewID != "" {
return m . reviewTurnEnded ( ctx , l , res )
}
2026-10-15 14:55:58 -07:00
2026-10-20 11:01:59 -07:00
defer m . endLanding ( l . taskID , l . id )
2026-10-15 14:55:58 -07:00
m . mu . Lock ()
interrupted := l . interrupted
rateLimited := l . rateLimited
compacting := l . compacting
2026-10-16 08:35:49 -07:00
asked := l . asked
2026-10-15 14:55:58 -07:00
m . mu . Unlock ()
finished := ! res . isError && ! interrupted && strings . TrimSpace ( res . result ) != ""
payload := res . payload
2026-10-18 08:48:57 -07:00
if l . member != "" {
payload [ "member" ] = l . member
}
owner := m . ownsTask ( l . id )
keep := false
if owner {
defer func () {
if ! keep {
m . endMergeTurn ( l . id , false )
}
}()
2026-10-20 11:01:59 -07:00
}
if ( owner || m . landingBy ( l . taskID , l . id ) != "" ) && ! finished {
err := git . AbortMerge ( l . dir )
if err != nil {
slog . Error ( "abort unfinished merge" , "session" , l . id , "err" , err )
2026-10-18 08:48:57 -07:00
}
}
unlock := func () {}
if ! owner {
locked := m . lockTask ( l . taskID )
once := sync . OnceFunc ( locked )
unlock = once
defer once ()
task , err := m . store . GetTask ( ctx , l . taskID )
if err != nil {
return err
}
if task . Status != api . TaskStatusActive {
return nil
}
}
2026-10-15 14:55:58 -07:00
sha , err := git . CommitAll ( l . dir , commitMessage ( res ), l . gitName , l . gitEmail )
if err != nil {
slog . Error ( "commit turn" , "session" , l . id , "err" , err )
2026-10-18 15:06:06 -07:00
m . recordError ( l , fmt . Sprintf ( "byre couldn't commit the turn's work, so the turn counts as unfinished: %v" , err ))
2026-10-16 12:49:45 -07:00
finished = false
2026-10-18 08:48:57 -07:00
if owner {
err = git . AbortMerge ( l . dir )
if err != nil {
slog . Error ( "abort unfinished merge" , "session" , l . id , "err" , err )
}
}
2026-10-15 14:55:58 -07:00
}
2026-10-16 12:49:45 -07:00
handoff := compacting && finished
2026-10-15 14:55:58 -07:00
if sha != "" {
payload [ "commit" ] = sha
}
2026-10-16 10:08:08 -07:00
discard , err := m . store . GetDiscard ( ctx , l . taskID )
if err != nil {
return err
}
2026-10-15 20:24:15 -07:00
mergingBy := ""
if finished && ! handoff {
mergingBy , err = m . store . MergingBy ( ctx , l . taskID )
if err != nil {
return err
}
}
2026-10-16 11:15:32 -07:00
snapshot := discard . State == store . DiscardNone
review := snapshot && mergingBy == ""
2026-10-16 11:28:48 -07:00
switch {
case handoff :
payload [ "handoff" ] = true
case finished && ! compacting && ! asked && review :
payload [ "summary" ] = true
}
2026-10-16 11:15:32 -07:00
go m . closeMergedSubtasks ( l . taskID , l . dir )
if payload [ "summary" ] == true && snapshot {
go m . reportToParent ( l . taskID , res . result )
2026-10-16 10:08:08 -07:00
}
2026-10-15 14:55:58 -07:00
if res . window > 0 {
err = m . updateContext ( ctx , l , l . contextUsed , res . window )
if err != nil {
return err
}
}
failure := fmt . Sprintf ( "The turn ended with an error (%s): %s" , res . subtype , res . result )
switch {
case ! res . isError || interrupted :
case rateLimited != nil :
2026-10-18 15:06:06 -07:00
m . recordRateLimited ( l , rateLimited , failure )
2026-10-15 14:55:58 -07:00
default :
2026-10-18 15:06:06 -07:00
m . recordError ( l , failure )
2026-10-15 14:55:58 -07:00
}
2026-10-15 22:11:25 -07:00
stageFinished := false
2026-10-16 10:08:08 -07:00
if l . stage != "" {
if finished && ! handoff && mergingBy == "" {
stageFinished , err = m . store . FinishStage ( ctx , l . id )
}
if err == nil && ! stageFinished {
err = m . store . DropStageOutput ( ctx , l . id )
}
2026-10-18 08:48:57 -07:00
if err == nil && stageFinished {
err = m . recordStage ( ctx , l )
}
2026-10-16 10:08:08 -07:00
if err != nil {
return err
}
2026-10-15 22:11:25 -07:00
}
2026-10-18 08:48:57 -07:00
unlock ()
2026-10-15 22:11:25 -07:00
2026-10-18 15:23:42 -07:00
held := len ( l . subagents ) > 0 || interrupted
2026-10-20 09:33:36 -07:00
var state map [ string ] any
if interrupted || res . isError {
state = map [ string ] any { "stopped" : true }
}
2026-10-16 11:15:32 -07:00
turn := m . event ( l , agentActor , "turn.ended" , payload )
2026-10-20 09:33:36 -07:00
next , finalizeBy , err := m . store . EndTurn ( ctx , l . id , agentActor , state , & turn , review && ! held , finished && ! handoff )
2026-10-15 14:55:58 -07:00
if err != nil {
return err
}
2026-10-17 10:37:46 -07:00
if snapshot && ! held {
2026-10-17 07:43:33 -07:00
m . snapshotSoon ( liveTarget ( l ), api . SnapshotTriggerTurnEnd , 0 , review , false )
2026-10-16 11:15:32 -07:00
}
2026-10-15 14:55:58 -07:00
2026-10-16 10:08:08 -07:00
switch next {
case store . DiscardClosing :
m . logUndelivered ( ctx , l . id , "turn ended in discard" )
go m . finishDiscard ( l . taskID )
return nil
case store . DiscardHeld :
go m . nextMerge ( l . projectID )
if stageFinished {
go m . advanceStage ( l )
return nil
}
go m . startDiscard ( l . taskID )
return nil
}
2026-10-15 14:55:58 -07:00
if handoff {
m . logUndelivered ( ctx , l . id , "turn ended in handoff" )
go m . continueFromHandoff ( l . id , res . result )
return nil
}
2026-10-16 12:46:28 -07:00
if finalizeBy != "" {
2026-10-15 14:55:58 -07:00
m . logUndelivered ( ctx , l . id , "turn ended in merge" )
2026-10-18 08:48:57 -07:00
keep = owner
2026-10-16 12:46:28 -07:00
go m . finishMerge ( l , finalizeBy )
2026-10-15 14:55:58 -07:00
return nil
2026-10-16 12:46:28 -07:00
}
2026-10-18 08:48:57 -07:00
mergeBy := ""
if m . leadsTask ( ctx , l . taskID , l . id ) {
mergeBy = m . takeMerge ( l . taskID )
}
2026-10-16 12:46:28 -07:00
switch {
2026-10-15 14:55:58 -07:00
case mergeBy != "" :
go m . releaseMerge ( l . projectID , l . taskID )
2026-10-16 09:16:26 -07:00
if ! res . isError || interrupted {
2026-10-18 15:06:06 -07:00
m . recordError ( l , "The Merge turn ended unfinished (interrupted, or with no final message), so byre didn't squash or push the task. Merge again to retry." )
2026-10-16 09:16:26 -07:00
}
2026-10-15 14:55:58 -07:00
default :
go m . nextMerge ( l . projectID )
}
2026-10-15 22:11:25 -07:00
if stageFinished {
go m . advanceStage ( l )
2026-10-15 22:53:54 -07:00
return nil
2026-10-15 22:11:25 -07:00
}
2026-10-16 14:21:23 -07:00
go m . afterTurn ( context . WithoutCancel ( ctx ), l )
2026-10-16 10:08:08 -07:00
return nil
}
func ( m * Manager ) afterTurn ( ctx context . Context , l * live ) {
discard , err := m . store . GetDiscard ( ctx , l . taskID )
if err != nil {
slog . Error ( "read discard state" , "task" , l . taskID , "err" , err )
return
}
if discard . State == store . DiscardHeld {
m . startDiscard ( l . taskID )
return
}
2026-10-19 14:08:56 -07:00
m . deliverTeam ( ctx , l )
if m . interrupted ( l . id ) {
m . logUndelivered ( ctx , l . id , "turn was interrupted" )
return
}
2026-10-15 14:55:58 -07:00
err = m . Deliver ( ctx , l . id )
if err != nil {
slog . Error ( "deliver inbox responses" , "session" , l . id , "err" , err )
}
}
func commitMessage ( res endResult ) string {
line , _ , _ := strings . Cut ( strings . TrimSpace ( res . result ), "\n" )
2026-10-14 19:35:34 -07:00
sentence , _ , found := strings . Cut ( line , ". " )
if found {
sentence += "."
}
sentence = strings . TrimSpace ( strings . ReplaceAll ( sentence , "**" , "" ))
if sentence == "" {
2026-10-15 14:55:58 -07:00
return fmt . Sprintf ( "Work from a turn that ended without a summary (%s)" , res . subtype )
2026-10-14 19:35:34 -07:00
}
runes := [] rune ( sentence )
if len ( runes ) > 100 {
return string ( runes [: 99 ]) + "…"
}
return sentence
}
2026-10-14 16:03:46 -07:00
func ( m * Manager ) updateContext ( ctx context . Context , l * live , used , window int ) error {
if used == l . contextUsed && window == l . contextWindow {
return nil
}
l . contextUsed = used
l . contextWindow = window
return m . emit ( ctx , l , agentActor , "session.context" , map [ string ] any { "model" : l . contextModel , "used" : used , "window" : window })
}
2026-10-15 11:36:51 -07:00
func ( m * Manager ) handleAssistantBlock ( ctx context . Context , l * live , b claude . Block , parent , uuid string ) error {
2026-10-13 22:30:39 -07:00
switch b . Type {
case "text" :
2026-10-15 11:36:51 -07:00
return m . emit ( ctx , l , agentActor , "agent.message" , withParent ( map [ string ] any { "text" : b . Text }, parent , uuid ))
2026-10-13 22:30:39 -07:00
case "thinking" :
if b . Thinking == "" {
return nil
}
2026-10-15 11:36:51 -07:00
return m . emit ( ctx , l , agentActor , "agent.thinking" , withParent ( map [ string ] any { "text" : b . Thinking }, parent , uuid ))
2026-10-13 22:30:39 -07:00
case "tool_use" :
input , err := decode ( b . Input )
if err != nil {
return err
}
2026-10-15 11:08:53 -07:00
return m . emit ( ctx , l , agentActor , "agent.tool_call" , withParent ( map [ string ] any {
2026-10-13 22:30:39 -07:00
"id" : b . ID ,
"name" : b . Name ,
"input" : input ,
2026-10-15 11:36:51 -07:00
}, parent , uuid ))
2026-10-13 22:30:39 -07:00
}
2026-10-15 11:36:51 -07:00
return m . emit ( ctx , l , agentActor , "agent.vendor" , withParent ( store . ToPayload ( b ), parent , uuid ))
2026-10-15 11:08:53 -07:00
}
2026-10-15 11:36:51 -07:00
func withParent ( payload map [ string ] any , parent , uuid string ) map [ string ] any {
2026-10-15 11:08:53 -07:00
if parent != "" {
payload [ "parent_tool_use_id" ] = parent
2026-10-15 11:36:51 -07:00
return payload
}
if uuid != "" {
payload [ "vendor_uuid" ] = uuid
2026-10-15 11:08:53 -07:00
}
return payload
2026-10-13 22:30:39 -07:00
}
func ( m * Manager ) emit ( ctx context . Context , l * live , actor , kind string , payload map [ string ] any ) error {
2026-10-15 09:40:30 -07:00
return m . store . AppendEvent ( ctx , m . event ( l , actor , kind , payload ))
}
func ( m * Manager ) event ( l * live , actor , kind string , payload map [ string ] any ) api . Event {
2026-10-20 11:49:59 -07:00
return newEvent ( l . id , l . owner , l . projectID , l . taskID , actor , kind , payload )
}
func newEvent ( sessionID , owner , projectID , taskID , actor , kind string , payload map [ string ] any ) api . Event {
2026-10-15 09:40:30 -07:00
ev := api . Event {
2026-10-13 22:30:39 -07:00
Kind : kind ,
2026-10-20 11:49:59 -07:00
AgentSessionId : & sessionID ,
2026-10-13 22:30:39 -07:00
Actor : actor ,
Payload : payload ,
2026-10-15 09:40:30 -07:00
}
2026-10-20 11:49:59 -07:00
if owner != "" {
ev . Owner = & owner
2026-10-15 09:40:30 -07:00
return ev
}
2026-10-20 11:49:59 -07:00
ev . ProjectId = & projectID
ev . TaskId = & taskID
2026-10-15 09:40:30 -07:00
return ev
2026-10-13 22:30:39 -07:00
}
func decode ( raw json . RawMessage ) ( any , error ) {
if len ( raw ) == 0 {
return nil , nil
}
var v any
err := json . Unmarshal ( raw , & v )
return v , err
}
func newUUID () string {
b := make ([] byte , 16 )
rand . Read ( b )
b [ 6 ] = b [ 6 ] & 0x0f | 0x40
b [ 8 ] = b [ 8 ] & 0x3f | 0x80
return fmt . Sprintf ( "%x-%x-%x-%x-%x" , b [ 0 : 4 ], b [ 4 : 6 ], b [ 6 : 8 ], b [ 8 : 10 ], b [ 10 :])
}