char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
2d39747
main
1import { assert , assertEquals , assertStringIncludes } from "@std/assert" ; 2import type { ConversationId , EntryId , EntryRecord , LiveState } from "@earendil-works/pi-durable" ; 3import { fauxAssistantMessage , fauxToolCall } from "@earendil-works/pi-ai/providers/faux" ; 4import { withBerry } from "./testing.ts" ; 5 6const entry = ( id :number , kind :string , message :unknown ) => 7({ 8id :id as EntryId , 9conversationId :1 as ConversationId , 10 kind, 11model :[ message ], 12}) as EntryRecord ; 13 14Deno . test ( "direct and codemode subagent cards link to the child, stream its transcript, retain its tool cards, and release their subscriptions" , async ( t ) => { 15await withBerry ( async ({ server, settle}) => { 16const { transcript} = await import ( "./transcript.tsx" ); 17const { watch} = await import ( "./client.ts" ); 18for ( const mode of [ "direct" , "codemode" ] as const ) { 19await t . step ( mode , async () => { 20const view = transcript (); 21const tid = "a2b3c4d5e6f7g" ; 22const args = { 23task :"Investigate **tests**" , 24model :{ provider :"faux" , modelId :"worker" }, 25}; 26const name = mode === "direct" ?"subagent" :"codemode" ; 27const entries = [ entry ( 281 , 29"pi.assistant" , 30fauxAssistantMessage ( 31fauxToolCall ( name , mode === "direct" ?args :{ code :"await tools.subagent(task);" }, { 32id :"call" , 33}), 34{ stopReason :"toolUse" }, 35), 36)]; 37const nested = { 38name :"subagent" , 39extension :"subagent" , 40 args, 41status :"running" , 42details :{ tid}, 43}; 44const details = mode === "direct" ?{ tid} :{ calls :[ nested ] }; 45try { 46view . update ( entries , { 47run :{ taskId :2 }, 48tools :[{ callId :"call" , name, status :"running" , details}], 49} as unknown as LiveState ); 50const link = () => view . element . querySelector ( "a.tid" )?. getAttribute ( "href" ); 51assertEquals ( link (), `#/ ${ tid } ` ); 52const pane = view . element . querySelector < HTMLElement >( 53"[aria-label='Subagent transcript']" , 54) ! ; 55assert ( pane ); 56const body = pane . parentElement ! ; 57assertEquals ( body . querySelector ( "strong" )?. textContent , "tests" ); 58assertStringIncludes ( body . textContent ! , "faux/worker" ); 59const childEntries = [ 60entry ( 1 , "pi.user" , { role :"user" , content :args . task , timestamp :0 }), 61]; 62const worker = fauxAssistantMessage ( 63fauxToolCall ( "inspect" , { path :"tests" }, { id :"inspect" }), 64{ 65stopReason :"toolUse" , 66}, 67); 68childEntries . push ( entry ( 2 , "pi.assistant" , worker )); 69server . snapshot ( `conversation: ${ tid } ` , { 70entries :childEntries , 71docs :{ 72"pi.agent" :{ model :args . model , thinkingLevel :"high" }, 73"pi.live" :{ 74run :{ taskId :3 }, 75tools :[{ callId :"inspect" , name :"inspect" , status :"running" }], 76}, 77}, 78}); 79await settle (); 80assert ( ! pane . hidden ); 81assertStringIncludes ( body . textContent ! , "high" ); 82assertStringIncludes ( pane . textContent ! , args . task ); 83const tool = pane . querySelector < HTMLButtonElement >( ".tool-header" ) ! ; 84tool . click (); 85childEntries . push ( entry ( 3 , "pi.tool-result" , { 86role :"toolResult" , 87toolCallId :"inspect" , 88toolName :"inspect" , 89content :[{ type :"text" , text :"located the failing test" }], 90isError :false , 91timestamp :0 , 92})); 93const final = fauxAssistantMessage ( "The fix is **ready**." ); 94server . snapshot ( `conversation: ${ tid } ` , { 95entries :childEntries , 96docs :{ "pi.live" :{ generation :{ message :final } } }, 97}); 98await settle (); 99assert ( pane . querySelector ( ".tool-header" ) === tool ); 100assertEquals ( tool . getAttribute ( "aria-expanded" ), "true" ); 101assertStringIncludes ( pane . textContent ! , "located the failing test" ); 102const paragraph = pane . querySelector ( ".message.assistant .markdown p" ) ! ; 103assertEquals ( paragraph . querySelector ( "strong" )?. textContent , "ready" ); 104childEntries . push ( entry ( 4 , "pi.assistant" , final )); 105server . snapshot ( `conversation: ${ tid } ` , { entries :childEntries , docs :{} }); 106const finishedDetails = mode === "direct" ?{ tid} :{ 107calls :[{ 108 ...nested , 109status :"ok" , 110value :{ conversation :tid , answer :"The fix is **ready**." }, 111}], 112}; 113view . update ([ 114 ...entries , 115entry ( 2 , "pi.tool-result" , { 116role :"toolResult" , 117toolCallId :"call" , 118toolName :name , 119content :[{ type :"text" , text :"The fix is **ready**." }], 120details :finishedDetails , 121isError :false , 122timestamp :0 , 123}), 124], {}); 125await settle (); 126assert ( pane . hidden ); 127assertEquals ( pane . textContent , "" ); 128assertEquals ( link (), `#/ ${ tid } ` ); 129assertEquals ( body . querySelector ( ".markdown:not([hidden]) strong" )?. textContent , "ready" ); 130assert ( ! body . textContent ! . includes ( "located the failing test" )); 131let cached = false ; 132const release = watch ( `conversation: ${ tid } ` , () => cached = true ); 133release (); 134assert ( ! cached ); 135} finally { 136view . dispose (); 137} 138}); 139} 140}); 141}); 142 143Deno . test ( "continuations leave earlier answers alone and only the running call follows the child" , async () => { 144await withBerry ( async ({ server, settle}) => { 145const { transcript} = await import ( "./transcript.tsx" ); 146const view = transcript (); 147const tid = "a2b3c4d5e6f7g" ; 148const result = ( call :string , text :string ) => ({ 149role :"toolResult" , 150toolCallId :call , 151toolName :"subagent" , 152content :[{ type :"text" , text}], 153details :{ tid}, 154isError :false , 155timestamp :0 , 156}); 157const entries = [ 158entry ( 1591 , 160"pi.assistant" , 161fauxAssistantMessage ([ 162fauxToolCall ( "subagent" , { task :"first task" }, { id :"first" }), 163fauxToolCall ( "subagent" , { task :"follow up" , conversation :tid }, { id :"next" }), 164], { stopReason :"toolUse" }), 165), 166entry ( 2 , "pi.tool-result" , result ( "first" , "First **answer**." )), 167]; 168try { 169view . update ( entries , { 170run :{ taskId :2 }, 171tools :[{ callId :"next" , name :"subagent" , status :"running" , details :{ tid} }], 172} as unknown as LiveState ); 173server . snapshot ( `conversation: ${ tid } ` , { 174entries :[ entry ( 1 , "pi.assistant" , fauxAssistantMessage ( "First **answer**." ))], 175docs :{ "pi.live" :{ generation :{ message :fauxAssistantMessage ( "Following up now…" ) } } }, 176}); 177await settle (); 178const bodies = view . element . querySelectorAll < HTMLElement >( ".subagent-body" ); 179assertEquals ( bodies . length , 2 ); 180assert ( bodies [ 0 ]. querySelector < HTMLElement >( ".subagent-history" ) ! . hidden ); 181assertEquals ( bodies [ 0 ]. querySelector ( "strong" )?. textContent , "answer" ); 182assert ( ! bodies [ 0 ]. textContent ! . includes ( "Following up now" )); 183assert ( ! bodies [ 1 ]. querySelector < HTMLElement >( ".subagent-history" ) ! . hidden ); 184assertStringIncludes ( bodies [ 1 ]. textContent ! , "Following up now" ); 185view . update ( 186[ ...entries , entry ( 3 , "pi.tool-result" , result ( "next" , "Second **answer**." ))], 187{}, 188); 189await settle (); 190assert ( bodies [ 1 ]. querySelector < HTMLElement >( ".subagent-history" ) ! . hidden ); 191assert ( ! bodies [ 1 ]. textContent ! . includes ( "Following up now" )); 192assertStringIncludes ( bodies [ 1 ]. textContent ! , "Second answer" ); 193assertStringIncludes ( bodies [ 0 ]. textContent ! , "First answer" ); 194} finally { 195view . dispose (); 196} 197}); 198}); 199 200Deno . test ( "a subagent report renders its status and answer and links to the child" , async () => { 201await withBerry ( async ({ settle}) => { 202const { transcript} = await import ( "./transcript.tsx" ); 203const { describe} = await import ( "./tree.tsx" ); 204const view = transcript (); 205const tid = "a2b3c4d5e6f7g" ; 206try { 207const record = { 208id :1 as EntryId , 209conversationId :1 as ConversationId , 210kind :"berry.subagent-report" , 211model :[{ 212role :"user" , 213content :`Background subagent ${ tid } finished its task.` , 214timestamp :0 , 215}], 216data :{ 217 tid, 218task :"Investigate tests" , 219status :"completed" , 220answer :"The fix is **ready**." , 221}, 222} as unknown as EntryRecord ; 223view . update ([ record ], {}); 224assertEquals ( describe ({ kind :"berry.subagent-report" , text :"finished" } as never ), [ 225"report" , 226"finished" , 227]); 228await settle (); 229const report = view . element . querySelector ( ".report" ) ! ; 230assert ( report ); 231assert ( ! report . closest ( ".message.user" )); 232assertStringIncludes ( report . textContent ! , "completed" ); 233assertStringIncludes ( report . textContent ! , "Investigate tests" ); 234assertEquals ( report . querySelector ( "strong" )?. textContent , "ready" ); 235assertEquals ( report . querySelector ( "a.tid" )?. getAttribute ( "href" ), `#/ ${ tid } ` ); 236// the raw kind never shows 237assert ( ! report . textContent ! . includes ( "berry.subagent-report" )); 238} finally { 239view . dispose (); 240} 241}); 242}); 243 244Deno . test ( "a background card keeps its transcript open while the child works and releases it when it idles" , async ( t ) => { 245await withBerry ( async ({ server, settle}) => { 246const { transcript} = await import ( "./transcript.tsx" ); 247const { watch} = await import ( "./client.ts" ); 248for ( const mode of [ "direct" , "codemode" ] as const ) { 249await t . step ( mode , async () => { 250const view = transcript (); 251const tid = "a2b3c4d5e6f7g" ; 252const args = { 253task :"Investigate tests" , 254model :{ provider :"faux" , modelId :"worker" }, 255background :true , 256}; 257const name = mode === "direct" ?"subagent" :"codemode" ; 258const entries = [ entry ( 2591 , 260"pi.assistant" , 261fauxAssistantMessage ( 262fauxToolCall ( name , mode === "direct" ?args :{ code :"await tools.subagent(task);" }, { 263id :"call" , 264}), 265{ stopReason :"toolUse" }, 266), 267)]; 268const running = { conversation :tid , status :"running" }; 269const nested = { 270name :"subagent" , 271extension :"subagent" , 272 args, 273status :"running" , 274details :{ tid, conversationId :1 , status :"running" }, 275value :running , 276}; 277const details = mode === "direct" 278 ?{ tid, conversationId :1 , status :"running" } 279 :{ calls :[ nested ] }; 280try { 281view . update ( entries , { 282run :{ taskId :2 }, 283tools :[{ callId :"call" , name, status :"running" , details}], 284} as unknown as LiveState ); 285const link = () => view . element . querySelector ( "a.tid" )?. getAttribute ( "href" ); 286assertEquals ( link (), `#/ ${ tid } ` ); 287const pane = view . element . querySelector < HTMLElement >( 288"[aria-label='Subagent transcript']" , 289) ! ; 290assert ( pane ); 291const body = pane . parentElement ! ; 292const childEntries = [ 293entry ( 1 , "pi.user" , { role :"user" , content :args . task , timestamp :0 }), 294]; 295server . snapshot ( `conversation: ${ tid } ` , { 296entries :childEntries , 297docs :{ 298"pi.agent" :{ model :args . model }, 299"pi.live" :{ run :{ taskId :3 }, tools :[] }, 300}, 301}); 302await settle (); 303assert ( ! pane . hidden ); 304assertStringIncludes ( body . textContent ! , args . task ); 305// the spawn settles while the child is still working: the card stays open 306const finishedDetails = mode === "direct" 307 ?{ tid, conversationId :1 , status :"running" } 308 :{ calls :[{ ...nested , value :running }] }; 309view . update ([ 310 ...entries , 311entry ( 2 , "pi.tool-result" , { 312role :"toolResult" , 313toolCallId :"call" , 314toolName :name , 315content :[{ type :"text" , text :"spawned" }], 316details :finishedDetails , 317isError :false , 318timestamp :0 , 319}), 320], {}); 321await settle (); 322assert ( ! pane . hidden ); 323assertEquals ( link (), `#/ ${ tid } ` ); 324assertStringIncludes ( body . textContent ! , args . task ); 325// once the child idles the card releases its subscription 326server . snapshot ( `conversation: ${ tid } ` , { entries :childEntries , docs :{} }); 327await settle (); 328assert ( pane . hidden ); 329assertEquals ( pane . textContent , "" ); 330assertEquals ( link (), `#/ ${ tid } ` ); 331let cached = false ; 332const release = watch ( `conversation: ${ tid } ` , () => cached = true ); 333release (); 334assert ( ! cached ); 335} finally { 336view . dispose (); 337} 338}); 339} 340}); 341});