char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
e8f8604
main
1import { assert , assertEquals , assertRejects , assertStringIncludes } from "@std/assert" ; 2import { BACKGROUND_CONTEXT as context } from "@earendil-works/chord/context" ; 3import { Type } from "@earendil-works/pi-ai" ; 4import { fauxAssistantMessage , fauxToolCall } from "@earendil-works/pi-ai/providers/faux" ; 5import { mkdir , symlink , writeFile } from "node:fs/promises" ; 6import { homedir } from "node:os" ; 7import { join } from "node:path" ; 8import { defineExtension } from "@berry/api" ; 9import { type Environment , localEnvironment } from "@berry/api/environment.ts" ; 10import { BerryError , Settings } from "./berry.ts" ; 11import { type Environments , type EnvironmentSettings , localOnly } from "./environment.ts" ; 12import { lastToolResult , script , sectionsSeen , setup , turn } from "./fixture.ts" ; 13import { tempDir } from "@berry/util" ; 14 15/** 16* This machine as `local`, a directory of it posing as a machine named `other`, and a machine named `down` that 17* cannot be reached. 18*/ 19function twoMachines ( otherHome :string ) :Environments { 20return { 21open ( name , cwd , during ) { 22if ( name === "down" ) return Promise . reject ( new Error ( "down is unreachable" )); 23if ( name !== "other" ) return localOnly . open ( name , cwd , during ); 24const env :Environment = Object . assign ( localEnvironment ( cwd ?? otherHome ), { 25 name, 26home :otherHome , 27}); 28return Promise . resolve ( env ); 29}, 30close :() => Promise . resolve (), 31}; 32} 33 34async function withDirectories ( test :( root :string ) => Promise < void >) { 35await usingrootDir = await tempDir ({ prefix :"berry-environment-" }); 36const root = rootDir . path ; 37await mkdir ( join ( root , "a" )); 38await mkdir ( join ( root , "b" )); 39await mkdir ( join ( root , "other" )); 40await writeFile ( join ( root , "file" ), "" ); 41await symlink ( join ( root , "a" ), join ( root , "link" )); 42await test ( root ); 43} 44 45Deno . test ( "a conversation works in the local home until bound to a directory its machine resolves, which /cd moves" , () => 46withDirectories ( async ( root ) => { 47await usingharness = await setup ( twoMachines ( join ( root , "other" ))); 48const { berry, model} = harness ; 49const home = await berry . create ({ extensions :{}, model}); 50assertEquals ( await berry . state ( home , "environment" ), { environment :"local" , cwd :null }); 51const env = await berry . environment ( home ); 52assertEquals ([ env . name , env . cwd ], [ "local" , homedir ()]); 53 54const tid = await berry . create ({ 55extensions :{ environment :{ cwd :join ( root , "link" ) } }, 56 model, 57}); 58assertEquals (( await berry . environment ( tid )). cwd , join ( root , "a" )); 59for ( const cwd of [ join ( root , "file" ), join ( root , "missing" )]) { 60await assertRejects (() => berry . create ({ extensions :{ environment :{ cwd} }, model})); 61} 62await assertRejects ( 63() => berry . create ({ extensions :{ environment :{ environment :"elsewhere" } }, model}), 64BerryError , 65"No environment named elsewhere" , 66); 67await berry . configureExtension ( tid , "environment" , { cwd :"../b" }); 68assertEquals (( await berry . environment ( tid )). cwd , join ( root , "b" )); 69 70const other = await berry . create ({ 71extensions :{ environment :{ environment :"other" , cwd :join ( root , "a" ) } }, 72 model, 73}); 74assertEquals ( await berry . command ( other , "cd" , "../link" ), join ( root , "a" )); 75assertEquals ( await berry . command ( other , "cd" , " ../b " ), join ( root , "b" )); 76await assertRejects (() => berry . command ( other , "cd" , "../file" ), BerryError , "Not a directory" ); 77await assertRejects (() => berry . command ( other , "cd" , "missing" ), BerryError ); 78// home is the machine's 79assertEquals ( await berry . command ( other , "cd" , "" ), join ( root , "other" )); 80assertEquals ( await berry . state ( other , "environment" ), { 81environment :"other" , 82cwd :join ( root , "other" ), 83}); 84})); 85 86Deno . test ( "the code extension's working directory carries over from old history" , async () => { 87await usingharness = await setup (); 88const { berry, model} = harness ; 89const tid = await berry . create ({ extensions :{}, model}); 90await ( await berry . emit ( tid , "coding" , { type :"cd" , cwd :"/first" })). wait ( context ); 91assertEquals ( await berry . state ( tid , "environment" ), { environment :"local" , cwd :"/first" }); 92await ( await berry . emit ( tid , "code" , { type :"cd" , cwd :"/second" })). wait ( context ); 93assertEquals ( await berry . state ( tid , "environment" ), { environment :"local" , cwd :"/second" }); 94}); 95 96Deno . test ( "only an idle conversation moves to another machine; a new directory waits for the boundary" , () => 97withDirectories ( async ( root ) => { 98await usingharness = await setup ( twoMachines ( join ( root , "other" ))); 99const { berry, model, faux, gate} = harness ; 100const tid = await berry . create ({ 101extensions :{ environment :{ cwd :join ( root , "a" ) }, gate :{} }, 102 model, 103}); 104faux . setResponses ([ 105fauxAssistantMessage ( fauxToolCall ( "hold" , {}), { stopReason :"toolUse" }), 106fauxAssistantMessage ( "done" ), 107]); 108const conversation = ( await berry . conversation ( tid )) ! ; 109await conversation . submit ({ type :"input" , content :"go" }, context ); 110await gate . entered . promise ; 111 112await assertRejects ( 113() => berry . configureExtension ( tid , "environment" , { environment :"other" }), 114BerryError , 115"busy" , 116); 117const moved = berry . configureExtension ( tid , "environment" , { cwd :join ( root , "b" ) }); 118assertEquals ( await berry . state ( tid , "environment" ), { 119environment :"local" , 120cwd :join ( root , "a" ), 121}); 122gate . release . resolve ( "released" ); 123assertEquals ( await moved , { environment :"local" , cwd :join ( root , "b" ) }); 124await conversation . waitForIdle ( context ); 125 126// the form still holds this machine's directory, which means nothing on the other 127assertEquals ( 128await berry . configureExtension ( tid , "environment" , { 129environment :"other" , 130cwd :join ( root , "b" ), 131}), 132{ environment :"other" , cwd :join ( root , "other" ) }, 133); 134const env = await berry . environment ( tid ); 135assertEquals ([ env . name , env . cwd ], [ "other" , join ( root , "other" )]); 136})); 137 138/** Reports the environment wherever an extension meets it. */ 139const probe = defineExtension ({ 140name :"probe" , 141initial :() => null , 142setup ( ext ) { 143const where = ( env :Environment ) => ` ${ env . name } : ${ env . cwd } ` ; 144ext . tool ({ 145name :"where" , 146description :"where calls run" , 147parameters :Type . Object ({}), 148execute :( _ , ctx ) => where ( ctx . env ), 149}); 150ext . prompt ( "named" , ({ env}) => env && `named ${ where ( env )} ` ); 151return ( conv ) => { 152conv . prompt (({ env}) => env && `in ${ where ( env )} ` ); 153conv . command ({ name :"go" , description :"" , execute :( path , ctx ) => ctx . cd ( path ) }); 154conv . command ({ name :"here" , description :"" , execute :( _ , ctx ) => where ( ctx . env ) }); 155conv . command ({ name :"ping" , description :"" , execute :() => "pong" }); 156}; 157}, 158}); 159 160Deno . test ( "calls, prompts, and commands all meet the conversation's environment, and fail with a machine out of reach" , () => 161withDirectories ( async ( root ) => { 162await usingharness = await setup ( twoMachines ( join ( root , "other" ))); 163const { berry, model} = harness ; 164await berry . install ( probe ); 165const tid = await berry . create ({ 166extensions :{ environment :{ cwd :join ( root , "a" ) }, probe :{} }, 167 model, 168}); 169const seen = await turn ( harness , tid , [ script ( "text(await tools.where({}))" )]); 170assertEquals ( lastToolResult ( seen [ 1 ]). text , `Script completed\nlocal: ${ join ( root , "a" )} ` ); 171const sections = sectionsSeen ( seen [ 0 ])[ 0 ]; 172assertEquals ( sections . probe , `<probe>\nin local: ${ join ( root , "a" )} \n</probe>` ); 173assertEquals ( 174sections . probe_named , 175`<probe_named>\nnamed local: ${ join ( root , "a" )} \n</probe_named>` , 176); 177assertEquals ( sections . environment , undefined ); 178 179assertEquals ( await berry . command ( tid , "go" , "../b" ), join ( root , "b" )); 180assertEquals ( await berry . command ( tid , "here" , "" ), `local: ${ join ( root , "b" )} ` ); 181await berry . configureExtension ( tid , "environment" , { environment :"other" }); 182const [ moved ] = await turn ( harness , tid , []); 183assertEquals ( 184sectionsSeen ( moved ). at ( - 1 )?. probe , 185`<probe>\nin other: ${ join ( root , "other" )} \n</probe>` , 186); 187assertEquals ( 188sectionsSeen ( moved ). at ( - 1 )?. environment ?. includes ( "files and commands are on other" ), 189true , 190); 191 192// as though it went down after the conversation moved there 193await ( await berry . emit ( tid , "environment" , { type :"bind" , environment :"down" , cwd :"/" })) 194. wait ( context ); 195const down = await turn ( harness , tid , [ script ( "text(await tools.where({}))" )]); 196const result = lastToolResult ( down [ 1 ]); 197assertEquals ( result . isError , true ); 198assertEquals ( result . text . includes ( "down is unreachable" ), true ); 199const unreachable = sectionsSeen ( down [ 0 ]). at ( - 1 ) ! ; 200assertEquals ( unreachable . probe , null ); 201assertEquals ( unreachable . environment ?. includes ( "down cannot be reached" ), true ); 202assertEquals ( await berry . command ( tid , "ping" , "" ), "pong" ); 203await assertRejects (() => berry . command ( tid , "here" , "" ), BerryError , "down is unreachable" ); 204})); 205 206Deno . test ( "disabling local fails its turns without contacting the model, but remote turns and recovery still work" , async () => { 207await usingroot = await tempDir (); 208await usingharness = await setup ( twoMachines ( root . path )); 209const { berry, model, faux} = harness ; 210const tid = await berry . create ({ extensions :{}, model}); 211await berry . setAllowLocalEnvironment ( false ); 212 213const calls = faux . state . callCount ; 214assertEquals ( await turn ( harness , tid , []), []); 215assertEquals ( faux . state . callCount , calls ); 216const conversation = ( await berry . conversation ( tid )) ! ; 217const entries = await conversation . entries ({ order :"ascending" }, 100 , undefined , context ); 218const failure = entries . items . flatMap (( entry ) => entry . model ?? []). findLast (( message ) => 219message . role === "assistant" 220); 221assert ( failure ?. role === "assistant" ); 222assertEquals ( failure . stopReason , "error" ); 223assertStringIncludes ( failure . errorMessage ?? "" , "local environment is disabled" ); 224await assertRejects (() => berry . environment ( tid ), BerryError , "local environment is disabled" ); 225await assertRejects ( 226() => berry . command ( tid , "cd" , "" ), 227BerryError , 228"local environment is disabled" , 229); 230 231await berry . configureExtension ( tid , "environment" , { environment :"other" }); 232assertEquals (( await turn ( harness , tid , [])). length , 1 ); 233await assertRejects ( 234() => berry . configureExtension ( tid , "environment" , { environment :"local" }), 235BerryError , 236"local environment is disabled" , 237); 238await berry . setAllowLocalEnvironment ( true ); 239await berry . configureExtension ( tid , "environment" , { environment :"local" }); 240assertEquals (( await turn ( harness , tid , [])). length , 1 ); 241}); 242 243Deno . test ( "settings name the other machines, and nothing ssh could take for an option" , async () => { 244await usingharness = await setup (); 245const { berry} = harness ; 246const refusals :Record < string , EnvironmentSettings >[] = [ 247{ local :{ ssh :"elsewhere" } }, 248{ "-oProxyCommand=x" :{ ssh :"elsewhere" } }, 249{ devbox :{ ssh :"-oProxyCommand=x" } }, 250{ devbox :{ ssh :" " } }, 251{ devbox :{ ssh :"devbox" , deno :"" } }, 252]; 253for ( const refused of refusals ) { 254await assertRejects (() => berry . setEnvironments ( refused ), BerryError ); 255} 256await berry . setEnvironments ({ devbox :{ ssh :" me@devbox " , deno :"/opt/deno" } }); 257assertEquals (( await berry . harness . snapshot ( Settings , context ))?. environments , { 258devbox :{ ssh :"me@devbox" , deno :"/opt/deno" }, 259}); 260});