okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
6e82811
main
1package daemon 2 3import ( 4"context" 5"crypto/rand" 6"encoding/json" 7"errors" 8"fmt" 9"io" 10"net/http" 11"reflect" 12"time" 13 14"albedo/cli/internal/daemon/protocol" 15) 16 17// OperationHandle freezes the chosen resource ID and exact admission payload. 18// Retries always use this same input or session resource. 19type OperationHandle struct { 20id string 21operation operation 22kind string 23sessionID string 24} 25 26func (handle * OperationHandle )ID ()string {return handle .id } 27func (handle * OperationHandle )IsCreation ()bool {return handle .kind == "creation" } 28 29func operationID () (string ,error ) { 30var id [16 ]byte 31if _ ,err := rand .Read (id [:]);err != nil { 32return "" ,fmt .Errorf ("create resource ID: %w" ,err ) 33 } 34stamp := uint64 (time .Now ().UnixMilli ()) 35for i := 5 ;i >= 0 ;i -- { 36id [i ]= byte (stamp ) 37stamp >>= 8 38 } 39id [6 ]= id [6 ]& 0x0f | 0x70 40id [8 ]= id [8 ]& 0x3f | 0x80 41return fmt .Sprintf ("%x-%x-%x-%x-%x" ,id [:4 ],id [4 :6 ],id [6 :8 ],id [8 :10 ],id [10 :]),nil 42} 43 44func NewSubmission (session string ,fields SubmissionRequest ) (* OperationHandle ,error ) { 45id := fields .SubmissionID 46if id == "" { 47var err error 48id ,err = operationID () 49if err != nil { 50return nil ,err 51 } 52 } 53kind := fields .Type 54if kind == "" || kind == "user" { 55kind = "message" 56 } 57var body any 58switch kind { 59case "message" : 60if fields .Content == nil { 61return nil ,errors .New ("message input requires text" ) 62 } 63uploaded := make ([]protocol.ImageUpload ,len (fields .Images )) 64for i ,image := range fields .Images { 65uploaded [i ]= protocol.ImageUpload {MimeType :string (image .MimeType ),Data :image .Data } 66 } 67body = protocol.MessageInputRequest {Kind :kind ,Text :* fields .Content ,Images :uploaded ,Pastes :fields .Pastes ,ClientID :optionalText (fields .ClientID )} 68case "continue" : 69body = protocol.ContinueInputRequest {Kind :kind ,ClientID :optionalText (fields .ClientID )} 70case "skill" : 71body = protocol.SkillInputRequest {Kind :kind ,CandidateID :fields .Name ,CatalogRevision :fields .CatalogRevision ,Arguments :fields .Arguments ,ClientID :optionalText (fields .ClientID )} 72case "command" : 73body = protocol.CommandInputRequest {Kind :kind ,CommandID :fields .Name ,Arguments :fields .CommandArguments ,ClientID :optionalText (fields .ClientID )} 74default : 75return nil ,fmt .Errorf ("unknown input kind %q" ,kind ) 76 } 77payload ,err := json .Marshal (body ) 78if err != nil { 79return nil ,err 80 } 81handle := & OperationHandle {id :id ,kind :kind ,sessionID :session } 82handle .operation = operation {Name :"admit input" ,BuildRequest :func (base string ,body io.Reader ) (* http.Request ,error ) { 83return protocol .NewPutInputRequestWithBody (base ,session ,id ,"application/json" ,body ) 84 },Body :json .RawMessage (payload ),Policy :receiptRecovery ,Handle :handle } 85return handle ,nil 86} 87 88func NewCreation (fields CreateSessionRequest ) (* OperationHandle ,error ) { 89kind := fields .Kind 90if kind == "" { 91kind = "new" 92 } 93var body any 94switch kind { 95case "new" : 96body = protocol.NewSessionRequest {Kind :kind ,Workspace :fields .Workspace ,Name :optionalText (fields .Name ),ProviderProfile :optionalText (fields .Provider ),Model :optionalText (fields .Model ),Effort :optionalText (fields .Effort )} 97case "fork" : 98body = protocol.ForkSessionRequest {Kind :kind ,SourceSessionID :fields .SourceSessionID ,CheckpointID :fields .CheckpointID ,Name :optionalText (fields .Name )} 99case "child" : 100inputID ,err := operationID () 101if err != nil { 102return nil ,err 103 } 104body = protocol.ChildSessionRequest {Kind :kind ,ParentID :fields .ParentID ,Address :fields .Address ,Name :fields .Name ,InitialInputID :inputID ,Task :fields .Task ,Model :optionalText (fields .Model ),Effort :optionalText (fields .Effort )} 105default : 106return nil ,fmt .Errorf ("unknown creation kind %q" ,kind ) 107 } 108payload ,err := json .Marshal (body ) 109if err != nil { 110return nil ,err 111 } 112id ,err := operationID () 113if err != nil { 114return nil ,err 115 } 116handle := & OperationHandle {id :id ,kind :"creation" ,sessionID :id } 117handle .operation = operation {Name :"create session" ,BuildRequest :func (base string ,body io.Reader ) (* http.Request ,error ) { 118return protocol .NewCreateSessionRequestWithBody (base ,id ,& protocol.CreateSessionParams {IfNoneMatch :"*" },"application/json" ,body ) 119 },Body :json .RawMessage (payload ),Headers : http.Header {"If-None-Match" : {"*" }},Policy :receiptRecovery ,Handle :handle } 120return handle ,nil 121} 122 123type InputReceipt protocol.Input 124 125func (receipt InputReceipt )Rejection ()error { 126if receipt .Admission != "rejected" { 127return nil 128 } 129err := & APIError {StatusCode :int (receipt .HTTPStatus )} 130if receipt .Problem != nil { 131err .Code ,err .Message = receipt .Problem .Code ,receipt .Problem .Detail 132if err .Message == "" { 133err .Message = receipt .Problem .Title 134 } 135 } 136return err 137} 138 139func (receipt InputReceipt )Pending ()bool {return value (receipt .Delivery )== "pending" } 140 141func (receipt InputReceipt )Settled ()bool { 142return value (receipt .Delivery )== "committed" || value (receipt .Delivery )== "cancelled" 143} 144 145func (receipt InputReceipt )InputOrder ()int64 {return value (receipt .AcceptanceOrder ) } 146 147func (receipt InputReceipt )BlockingDetail ()string { 148if receipt .BlockingReason != nil { 149return receipt .BlockingReason .Detail 150 } 151return "" 152} 153 154func GetInput (ctx context.Context ,conn * Connection ,session ,id string ) (InputReceipt ,error ) { 155var input protocol.Input 156err := executeRead (ctx ,conn ,operation {Name :"read input" ,BuildRequest :func (base string ,body io.Reader ) (* http.Request ,error ) { 157return protocol .NewGetInputRequest (base ,session ,id ) 158 },Policy :readRecovery },func (data []byte )error { 159if err := decodeRequired (data ,& input );err != nil { 160return err 161 } 162if input .ID != id || input .SessionID != session { 163return fieldError ("input identity" ) 164 } 165return validInput (input ) 166 }) 167if err != nil { 168return InputReceipt {},err 169 } 170return InputReceipt (input ),nil 171} 172func validInput (input protocol.Input )error { 173if input .ID == "" || input .SessionID == "" { 174return fieldError ("input identity" ) 175 } 176switch input .Kind { 177case "message" ,"continue" ,"skill" ,"command" : 178default : 179return fieldError ("input kind" ) 180 } 181if input .Admission == "accepted" { 182if input .HTTPStatus != 202 || input .AcceptedAt == nil || input .AcceptanceOrder == nil || * input .AcceptanceOrder < 1 || input .Problem != nil { 183return fieldError ("input admission" ) 184 } 185switch value (input .Delivery ) { 186case "pending" ,"committed" ,"cancelled" : 187return nil 188 } 189 }else if input .Admission == "rejected" && input .HTTPStatus >= 400 && input .HTTPStatus <= 599 && input .Problem != nil && input .Delivery == nil && input .AcceptedAt == nil && input .AcceptanceOrder == nil && input .Turn == nil { 190return nil 191 } 192return fieldError ("input admission" ) 193} 194func ResolveOperation (ctx context.Context ,conn * Connection ,handle * OperationHandle ) (InputReceipt ,error ) { 195return GetInput (ctx ,conn ,handle .sessionID ,handle .id ) 196} 197 198// A found outcome is authoritative even when it rejects the input. A failed 199// lookup leaves admission uncertain and may permit replay of the frozen request. 200func executeReceipt [T any ](ctx context.Context ,conn * Connection ,handle * OperationHandle ,statuses []int ,decode func ([]byte ,int ) (T ,error ),resolve func (context.Context ) (T ,bool ,error )) (T ,error ) { 201var result T 202if ctx == nil { 203ctx = context .Background () 204 } 205ctx ,cancel := context .WithTimeout (ctx ,20 * time .Second ) 206defer cancel () 207for attempt := range 2 { 208err := executeMutation (ctx ,conn ,handle .operation ,statuses ,func (body []byte ,status int )error { 209var decodeErr error 210result ,decodeErr = decode (body ,status ) 211return decodeErr 212 }) 213if err == nil { 214return result ,nil 215 } 216var uncertain * UncertainOutcomeError 217collision := false 218if api ,ok := errors.AsType [* APIError ](err );ok { 219collision = handle .kind == "creation" && api .StatusCode == 412 220 } 221if ! errors .As (err ,& uncertain )&& ! collision { 222return result ,err 223 } 224if ctx .Err ()!= nil { 225return result ,uncertainOperation (handle .operation ,err ) 226 } 227resolved ,found ,lookupErr := resolve (ctx ) 228if found { 229return resolved ,lookupErr 230 } 231if IsOperationExpired (lookupErr ) { 232return result ,uncertainOperation (handle .operation ,lookupErr ) 233 } 234if handle .kind == "creation" { 235if api ,ok := errors.AsType [* APIError ](lookupErr );ok { 236if api .Code == "creation_conflict" { 237return result ,lookupErr 238 } 239if len (api .Decision )> 0 && api .StatusCode >= 400 && api .StatusCode < 500 && api .StatusCode != 404 { 240return result ,lookupErr 241 } 242 } 243 } 244if attempt == 1 { 245return result ,uncertainOperation (handle .operation ,err ) 246 } 247 } 248panic ("unreachable admission retry budget" ) 249} 250func ResolveCreation (ctx context.Context ,conn * Connection ,handle * OperationHandle ) (Session ,error ) { 251session ,err := GetSession (ctx ,conn ,handle .id ) 252if err != nil { 253if api ,ok := errors.AsType [* APIError ](err );ok && len (api .Decision )> 0 { 254var decision protocol.CreationDecision 255if parseErr := decodeRequired (api .Decision ,& decision );parseErr != nil { 256return Session {},& ProtocolError {Code :"invalid_response" ,Operation :"read creation decision" ,Cause :parseErr } 257 } 258if decision .Kind != "creation" || decision .SessionID != handle .ID () { 259return Session {},fieldError ("creation decision identity" ) 260 } 261if decision .Creation != nil && ! sameCreationIntent (handle .operation .Body .(json.RawMessage ),decision .Creation .Submitted ) { 262return Session {},& APIError {StatusCode :409 ,Code :"creation_conflict" ,Message :"the session ID belongs to another creation intent" } 263 } 264 } 265return Session {},err 266 } 267if session .wire .Creation == nil { 268return Session {},fieldError ("creation" ) 269 } 270if ! sameCreationIntent (handle .operation .Body .(json.RawMessage ),session .wire .Creation .Submitted ) { 271return Session {},& APIError {StatusCode :409 ,Code :"creation_conflict" ,Message :"the session ID belongs to another creation intent" } 272 } 273 274return session ,nil 275} 276func IsOperationExpired (err error )bool { 277api ,ok := errors.AsType [* APIError ](err ) 278return ok && api .StatusCode == http .StatusGone 279} 280 281func sameCreationIntent (expected ,stored json.RawMessage )bool { 282var left ,right map [string ]any 283if json .Unmarshal (expected ,& left )!= nil || json .Unmarshal (stored ,& right )!= nil { 284return false 285 } 286var optional []string 287switch left ["kind" ] { 288case "new" : 289optional = []string {"name" ,"provider_profile" ,"model" ,"effort" } 290case "fork" : 291optional = []string {"name" } 292case "child" : 293optional = []string {"model" ,"effort" } 294 } 295for _ ,key := range optional { 296if _ ,ok := left [key ];! ok { 297left [key ]= nil 298 } 299 } 300return reflect .DeepEqual (left ,right ) 301}