okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
4434577
main
1// Package app owns daemon-backed workflows without terminal interaction or rendering. 2package app 3 4import ( 5"context" 6"errors" 7"fmt" 8"net/http" 9"path/filepath" 10"regexp" 11"slices" 12"strings" 13 14"albedo/cli/internal/daemon" 15) 16 17// Service obtains connections only when an operation needs one. 18type Service struct { 19Connect func (context.Context ) (* daemon.Connection ,error ) 20Existing func (context.Context ) (* daemon.Connection ,error ) 21} 22type OpenOptions struct { 23SessionID ,Workspace string 24Fresh ,Terminal bool 25} 26type PreparedOpen struct { 27Connection * daemon.Connection 28Settings daemon.Settings 29Selected * daemon.Session 30Workspace string 31Sessions []daemon.Session 32LoginRequired bool 33} 34 35// absoluteWorkspace resolves a local workspace against the current 36// directory; a location on another host is the daemon's to read. 37func absoluteWorkspace (workspace string )string { 38if host ,_ := daemon .SplitLocation (workspace );host != "" { 39return workspace 40 } 41if abs ,err := filepath .Abs (workspace );err == nil { 42return abs 43 } 44return workspace 45} 46 47func (s * Service )PrepareOpen (ctx context.Context ,options OpenOptions ) (PreparedOpen ,error ) { 48id ,workspace ,fresh := options .SessionID ,options .Workspace ,options .Fresh 49absWorkspace := absoluteWorkspace (workspace ) 50 51conn ,err := s .Connect (ctx ) 52if err != nil { 53return PreparedOpen {},err 54 } 55 56sessions ,err := daemon .ListSessions (ctx ,conn ) 57if err != nil { 58return PreparedOpen {},err 59 } 60 61selectedID := "" 62if id != "" { 63resolved ,matchErr := resolveSessionID (id ,sessions ) 64if matchErr != nil { 65return PreparedOpen {},matchErr 66 } 67selectedID = resolved 68 } 69 70settings ,err := daemon .GetSettings (ctx ,conn ) 71if err != nil { 72return PreparedOpen {},err 73 } 74configured := settings .Profiles .Active != "" 75 76if ! configured && ! options .Terminal { 77return PreparedOpen {},errors .New ("no model provider is configured; run albedo login in a terminal to set one up" ) 78 } 79 80var initial * daemon.Session 81if selectedID != "" { 82captured ,err := daemon .GetSession (ctx ,conn ,selectedID ) 83if err != nil { 84return PreparedOpen {},sessionLookupError (id ,err ) 85 } 86initial = & captured 87 }else if configured && (fresh || len (sessions )== 0 ) { 88created ,createErr := daemon .CreateSession (ctx ,conn , daemon.CreateSessionRequest {Workspace :absWorkspace }) 89if createErr != nil { 90return PreparedOpen {},createErr 91 } 92initial = & created 93 } 94 95if initial != nil { 96if i := slices .IndexFunc (sessions ,func (s daemon.Session )bool {return s .ID == initial .ID });i >= 0 { 97sessions [i ]= * initial 98 }else { 99sessions = append ([]daemon.Session {* initial },sessions ... ) 100 } 101 } 102return PreparedOpen {Connection :conn ,Settings :settings ,Sessions :sessions ,Selected :initial ,Workspace :absWorkspace ,LoginRequired :! configured },nil 103} 104 105func (s * Service )Sessions (ctx context.Context ) ([]daemon.Session ,error ) { 106conn ,err := s .Connect (ctx ) 107if err != nil { 108return nil ,err 109 } 110sessions ,err := daemon .ListSessions (ctx ,conn ) 111if err != nil { 112return nil ,err 113 } 114return slices .DeleteFunc (sessions ,func (session daemon.Session )bool {return session .Archived }),nil 115} 116 117// resolveSessionID addresses full IDs directly and expands prefixes against roots. 118func resolveSessionID (id string ,sessions []daemon.Session ) (string ,error ) { 119if fullSessionID .MatchString (id ) { 120return id ,nil 121 } 122selectedIndex ,matchCount := - 1 ,0 123for i := range sessions { 124if sessions [i ].ID == id { 125return sessions [i ].ID ,nil 126 } 127if strings .HasPrefix (sessions [i ].ID ,id ) { 128selectedIndex = i 129matchCount ++ 130 } 131 } 132switch matchCount { 133case 0 : 134return "" ,noSessionMatch (id ) 135case 1 : 136return sessions [selectedIndex ].ID ,nil 137default : 138return "" ,fmt .Errorf ("more than one session ID starts with %q; use a longer ID or run albedo sessions to find it" ,id ) 139 } 140} 141 142// The UUIDv7 shape in the daemon's operation validator and OpenAPI schema. 143var fullSessionID = regexp .MustCompile (`^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$` ) 144 145func noSessionMatch (id string )error { 146return fmt .Errorf ("no session matches %q; run albedo sessions to see the available sessions" ,id ) 147} 148 149func sessionLookupError (id string ,err error )error { 150if problem ,ok := errors.AsType [* daemon.APIError ](err );ok && problem .StatusCode == http .StatusNotFound && fullSessionID .MatchString (id ) { 151return noSessionMatch (id ) 152 } 153return err 154} 155 156// resolveSession validates full IDs directly and lists roots only for prefixes. 157func resolveSession (ctx context.Context ,conn * daemon.Connection ,id string ) (string ,error ) { 158var sessions []daemon.Session 159if ! fullSessionID .MatchString (id ) { 160var err error 161sessions ,err = daemon .ListSessions (ctx ,conn ) 162if err != nil { 163return "" ,err 164 } 165 } 166resolved ,err := resolveSessionID (id ,sessions ) 167if err != nil { 168return "" ,err 169 } 170if fullSessionID .MatchString (id ) { 171configuration ,err := daemon .GetSessionConfiguration (ctx ,conn ,resolved ) 172return configuration .Value .ID ,sessionLookupError (id ,err ) 173 } 174return resolved ,nil 175} 176 177func (s * Service )Send (ctx context.Context ,id ,prompt string ) (daemon.SendResult ,error ) { 178conn ,err := s .Connect (ctx ) 179if err != nil { 180return daemon.SendResult {},err 181 } 182id ,err = resolveSession (ctx ,conn ,id ) 183if err != nil { 184return daemon.SendResult {},err 185 } 186return daemon .Submit (ctx ,conn ,id , daemon.SubmissionRequest {Content :& prompt }) 187} 188 189type InterruptionResult struct { 190Interrupted bool `json:"interrupted"` 191} 192 193func (s * Service )Stop (ctx context.Context ,id string ) (InterruptionResult ,error ) { 194conn ,err := s .Connect (ctx ) 195if err != nil { 196return InterruptionResult {},err 197 } 198id ,err = resolveSession (ctx ,conn ,id ) 199if err != nil { 200return InterruptionResult {},err 201 } 202interrupted ,err := daemon .InterruptSession (ctx ,conn ,id ) 203return InterruptionResult {Interrupted :interrupted },err 204} 205func (s * Service )DeleteSessions (ctx context.Context ,ids []string )error { 206conn ,err := s .Connect (ctx ) 207if err != nil { 208return err 209 } 210for _ ,id := range ids { 211configuration ,err := daemon .GetSessionConfiguration (ctx ,conn ,id ) 212if err != nil { 213return err 214 } 215result ,err := daemon .DeleteSession (ctx ,conn ,id ,false , daemon.SessionCondition {ETag :configuration .ETag }) 216if err != nil { 217return err 218 } 219if ! result .OK { 220return errors .New (result .Message ) 221 } 222 } 223return nil 224} 225func (s * Service )StopDaemon (ctx context.Context )error { 226conn ,err := s .Existing (ctx ) 227if err != nil || conn == nil { 228return err 229 } 230return daemon .StopDaemon (ctx ,conn ) 231} 232func (s * Service )PrepareLogin (ctx context.Context ) (PreparedOpen ,error ) { 233conn ,err := s .Connect (ctx ) 234if err != nil { 235return PreparedOpen {},err 236 } 237settings ,err := daemon .GetSettings (ctx ,conn ) 238return PreparedOpen {Connection :conn ,Settings :settings },err 239}