okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
7fd8cf5
main
1package daemon 2 3import ( 4"bufio" 5"context" 6"encoding/json" 7"errors" 8"io" 9"net/http" 10"unicode/utf8" 11 12"albedo/cli/internal/daemon/protocol" 13) 14 15type Activity = protocol.Activity 16type Cursor = protocol.Cursor 17 18func validGeneration (generation string )bool { 19if len (generation )!= 22 { 20return false 21 } 22for i := range len (generation ) { 23c := generation [i ] 24if ! (c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '_' || c == '-' ) { 25return false 26 } 27 } 28return true 29} 30 31type AgentEvent struct { 32MailID string 33Progress * ToolProgress 34Type ,Session ,Parent ,Name ,Model ,From ,To ,FromName ,Kind ,Text ,CallID ,ProgressCallID ,Output ,Source string 35Depth ,Bytes int 36Running bool 37Cursor * Cursor 38Status * AgentStatus 39CurrentProgress []ToolProgress 40Activity * Activity 41SessionIDs []string 42ScopeDirty bool 43} 44type AgentNode struct { 45Parent ,Address * string 46Session Session 47Name string 48Depth int 49Running ,Closed bool 50Cursor * Cursor 51Activity Activity 52CurrentProgress []ToolProgress 53} 54type AgentsSnapshot struct { 55Root ,FamilyRevision string 56Nodes []AgentNode 57} 58 59func StreamAgents (ctx context.Context ,conn * Connection ,onBatch func ([]AgentEvent )error )error { 60first := true 61err := scanEventStream (ctx ,conn ,operation {Name :"watch sessions" ,BuildRequest :func (base string ,body io.Reader ) (* http.Request ,error ) { 62return protocol .NewListSessionsRequest (base ,nil ) 63 },Policy :readRecovery },streamLimits {requireSSE :true ,lineBytes :1048577 },func (scanner * bufio.Scanner )error { 64return scanSSEFrames (ctx ,scanner ,func (payload []byte )error { 65var w struct { 66Events []json.RawMessage `json:"events"` 67 } 68if err := decodeRequired (payload ,& w ,"events" );err != nil { 69return streamFailure (StreamProtocol ,err ) 70 } 71if w .Events == nil || len (w .Events )> 256 { 72return streamFailure (StreamProtocol ,fieldError ("collection batch" )) 73 } 74events := []AgentEvent {} 75types := []string {} 76for _ ,raw := range w .Events { 77event ,err := decodeAgentEvent (raw ) 78if err != nil { 79return streamFailure (StreamProtocol ,err ) 80 } 81var kind struct { 82Type string `json:"type"` 83 } 84_ = json .Unmarshal (raw ,& kind ) 85types = append (types ,kind .Type ) 86if event != nil { 87events = append (events ,* event ) 88 } 89 } 90if first { 91if len (types )!= 2 || types [0 ]!= "ready" || types [1 ]!= "reset" { 92return streamFailure (StreamProtocol ,errors .New ("collection stream requires ready and reset before snapshots" )) 93 } 94first = false 95 }else { 96for _ ,kind := range types { 97if kind == "ready" || kind == "reset" { 98return streamFailure (StreamProtocol ,fieldError ("collection readiness" )) 99 } 100if kind == "overflow" || kind == "failure" { 101if len (types )!= 1 { 102return streamFailure (StreamProtocol ,fieldError ("terminal collection batch" )) 103 } 104 } 105 } 106 } 107if err := onBatch (events );err != nil { 108return streamFailure (StreamTerminal ,err ) 109 } 110if len (types )== 1 && types [0 ]== "overflow" { 111return streamFailure (StreamTransient ,errors .New ("collection stream overflow; capture a fresh snapshot" )) 112 } 113return nil 114 }) 115 }) 116return classifyStreamFailure (err ) 117} 118func decodeAgentEvent (raw json.RawMessage ) (* AgentEvent ,error ) { 119var envelope struct { 120Type string `json:"type"` 121Data json.RawMessage `json:"data"` 122 } 123if err := decodeRequired (raw ,& envelope ,"type" ,"data" );err != nil { 124return nil ,err 125 } 126if envelope .Type == "" || envelope .Data == nil || string (envelope .Data )== "null" { 127return nil ,fieldError ("collection event" ) 128 } 129event := & AgentEvent {Type :envelope .Type } 130switch envelope .Type { 131case "ready" ,"overflow" : 132case "reset" : 133var d struct { 134Reason string `json:"reason"` 135 } 136if err := decodeRequired (envelope .Data ,& d ,"reason" );err != nil { 137return nil ,err 138 } 139if d .Reason != "initial" { 140return nil ,fieldError ("collection reset" ) 141 } 142case "activity" : 143var d struct { 144SessionID string `json:"session_id"` 145Cursor Cursor `json:"cursor"` 146Status protocol.SessionStatus `json:"status"` 147Progress []protocol.ToolProgress `json:"current_progress"` 148Activity Activity `json:"activity"` 149 } 150if err := decodeRequired (envelope .Data ,& d ,"session_id" ,"cursor" ,"status" ,"current_progress" ,"activity" );err != nil { 151return nil ,err 152 } 153if d .SessionID == "" || ! validGeneration (d .Cursor .Generation )|| d .Cursor .Sequence < 0 || len (d .Progress )> maxActiveToolProgress { 154return nil ,fieldError ("collection activity" ) 155 } 156if err := validateSessionStatus (d .Status );err != nil { 157return nil ,err 158 } 159if err := validateActivity (d .Activity );err != nil { 160return nil ,err 161 } 162event .Session = d .SessionID 163event .Cursor = & d .Cursor 164status := statusValue (d .Status , protocol.Kernel {}) 165event .Status = & status 166event .Running = status .Running 167event .Activity = & d .Activity 168event .CurrentProgress = []ToolProgress {} 169seen := map [string ]bool {} 170for _ ,p := range d .Progress { 171if seen [p .CallID ] { 172return nil ,fieldError ("duplicate collection progress" ) 173 } 174seen [p .CallID ]= true 175progress ,err := validateToolProgress (p ) 176if err != nil || progress == nil { 177return nil ,fieldError ("collection progress" ) 178 } 179event .CurrentProgress = append (event .CurrentProgress ,* progress ) 180 } 181case "mail" : 182var d protocol.MailMetadata 183if err := decodeRequired (envelope .Data ,& d );err != nil { 184return nil ,err 185 } 186event .MailID = d .MailID 187event .From ,event .To ,event .FromName ,event .Kind ,event .Bytes = value (d .SenderSessionID ),d .ReceiverSessionID ,value (d .SenderLabel ),d .Kind ,int (d .Bytes ) 188case "invalidate" : 189var d struct { 190SessionIDs []string `json:"session_ids"` 191ScopeDirty bool `json:"scope_dirty"` 192 } 193if err := decodeRequired (envelope .Data ,& d ,"urls" ,"session_ids" ,"scope_dirty" );err != nil { 194return nil ,err 195 } 196event .SessionIDs ,event .ScopeDirty = d .SessionIDs ,d .ScopeDirty 197case "failure" : 198var d protocol.SafeReason 199if err := decodeRequired (envelope .Data ,& d );err != nil { 200return nil ,err 201 } 202return nil ,streamFailure (StreamTerminal ,& APIError {Code :d .Code ,Message :d .Detail }) 203default : 204return nil ,nil 205 } 206return event ,nil 207} 208func GetAgents (ctx context.Context ,conn * Connection ,id string ) (AgentsSnapshot ,error ) { 209selected ,err := GetSession (ctx ,conn ,id ) 210if err != nil { 211return AgentsSnapshot {},err 212 } 213params := protocol.ListSessionsParams {Scope :new ("all "),FamilyID :& selected.RootID ,Limit :new (int64 (200 ))} 214result := AgentsSnapshot {Root :selected .RootID ,Nodes : []AgentNode {}} 215err = walkPages (func (next * string ) (protocol.SessionPage ,* string ,error ) { 216params .Next = next 217var page protocol.SessionPage 218pageErr := executeRead (ctx ,conn ,operation {Name :"read session family" ,BuildRequest :func (base string ,body io.Reader ) (* http.Request ,error ) { 219return protocol .NewListSessionsRequest (base ,& params ) 220 },Policy :readRecovery },func (data []byte )error {return decodeRequired (data ,& page ,"family" ) }) 221return page ,page .Next ,pageErr 222 },func (page protocol.SessionPage )error { 223if result .FamilyRevision != "" && page .Family .Revision != result .FamilyRevision { 224return & APIError {StatusCode :409 ,Code :"family_changed" ,Message :"family changed while paging; refresh it" } 225 } 226result .FamilyRevision = page .Family .Revision 227for _ ,row := range page .Items { 228session := summaryValue (row ) 229session .FamilyRevision = result .FamilyRevision 230node := AgentNode {Parent :row .ParentID ,Address :row .Address ,Session :session ,Name :session .Title ,Depth :session .Depth ,Running :session .Status .Running ,Closed :session .Closed ,Cursor :row .Cursor ,Activity :row .Activity ,CurrentProgress : []ToolProgress {}} 231for _ ,p := range row .CurrentProgress { 232progress ,progressErr := validateToolProgress (p ) 233if progressErr != nil || progress == nil { 234return fieldError ("family progress" ) 235 } 236node .CurrentProgress = append (node .CurrentProgress ,* progress ) 237 } 238result .Nodes = append (result .Nodes ,node ) 239 } 240return nil 241 },"family cursor" ) 242if err != nil { 243return result ,err 244 } 245return result ,nil 246} 247 248func validateActivity (activity Activity )error { 249if activity .CurrentRequest != nil && (activity .CurrentRequest .InputID == "" || len ([]rune (activity .CurrentRequest .Text ))> 512 ) { 250return fieldError ("activity request" ) 251 } 252if activity .LatestProgress != nil && len ([]rune (* activity .LatestProgress ))> 512 { 253return fieldError ("activity progress" ) 254 } 255if len (activity .Lines )> 12 || activity .Lines == nil || activity .OutputScalars < 0 || activity .OutputUTF8Bytes < 0 || timestampMilliseconds (activity .ObservedAt )== nil { 256return fieldError ("activity" ) 257 } 258for _ ,line := range activity .Lines { 259if len (line .Text )> 1024 || utf8 .RuneCountInString (line .Text )> 256 { 260return fieldError ("activity line" ) 261 } 262switch line .Kind { 263case "assistant" ,"thinking" ,"tool" ,"input" ,"note" ,"error" : 264default : 265return fieldError ("activity kind" ) 266 } 267 } 268return nil 269}