okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
6e82811
main
1package app 2 3import ( 4"context" 5"errors" 6"fmt" 7"slices" 8"time" 9 10"albedo/cli/internal/daemon" 11) 12 13type PromptOptions struct { 14Prompt ,SessionID ,Workspace ,Model string 15Timeout time.Duration 16} 17type PromptResult struct {SessionID ,Answer string } 18 19// RunPrompt waits for the reply to this invocation's turn. Zero timeout is unlimited. 20func (s * Service )RunPrompt (ctx context.Context ,options PromptOptions ) (PromptResult ,error ) { 21prompt ,sessionID ,cwd ,model ,timeout := options .Prompt ,options .SessionID ,options .Workspace ,options .Model ,options .Timeout 22if timeout > 0 { 23var cancel context.CancelFunc 24ctx ,cancel = context .WithTimeout (ctx ,timeout ) 25defer cancel () 26 } 27conn ,err := s .Connect (ctx ) 28if err != nil { 29return PromptResult {},err 30 } 31var choice modelChoice 32if model != "" { 33profiles ,profileErr := daemon .ProviderProfiles (ctx ,conn ) 34if profileErr != nil { 35return PromptResult {},profileErr 36 } 37if choice ,err = chooseModel (configuredModels (ctx ,conn ,profiles ),profiles .Active ,model );err != nil { 38return PromptResult {},err 39 } 40 } 41if sessionID != "" { 42if sessionID ,err = resolveSession (ctx ,conn ,sessionID );err != nil { 43return PromptResult {},err 44 } 45 } 46if sessionID == "" { 47create := daemon.CreateSessionRequest {Workspace :cwd } 48if model != "" { 49create .Provider ,create .Model = choice .provider ,choice .model 50 } 51session ,createErr := daemon .CreateSession (ctx ,conn ,create ) 52if createErr != nil { 53return PromptResult {},createErr 54 } 55sessionID = session .ID 56 }else if model != "" { 57if switchErr := switchModel (ctx ,conn ,sessionID ,choice );switchErr != nil { 58return PromptResult {},switchErr 59 } 60 } 61 62client := daemon .NewChatClient (conn ,sessionID ) 63answer ,err := awaitReply (ctx ,client ,prompt ) 64if err == nil { 65return PromptResult {SessionID :sessionID ,Answer :answer },nil 66 } 67switch { 68case errors .Is (ctx .Err (),context .DeadlineExceeded ): 69return PromptResult {},fmt .Errorf ("timed out after %s; %w; the session is %s" ,timeout ,err ,sessionID ) 70case ctx .Err ()!= nil : 71return PromptResult {},fmt .Errorf ("interrupted; %w; the session is %s" ,err ,sessionID ) 72 } 73if _ ,uncertain := errors.AsType [* daemon.UncertainOutcomeError ](err );uncertain { 74return PromptResult {},fmt .Errorf ("%w; the session is %s" ,err ,sessionID ) 75 } 76return PromptResult {},err 77} 78 79// awaitReply follows the logical turn containing this submission. The actor 80// publishes membership before any worker events, and completion after them. 81func awaitReply (ctx context.Context ,client * daemon.ChatClient ,prompt string ) (answer string ,failure error ) { 82handle ,err := client .PrepareTurn (prompt ,nil ,nil ,false ) 83if err != nil { 84return "" ,err 85 } 86submissionID := handle .ID () 87defer func () { 88if failure != nil && ctx .Err ()!= nil { 89if _ ,uncertain := errors.AsType [* daemon.UncertainOutcomeError ](failure );! uncertain { 90uncertain := & daemon.UncertainOutcomeError {Operation :"await turn" ,Handle :handle ,Cause :failure } 91failure = fmt .Errorf ("%w; %v" ,uncertain ,failure ) 92 } 93 } 94 }() 95streamCtx ,cancel := context .WithCancel (ctx ) 96defer cancel () 97submitted := false 98defer func () { 99if failure == nil || ! submitted { 100return 101 } 102cleanupCtx ,stop := context .WithTimeout (context .Background (),5 * time .Second ) 103defer stop () 104outcome ,err := client .CancelSubmission (cleanupCtx ,submissionID ) 105if err != nil { 106failure = fmt .Errorf ("%w; cancellation unconfirmed: %v" ,failure ,err ) 107return 108 } 109switch outcome { 110case "cancelled_queued" : 111failure = fmt .Errorf ("%w; queued submission cancelled" ,failure ) 112case "interrupt_requested" : 113failure = fmt .Errorf ("%w; interruption requested for the turn" ,failure ) 114case "shared_running" : 115failure = fmt .Errorf ("%w; the shared turn is still running" ,failure ) 116case "not_pending" : 117failure = fmt .Errorf ("%w; submission is no longer pending" ,failure ) 118 } 119 }() 120ready := make (chan struct {}) 121events := make (chan daemon.StreamEvent ,64 ) 122streamDone := make (chan error ,1 ) 123go func () { 124streamDone <- client .Stream (streamCtx ,0 ,func (event daemon.StreamEvent )error { 125if event .Type == daemon .EventReset { 126select { 127case <- ready : 128default : 129close (ready ) 130 } 131 } 132if event .Replayed { 133return nil 134 } 135select { 136case events <- event : 137return nil 138case <- streamCtx .Done (): 139return streamCtx .Err () 140 } 141 }) 142 }() 143select { 144case <- ready : 145case err := <- streamDone : 146return "" ,fmt .Errorf ("could not follow the session: %w" ,err ) 147case <- ctx .Done (): 148return "" ,ctx .Err () 149 } 150// A failed request can already have reached the actor. Cleanup always uses 151// its identity, so it cannot interrupt a different client's turn. 152submitted = true 153if _ ,err := client .SubmitOperation (ctx ,handle );err != nil { 154return "" ,err 155 } 156turnID := "" 157// Discard the initial reset already acknowledged before submission. 158initialReset := true 159for { 160select { 161case <- ctx .Done (): 162return "" ,ctx .Err () 163case err := <- streamDone : 164return "" ,& daemon.UncertainOutcomeError {Operation :"await turn" ,Handle :handle ,Cause :fmt .Errorf ("lost the session stream before the turn finished: %v" ,err )} 165case event := <- events : 166if event .Type == daemon .EventReset && initialReset { 167initialReset = false 168continue 169 } 170if event .Type == daemon .EventReset { 171return "" ,& daemon.UncertainOutcomeError {Operation :"await turn" ,Handle :handle ,Cause :errors .New ("session stream reset before the submission completed" )} 172 } 173if event .Type == daemon .EventTurnMembership && slices .Contains (event .SubmissionIDs ,submissionID ) { 174turnID = event .TurnID 175 } 176if turnID == "" || event .TurnID != "" && event .TurnID != turnID { 177continue 178 } 179switch event .Type { 180case daemon .EventMessage : 181answer ,failure = event .Text ,nil 182case daemon .EventError : 183failure = errors .New (event .Text ) 184case daemon .EventInterrupted : 185failure = errors .New ("the turn was interrupted" ) 186case daemon .EventRetry : 187failure = nil 188case daemon .EventTurnCompleted : 189if event .TurnID != turnID { 190continue 191 } 192if event .Source == "interrupted" { 193return "" ,errors .New ("the turn was interrupted" ) 194 } 195if event .Source == "failed" && failure == nil { 196failure = errors .New ("the turn failed" ) 197 } 198if failure != nil { 199return "" ,failure 200 } 201if answer == "" { 202return "" ,errors .New ("the turn ended without an assistant reply" ) 203 } 204return answer ,nil 205 } 206 } 207 } 208}