char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
ed34ebd
main
1import { homedir } from "node:os" ; 2import { serveDir } from "@std/http/file-server" ; 3import { fromFileUrl , join , resolve } from "@std/path" ; 4import { builtinModels } from "@earendil-works/pi-ai/providers/all" ; 5import { openNodeSqliteStorage } from "@earendil-works/pi-durable/storage/sqlite/node" ; 6import { piAuthStore } from "./auth.ts" ; 7import { type Host , openBerry } from "./berry.ts" ; 8import { createHandler } from "./http.ts" ; 9import { denoCheck , extensionLoader } from "./loader.ts" ; 10import { RESTART } from "./supervisor.ts" ; 11import { extensionWeb } from "./web.ts" ; 12 13const token = Deno . env . get ( "BERRY_TOKEN" ); 14if ( ! token ) { 15console . error ( "BERRY_TOKEN must be set" ); 16Deno . exit ( 1 ); 17} 18 19const root = fromFileUrl ( new URL ( "../" , import . meta. url )); 20const directories = [ 21join ( root , "extensions" ), 22resolve ( Deno . env . get ( "XDG_CONFIG_HOME" ) || join ( homedir (), ".config" ), "berry" , "extensions" ), 23]; 24const credentials = piAuthStore ( Deno . env . get ( "BERRY_AUTH" )); 25const models = builtinModels ({ credentials}); 26if ( Deno . env . get ( "BERRY_DEV_MODEL" )) ( await import ( "./dev.ts" )). installDevModel ( models ); 27const storage = await openNodeSqliteStorage ( 28Deno . env . get ( "BERRY_DB" ) ?? join ( root , "data" , "berry.sqlite" ), 29); 30 31// a restart or a signal takes the process down; a check still running would hold it up 32const checks = new AbortController (); 33 34const host :Host = { 35 root, 36extensionDirectories :directories , 37// the server, and the extensions it loads at startup, which it imports by name rather than statically 38check :async () => { 39const started = performance . now (); 40const failures = await denoCheck ( 41root , 42[ 43"server/main.ts" , 44 ...loader . files (), 45], 46checks . signal , 47); 48// a restart's whole budget: this, then the stop below, then the new process saying it is listening 49console . log ( `[berry] checked in ${ Math . round ( performance . now () - started )} ms` ); 50return failures ; 51}, 52restart :() => void stop ( RESTART ), 53extensions :() => loader . entries (), 54reload :( name ) => loader . reload ( name ), 55}; 56 57const berry = await openBerry ( storage , models , host ); 58const web = extensionWeb ({ 59cacheDirectory :join ( 60Deno . env . get ( "XDG_CACHE_HOME" ) || join ( homedir (), ".cache" ), 61"berry" , 62"frontends" , 63), 64}); 65const loader = extensionLoader ({ berry, web, root, directories}); 66await loader . loadAll (); 67loader . watch (); 68berry . harness . resume (); 69 70const handler = createHandler ({ 71 berry, 72 models, 73 credentials, 74 web, 75 loader, 76 host, 77 token, 78// the built frontend, so one port serves everything; `deno task web` serves it with hmr instead 79fallback :( request ) => serveDir ( request , { fsRoot :join ( root , "web" , "dist" ), quiet :true }), 80}); 81const server = Deno . serve ({ 82hostname :Deno . env . get ( "BERRY_HOST" ) ?? "127.0.0.1" , 83port :Number ( Deno . env . get ( "BERRY_PORT" ) ?? 8787 ), 84}, handler ); 85// from process start, so a restart's budget is this and the checked/stopped lines beside it 86console . log ( `[berry] ready in ${ Math . round ( performance . now ())} ms` ); 87 88// once, whatever asks first: a signal or a restart; another request waits for that one 89let stopping :Promise < never > | undefined ; 90/** 91* How long teardown may take before the process leaves anyway: the database is a WAL, and every child watches its own 92* stdin, so what is still closing is left to end on its own rather than holding a restart. 93*/ 94const STOP_DEADLINE_MS = 2_000 ; 95/** How long an in-flight request may take to finish before the process leaves without it. */ 96const REQUEST_GRACE_MS = 500 ; 97function stop ( code :number ) :Promise < never > { 98stopping ??= ( async () => { 99checks . abort ( new Error ( "berry is stopping" )); 100const started = performance . now (); 101// where a slow shutdown goes; the only place these numbers can be seen 102const phases :string [] = []; 103const phase = async ( name :string , run :() => unknown ) => { 104const at = performance . now (); 105await run (); 106phases . push ( ` ${ name } ${ Math . round ( performance . now () - at )} ms` ); 107}; 108const closing = ( async () => { 109await phase ( "sockets" , () => handler . closeSockets ()); 110// a request of its own, such as a model refresh from a slow provider, ends with the process rather than holding it 111await phase ( 112"http" , 113() => 114Promise . race ([ 115server . shutdown (), 116new Promise (( resolve ) => setTimeout ( resolve , REQUEST_GRACE_MS )), 117]), 118); 119await phase ( "extensions" , () => loader . close ()); 120// signals every running call, and waits for them to end; durable resumes the work when berry opens again 121await phase ( "berry" , () => berry . close ()); 122})(). catch (( error ) => console . error ( "[berry] stopping:" , error )); 123const overdue = await Promise . race ([ 124closing . then (() => false ), 125new Promise < boolean >(( resolve ) => setTimeout (() => resolve ( true ), STOP_DEADLINE_MS )), 126]); 127const left = [ "sockets" , "http" , "extensions" , "berry" ]. filter (( name ) => 128! phases . some (( line ) => line . startsWith ( ` ${ name } ` )) 129); 130console . log ( 131`[berry] stopped in ${ Math . round ( performance . now () - started )} ms ( ${ phases . join ( ", " )} )` + 132( overdue && left . length 133 ?`; gave up on ${ left . join ( ", " )} after ${ STOP_DEADLINE_MS } ms` 134 :"" ), 135); 136Deno . exit ( code ); 137})(); 138return stopping ; 139} 140for ( const signal of [ "SIGINT" , "SIGTERM" ] as const ) { 141Deno . addSignalListener ( signal , () => void stop ( 0 )); 142}