okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
d9c1a13
main
1package daemon 2 3import ( 4"bufio" 5"context" 6"encoding/json" 7"errors" 8"io" 9"net" 10"net/http" 11"strings" 12"unicode/utf8" 13 14"albedo/cli/internal/daemon/protocol" 15) 16 17type streamBatch struct { 18Generation string `json:"generation"` 19Cursor * int64 `json:"cursor"` 20Events []json.RawMessage `json:"events"` 21Snapshot json.RawMessage `json:"snapshot"` 22} 23 24func (c * ChatClient )ResetStream () { 25c .mu .Lock () 26defer c .mu .Unlock () 27c .progressCallIDs = nil 28c .afterSeq = - 1 29c .afterGeneration = "" 30} 31func streamFailure (kind StreamFailureKind ,cause error )error { 32return & StreamError {Kind :kind ,Cause :cause } 33} 34func classifyStreamFailure (err error )error { 35if err == nil { 36return nil 37 } 38if _ ,ok := errors.AsType [* StreamError ](err );ok { 39return err 40 } 41if api ,ok := errors.AsType [* APIError ](err );ok { 42kind := StreamTerminal 43if api .StatusCode == 408 || api .StatusCode == 429 || api .StatusCode >= 500 { 44kind = StreamTransient 45 } 46return streamFailure (kind ,err ) 47 } 48if errors .Is (err ,bufio .ErrTooLong ) { 49return streamFailure (StreamProtocol ,err ) 50 } 51if _ ,ok := errors.AsType [net.Error ](err );ok { 52return streamFailure (StreamTransient ,err ) 53 } 54if isConnectionError (err )|| errors .Is (err ,io .ErrUnexpectedEOF )|| errors .Is (err ,context .Canceled )|| errors .Is (err ,context .DeadlineExceeded ) { 55return streamFailure (StreamTransient ,err ) 56 } 57return streamFailure (StreamTerminal ,err ) 58} 59func (c * ChatClient )Stream (ctx context.Context ,tail int ,onEvent func (StreamEvent )error )error { 60return c .StreamWithProgress (ctx ,tail ,onEvent ,nil ) 61} 62func (c * ChatClient )StreamWithProgress (ctx context.Context ,tail int ,onEvent func (StreamEvent )error ,onBatchConsumed func ())error { 63defer func () { 64if ctx .Err ()!= nil { 65c .ResetStream () 66 } 67 }() 68c .mu .Lock () 69after ,generation := c .afterSeq ,c .afterGeneration 70c .mu .Unlock () 71params := protocol.GetSessionParams {Tail :new (int64 (min (200 ,max (0 ,tail ))))} 72if generation != "" { 73params .AfterGeneration = & generation 74params .AfterSeq = & after 75 } 76err := scanEventStream (ctx ,c .conn ,operation {Name :"watch session" ,BuildRequest :func (base string ,body io.Reader ) (* http.Request ,error ) { 77return protocol .NewGetSessionRequest (base ,c .agentID ,& params ) 78 },Policy :readRecovery },streamLimits {requireSSE :true ,lineBytes :1048577 ,errorBytes :65536 },func (scanner * bufio.Scanner )error {return c .readStream (ctx ,scanner ,onBatchConsumed ,onEvent ) }) 79return classifyStreamFailure (err ) 80} 81 82// SSE framing is independent of network fragmentation and supports multiline data. 83func scanSSEFrames (ctx context.Context ,scanner * bufio.Scanner ,consume func ([]byte )error )error { 84var data strings.Builder 85for scanner .Scan () { 86if err := ctx .Err ();err != nil { 87return err 88 } 89line := scanner .Text () 90if line == "" { 91if data .Len ()> 0 { 92payload := strings .TrimSuffix (data .String (),"\n" ) 93if ! utf8 .ValidString (payload ) { 94return streamFailure (StreamProtocol ,errors .New ("SSE data is not UTF-8" )) 95 } 96if err := consume ([]byte (payload ));err != nil { 97return err 98 } 99data .Reset () 100 } 101continue 102 } 103if after ,ok := strings .CutPrefix (line ,"data:" );ok { 104fragment := after 105fragment = strings .TrimPrefix (fragment ," " ) 106if data .Len ()+ len (fragment )+ 1 > 1048577 { 107return streamFailure (StreamProtocol ,errors .New ("SSE frame exceeds 1 MiB" )) 108 } 109data .WriteString (fragment ) 110data .WriteByte ('\n' ) 111 } 112 } 113if err := scanner .Err ();err != nil { 114return err 115 } 116if data .Len ()!= 0 { 117return io .ErrUnexpectedEOF 118 } 119return nil 120} 121func (c * ChatClient )readStream (ctx context.Context ,scanner * bufio.Scanner ,onBatchConsumed func (),onEvent func (StreamEvent )error )error { 122return scanSSEFrames (ctx ,scanner ,func (payload []byte )error { 123var batch streamBatch 124if err := decodeRequired (payload ,& batch ,"generation" ,"cursor" ,"events" );err != nil { 125return streamFailure (StreamProtocol ,err ) 126 } 127if ! validGeneration (batch .Generation )|| batch .Cursor == nil || * batch .Cursor < 0 || batch .Events == nil || len (batch .Events )> 256 { 128return streamFailure (StreamProtocol ,fieldError ("stream batch" )) 129 } 130var first sessionEventEnvelope 131if len (batch .Events )> 0 { 132if err := json .Unmarshal (batch .Events [0 ],& first );err != nil { 133return streamFailure (StreamProtocol ,err ) 134 } 135 } 136if first .Type == "failure" { 137if len (batch .Events )!= 1 || batch .Snapshot != nil { 138return streamFailure (StreamProtocol ,fieldError ("failure batch" )) 139 } 140var reason protocol.SafeReason 141if err := json .Unmarshal (first .Data ,& reason );err != nil || reason .Code == "" { 142return streamFailure (StreamProtocol ,fieldError ("failure reason" )) 143 } 144failure := & APIError {Code :reason .Code ,Message :reason .Detail } 145if failure .DaemonRestarting () { 146// older daemons end streams this way while they shut down 147return streamFailure (StreamTransient ,failure ) 148 } 149return streamFailure (StreamTerminal ,failure ) 150 } 151reset := first .Type == "reset" 152c .mu .Lock () 153previous ,generation := c .afterSeq ,c .afterGeneration 154c .mu .Unlock () 155sequence := previous 156var events []wireChatEvent 157var current []ToolProgress 158var outputs []protocol.ActiveOutput 159if reset { 160snapshot ,err := decodeSession (batch .Snapshot ) 161if err != nil { 162return streamFailure (StreamProtocol ,err ) 163 } 164if snapshot .ID != c .agentID || snapshot .wire .Cursor .Generation != batch .Generation { 165return streamFailure (StreamProtocol ,fieldError ("snapshot identity" )) 166 } 167sequence = snapshot .wire .Cursor .Sequence 168outputs = snapshot .wire .ActiveOutput 169before := int64 (0 ) 170if len (snapshot .wire .History .Items )> 0 { 171before = snapshot .wire .History .Items [0 ].Position 172 } 173events = append (events ,wireChatEvent {StreamEvent :StreamEvent {Type :EventReset ,Before :before ,More :snapshot .wire .History .Older != nil ,Snapshot :& snapshot }}) 174hydrated ,err := hydrateHistory (ctx ,c .conn ,c .agentID ,snapshot .wire .History .Items ) 175if err != nil { 176return streamFailure (StreamTransient ,err ) 177 } 178history ,err := historyEvents (hydrated ) 179if err != nil { 180return streamFailure (StreamProtocol ,err ) 181 } 182for _ ,event := range history { 183events = append (events ,wireChatEvent {StreamEvent :event }) 184 } 185events = append (events ,wireChatEvent {StreamEvent :StreamEvent {Type :EventUsage ,Usage :usageValue (snapshot .wire .Usage ),Replayed :true }}) 186for _ ,progress := range snapshot .wire .CurrentProgress { 187parsed ,err := validateToolProgress (progress ) 188if err != nil || parsed == nil { 189return streamFailure (StreamProtocol ,fieldError ("current progress" )) 190 } 191current = append (current ,* parsed ) 192events = append (events ,wireChatEvent {StreamEvent :StreamEvent {Type :EventToolProgress ,Progress :parsed }}) 193 } 194 }else if batch .Snapshot != nil || generation == "" || generation != batch .Generation { 195return streamFailure (StreamProtocol ,errors .New ("initial or changed generation requires a snapshot reset" )) 196 } 197restoredEvents := len (events ) 198for index ,raw := range batch .Events { 199var envelope sessionEventEnvelope 200if err := decodeRequired (raw ,& envelope ,"type" ,"data" );err != nil { 201return streamFailure (StreamProtocol ,err ) 202 } 203if reset && index == 0 { 204if _ ,err := decodeChatEvent (raw );err != nil { 205return streamFailure (StreamProtocol ,err ) 206 } 207continue 208 } 209if envelope .Type == "reset" || envelope .Type == "failure" || envelope .Sequence == nil || * envelope .Sequence != sequence + 1 { 210return streamFailure (StreamProtocol ,errors .New ("stream events are not contiguous" )) 211 } 212sequence = * envelope .Sequence 213event ,err := decodeChatEnvelope (envelope ,len (raw ),& eventContentReader {ctx :ctx ,conn :c .conn ,session :c .agentID }) 214if err != nil { 215if api ,ok := errors.AsType [* APIError ](err );ok && api .Code == "history_publication_failed" { 216c .ResetStream () 217return streamFailure (StreamTransient ,err ) 218 } 219if _ ,ok := errors.AsType [* StreamError ](err );ok { 220return err 221 } 222return streamFailure (StreamProtocol ,err ) 223 } 224if event != nil { 225events = append (events ,* event ) 226 } 227 } 228if sequence != * batch .Cursor { 229return streamFailure (StreamProtocol ,errors .New ("batch cursor does not cover its events" )) 230 } 231active ,err := c .nextProgressCallIDs (current ,events ,reset ) 232if err != nil { 233return streamFailure (StreamProtocol ,err ) 234 } 235stage ,err := stageActiveOutput (ctx ,c .conn ,outputs ) 236if err != nil { 237if api ,ok := errors.AsType [* APIError ](err );ok && (api .StatusCode == 404 || api .StatusCode == 410 ) { 238c .ResetStream () 239return streamFailure (StreamTransient ,err ) 240 } 241return classifyStreamFailure (err ) 242 } 243prefixDelivered := ! reset 244for index ,event := range events { 245if ! prefixDelivered && index == restoredEvents { 246if err := stage .deliver (ctx ,onEvent );err != nil { 247stage .close () 248return streamFailure (StreamTerminal ,err ) 249 } 250prefixDelivered = true 251 } 252if err := onEvent (event .StreamEvent );err != nil { 253stage .close () 254return streamFailure (StreamTerminal ,err ) 255 } 256 } 257if ! prefixDelivered { 258if err := stage .deliver (ctx ,onEvent );err != nil { 259stage .close () 260return streamFailure (StreamTerminal ,err ) 261 } 262 } 263stage .close () 264c .mu .Lock () 265c .progressCallIDs = active 266c .afterSeq = * batch .Cursor 267c .afterGeneration = batch .Generation 268c .mu .Unlock () 269if onBatchConsumed != nil { 270onBatchConsumed () 271 } 272return nil 273 }) 274} 275func (c * ChatClient )nextProgressCallIDs (snapshot []ToolProgress ,events []wireChatEvent ,reset bool ) (map [string ]struct {},error ) { 276active := map [string ]struct {}{} 277if reset { 278for _ ,progress := range snapshot { 279if _ ,ok := active [progress .CallID ];ok { 280return nil ,errors .New ("duplicate progress call" ) 281 } 282active [progress .CallID ]= struct {}{} 283 } 284 }else { 285c .mu .Lock () 286for id := range c .progressCallIDs { 287active [id ]= struct {}{} 288 } 289c .mu .Unlock () 290 } 291for _ ,event := range events { 292if event .Replayed { 293continue 294 } 295switch event .Type { 296case EventRetry ,EventUser ,EventMessage ,EventError ,EventInterrupted ,EventTurnCompleted : 297clear (active ) 298case EventToolProgress : 299if event .Progress == nil { 300clear (active ) 301 }else { 302active [event .Progress .CallID ]= struct {}{} 303 } 304case EventTool : 305delete (active ,event .ProgressCallID ) 306 } 307if len (active )> maxActiveToolProgress { 308return nil ,errors .New ("too many active progress calls" ) 309 } 310 } 311if len (active )> maxActiveToolProgress { 312return nil ,errors .New ("too many active progress calls" ) 313 } 314return active ,nil 315}