char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
1188259
main
1import { 2assert , 3assertEquals , 4assertMatch , 5assertRejects , 6assertStringIncludes , 7} from "@std/assert" ; 8import { BACKGROUND_CONTEXT as context } from "@earendil-works/chord/context" ; 9import { 10type ImageContent , 11type Message , 12type ToolResultMessage , 13Type , 14} from "@earendil-works/pi-ai" ; 15import { createModels } from "@earendil-works/pi-ai/models" ; 16import { 17fauxAssistantMessage , 18fauxProvider , 19fauxToolCall , 20} from "@earendil-works/pi-ai/providers/faux" ; 21import { MemoryStorage } from "@earendil-works/pi-durable" ; 22import { defineExtension } from "@berry/api" ; 23import { mkdir , writeFile } from "node:fs/promises" ; 24import { join } from "node:path" ; 25import { userInfo } from "node:os" ; 26import code from "../extensions/code/mod.ts" ; 27import { BerryError , Conversations , openBerry , Settings } from "./berry.ts" ; 28import { dir , lastToolResult , sectionsSeen , setup , toolsOffered , turn } from "./fixture.ts" ; 29import { TID_PATTERN } from "./tid.ts" ; 30import { localOnly } from "./environment.ts" ; 31import { tempDir } from "@berry/util" ; 32 33Deno . test ( "configured and emitted state reaches the prompt, as an extension's section and as named ones" , async () => { 34await usingharness = await setup (); 35const { berry, model} = harness ; 36await assertRejects ( 37() => berry . create ({ extensions :{ dir :{ cwd :5 } }, model}), 38BerryError , 39"dir: cwd:" , 40); 41await assertRejects ( 42() => berry . create ({ extensions :{ dir :{ cwd :"here" } }, model}), 43BerryError , 44"dir: not absolute: here" , 45); 46assertEquals ( Object . keys ( berry . configurationParameters ( "dir" )?. properties ?? {}), [ "cwd" ]); 47assertEquals ( berry . configurationParameters ( "gate" ), null ); 48 49const tid = await berry . create ({ extensions :{ dir :{ cwd :"/seed" } }, model}); 50assertMatch ( tid , TID_PATTERN ); 51const seen = await turn ( harness , tid , [ 52fauxAssistantMessage ( fauxToolCall ( "cd" , { path :"/next" }), { stopReason :"toolUse" }), 53]); 54assertEquals ( sectionsSeen ( seen [ 0 ])[ 0 ]. dir , "<dir>\ncwd: /seed\n</dir>" ); 55assertEquals ( sectionsSeen ( seen [ 1 ]). at ( - 1 )?. dir , "<dir>\ncwd: /next\n</dir>" ); 56assertEquals ( await berry . state ( tid , "dir" ), { cwd :"/next" }); 57 58await berry . install ( defineExtension ({ 59name :"sections" , 60initial :() => ({ text :"first" }), 61apply :( _state , event :{ text :string }) => event , 62setup ( ext ) { 63ext . prompt ( "extra" , ({ state}) => state . text || undefined ); 64return ( conv ) => conv . prompt (() => "stable" ); 65}, 66})); 67const named = await berry . create ({ extensions :{ sections :{} }, model}); 68const [ initial ] = await turn ( harness , named , []); 69const shown = sectionsSeen ( initial )[ 0 ]; 70assertEquals ( shown . sections , "<sections>\nstable\n</sections>" ); 71assertEquals ( shown [ "sections_extra" ], "<sections_extra>\nfirst\n</sections_extra>" ); 72for ( const text of [ "second" , "" ]) { 73await ( await berry . emit ( named , "sections" , { text})). wait ( context ); 74const [ seen ] = await turn ( harness , named , []); 75assertEquals ( sectionsSeen ( seen ). at ( - 1 ), { 76"sections_extra" :text ?`<sections_extra>\n ${ text } \n</sections_extra>` :null , 77}); 78} 79}); 80 81Deno . test ( "global prompt edits reach existing conversations and code prose follows the code extension" , async () => { 82await usingharness = await setup (); 83const { berry, model} = harness ; 84await berry . install ( code ); 85const coding = await berry . create ({ extensions :{ code :{} }, model}); 86const plain = await berry . create ({ extensions :{}, model}); 87await turn ( harness , coding , []); 88await turn ( harness , plain , []); 89 90await berry . setSystemPrompt ({ 91userName :"Alex" , 92generalGuidelines :"Ask $USER before deleting anything." , 93promptProse :{ code :"Keep $USER's APIs small." }, 94}); 95const [ changed ] = await turn ( harness , coding , []); 96const patch = sectionsSeen ( changed ). at ( - 1 ) ! ; 97assertStringIncludes ( patch . lede ! , "(Alex)" ); 98assertStringIncludes ( patch . guidelines ! , "Ask Alex before deleting anything." ); 99assertStringIncludes ( patch . code ! , "Keep Alex's APIs small." ); 100assertStringIncludes ( patch . code ! , "current working directory:" ); 101assert ( ! Object . hasOwn ( patch , "code_guidelines" )); 102assert ( ! Object . hasOwn ( patch , "code_project" ) || patch [ "code_project" ] === null ); 103 104const [ withoutCode ] = await turn ( harness , plain , []); 105const plainSections = Object . assign ({}, ...sectionsSeen ( withoutCode )); 106assertStringIncludes ( plainSections . guidelines ! , "Ask Alex before deleting anything." ); 107assert ( ! plainSections . code ); 108 109await berry . setSystemPrompt ({ generalGuidelines :"" , promptProse :{ code :"" } }); 110const [ cleared ] = await turn ( harness , coding , []); 111const removed = sectionsSeen ( cleared ). at ( - 1 ) ! ; 112assertEquals ( removed . guidelines , null ); 113// blank prose leaves the dynamic part of the section behind 114assertStringIncludes ( removed . code ! , "current working directory:" ); 115assert ( ! removed . code ! . includes ( "Keep" )); 116assert ( ! Object . hasOwn ( removed , "lede" )); 117}); 118 119Deno . test ( "startup fills missing prompt settings without replacing saved fields or other settings" , async () => { 120await usingdirectory = await tempDir (); 121const path = join ( directory . path , "settings.sqlite" ); 122const { openNodeSqliteStorage} = await import ( "@earendil-works/pi-durable/storage/sqlite/node" ); 123const models = createModels (); 124const berry = await openBerry ( await openNodeSqliteStorage ( path ), models ); 125assertEquals ( 126( await berry . harness . snapshot ( Settings , context ))?. systemPrompt ?. userName , 127userInfo (). username , 128); 129await berry . harness . commit ( async ( tx ) => { 130const settings = await tx . doc ( Settings ); 131settings . models = [{ provider :"offline" , modelId :"kept" }]; 132settings . environments = { remote :{ ssh :"host" } }; 133settings . systemPrompt = { userName :"Sam" , generalGuidelines :"" } as NonNullable < 134typeof settings . systemPrompt 135>; 136}, context ); 137await berry . close (); 138const reopened = await openBerry ( await openNodeSqliteStorage ( path ), models ); 139try { 140const settings = ( await reopened . harness . snapshot ( Settings , context )) ! ; 141assertEquals ( settings . systemPrompt ?. userName , "Sam" ); 142assertEquals ( settings . systemPrompt ?. generalGuidelines , "" ); 143assertEquals ( settings . systemPrompt ?. promptProse ?? {}, {}); 144assertEquals ( settings . models , [{ provider :"offline" , modelId :"kept" }]); 145assertEquals ( settings . environments , { remote :{ ssh :"host" } }); 146} finally { 147await reopened . close (); 148} 149}); 150 151Deno . test ( "AGENTS.md edits update the project section without repeating code guidelines" , async () => { 152await usingrootDir = await tempDir (); 153const root = rootDir . path ; 154await usingharness = await setup (); 155const { berry, model} = harness ; 156await mkdir ( join ( root , "inner" )); 157await writeFile ( join ( root , "AGENTS.md" ), "outer rules" ); 158await writeFile ( join ( root , "inner" , "AGENTS.md" ), "inner rules" ); 159await berry . install ( code ); 160const tid = await berry . create ({ extensions :{ environment :{ cwd :root }, code :{} }, model}); 161const [ initial ] = await turn ( harness , tid , []); 162const shown = sectionsSeen ( initial )[ 0 ]; 163assertStringIncludes ( shown . code ! , `current working directory: ${ root } ` ); 164assertStringIncludes ( shown . code ! , "guidelines:" ); 165assert ( ! shown . code ! . includes ( "outer rules" )); 166assertStringIncludes ( shown [ "code_project" ] ! , "outer rules" ); 167assert ( ! shown [ "code_project" ] ! . includes ( "current working directory:" )); 168 169await writeFile ( join ( root , "AGENTS.md" ), "revised rules" ); 170const [ edited ] = await turn ( harness , tid , []); 171const patch = sectionsSeen ( edited ). at ( - 1 ) ! ; 172assertEquals ( Object . keys ( patch ), [ "code_project" ]); 173assertStringIncludes ( patch [ "code_project" ] ! , "revised rules" ); 174 175await berry . command ( tid , "cd" , "inner" ); 176const [ moved ] = await turn ( harness , tid , []); 177const changed = sectionsSeen ( moved ). at ( - 1 ) ! ; 178assertStringIncludes ( changed . code ! , `current working directory: ${ join ( root , "inner" )} ` ); 179assertStringIncludes ( changed [ "code_project" ] ! , "inner rules" ); 180}); 181 182Deno . test ( "direct tools are offered beside codemode, and the model reads their text or their images" , async () => { 183const image :ImageContent = { type :"image" , data :"bytes" , mimeType :"image/png" }; 184const values = [ "plain text" , { ...image , id :"capture" }, { type :"image" , data :"bytes" }]; 185await usingharness = await setup (); 186const { berry, model} = harness ; 187await berry . install ( defineExtension ({ 188name :"results" , 189initial :() => null , 190setup ( ext ) { 191ext . tool ({ 192name :"result" , 193description :"Return a value" , 194parameters :Type . Object ({ index :Type . Integer () }), 195expose :"model" , 196execute :({ index}) => values [ index ], 197}); 198}, 199})); 200const tid = await berry . create ({ extensions :{ dir :{}, results :{} }, model}); 201const call = ( name :string , args = {}) => 202fauxAssistantMessage ( fauxToolCall ( name , args ), { stopReason :"toolUse" }); 203const seen = await turn ( harness , tid , [ 204call ( "shout" ), 205 ...values . map (( _ , index ) => call ( "result" , { index})), 206]); 207assertEquals ( toolsOffered ( seen [ 0 ]). sort (), [ "cd" , "codemode" , "result" , "shout" ]); 208assertEquals ( lastToolResult ( seen [ 1 ]). text , "HEY!" ); 209const expected :ToolResultMessage [ "content" ][] = [ 210[{ type :"text" , text :"plain text" }], 211[ image ], 212[{ type :"text" , text :JSON . stringify ( values [ 2 ], null , 2 ) }], 213]; 214for ( const [ index , content ] of expected . entries ()) { 215const result = seen [ index + 2 ]. findLast (( message ) => message . role === "toolResult" ); 216assert ( result ?. role === "toolResult" ); 217assertEquals ( result . content , content ); 218} 219}); 220 221Deno . test ( "an extension installs whole or not at all, and a new reducer re-derives state from history" , async () => { 222await usingharness = await setup (); 223const { berry, model} = harness ; 224for ( const name of [ "berry" , "environment" , "subagent" ]) { 225await assertRejects ( 226() => berry . install ( defineExtension ({ name, initial :() => null , setup () {} })), 227BerryError , 228); 229} 230let disposed = false ; 231const shadow = ( expose :"model" | "codemode" ) => 232defineExtension ({ 233name :"shadow" , 234initial :() => null , 235setup ( ext ) { 236ext . tool ({ 237name :"codemode" , 238description :"" , 239parameters :Type . Object ({}), 240 expose, 241execute :() => "" , 242}); 243ext . onDispose (() => void ( disposed = true )); 244}, 245}); 246await assertRejects (() => berry . install ( shadow ( "model" )), BerryError ); 247assert ( disposed , "a refused extension is disposed" ); 248assertEquals ( berry . extensions (). map (( extension ) => extension . name ), [ "dir" , "gate" ]); 249// scripts may have a tool by that name; it cannot reach the model 250await berry . install ( shadow ( "codemode" )); 251 252const tid = await berry . create ({ extensions :{ dir :{ cwd :"/a" } }, model}); 253await ( await berry . emit ( tid , "dir" , { type :"cd" , cwd :"b" })). wait ( context ); 254await assertRejects (() => 255berry . install ( defineExtension ({ 256name :"dir" , 257initial :() => ({}), 258setup () { 259throw new Error ( "broken" ); 260}, 261})) 262); 263assertEquals ( await berry . state ( tid , "dir" ), { cwd :"b" }); 264// v2 resolves relative paths against the current cwd 265await berry . install ( 266dir (( s , e ) => ({ cwd :e . cwd . startsWith ( "/" ) ?e . cwd :` ${ s . cwd } / ${ e . cwd } ` })), 267); 268assertEquals ( await berry . state ( tid , "dir" ), { cwd :"/a/b" }); 269}); 270 271Deno . test ( "a conversation has its chosen extensions, the always-on ones, and the harness's own, under any name they have had" , async () => { 272await usingharness = await setup (); 273const { berry, model} = harness ; 274const names = async ( id :string ) => 275( await ( await berry . conversation ( id )) ! . agent ( context )). extensions . map (( e ) => e . name ); 276// naming the harness's own changes nothing: every conversation has it 277const tid = await berry . create ({ extensions :{ berry :{}, dir :{ cwd :"/a" } }, model}); 278assertEquals ( await names ( tid ), [ "berry" , "environment" , "dir" ]); 279await berry . install ( defineExtension ({ 280name :"always" , 281alwaysOn :true , 282initial :() => null , 283setup () { 284return ( conv ) => { 285conv . command ({ name :"ping" , description :"always there" , execute :() => "pong" }); 286}; 287}, 288})); 289assertEquals ( await names ( tid ), [ "berry" , "environment" , "always" , "dir" ]); 290const named = await berry . create ({ extensions :{ always :{}, dir :{} }, model}); 291assertEquals ( await names ( named ), [ "berry" , "environment" , "always" , "dir" ]); 292assertEquals (( await berry . commands ( tid )). map (({ name}) => name ), [ "cd" , "ping" , "go" ]); 293assertEquals ( await berry . command ( tid , "ping" , "" ), "pong" ); 294await berry . configure ( named , { extensions :[] }); 295assertEquals ( await names ( named ), [ "berry" , "environment" , "always" ]); 296 297const conversation = ( await berry . conversation ( tid )) ! ; 298const [, seed ] = 299( await conversation . entries ({ order :"ascending" }, 10 , undefined , context )). items ; 300await ( await berry . emit ( tid , "dir" , { type :"cd" , cwd :"/b" })). wait ( context ); 301berry . uninstall ( "dir" ); 302await berry . install ({ ...dir (), name :"place" , formerly :[ "dir" ] }); 303assertEquals ( await names ( tid ), [ "berry" , "environment" , "always" , "place" ]); 304assertEquals ( await berry . state ( tid , "place" ), { cwd :"/b" }); 305// a fork from before the rename, and a conversation asked for by the old name 306const fork = await conversation . fork ( seed . id , { ownership :{ kind :"ownerless" } }, context ); 307const forkTid = await berry . tidOf ( fork . id ); 308assertEquals ( await names ( forkTid ), [ "berry" , "environment" , "always" , "place" ]); 309assertEquals ( await berry . state ( forkTid , "place" ), { cwd :"/a" }); 310const asked = await berry . create ({ extensions :{ dir :{ cwd :"/c" } }, model}); 311assertEquals ( await berry . state ( asked , "place" ), { cwd :"/c" }); 312}); 313 314Deno . test ( "a replaced load closes its channels and is disposed once the calls it started end" , async () => { 315const { faux, berry, model} = await setup (); 316const held = { entered :Promise . withResolvers < void >(), release :Promise . withResolvers < string >() }; 317const opened :number [] = []; 318let disposed = 0 ; 319const version = ( mark :number ) => 320defineExtension ({ 321name :"slow" , 322alwaysOn :true , 323initial :() => null , 324setup ( ext ) { 325ext . channel ({ 326name :"line" , 327parameters :Type . Object ({}), 328open :() => void opened . push ( mark ), 329}); 330ext . tool ({ 331name :"hold" , 332description :"wait" , 333parameters :Type . Object ({}), 334expose :"model" , 335execute :() => { 336held . entered . resolve (); 337return held . release . promise ; 338}, 339}); 340ext . onDispose (() => void ( disposed += mark )); 341}, 342}); 343await berry . install ( version ( 1 )); 344const tid = await berry . create ({ extensions :{}, model}); 345let closed = 0 ; 346const channel = { send () {}, messages :new ReadableStream (), close :() => void closed ++ }; 347await berry . channel ( tid , "slow" , "line" , {}, channel , new AbortController (). signal ); 348faux . setResponses ([ 349fauxAssistantMessage ( fauxToolCall ( "hold" , {}), { stopReason :"toolUse" }), 350fauxAssistantMessage ( "done" ), 351]); 352const conversation = ( await berry . conversation ( tid )) ! ; 353const submission = await conversation . submit ({ type :"input" , content :"go" }, context ); 354await held . entered . promise ; 355 356await berry . install ( version ( 10 )); 357assertEquals ( disposed , 0 ); 358held . release . resolve ( "released" ); 359await submission . wait ( context ); 360await conversation . waitForIdle ( context ); 361assertEquals ({ opened, closed, disposed}, { opened :[ 1 ], closed :1 , disposed :1 }); 362await berry . close (); 363assertEquals ( disposed , 11 ); 364}); 365 366Deno . test ( "handlers answer only in conversations that have their extension" , async () => { 367await usingharness = await setup (); 368const { berry, model} = harness ; 369await berry . install ( defineExtension ({ 370name :"asked" , 371initial :() => "folded" , 372setup ( ext ) { 373ext . handle ({ 374name :"echo" , 375parameters :Type . Object ({ times :Type . Integer ({ default :1 }) }), 376execute :({ times}, ctx ) => ({ state :ctx . state , times, aborted :ctx . signal . aborted }), 377}); 378ext . handle ({ 379name :"fail" , 380parameters :Type . Object ({}), 381execute :() => { 382throw new Error ( "no such thing" ); 383}, 384}); 385}, 386})); 387const tid = await berry . create ({ extensions :{ asked :{} }, model}); 388assertEquals ( await berry . request ( tid , "asked" , "echo" , undefined ), { 389state :"folded" , 390times :1 , 391aborted :false , 392}); 393assertEquals ( 394( await berry . request ( tid , "asked" , "echo" , { times :3 }) as { times :number }). times , 3953 , 396); 397await assertRejects ( 398() => berry . request ( tid , "asked" , "echo" , { times :"3" }), 399BerryError , 400"times" , 401); 402await assertRejects (() => berry . request ( tid , "asked" , "fail" , {}), BerryError , "no such thing" ); 403await assertRejects (() => berry . request ( tid , "asked" , "nope" , {}), BerryError , "no handler" ); 404const without = await berry . create ({ extensions :{}, model}); 405await assertRejects ( 406() => berry . request ( without , "asked" , "echo" , {}), 407BerryError , 408"not in this conversation" , 409); 410}); 411 412Deno . test ( "forks and branches start from the state at their entry, and a branch keeps the conversation's tid, agent, and name" , async () => { 413await usingharness = await setup (); 414const { faux, berry, model, gate} = harness ; 415const tid = await berry . create ({ extensions :{ dir :{ cwd :"/a" } }, model}); 416const conversation = ( await berry . conversation ( tid )) ! ; 417const [ start , seed ] = 418( await conversation . entries ({ order :"ascending" }, 10 , undefined , context )). items ; 419assertEquals ([ start . kind , seed . kind ], [ "berry.start" , "berry.event" ]); 420await ( await berry . emit ( tid , "dir" , { type :"cd" , cwd :"/b" })). wait ( context ); 421 422const fork = await conversation . fork ( seed . id , { ownership :{ kind :"ownerless" } }, context ); 423const forkTid = await berry . tidOf ( fork . id ); 424assert ( forkTid !== tid ); 425assertEquals ( await berry . state ( forkTid , "dir" ), { cwd :"/a" }); 426assertEquals ( await berry . state ( tid , "dir" ), { cwd :"/b" }); 427 428await berry . configure ( tid , { extensions :[ "dir" , "gate" ] }); 429faux . setResponses ([ 430fauxAssistantMessage ( fauxToolCall ( "hold" , {}), { stopReason :"toolUse" }), 431fauxAssistantMessage ( "done" ), 432]); 433const submission = await conversation . submit ({ type :"input" , content :"go" }, context ); 434await gate . entered . promise ; 435await assertRejects (() => berry . branch ( tid , seed . id ), BerryError , "busy" ); 436gate . release . resolve ( "released" ); 437await submission . wait ( context ); 438await conversation . waitForIdle ( context ); 439const tip = ( await conversation . context ( context )). entries . at ( - 1 ) ! . id ; 440await berry . markRead ( tid , tip ); 441await berry . rename ( tid , " a custom name " ); 442 443const feed = ( await berry . watchState ( tid , "dir" )) ! ; 444const moved = Promise . withResolvers < unknown >(); 445feed . start (( value ) => Promise . resolve ( moved . resolve ( value ))); 446await berry . branch ( tid , seed . id ); 447const branch = ( await berry . conversation ( tid )) ! ; 448assert ( branch . id !== conversation . id ); 449assertEquals ( await berry . tidOf ( branch . id ), tid ); 450const index = ( await berry . harness . snapshot ( Conversations , context )) ! ; 451assertEquals ( Object . keys ( index ). sort (), [ tid , forkTid ]. sort ()); 452assertEquals ([ index [ tid ]. title , index [ tid ]. readThrough ], [ "a custom name" , tip ]); 453assertEquals ( await berry . state ( tid , "dir" ), { cwd :"/a" }); 454assertEquals ( await moved . promise , { cwd :"/a" }); 455assertEquals ( 456( await branch . agent ( context )). extensions . map (( extension ) => extension . name ), 457[ "berry" , "environment" , "dir" , "gate" ], 458); 459await feed . stop (); 460 461await berry . branch ( tid , tip ); 462assertEquals (( await berry . harness . snapshot ( Conversations , context )) ! [ tid ]. title , "a custom name" ); 463await berry . rename ( tid , " " ); 464assertEquals (( await berry . harness . snapshot ( Conversations , context )) ! [ tid ]. title , undefined ); 465await assertRejects (() => berry . rename ( "missing" , "name" ), BerryError , "No conversation" ); 466}); 467 468Deno . test ( "commands are listed per conversation and run as the user, landing at once while busy" , async () => { 469await usingharness = await setup (); 470const { faux, berry, model, gate} = harness ; 471const tid = await berry . create ({ extensions :{ dir :{}, gate :{} }, model}); 472const conversation = ( await berry . conversation ( tid )) ! ; 473assertEquals (( await berry . commands ( tid )). map (({ name}) => name ), [ "cd" , "go" ]); 474assertEquals ( await berry . command ( tid , "go" , "/cmd" ), "/cmd" ); 475await conversation . waitForIdle ( context ); 476assertEquals ( await berry . state ( tid , "dir" ), { cwd :"/cmd" }); 477await assertRejects (() => berry . command ( tid , "go" , "relative" ), BerryError , "not absolute" ); 478await assertRejects (() => berry . command ( tid , "nope" , "" ), BerryError , "No command /nope" ); 479const other = await berry . create ({ extensions :{}, model}); 480assertEquals (( await berry . commands ( other )). map (({ name}) => name ), [ "cd" ]); 481 482let next :readonly Message [] = []; 483faux . setResponses ([ 484fauxAssistantMessage ( fauxToolCall ( "hold" , {}), { stopReason :"toolUse" }), 485( ctx ) => ( next = ctx . messages , fauxAssistantMessage ( "done" )), 486]); 487const submission = await conversation . submit ({ type :"input" , content :"go" }, context ); 488await gate . entered . promise ; 489assertEquals ( await berry . command ( tid , "go" , "/mid" ), "/mid" ); 490assertEquals ( await berry . state ( tid , "dir" ), { cwd :"/mid" }); 491gate . release . resolve ( "released" ); 492await submission . wait ( context ); 493await conversation . waitForIdle ( context ); 494assertEquals ( await berry . state ( tid , "dir" ), { cwd :"/mid" }); 495// and the model hears of it at the next request, not only on the next input 496assertEquals ( sectionsSeen ( next ). at ( - 1 )?. dir , "<dir>\ncwd: /mid\n</dir>" ); 497}); 498 499Deno . test ( "conversations use only models enabled in settings, at a thinking level the model supports" , async () => { 500await usingharness = await setup ( undefined , { 501models :[{ id :"thinker" , reasoning :true }, { id :"plain" , reasoning :false }], 502}); 503const { berry, model} = harness ; 504const plain = { provider :"faux" , modelId :"plain" }; 505await assertRejects ( 506() => berry . create ({ extensions :{}, model :plain }), 507BerryError , 508"not enabled" , 509); 510const tid = await berry . create ({ extensions :{}, model, thinkingLevel :"high" }); 511await assertRejects (() => berry . configure ( tid , { model :plain }), BerryError , "not enabled" ); 512await berry . enableModels ([ model , plain , model ]); 513assertEquals (( await berry . harness . snapshot ( Settings , context ))?. models , [ model , plain ]); 514 515const agent = async ( id :string ) => await ( await berry . conversation ( id )) ! . agent ( context ); 516assertEquals (( await agent ( tid )). thinkingLevel , "high" ); 517await berry . configure ( tid , { model :plain }); 518assertEquals ([( await agent ( tid )). model , ( await agent ( tid )). thinkingLevel ], [ plain , "off" ]); 519await berry . configure ( tid , { model, thinkingLevel :"max" }); 520assertEquals (( await agent ( tid )). thinkingLevel , "high" ); 521const created = await berry . create ({ extensions :{}, model :plain , thinkingLevel :"high" }); 522assertEquals (( await agent ( created )). thinkingLevel , "off" ); 523}); 524 525Deno . test ( "extension prose defaults, overrides, blanks, selection, and $USER" , async () => { 526await usingharness = await setup (); 527const { berry, model} = harness ; 528await berry . install ( defineExtension ({ 529name :"notes" , 530initial :() => null , 531setup ( ext ) { 532ext . promptProse ({ default :"Greet $USER warmly." }); 533return ( conv ) => conv . prompt (() => "dynamic content" ); 534}, 535})); 536assertEquals ( berry . promptProseDefault ( "notes" ), "Greet $USER warmly." ); 537assertEquals ( berry . promptProseDefault ( "dir" ), null ); 538const withNotes = await berry . create ({ extensions :{ notes :{} }, model}); 539const withoutNotes = await berry . create ({ extensions :{}, model}); 540const [ first ] = await turn ( harness , withNotes , []); 541const shown = sectionsSeen ( first )[ 0 ]; 542assertStringIncludes ( shown . notes ! , "Greet" ); 543assert ( ! shown . notes ! . includes ( "$USER" )); 544assert ( ! shown . notes ! . includes ( "guidelines:" )); 545assertStringIncludes ( shown . notes ! , "dynamic content" ); 546const [ absent ] = await turn ( harness , withoutNotes , []); 547assert ( ! Object . assign ({}, ...sectionsSeen ( absent )). notes ); 548 549await berry . setSystemPrompt ({ userName :"Sam" , promptProse :{ notes :"Bid $USER farewell." } }); 550const [ overrode ] = await turn ( harness , withNotes , []); 551assertStringIncludes ( sectionsSeen ( overrode ). at ( - 1 ) ! . notes ! , "Bid Sam farewell." ); 552assertStringIncludes ( sectionsSeen ( overrode ). at ( - 1 ) ! . notes ! , "dynamic content" ); 553 554await berry . setSystemPrompt ({ promptProse :{ notes :"" } }); 555const [ blanked ] = await turn ( harness , withNotes , []); 556assertStringIncludes ( sectionsSeen ( blanked ). at ( - 1 ) ! . notes ! , "dynamic content" ); 557assert ( ! sectionsSeen ( blanked ). at ( - 1 ) ! . notes ! . includes ( "Bid" )); 558 559const stored = ( await berry . harness . snapshot ( Settings , context ))?. systemPrompt ; 560assertEquals ( stored ?. promptProse ?. notes , "" ); 561}); 562 563Deno . test ( "startup migrates legacy code prose, preserving blank values and newer overrides" , async () => { 564const { openNodeSqliteStorage} = await import ( "@earendil-works/pi-durable/storage/sqlite/node" ); 565for ( 566const { legacy, override, expected} of [ 567{ 568legacy :"Legacy for $USER." , 569override :undefined , 570expected :"guidelines:\nLegacy for $USER." , 571}, 572{ legacy :"" , override :undefined , expected :"" }, 573{ legacy :"Stale legacy." , override :"" , expected :"" }, 574] 575) { 576await usingdirectory = await tempDir (); 577const path = join ( directory . path , "settings.sqlite" ); 578const models = createModels (); 579const seed = await openBerry ( await openNodeSqliteStorage ( path ), models ); 580try { 581await seed . harness . commit ( async ( tx ) => { 582const settings = await tx . doc ( Settings ); 583const stored = { 584userName :"Sam" , 585generalGuidelines :"" , 586codingGuidelines :legacy , 587promptProse :{ 588other :"untouched" , 589 ...( override === undefined ?{} :{ code :override }), 590}, 591}; 592settings . systemPrompt = stored ; 593}, context ); 594} finally { 595await seed . close (); 596} 597const berry = await openBerry ( await openNodeSqliteStorage ( path ), models ); 598try { 599const stored = ( await berry . harness . snapshot ( Settings , context ))?. systemPrompt ; 600assertEquals ( stored ?. promptProse , { code :expected , other :"untouched" }); 601assertEquals ( stored ?. userName , "Sam" ); 602assertEquals ( stored ?. generalGuidelines , "" ); 603} finally { 604await berry . close (); 605} 606} 607}); 608 609Deno . test ( "prompt prose overrides survive a missing extension" , async () => { 610await usingharness = await setup (); 611const { berry} = harness ; 612await berry . install ( defineExtension ({ 613name :"ephemeral" , 614initial :() => null , 615setup ( ext ) { 616ext . promptProse ({ default :"default prose" }); 617}, 618})); 619await berry . setSystemPrompt ({ promptProse :{ ephemeral :"kept override" } }); 620berry . uninstall ( "ephemeral" ); 621assertEquals ( berry . promptProseDefault ( "ephemeral" ), null ); 622assertEquals ( 623( await berry . harness . snapshot ( Settings , context ))?. systemPrompt ?. promptProse ?. ephemeral , 624"kept override" , 625); 626await berry . install ( defineExtension ({ 627name :"ephemeral" , 628initial :() => null , 629setup ( ext ) { 630ext . promptProse ({ default :"default prose" }); 631}, 632})); 633assertEquals ( berry . promptProseDefault ( "ephemeral" ), "default prose" ); 634assertEquals ( 635( await berry . harness . snapshot ( Settings , context ))?. systemPrompt ?. promptProse ?. ephemeral , 636"kept override" , 637); 638}); 639 640Deno . test ( "a host lends /reload its extensions, and the prompt a guide to berry's source" , async () => { 641const faux = fauxProvider (); 642const models = createModels (); 643models . setProvider ( faux . provider ); 644const model = { provider :"faux" , modelId :faux . getModel (). id }; 645const reloaded :string [] = []; 646const loaded = ( name :string ) => 647({ name, description :null , status :"loaded" , error :null }) as const ; 648const failed = { ...loaded ( "two" ), status :"failed" , error :"no mod.ts" } as const ; 649const berry = await openBerry ( new MemoryStorage (), models , { 650root :"/somewhere/berry" , 651check :() => Promise . resolve ( null ), 652restart :() => {}, 653extensions :() => [ loaded ( "one" ), failed ], 654reload :( name ) => { 655reloaded . push ( name ); 656return Promise . resolve ( name === "two" ?failed :loaded ( name )); 657}, 658}, { 659async open ( name , cwd , during ) { 660return Object . assign ( await localOnly . open ( "local" , cwd , during ), { name}); 661}, 662close :() => Promise . resolve (), 663}); 664try { 665await berry . enableModels ([ model ]); 666const tid = await berry . create ({ extensions :{}, model}); 667const seen = await turn ({ faux, berry}, tid , []); 668assertStringIncludes ( sectionsSeen ( seen [ 0 ])[ 0 ]. berry ?? "" , "/somewhere/berry" ); 669assertEquals ( toolsOffered ( seen [ 0 ]), [ "codemode" ]); 670 671assertEquals ( await berry . command ( tid , "reload" , " one " ), "reloaded one" ); 672await assertRejects (() => berry . command ( tid , "reload" , "" ), BerryError , "two: no mod.ts" ); 673assertEquals ( reloaded , [ "one" , "one" , "two" ]); 674 675await berry . configureExtension ( tid , "environment" , { environment :"remote" }); 676const [ remote ] = await turn ({ faux, berry}, tid , []); 677assertEquals ( sectionsSeen ( remote ). at ( - 1 )?. berry , null ); 678const fresh = await berry . create ({ 679extensions :{ environment :{ environment :"remote" } }, 680 model, 681}); 682const [ firstRemote ] = await turn ({ faux, berry}, fresh , []); 683assertEquals ( sectionsSeen ( firstRemote ). at ( - 1 )?. berry , undefined ); 684 685await berry . configureExtension ( tid , "environment" , { environment :"local" }); 686const [ local ] = await turn ({ faux, berry}, tid , []); 687assertStringIncludes ( sectionsSeen ( local ). at ( - 1 )?. berry ?? "" , "/somewhere/berry" ); 688} finally { 689await berry . close (); 690} 691});