okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
a229862
main
1package daemon 2 3import ( 4"context" 5"errors" 6"fmt" 7"io" 8"net/http" 9"slices" 10"strings" 11 12"albedo/cli/internal/daemon/protocol" 13) 14 15type DeletionResult struct { 16OK bool 17Deleted int 18Remaining int 19State string 20DeletedIDs []string 21Truncated bool 22Message string 23} 24type PreviewItem struct { 25Type string 26Preview string 27} 28type SessionPreview struct { 29Items []PreviewItem 30Session * Session 31} 32type CreateSessionRequest struct {Kind ,Workspace ,Provider ,Model ,Effort ,Name ,SourceSessionID ,CheckpointID ,ParentID ,Address ,Task string } 33type ForkRequest struct {Checkpoint string } 34type Session struct { 35ID string `json:"id"` 36Title string `json:"name"` 37LastAssistantAt * int64 `json:"activity_at"` // Unix seconds, when observed. 38TranscriptCount int `json:"-"` 39Workspace string `json:"workspace"` 40Location * Location `json:"-"` 41Cursor * Cursor `json:"-"` 42Model string `json:"model"` 43Effort string `json:"effort"` 44Protocol string `json:"-"` 45Provider string `json:"provider_profile"` 46ETag string `json:"-"` 47FamilyRevision string `json:"family_revision"` 48ParentID * string `json:"parent_id"` 49RootID string `json:"root_id"` 50Depth int `json:"depth"` 51Closed bool `json:"closed"` 52Pinned ,Archived bool 53Opens int 54Status AgentStatus 55Glances []PageGlance `json:"-"` 56wire protocol.Session 57} 58type Location struct { 59Host * string `json:"host"` 60User * string `json:"user"` 61Path string `json:"path"` 62Label * string `json:"label"` 63} 64 65func SplitLocation (workspace string ) (host ,path string ) { 66if workspace == "" || workspace [0 ]== '/' || workspace [0 ]== '~' { 67return "" ,workspace 68 } 69i := strings .Index (workspace ,":/" ) 70if i <= 0 || strings .Contains (workspace [:i ],"/" ) { 71return "" ,workspace 72 } 73return workspace [:i ],workspace [i + 1 :] 74} 75 76func sessionValue (wire protocol.Session )Session { 77location := & Location {Host :wire .Location .Host ,User :wire .Location .User ,Path :wire .Location .Path ,Label :wire .Location .Label } 78session := Session {ID :wire .ID ,Title :wire .Name ,Cursor :wire .Cursor ,LastAssistantAt :timestampSeconds (value (wire .ActivityAt )),TranscriptCount :int (wire .Preview .TranscriptCount ),Workspace :wire .Workspace ,Location :location ,Model :value (wire .Model ),Effort :value (wire .Effort ),Provider :value (wire .ProviderProfile ),ETag :wire .ConfigurationResource .ETag ,FamilyRevision :wire .FamilyRevision ,ParentID :wire .ParentID ,RootID :wire .RootID ,Depth :int (wire .Depth ),Closed :wire .Closed ,Pinned :wire .Preferences .Pinned ,Archived :wire .Preferences .Archived ,Opens :int (wire .Preferences .Opens ),Status :capturedStatus (wire ),wire :wire } 79for _ ,glance := range wire .Glances { 80session .Glances = append (session .Glances ,* glanceValue (glance )) 81 } 82slices .SortFunc (session .Glances ,func (first ,second PageGlance )int {return strings .Compare (first .Title ,second .Title ) }) 83return session 84} 85func decodeSession (data []byte ) (Session ,error ) { 86var wire protocol.Session 87if err := decodeRequired (data ,& wire );err != nil { 88return Session {},err 89 } 90if err := validateSession (wire );err != nil { 91return Session {},err 92 } 93return sessionValue (wire ),nil 94} 95func validateSession (wire protocol.Session )error { 96if wire .ID == "" || wire .RootID == "" || wire .Cursor == nil || ! validGeneration (wire .Cursor .Generation )|| wire .Cursor .Sequence < 0 || wire .ConfigurationResource .ETag == "" || wire .History .Items == nil || wire .ActiveOutput == nil { 97return fieldError ("session" ) 98 } 99if err := validateActiveOutput (wire .ID ,wire .ActiveOutput );err != nil { 100return err 101 } 102return validateSessionStatus (wire .Status ) 103} 104func CreateSession (ctx context.Context ,conn * Connection ,request CreateSessionRequest ) (Session ,error ) { 105handle ,err := NewCreation (request ) 106if err != nil { 107return Session {},err 108 } 109return CreateSessionOperation (ctx ,conn ,handle ) 110} 111func CreateSessionOperation (ctx context.Context ,conn * Connection ,handle * OperationHandle ) (Session ,error ) { 112return executeReceipt (ctx ,conn ,handle , []int {201 },func (body []byte ,_ int ) (Session ,error ) { 113result ,err := decodeSession (body ) 114if err == nil && result .ID != handle .ID () { 115return result ,fieldError ("session ID" ) 116 } 117return result ,err 118 },func (ctx context.Context ) (Session ,bool ,error ) { 119result ,err := ResolveCreation (ctx ,conn ,handle ) 120return result ,err == nil ,err 121 }) 122} 123func GetSession (ctx context.Context ,conn * Connection ,id string ) (Session ,error ) { 124var result Session 125err := executeRead (ctx ,conn ,operation {Name :"read session" ,BuildRequest :func (base string ,body io.Reader ) (* http.Request ,error ) { 126return protocol .NewGetSessionRequest (base ,id ,& protocol.GetSessionParams {Tail :new (int64 (0 ))}) 127 },Policy :readRecovery },func (data []byte )error { 128var err error 129result ,err = decodeSession (data ) 130if err == nil && result .ID != id { 131return fieldError ("session ID" ) 132 } 133return err 134 }) 135return result ,err 136} 137 138type SessionCondition struct {ETag ,FamilyRevision string } 139type SessionConfiguration struct { 140Value protocol.SessionConfiguration 141ETag string 142} 143 144func GetSessionConfiguration (ctx context.Context ,conn * Connection ,id string ) (SessionConfiguration ,error ) { 145var result SessionConfiguration 146err := executeRead (ctx ,conn ,operation {Name :"read session configuration" ,BuildRequest :func (base string ,body io.Reader ) (* http.Request ,error ) { 147return protocol .NewGetSessionRequest (base ,id ,& protocol.GetSessionParams {View :new ("configuration ")}) 148 },Validator :& result .ETag ,Policy :readRecovery },func (data []byte )error { 149if err := decodeRequired (data ,& result .Value );err != nil { 150return err 151 } 152if result .ETag == "" || result .Value .ID != id { 153return fieldError ("configuration validator" ) 154 } 155return nil 156 }) 157return result ,err 158} 159func patchSession (ctx context.Context ,conn * Connection ,id ,etag string ,body any ) (Session ,error ) { 160headers ,err := observedHeaders (etag ) 161if err != nil { 162return Session {},err 163 } 164var result Session 165err = executeMutation (ctx ,conn ,operation {Name :"edit session configuration" ,BuildRequest :func (base string ,body io.Reader ) (* http.Request ,error ) { 166return protocol .NewPatchSessionRequestWithBody (base ,id ,& protocol.PatchSessionParams {View :"configuration" ,IfMatch :headers .Get ("If-Match" )},"application/merge-patch+json" ,body ) 167 },Headers :headers ,Body :body ,Policy :authRecovery }, []int {200 },func (data []byte ,_ int )error { 168var change protocol.SessionChange 169if err := decodeRequired (data ,& change );err != nil { 170return err 171 } 172if change .Resource .ETag == "" || change .Session .ID != id || change .Resource .Value .ID != id || change .Resource .ETag != change .Session .ConfigurationResource .ETag { 173return fieldError ("session change" ) 174 } 175if err := validateSession (change .Session );err != nil { 176return err 177 } 178result = sessionValue (change .Session ) 179return nil 180 }) 181return result ,err 182} 183func RenameSession (ctx context.Context ,conn * Connection ,id ,name ,etag string ) (Session ,error ) { 184return patchSession (ctx ,conn ,id ,etag , protocol.SessionPatch {Name :& name }) 185} 186func ForkSession (ctx context.Context ,conn * Connection ,id string ,body ForkRequest ) (Session ,error ) { 187return CreateSession (ctx ,conn ,CreateSessionRequest {Kind :"fork" ,SourceSessionID :id ,CheckpointID :body .Checkpoint }) 188} 189func DeleteSession (ctx context.Context ,conn * Connection ,id string ,tree bool ,condition SessionCondition ) (DeletionResult ,error ) { 190headers ,err := observedHeaders (condition .ETag ) 191if err != nil { 192return DeletionResult {},err 193 } 194params := protocol.DeleteSessionParams {View :"configuration" ,Scope :new ("leaf "),IfMatch :condition .ETag } 195if tree { 196if condition .FamilyRevision == "" { 197return DeletionResult {},errors .New ("subtree deletion requires the observed family revision" ) 198 } 199params .Scope = new ("subtree ") 200params .FamilyRevision = & condition .FamilyRevision 201 } 202var result DeletionResult 203err = executeMutation (ctx ,conn ,operation {Name :"delete session" ,BuildRequest :func (base string ,body io.Reader ) (* http.Request ,error ) { 204return protocol .NewDeleteSessionRequest (base ,id ,& params ) 205 },Headers :headers ,Policy :authRecovery }, []int {200 },func (data []byte ,_ int )error { 206var wire protocol.SessionDeletion 207if err := decodeRequired (data ,& wire );err != nil { 208return err 209 } 210if wire .DeletedCount < 0 || wire .RemainingCount < 0 || wire .State != "complete" && wire .State != "partial" || wire .State == "complete" && wire .RemainingCount != 0 { 211return fieldError ("deletion counts" ) 212 } 213result = DeletionResult {OK :wire .State == "complete" ,Deleted :int (wire .DeletedCount ),Remaining :int (wire .RemainingCount ),State :wire .State ,DeletedIDs :wire .DeletedIDs ,Truncated :wire .Truncated ,Message :fmt .Sprintf ("Deleted %d sessions; %d remain." ,wire .DeletedCount ,wire .RemainingCount )} 214for _ ,remaining := range wire .Remaining { 215result .Message += " " + remaining .ID + ": " + remaining .Reason .Detail 216 } 217if wire .Truncated { 218result .Message += " Individual results are truncated; refresh the session list." 219 } 220return nil 221 }) 222return result ,err 223} 224func ListSessions (ctx context.Context ,conn * Connection ) ([]Session ,error ) { 225return listSessions (ctx ,conn , protocol.ListSessionsParams {Scope :new ("roots ")}) 226} 227func listSessions (ctx context.Context ,conn * Connection ,query protocol.ListSessionsParams ) ([]Session ,error ) { 228result := []Session {} 229query .Limit = new (int64 (200 )) 230err := walkPages (func (next * string ) (protocol.SessionPage ,* string ,error ) { 231query .Next = next 232var page protocol.SessionPage 233pageErr := executeRead (ctx ,conn ,operation {Name :"list sessions" ,BuildRequest :func (base string ,body io.Reader ) (* http.Request ,error ) { 234return protocol .NewListSessionsRequest (base ,& query ) 235 },Policy :readRecovery },func (data []byte )error {return decodeRequired (data ,& page ) }) 236return page ,page .Next ,pageErr 237 },func (page protocol.SessionPage )error { 238if page .Items == nil { 239return fieldError ("items" ) 240 } 241for _ ,row := range page .Items { 242if row .ID == "" || row .RootID == "" || row .Cursor != nil && (! validGeneration (row .Cursor .Generation )|| row .Cursor .Sequence < 0 ) { 243return fieldError ("session ID" ) 244 } 245// Summaries intentionally contain no configuration validator. 246result = append (result ,summaryValue (row )) 247 } 248return nil 249 },"repeated session page" ) 250if err != nil { 251return nil ,err 252 } 253return result ,nil 254} 255 256func GetSessionPreview (ctx context.Context ,conn * Connection ,id string ,limit int ) (SessionPreview ,error ) { 257var wire protocol.Session 258err := executeRead (ctx ,conn ,operation {Name :"read session preview" ,BuildRequest :func (base string ,body io.Reader ) (* http.Request ,error ) { 259return protocol .NewGetSessionRequest (base ,id ,& protocol.GetSessionParams {Tail :new (int64 (min (200 ,max (0 ,limit ))))}) 260 },Policy :readRecovery },func (data []byte )error {session ,err := decodeSession (data );wire = session .wire ;return err }) 261if err != nil { 262return SessionPreview {},err 263 } 264session := sessionValue (wire ) 265items ,err := previewItems (ctx ,conn ,id ,wire .History .Items ) 266return SessionPreview {Items :items ,Session :& session },err 267} 268 269// PreviewHistory reads a session's newest entries from its stored history. 270// Unlike GetSessionPreview it never loads the session in the daemon. 271func PreviewHistory (ctx context.Context ,conn * Connection ,id string ,limit int ) (SessionPreview ,error ) { 272wire ,err := readHistory (ctx ,conn ,id , protocol.GetHistoryParams {Limit :new (int64 (min (200 ,max (1 ,limit ))))}) 273if err != nil { 274return SessionPreview {},err 275 } 276items ,err := previewItems (ctx ,conn ,id ,wire .Items ) 277return SessionPreview {Items :items },err 278} 279 280func previewItems (ctx context.Context ,conn * Connection ,id string ,entries []protocol.HistoryEntry ) ([]PreviewItem ,error ) { 281// Long tool results arrive as references, read in full like the chat's. 282entries ,err := hydrateHistory (ctx ,conn ,id ,entries ) 283if err != nil { 284return nil ,err 285 } 286events ,err := historyEvents (entries ) 287if err != nil { 288return nil ,err 289 } 290items := []PreviewItem {} 291for _ ,event := range events { 292if event .Type == EventCommitted { 293continue 294 } 295text := event .Text 296if event .Type == EventTool { 297text = event .ToolName + ": " + event .ToolResult 298 } 299items = append (items ,PreviewItem {Type :string (event .Type ),Preview :text }) 300 } 301return items ,nil 302} 303 304func summaryValue (row protocol.SessionSummary )Session { 305return sessionValue (protocol.Session {ID :row .ID ,Name :row .Name ,AutomaticName :row .AutomaticName ,Workspace :row .Workspace ,Location :row .Location ,ParentID :row .ParentID ,RootID :row .RootID ,Address :row .Address ,Depth :row .Depth ,Closed :row .Closed ,CreatedAt :row .CreatedAt ,ActivityAt :row .ActivityAt ,ProviderProfile :row .ProviderProfile ,Model :row .Model ,Effort :row .Effort ,Status :row .Status ,Preview :row .Preview ,Preferences :row .Preferences ,CurrentProgress :row .CurrentProgress ,Activity :row .Activity ,Cursor :row .Cursor }) 306} 307func ListAllSessions (ctx context.Context ,conn * Connection ) ([]Session ,error ) { 308return listSessions (ctx ,conn , protocol.ListSessionsParams {Scope :new ("all ")}) 309} 310 311func capturedStatus (w protocol.Session )AgentStatus { 312status := statusValue (w .Status ,w .Kernel ) 313status .InputOrder = w .InputOrder 314return status 315}