char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
e6df845
main
1// Provider logins run here, on the server; their prompts and events cross to the browser as a live state. 2 3import type { AuthEvent , AuthPrompt , AuthType , Models } from "@earendil-works/pi-ai" ; 4import { BerryError } from "./berry.ts" ; 5import { type Feed , published } from "./pubsub.ts" ; 6 7type Prompt = Omit < AuthPrompt , "signal" > & { id :number }; 8 9export type LoginState = { 10provider :string ; 11type :AuthType ; 12status :"running" | "done" | "failed" | "cancelled" ; 13events :AuthEvent []; 14/** What the flow is waiting for the user to answer, if anything. */ 15prompt :Prompt | null ; 16error :string | null ; 17}; 18 19const SETTLED_LINGER_MS = 10 * 60_000 ; 20 21export function createLogins ( models :Models ) { 22const logins = new Map < string , { 23state :ReturnType < typeof published < LoginState >>; 24answers :Map < number , PromiseWithResolvers < string >>; 25controller :AbortController ; 26}>(); 27let nextPrompt = 0 ; 28 29return { 30start ( provider :string , type :AuthType ) :string { 31const methods = models . getProvider ( provider )?. auth ; 32if ( ! ( type === "oauth" ?methods ?. oauth :methods ?. apiKey ?. login )) { 33throw new BerryError ( ` ${ provider } has no ${ type === "oauth" ? "oauth" : "api key" } login` ); 34} 35const id = crypto . randomUUID (); 36const state = published < LoginState >({ 37 provider, 38 type, 39status :"running" , 40events :[], 41prompt :null , 42error :null , 43}); 44const update = ( change :Partial < LoginState >) => state . set ({ ...state . value , ...change }); 45const answers = new Map < number , PromiseWithResolvers < string >>(); 46const controller = new AbortController (); 47logins . set ( id , { state, answers, controller}); 48 49void models . login ( provider , type , { 50signal :controller . signal , 51notify :( event ) => update ({ events :[ ...state . value . events , event ] }), 52prompt :({ signal, ...prompt }) => { 53const answer = Promise . withResolvers < string >(); 54const promptId = nextPrompt ++ ; 55answers . set ( promptId , answer ); 56update ({ prompt :{ ...prompt , id :promptId } as Prompt }); 57// the flow may withdraw its question, as when its callback server answers it first 58const withdraw = () => answer . reject ( signal ?. reason ?? new Error ( "withdrawn" )); 59signal ?. addEventListener ( "abort" , withdraw , { once :true }); 60return answer . promise . finally (() => { 61signal ?. removeEventListener ( "abort" , withdraw ); 62answers . delete ( promptId ); 63if ( state . value . prompt ?. id === promptId ) update ({ prompt :null }); 64}); 65}, 66}) 67. then (() => update ({ status :"done" , prompt :null })) 68. catch (( error ) => 69update ({ 70status :controller . signal . aborted ?"cancelled" :"failed" , 71prompt :null , 72error :controller . signal . aborted 73 ?null 74 :error instanceof Error 75 ?error . message 76 :String ( error ), 77}) 78) 79. finally (() => Deno . unrefTimer ( setTimeout (() => logins . delete ( id ), SETTLED_LINGER_MS ))); 80return id ; 81}, 82 83answer ( id :string , prompt :number , value :string ) { 84const answer = logins . get ( id )?. answers . get ( prompt ); 85if ( answer === undefined ) throw new BerryError ( "that question is no longer asked" ); 86answer . resolve ( value ); 87}, 88 89cancel ( id :string ) { 90logins . get ( id )?. controller . abort (); 91}, 92 93feed ( id :string ) :Feed | undefined { 94return logins . get ( id )?. state . feed (); 95}, 96}; 97} 98 99export type Logins = ReturnType < typeof createLogins >;