char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
f4dfe65
main
1// The harness's one direct tool: model-written javascript that calls every other tool. 2 3import type { JsonValue } from "@earendil-works/chord" ; 4import type { ImageContent , TextContent , TSchema } from "@earendil-works/pi-ai" ; 5import { 6type CodemodeJsonSchema , 7type CodemodeOutputItem , 8type CodemodeResult , 9CodemodeSandbox , 10CodemodeSourceError , 11parseCodemodeSource , 12renderToolOutputType , 13renderToolSignature , 14toCodemodeIdentifier , 15} from "@earendil-works/pi-codemode" ; 16 17export const DESCRIPTION = 18`Run raw JavaScript (not JSON, no code fence) as an async function body in a QuickJS sandbox, without Node, file system, network, or timers. 19- \`await tools.<name>(args)\` calls a tool declared in the system prompt, rejecting on failure; calls still running when the script ends are cancelled. 20- Only \`text(value)\`, \`image(imageBlock)\`, \`console.log(...)\`, and \`return value\` reach you; \`exit()\` ends the script. 21- \`store(key, value)\` and \`load(key)\` keep JSON values across calls. 22- Optional first line: \`// @options: {"max_output_tokens": 10000, "timeout_ms": 60000}\` 23- Batch independent calls (\`Promise.allSettled\`) and filter large values in one script rather than many calls.` ; 24 25export type StoreWrite = { set :Record < string , JsonValue >; delete :string [] }; 26 27/** How a script's `store()` writes fold into the values `load()` reads. */ 28export function applyStoreWrite ( 29store :Record < string , JsonValue >, 30write :StoreWrite , 31) :Record < string , JsonValue > { 32const next = { ...store }; 33for ( const key of write . delete ) delete next [ key ]; 34return Object . assign ( next , write . set ); 35} 36 37/** A tool as scripts see it. */ 38export type Declared = { 39readonly name :string ; 40readonly extension :string ; 41readonly description :string ; 42readonly parameters :TSchema ; 43readonly returns ?:TSchema ; 44/** Whether the model can also call it directly, and so already has its description. */ 45readonly direct ?:boolean ; 46}; 47 48export type NestedTool = Declared & { 49call ( args :JsonValue , options :{ 50signal :AbortSignal ; 51output ( chunk :string ) :void ; 52progress ( details :JsonValue ) :void ; 53}) :Promise < JsonValue | undefined >; 54}; 55 56/** One nested call as the ui sees it. Values, output, and errors are clipped; the model never sees any of it. */ 57export type NestedCall = { 58name :string ; 59extension :string ; 60args :JsonValue ; 61status :"running" | "ok" | "error" | "cancelled" ; 62/** Wall-clock milliseconds, so a reader that comes back mid-call can still tell how long it has run. */ 63startedAt :number ; 64durationMs ?:number ; 65output ?:string ; 66details ?:JsonValue ; 67value ?:JsonValue ; 68error ?:string ; 69}; 70 71export type CodemodeDetails = { 72/** Wall-clock milliseconds, as a nested call's. */ 73startedAt :number ; 74/** Once the script has ended. */ 75durationMs ?:number ; 76calls :NestedCall []; 77fullOutputPath ?:string ; 78}; 79 80const MEMORY_LIMIT_BYTES = 256 * 1024 * 1024 ; 81const DEFAULT_MAX_OUTPUT_TOKENS = 10_000 ; 82const CHARS_PER_TOKEN = 4 ; 83const DETAIL_CHARS = 16 * 1024 ; 84 85const declaration = ( tool :Declared ) => ({ 86name :tool . name , 87description :tool . description , 88inputSchema :tool . parameters as CodemodeJsonSchema , 89outputSchema :( tool . returns ?? { type :"string" }) as CodemodeJsonSchema , 90}); 91 92/** The codemode section of the system prompt: direct tools' results and codemode-only declarations. */ 93export function renderCatalog ( tools :readonly Declared []) :string | undefined { 94if ( tools . length === 0 ) return undefined ; 95const direct :string [] = []; 96const nested :string [] = []; 97for ( const tool of tools ) { 98const declared = declaration ( tool ); 99if ( tool . direct ) { 100direct . push ( 101` ${ toCodemodeIdentifier ( tool . name )} : ${ 102renderToolOutputType ( declared . outputSchema ). replaceAll ( "\n" , "\n " ) 103} ;` , 104); 105} else { 106const doc = tool . description . trim (). replaceAll ( "*/" , "*\\/" ). replaceAll ( "\n" , "\n * " ); 107nested . push ( 108` ${ doc ? ` /** ${ doc } */\n` : "" } ${ 109renderToolSignature ( declared ). replaceAll ( "\n" , "\n " ) 110} ` , 111); 112} 113} 114const blocks = [ 115 ...( direct . length ?[ `type DirectToolResults = {\n ${ direct . join ( "\n" )} \n};` ] :[]), 116 ...( nested . length ?[ `declare const tools: {\n ${ nested . join ( "\n" )} \n};` ] :[]), 117]; 118return "Codemode tools: DirectToolResults lists direct tools callable as `await tools.<name>(args)` " + 119"with their native arguments and the resolved return types. Additional tools are declared below.\n\n" + 120`\`\`\`ts\n ${ blocks . join ( "\n\n" )} \n\`\`\`` ; 121} 122 123const clip = ( 124text :string , 125) => ( text . length > DETAIL_CHARS ?` ${ text . slice ( 0 , DETAIL_CHARS )} …` :text ); 126const messageOf = ( error :unknown ) => ( error instanceof Error ?error . message :String ( error )); 127 128/** 129* With more than one text item, each starts with a `==> text N/M <==` line, since providers join adjacent text 130* blocks; `console.*` lines follow everything else in one `<console_output>` block. 131*/ 132function layout ( output :readonly CodemodeOutputItem []) :( TextContent | ImageContent )[] { 133const total = output . filter (( item ) => item . type === "text" && ! item . console ). length ; 134const items :( TextContent | ImageContent )[] = []; 135const consoleLines :string [] = []; 136let index = 0 ; 137for ( const item of output ) { 138if ( item . type === "image" ) items . push ( item ); 139else if ( item . console ) consoleLines . push ( item . text ); 140else { items . push ({ 141type :"text" , 142text :total > 1 ?`==> text ${ ++ index } / ${ total } <==\n ${ item . text } ` :item . text , 143});} 144} 145if ( consoleLines . length > 0 ) { 146items . push ({ 147type :"text" , 148text :`<console_output>\n ${ consoleLines . join ( "\n" )} \n</console_output>` , 149}); 150} 151return items ; 152} 153 154/** Over budget, the text keeps its start and end, images follow it, and the whole text goes to a file. */ 155async function budget ( 156items :( TextContent | ImageContent )[], 157maxTokens :number , 158) :Promise <{ items :( TextContent | ImageContent )[]; fullOutputPath ?:string }> { 159const text = items . flatMap (( item ) => ( item . type === "text" ?[ item . text ] :[])). join ( "\n" ); 160const chars = maxTokens * CHARS_PER_TOKEN ; 161if ( text . length <= chars ) return { items}; 162const head = text . slice ( 0 , Math . floor ( chars / 2 )); 163const tail = text . slice ( text . length - Math . ceil ( chars / 2 )); 164const omitted = Math . ceil (( text . length - head . length - tail . length ) / CHARS_PER_TOKEN ); 165const fullOutputPath = await Deno . makeTempFile ({ prefix :"berry-codemode-" , suffix :".txt" }); 166await Deno . writeTextFile ( fullOutputPath , text ); 167return { 168items :[ 169{ 170type :"text" , 171text :` ${ head } … ${ omitted } tokens truncated… ${ tail } \n\n[Full output: ${ fullOutputPath } ]` , 172}, 173 ...items . filter (( item ) => item . type === "image" ), 174], 175 fullOutputPath, 176}; 177} 178 179function failure ( 180result :Extract < CodemodeResult , { ok :false }>, 181calls :readonly NestedCall [], 182) :string { 183const { error} = result ; 184const head = error . kind === "script" 185 ?( error . stack ?? ` ${ error . name ?? "Error" } : ${ error . message } ` ) 186 :`Script ${ 187error . kind === "sandbox" 188? "sandbox failed" 189: error . kind === "timeout" 190? "timed out" 191: "aborted" 192} : ${ error . message } ` ; 193const made = calls . length === 0 194 ?"No tool calls were made." 195 :`Tool calls made before the failure (they are not undone): ${ 196calls . map (( c ) => ` ${ c . name } ( ${ c . status } )` ). join ( ", " ) 197} ` ; 198return `Script error:\n ${ head } \n\n ${ made } ` ; 199} 200 201export type ScriptResult = { 202content :( TextContent | ImageContent )[]; 203isError :boolean ; 204details :CodemodeDetails ; 205/** Only successful scripts write. */ 206storeWrites ?:StoreWrite ; 207}; 208 209export async function runScript ( source :string , { tools, store, signal, progress} :{ 210tools :readonly NestedTool []; 211store :Readonly < Record < string , JsonValue >>; 212signal :AbortSignal ; 213progress ( details :CodemodeDetails ) :void ; 214}) :Promise < ScriptResult > { 215let parsed ; 216try { 217parsed = parseCodemodeSource ( source ); 218} catch ( error ) { 219if ( ! ( error instanceof CodemodeSourceError )) throw error ; 220return { 221content :[{ type :"text" , text :`Script failed\n\n ${ error . message } ` }], 222isError :true , 223details :{ startedAt :Date . now (), durationMs :0 , calls :[] }, 224}; 225} 226 227const startedAt = Date . now (); 228const calls :NestedCall [] = []; 229const publish = () => progress ({ startedAt, calls :structuredClone ( calls ) }); 230publish (); 231const sandbox = new CodemodeSandbox ({ 232tools :tools . map (( tool ) => ({ 233 ...declaration ( tool ), 234execute :async ( args , { signal}) => { 235const call :NestedCall = { 236name :tool . name , 237extension :tool . extension , 238args :args as JsonValue , 239status :"running" , 240startedAt :Date . now (), 241}; 242calls . push ( call ); 243publish (); 244try { 245const value = await tool . call ( args as JsonValue , { 246 signal, 247output :( chunk ) => { 248call . output = clip (( call . output ?? "" ) + chunk ); 249publish (); 250}, 251progress :( details ) => { 252call . details = details ; 253publish (); 254}, 255}); 256call . status = "ok" ; 257if ( value !== undefined && JSON . stringify ( value ). length <= DETAIL_CHARS ) { 258call . value = value ; 259} 260return value ; 261} catch ( error ) { 262call . status = signal . aborted ?"cancelled" :"error" ; 263call . error = clip ( messageOf ( error )); 264throw error ; 265} finally { 266call . durationMs = Date . now () - call . startedAt ; 267publish (); 268} 269}, 270})), 271timeoutMs :parsed . options . timeoutMs ?? Number . POSITIVE_INFINITY , 272memoryLimitBytes :MEMORY_LIMIT_BYTES , 273}); 274 275let result :CodemodeResult ; 276try { 277result = await sandbox . execute ( parsed . code , { signal, store}); 278} finally { 279await sandbox . close (); 280} 281// still running: cut off by the script ending, a timeout, or an abort 282for ( const call of calls ) if ( call . status === "running" ) call . status = "cancelled" ; 283 284const output = [ ...result . output ]; 285if ( result . ok && result . value !== undefined ) { 286output . push ({ 287type :"text" , 288text :typeof result . value === "string" ?result . value :JSON . stringify ( result . value ), 289}); 290} 291const items = layout ( output ); 292if ( ! result . ok ) items . push ({ type :"text" , text :failure ( result , calls ) }); 293const { items :shown , fullOutputPath} = await budget ( 294items , 295parsed . options . maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS , 296); 297 298const writes = result . ok ?result . storeWrites :undefined ; 299const wrote = writes && ( Object . keys ( writes . set ). length > 0 || writes . delete . length > 0 ); 300return { 301content :[{ type :"text" , text :result . ok ?"Script completed" :"Script failed" }, ...shown ], 302isError :! result . ok , 303details :{ 304 startedAt, 305durationMs :Date . now () - startedAt , 306 calls, 307 ...( fullOutputPath ?{ fullOutputPath} :{}), 308}, 309 ...( wrote 310 ?{ storeWrites :{ set :writes . set as Record < string , JsonValue >, delete :writes . delete } } 311 :{}), 312}; 313}