char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
2c34403
main
1import { assert , assertEquals , assertRejects , assertStringIncludes } from "@std/assert" ; 2import { BACKGROUND_CONTEXT as context } from "@earendil-works/chord/context" ; 3import { 4type JsonObject , 5type Message , 6type ModelThinkingLevel , 7Type , 8} from "@earendil-works/pi-ai" ; 9import { getCurrentSystemPrompt , getCurrentTools } from "@earendil-works/pi-ai/utils/transcript" ; 10import { createModels } from "@earendil-works/pi-ai/models" ; 11import { 12fauxAssistantMessage , 13fauxProvider , 14fauxToolCall , 15} from "@earendil-works/pi-ai/providers/faux" ; 16import { AgentDoc , LiveDoc , MemoryStorage } from "@earendil-works/pi-durable" ; 17import { openNodeSqliteStorage } from "@earendil-works/pi-durable/storage/sqlite/node" ; 18import { defineExtension } from "@berry/api" ; 19import { localEnvironment } from "@berry/api/environment.ts" ; 20import { Conversations , openBerry } from "./berry.ts" ; 21import { 22blocking , 23dir , 24lastToolResult , 25script , 26sectionsSeen , 27setup , 28toolsOffered , 29turn , 30} from "./fixture.ts" ; 31import { tempDir , until } from "@berry/util" ; 32 33async function within < T > ( promise :Promise < T >) :Promise < T > { 34let timer :ReturnType < typeof setTimeout > | undefined ; 35try { 36return await Promise . race ([ 37promise , 38new Promise < never >(( _ , reject ) => 39timer = setTimeout (() => reject ( new Error ( "Timed out" )), 5000 ) 40), 41]); 42} finally { 43clearTimeout ( timer ); 44} 45} 46 47const last = ( messages :readonly Message []) => messages . findLast (( m ) => m . role !== "system" ) ! ; 48const textOf = ( message :Message ) => 49typeof message . content === "string" 50 ?message . content 51 :message . content . flatMap (( part ) => part . type === "text" ?[ part . text ] :[]). join ( "\n" ); 52 53Deno . test ( "selecting subagent controls direct tools, the codemode catalog, and script invocation, and old conversations keep it" , async () => { 54await usingharness = await setup (); 55const { berry, model} = harness ; 56const tid = await berry . create ({ extensions :{}, model}); 57const [ initial ] = await turn ( harness , tid , []); 58assert ( ! getCurrentTools ( initial ). some (( tool ) => tool . name === "subagent" )); 59assert ( ! sectionsSeen ( initial ). some (( section ) => section . codemode ?. includes ( "subagent:" ))); 60 61await berry . configure ( tid , { extensions :[ "subagent" ] }); 62const [ enabled ] = await turn ( harness , tid , []); 63assert ( getCurrentTools ( enabled ). some (( tool ) => tool . name === "subagent" )); 64assert ( sectionsSeen ( enabled ). some (( section ) => section . codemode ?. includes ( "subagent:" ))); 65 66await berry . configure ( tid , { extensions :[] }); 67const disabled = await turn ( harness , tid , [ 68script ( `return await tools.subagent({task:"must not run", model: ${ JSON . stringify ( model )} });` ), 69]); 70assert ( ! getCurrentTools ( disabled [ 0 ]). some (( tool ) => tool . name === "subagent" )); 71const catalog = 72sectionsSeen ( disabled [ 0 ]). findLast (( section ) => section . codemode !== undefined )?. codemode ?? "" ; 73assert ( ! catalog . includes ( "subagent:" )); 74const result = lastToolResult ( disabled [ 1 ]); 75assert ( result . isError ); 76assertStringIncludes ( result . text , "subagent" ); 77assertEquals ( Object . keys (( await berry . harness . snapshot ( Conversations , context )) ! ), [ tid ]); 78 79// as conversations from before subagent was a choice stored their extensions, which keep it 80const selected = async ( tid :string ) => 81( await ( await berry . conversation ( tid )) ! . agent ( context )). extensions . some (( ext ) => 82ext . name === "subagent" 83); 84const legacy = await berry . harness . createConversation ({ 85ownership :{ kind :"ownerless" }, 86agent :{ extensions :{ add :[] } }, 87}, context ); 88const old = await berry . tidOf ( legacy . id ); 89assertEquals ( await selected ( old ), true ); 90await berry . configure ( old , { extensions :[] }); 91assertEquals ( await selected ( old ), false ); 92}); 93 94Deno . test ( "delegation needs a worker model enabled for it, and continues only the conversation's own subagents" , async () => { 95await usingharness = await setup ( undefined , { 96models :[ 97{ id :"parent" }, 98{ id :"chosen-worker" }, 99{ id :"other-worker" }, 100{ id :"disabled-worker" }, 101], 102}); 103const { berry, faux, model} = harness ; 104const worker = { provider :"faux" , modelId :"chosen-worker" }; 105const other = { provider :"faux" , modelId :"other-worker" }; 106const ran :string [] = []; 107const delegate = async ( tid :string , args :JsonObject ) => { 108faux . setResponses ( Array . from ({ length :4 }, () => ( request , _ , __ , chosen ) => { 109const message = last ( request . messages ); 110if ( message . role === "toolResult" ) return fauxAssistantMessage ( "done" ); 111if ( textOf ( message ) === "go" ) { 112return fauxAssistantMessage ( fauxToolCall ( "subagent" , args ), { stopReason :"toolUse" }); 113} 114ran . push ( chosen . id ); 115return fauxAssistantMessage ( "worker answer" ); 116})); 117const parent = ( await berry . conversation ( tid )) ! ; 118await within (( await parent . submit ({ type :"input" , content :"go" }, context )). wait ( context )); 119await within ( parent . waitForIdle ( context )); 120const messages = ( await parent . context ( context )). messages ; 121return { result :lastToolResult ( messages ), prompt :getCurrentSystemPrompt ( messages ) }; 122}; 123const children = async ( tid :string ) => { 124const index = ( await berry . harness . snapshot ( Conversations , context )) ! ; 125return Object . keys ( index ). filter (( key ) => index [ key ]. owner === tid ); 126}; 127await berry . enableModels ([ model , worker , other ]); 128const owner = await berry . create ({ 129extensions :{ subagent :{ models :[ "faux/chosen-worker" ] } }, 130 model, 131}); 132const refusals :[ JsonObject , string ][] = [ 133[{ task :"must not run" }, "model" ], 134[ 135{ task :"must not run" , model :{ provider :"faux" , modelId :"disabled-worker" } }, 136"not enabled" , 137], 138[{ task :"must not run" , model :other }, "not enabled for delegation" ], 139]; 140for ( const [ args , error ] of refusals ) { 141const { result, prompt} = await delegate ( owner , args ); 142assert ( result . isError ); 143assertStringIncludes ( result . text , error ); 144assertStringIncludes ( prompt , "chosen-worker" ); 145assert ( ! prompt . includes ( "other-worker" ) && ! prompt . includes ( "disabled-worker" )); 146} 147assertEquals ( await children ( owner ), []); 148const graph = await berry . harness . taskGraph ( context ); 149assertEquals ( Object . keys ( graph . value . tasks ), []); 150graph . dispose (); 151assert ( ! ( await delegate ( owner , { task :"start" , model :worker })). result . isError ); 152const [ child ] = await children ( owner ); 153 154const stranger = await berry . create ({ extensions :{ subagent :{} }, model}); 155const foreign = ( await delegate ( stranger , { task :"steal" , conversation :child })). result ; 156assert ( foreign . isError ); 157assertStringIncludes ( foreign . text , "not one of this conversation's subagents" ); 158 159await berry . configureExtension ( owner , "subagent" , { models :[ "faux/other-worker" ] }); 160const stale = ( await delegate ( owner , { task :"again" , conversation :child })). result ; 161assert ( stale . isError ); 162assertStringIncludes ( stale . text , "faux/chosen-worker is not enabled for delegation" ); 163const moved = await delegate ( owner , { task :"again" , conversation :child , model :other }); 164assert ( ! moved . result . isError ); 165assertEquals (( await ( await berry . conversation ( child )) ! . agent ( context )). model , other ); 166 167// choosing none leaves every model enabled in settings 168assertEquals ( await berry . configureExtension ( owner , "subagent" , { models :[] }), { 169models :[], 170limit :4 , 171}); 172const fallback = await delegate ( owner , { task :"more" , model :worker }); 173assert ( ! fallback . result . isError ); 174assertStringIncludes ( fallback . prompt , "other-worker" ); 175assertEquals ( ran , [ "chosen-worker" , "other-worker" , "chosen-worker" ]); 176await assertRejects ( 177() => berry . configureExtension ( owner , "subagent" , { models :[ "faux/disabled-worker" ] }), 178Error , 179"faux/disabled-worker is not enabled in settings" , 180); 181}); 182 183Deno . test ( "delegation can override thinking without changing the parent" , async ( t ) => { 184await usingharness = await setup ( undefined , { 185models :[ 186{ id :"parent" , reasoning :true }, 187{ id :"worker" , reasoning :true }, 188{ id :"plain-worker" , reasoning :false }, 189], 190}); 191const { berry, faux, model} = harness ; 192const worker = { provider :"faux" , modelId :"worker" }; 193const plain = { provider :"faux" , modelId :"plain-worker" }; 194await berry . enableModels ([ model , worker , plain ]); 195const cases :{ 196name :string ; 197requested ?:ModelThinkingLevel ; 198expected :ModelThinkingLevel ; 199plain ?:boolean ; 200}[] = [ 201{ name :"omission inherits" , expected :"high" }, 202{ name :"a request overrides" , requested :"low" , expected :"low" }, 203{ 204name :"a non-reasoning worker clamps to off" , 205requested :"high" , 206expected :"off" , 207plain :true , 208}, 209]; 210for ( const scenario of cases ) { 211await t . step ( scenario . name , async () => { 212const selected = scenario . plain ?plain :worker ; 213const tid = await berry . create ({ 214extensions :{ subagent :{} }, 215 model, 216thinkingLevel :"high" , 217}); 218const parent = ( await berry . conversation ( tid )) ! ; 219const args = { 220task :scenario . name , 221model :selected , 222 ...( scenario . requested === undefined ?{} :{ thinkingLevel :scenario . requested }), 223}; 224const requests :{ modelId :string ; thinking :ModelThinkingLevel }[] = []; 225faux . setResponses ([ 226fauxAssistantMessage ( fauxToolCall ( "subagent" , args ), { stopReason :"toolUse" }), 227( _ , options , __ , model ) => { 228requests . push ({ modelId :model . id , thinking :options ?. reasoning ?? "off" }); 229return fauxAssistantMessage ( "worker answer" ); 230}, 231fauxAssistantMessage ( "done" ), 232]); 233await ( await parent . submit ({ type :"input" , content :"go" }, context )). wait ( context ); 234await parent . waitForIdle ( context ); 235assertEquals ( requests , [{ modelId :selected . modelId , thinking :scenario . expected }]); 236assert ( ! lastToolResult (( await parent . context ( context )). messages ). isError ); 237const index = ( await berry . harness . snapshot ( Conversations , context )) ! ; 238const children = Object . keys ( index ). filter (( child ) => index [ child ]. owner === tid ); 239assertEquals ( children . length , 1 ); 240const child = ( await berry . conversation ( children [ 0 ])) ! ; 241assertEquals (( await child . agent ( context )). thinkingLevel , scenario . expected ); 242const unchanged = await parent . agent ( context ); 243assertEquals ( unchanged . model , model ); 244assertEquals ( unchanged . thinkingLevel , "high" ); 245}); 246} 247}); 248 249Deno . test ( "a continued subagent keeps its conversation, context, model, and thinking, and queues follow-ups" , async () => { 250await usingharness = await setup ( undefined , { 251models :[{ id :"parent" }, { id :"worker" , reasoning :true }], 252}); 253const { berry, faux, model} = harness ; 254const worker = { provider :"faux" , modelId :"worker" }; 255const children :{ model :string ; thinking :unknown ; users :string [] }[] = []; 256faux . setResponses ( Array . from ({ length :8 }, () => ( request , options , _ , chosen ) => { 257const users = request . messages . flatMap (( m ) => m . role === "user" ?[ textOf ( m )] :[]); 258if ( users [ 0 ] !== "go" ) { 259children . push ({ model :chosen . id , thinking :options ?. reasoning , users}); 260return fauxAssistantMessage ( ` ${ users . at ( - 1 )} after ${ users . length - 1 } ` ); 261} 262if ( last ( request . messages ). role === "toolResult" ) return fauxAssistantMessage ( "done" ); 263return script ( ` 264const first = await tools.subagent({ task: "one", model: ${ JSON . stringify ( worker )} , 265thinkingLevel: "low" }); 266const more = await Promise.all(["two", "three"].map((task) => 267tools.subagent({ task, conversation: first.conversation }))); 268text(JSON.stringify([first, ...more])); 269` ); 270})); 271await berry . enableModels ([ model , worker ]); 272const tid = await berry . create ({ extensions :{ subagent :{} }, model}); 273const parent = ( await berry . conversation ( tid )) ! ; 274await within (( await parent . submit ({ type :"input" , content :"go" }, context )). wait ( context )); 275await within ( parent . waitForIdle ( context )); 276const result = lastToolResult (( await parent . context ( context )). messages ); 277assert ( ! result . isError ); 278const answers = JSON . parse ( result . text . slice ( result . text . indexOf ( "[" ))) as { 279conversation :string ; 280answer :string ; 281}[]; 282const index = ( await berry . harness . snapshot ( Conversations , context )) ! ; 283const [ child ] = Object . keys ( index ). filter (( key ) => index [ key ]. owner === tid ); 284assertEquals ( answers . map (( a ) => a . conversation ), [ child , child , child ]); 285const [ task , after ] = [ 0 , 1 ]. map (( i ) => answers . map (( a ) => a . answer . split ( " after " )[ i ])); 286assertEquals ( task , [ "one" , "two" , "three" ]); 287assertEquals ([ after [ 0 ], ...after . slice ( 1 ). sort ()], [ "0" , "1" , "2" ]); 288assertEquals ( children . map (( c ) => c . users . length ), [ 1 , 2 , 3 ]); 289assertEquals ( children [ 2 ]. users [ 0 ], "one" ); 290for ( const request of children ) { 291assertEquals ([ request . model , request . thinking ], [ "worker" , "low" ]); 292} 293}); 294 295Deno . test ( "ending a codemode script cancels its unawaited subagents, new or continued, while the parent continues" , async () => { 296await usingharness = await setup (); 297const { berry, faux, model} = harness ; 298const blocker = blocking (); 299await berry . install ( blocker . extension ); 300const tid = await berry . create ({ extensions :{ subagent :{}, blocking :{} }, model}); 301const parent = ( await berry . conversation ( tid )) ! ; 302const unawaited = async ( args :JsonObject ) => { 303faux . setResponses ([ 304script ( 305`tools.subagent( ${ JSON . stringify ( args )} ); await tools.ready({}); return "moving on";` , 306), 307fauxAssistantMessage ( fauxToolCall ( "block" , {}), { stopReason :"toolUse" }), 308fauxAssistantMessage ( "done" ), 309]); 310const cancelled = blocker . cancelled . length ; 311const submission = await parent . submit ({ type :"input" , content :"go" }, context ); 312await until (() => blocker . cancelled . length > cancelled ); 313assertEquals (( await within ( submission . wait ( context ))). status , "done" ); 314await within ( berry . harness . waitForIdle ( context )); 315const result = lastToolResult (( await parent . context ( context )). messages ); 316assert ( ! result . isError ); 317assertStringIncludes ( result . text , "moving on" ); 318const graph = await berry . harness . taskGraph ( context ); 319assertEquals ( Object . keys ( graph . value . tasks ), []); 320graph . dispose (); 321}; 322 323await unawaited ({ task :"block" , model}); 324const index = ( await berry . harness . snapshot ( Conversations , context )) ! ; 325const [ child ] = Object . keys ( index ). filter (( key ) => index [ key ]. owner === tid ); 326await unawaited ({ task :"block" , conversation :child }); 327const childId = ( await berry . conversation ( child )) ! . id ; 328assertEquals (( await berry . harness . snapshot ( LiveDoc , childId , context ))?. run , undefined ); 329}); 330 331Deno . test ( "delegation fans out on a selected cheaper model with inherited configuration and no recursive delegation" , async () => { 332const faux = fauxProvider ({ 333models :[ 334{ id :"parent-thinker" , reasoning :true }, 335{ id :"cheap-worker" , reasoning :false }, 336], 337}); 338const models = createModels (); 339models . setProvider ( faux . provider ); 340const berry = await openBerry ( new MemoryStorage (), models , undefined , { 341open :( name , cwd ) => 342Promise . resolve ( Object . assign ( localEnvironment ( cwd ?? Deno . cwd ()), { name})), 343close :() => Promise . resolve (), 344}); 345const model = { provider :"faux" , modelId :"parent-thinker" }; 346const worker = { provider :"faux" , modelId :"cheap-worker" }; 347await berry . enableModels ([ model , worker ]); 348await berry . install ( dir ()); 349const entered = Promise . withResolvers < void >(); 350const release = Promise . withResolvers < void >(); 351const held :{ name :string ; tid :string ; cwd :string ; machine :string }[] = []; 352const requests :( readonly Message [])[] = []; 353await berry . install ( defineExtension ({ 354name :"workers" , 355initial :() => null , 356setup ( ext ) { 357ext . tool ({ 358name :"hold" , 359description :"hold until released" , 360parameters :Type . Object ({ name :Type . String () }), 361expose :"both" , 362async execute ({ name}, ctx ) { 363held . push ({ name, tid :ctx . tid , cwd :ctx . env . cwd , machine :ctx . env . name }); 364if ( held . length === 2 ) entered . resolve (); 365await release . promise ; 366return `answer ${ name } ` ; 367}, 368}); 369}, 370})); 371const tid = await berry . create ({ 372extensions :{ 373subagent :{}, 374dir :{ cwd :"/chosen" }, 375workers :{}, 376environment :{ environment :"remote" , cwd :Deno . cwd () }, 377}, 378 model, 379thinkingLevel :"high" , 380}); 381const parent = ( await berry . conversation ( tid )) ! ; 382await ( await berry . emit ( tid , "dir" , { type :"cd" , cwd :"/updated" })). wait ( context ); 383const router = ( request :{ messages :readonly Message [] }) => { 384requests . push ( request . messages ); 385const message = last ( request . messages ); 386const text = textOf ( message ); 387if ( message . role === "user" && text === "go" ) { 388return fauxAssistantMessage ([ 389fauxToolCall ( "subagent" , { task :"alpha" , model :worker }, { id :"alpha" }), 390fauxToolCall ( "subagent" , { task :"beta" , model :worker }, { id :"beta" }), 391], { stopReason :"toolUse" }); 392} 393if ( message . role === "user" ) { 394assert ([ "alpha" , "beta" ]. includes ( text )); 395assert ( ! request . messages . some (( m ) => m . role === "user" && textOf ( m ) === "go" )); 396assert ( ! toolsOffered ( request . messages ). includes ( "subagent" )); 397assertStringIncludes ( 398sectionsSeen ( request . messages ). find (( s ) => s . dir )?. dir ?? "" , 399"/updated" , 400); 401return fauxAssistantMessage ( fauxToolCall ( "hold" , { name :text }), { 402stopReason :"toolUse" , 403}); 404} 405if ( message . role === "toolResult" && message . toolName === "hold" ) { 406return fauxAssistantMessage ( text ); 407} 408return fauxAssistantMessage ( "done" ); 409}; 410faux . setResponses ( Array . from ({ length :15 }, () => ( request , _options , _state , selected ) => { 411const isParent = request . messages . some (( m ) => m . role === "user" && textOf ( m ) === "go" ); 412assertEquals ( 413{ provider :selected . provider , modelId :selected . id }, 414isParent ?model :worker , 415); 416return router ( request ); 417})); 418try { 419const submission = await parent . submit ({ type :"input" , content :"go" }, context ); 420await within ( entered . promise ); 421assert (( await berry . harness . snapshot ( LiveDoc , parent . id , context ))?. run ); 422assertEquals ( held . map (( item ) => item . name ). sort (), [ "alpha" , "beta" ]); 423assertEquals ( new Set ( held . map (( item ) => item . tid )). size , 2 ); 424const index = ( await berry . harness . snapshot ( Conversations , context )) ! ; 425const parentAgent = await parent . agent ( context ); 426assertEquals ( parentAgent . model , model ); 427assertEquals ( parentAgent . thinkingLevel , "high" ); 428const regular = await berry . create ({ extensions :{ dir :{}, workers :{} }, model :worker }); 429const regularAgent = await berry . harness . snapshot ( 430AgentDoc , 431( await berry . conversation ( regular )) ! . id , 432context , 433); 434for ( const child of held ) { 435assertEquals ( index [ child . tid ]. owner , tid ); 436assertEquals ( child . cwd , Deno . cwd ()); 437assertEquals ( child . machine , "remote" ); 438assertEquals ( await berry . state ( child . tid , "dir" ), { cwd :"/updated" }); 439const conversation = ( await berry . conversation ( child . tid )) ! ; 440const stored = await berry . harness . snapshot ( AgentDoc , conversation . id , context ); 441assertEquals ( stored ?. extensions , regularAgent ?. extensions ); 442const agent = await conversation . agent ( context ); 443assertEquals ( agent . model , worker ); 444assertEquals ( agent . thinkingLevel , "off" ); 445assertEquals ( 446agent . extensions . map (( ext ) => ext . name ), 447parentAgent . extensions . map (( ext ) => ext . name ). filter (( name ) => name !== "subagent" ), 448); 449} 450release . resolve (); 451assertEquals (( await within ( submission . wait ( context ))). status , "done" ); 452await within ( parent . waitForIdle ( context )); 453assertEquals (( await parent . agent ( context )). model , model ); 454assertEquals (( await parent . agent ( context )). thinkingLevel , "high" ); 455const resumed = requests . at ( - 1 ) ! ; 456const results = resumed . filter (( m ) => m . role === "toolResult" ); 457const text = results . map ( textOf ). join ( "\n" ); 458assertStringIncludes ( text , "answer alpha" ); 459assertStringIncludes ( text , "answer beta" ); 460for ( const result of results ) { 461const details = result . role === "toolResult" ?result . details as { tid ?:string } :undefined ; 462assert ( held . some (( child ) => child . tid === details ?. tid )); 463} 464const graph = await berry . harness . taskGraph ( context ); 465assertEquals ( Object . keys ( graph . value . tasks ), []); 466graph . dispose (); 467} finally { 468release . resolve (); 469await berry . close (); 470} 471}); 472 473Deno . test ( "delegation is reserved and an owned conversation cannot re-enable it to delegate" , async () => { 474await usingharness = await setup (); 475const { berry, faux, model} = harness ; 476await assertRejects ( 477() => berry . install ( defineExtension ({ name :"subagent" , initial :() => null , setup () {} })), 478Error , 479"harness's own" , 480); 481const tid = await berry . create ({ extensions :{ subagent :{} }, model}); 482const parent = ( await berry . conversation ( tid )) ! ; 483const anchor = Promise . withResolvers < void >(); 484let childTid = "" ; 485await berry . install ( defineExtension ({ 486name :"capture" , 487initial :() => null , 488setup ( ext ) { 489ext . tool ({ 490name :"capture" , 491description :"capture child" , 492parameters :Type . Object ({}), 493expose :"model" , 494execute ( _ , ctx ) { 495childTid = ctx . tid ; 496anchor . resolve (); 497return "captured" ; 498}, 499}); 500}, 501})); 502await berry . configure ( tid , { extensions :[ "subagent" , "capture" ] }); 503faux . setResponses ([ 504fauxAssistantMessage ( fauxToolCall ( "subagent" , { task :"child" , model}), { 505stopReason :"toolUse" , 506}), 507fauxAssistantMessage ( fauxToolCall ( "capture" , {}), { stopReason :"toolUse" }), 508fauxAssistantMessage ( "child answer" ), 509fauxAssistantMessage ( "done" ), 510]); 511await within (( await parent . submit ({ type :"input" , content :"go" }, context )). wait ( context )); 512await within ( anchor . promise ); 513await berry . configure ( childTid , { extensions :[ "subagent" ] }); 514faux . setResponses ([ 515fauxAssistantMessage ( fauxToolCall ( "subagent" , { task :"grandchild" , model}), { 516stopReason :"toolUse" , 517}), 518fauxAssistantMessage ( "done" ), 519]); 520const child = ( await berry . conversation ( childTid )) ! ; 521await within ( 522( await child . submit ({ type :"input" , content :"try again" }, context )). wait ( context ), 523); 524const entries = ( await child . entries ({ order :"ascending" }, 256 , undefined , context )). items ; 525const result = entries . flatMap (( entry ) => entry . model ?? []). findLast (( m ) => 526m . role === "toolResult" 527); 528assert ( result ?. role === "toolResult" && result . isError ); 529assertStringIncludes ( textOf ( result ), "Only top-level agents" ); 530assertEquals ( Object . keys (( await berry . harness . snapshot ( Conversations , context )) ! ). length , 2 ); 531}); 532 533/** Answers "go" with two subagent calls, alpha and beta, and has each child block under its task's name. */ 534const fanOut = ( 535model :{ provider :string ; modelId :string }, 536request :{ messages :readonly Message [] }, 537) => { 538const message = last ( request . messages ); 539if ( message . role === "user" && textOf ( message ) === "go" ) { 540return fauxAssistantMessage ([ 541fauxToolCall ( "subagent" , { task :"alpha" , model}, { id :"alpha" }), 542fauxToolCall ( "subagent" , { task :"beta" , model}, { id :"beta" }), 543], { stopReason :"toolUse" }); 544} 545if ( message . role === "user" ) { 546return fauxAssistantMessage ( fauxToolCall ( "block" , { name :textOf ( message ) }), { 547stopReason :"toolUse" , 548}); 549} 550if ( message . role === "toolResult" && message . toolName === "block" ) { 551return fauxAssistantMessage ( textOf ( message )); 552} 553return fauxAssistantMessage ( "done" ); 554}; 555 556Deno . test ( "aborting the parent stops every foreground child" , async () => { 557await usingharness = await setup (); 558const { berry, faux, model} = harness ; 559const blocker = blocking (); 560await berry . install ( blocker . extension ); 561const tid = await berry . create ({ extensions :{ subagent :{}, blocking :{} }, model}); 562const parent = ( await berry . conversation ( tid )) ! ; 563faux . setResponses ( Array . from ({ length :10 }, () => ( request ) => fanOut ( model , request ))); 564const submission = await parent . submit ({ type :"input" , content :"go" }, context ); 565await until (() => blocker . running === 2 ); 566await within ( parent . abort ( context )); 567const settled = await within ( submission . wait ( context )); 568assert ( settled . status === "unanswered" && settled . reason === "aborted" ); 569assertEquals ( blocker . cancelled . sort (), [ "alpha" , "beta" ]); 570const graph = await berry . harness . taskGraph ( context ); 571assertEquals ( Object . keys ( graph . value . tasks ), []); 572graph . dispose (); 573}); 574 575Deno . test ( "a failed delegation leaves its sibling working" , async () => { 576await usingharness = await setup (); 577const { berry, faux, model} = harness ; 578const blocker = blocking (); 579await berry . install ( blocker . extension ); 580let resumed :readonly Message [] = []; 581const tid = await berry . create ({ extensions :{ subagent :{}, blocking :{} }, model}); 582const parent = ( await berry . conversation ( tid )) ! ; 583faux . setResponses ( Array . from ({ length :15 }, () => ( request ) => { 584const message = last ( request . messages ); 585if ( message . role === "toolResult" && message . toolName === "subagent" ) { 586resumed = request . messages ; 587} 588return fanOut ( model , request ); 589})); 590try { 591const submission = await parent . submit ({ type :"input" , content :"go" }, context ); 592await until (() => blocker . running === 2 ); 593await within (( await berry . conversation ( blocker . started . get ( "alpha" ) ! )) ! . abort ( context )); 594assertEquals ( blocker . cancelled , [ "alpha" ]); 595assert (( await berry . harness . snapshot ( LiveDoc , parent . id , context ))?. run ); 596blocker . release . resolve ( "beta finished" ); 597assertEquals (( await within ( submission . wait ( context ))). status , "done" ); 598await within ( parent . waitForIdle ( context )); 599const results = resumed . filter (( m ) => m . role === "toolResult" ). map ( textOf ). join ( "\n" ); 600assertStringIncludes ( results , "Subagent failed: aborted" ); 601const failure = resumed . find (( m ) => m . role === "toolResult" && m . isError ) ! ; 602assertStringIncludes ( textOf ( failure ), blocker . started . get ( "alpha" ) ! ); 603assertStringIncludes ( results , "beta finished" ); 604assertEquals ( blocker . cancelled , [ "alpha" ]); 605} finally { 606blocker . release . resolve ( "cleanup" ); 607} 608}); 609 610Deno . test ( "an aborted codemode subagent reports a conversation the parent can continue" , async () => { 611await usingharness = await setup (); 612const { berry, faux, model} = harness ; 613const blocker = blocking (); 614await berry . install ( blocker . extension ); 615const tid = await berry . create ({ extensions :{ subagent :{}, blocking :{} }, model}); 616const parent = ( await berry . conversation ( tid )) ! ; 617faux . setResponses ([ 618script ( `return await tools.subagent({ task: "block", model: ${ JSON . stringify ( model )} });` ), 619fauxAssistantMessage ( fauxToolCall ( "block" , {}), { stopReason :"toolUse" }), 620fauxAssistantMessage ( "paused" ), 621]); 622const submission = await parent . submit ({ type :"input" , content :"go" }, context ); 623await until (() => blocker . running === 1 ); 624const child = blocker . started . get ( "" ) ! ; 625await within (( await berry . conversation ( child )) ! . abort ( context )); 626assertEquals (( await within ( submission . wait ( context ))). status , "done" ); 627const result = lastToolResult (( await parent . context ( context )). messages ); 628assert ( result . isError ); 629assertStringIncludes ( result . text , "aborted" ); 630assertStringIncludes ( result . text , child ); 631 632const reported = result . text . match ( / subagent conversation ([^;]+); / )?.[ 1 ]; 633assertEquals ( reported , child ); 634const seen = await turn ( harness , tid , [ 635script ( 636`return await tools.subagent({ task: "continue", conversation: ${ 637JSON . stringify ( reported ) 638} });` , 639), 640fauxAssistantMessage ( "recovered answer" ), 641]); 642const continued = lastToolResult ( seen . at ( - 1 ) ! ); 643assert ( ! continued . isError ); 644assertStringIncludes ( continued . text , "recovered answer" ); 645assertStringIncludes ( continued . text , child ); 646}); 647 648// a codemode script cannot resume after a restart, so recovery differs by how the call was made 649for ( const mode of [ "direct" , "codemode" ] as const ) { 650Deno . test ( ` ${ mode } delegation recovers without duplicating children or submissions` , async () => { 651await usingtemporary = await tempDir (); 652const directory = temporary . path ; 653const faux = fauxProvider ({ 654models :[{ id :"parent" , reasoning :true }, { id :"worker" , reasoning :true }], 655}); 656const models = createModels (); 657models . setProvider ( faux . provider ); 658const model = { provider :"faux" , modelId :"parent" }; 659const worker = { provider :"faux" , modelId :"worker" }; 660const open = () => 661openNodeSqliteStorage ( ` ${ directory } /session.sqlite` ). then (( storage ) => 662openBerry ( storage , models ) 663); 664let berry = await open (); 665const blocker = blocking (); 666try { 667await berry . install ( blocker . extension ); 668await berry . enableModels ([ model , worker ]); 669const tid = await berry . create ({ 670extensions :{ subagent :{}, blocking :{} }, 671 model, 672thinkingLevel :"high" , 673}); 674const parent = ( await berry . conversation ( tid )) ! ; 675const args = { task :"child" , model :worker , thinkingLevel :"low" }; 676faux . setResponses ([ 677mode === "codemode" 678 ?script ( `return await tools.subagent( ${ JSON . stringify ( args )} );` ) 679 :fauxAssistantMessage ( fauxToolCall ( "subagent" , args ), { stopReason :"toolUse" }), 680fauxAssistantMessage ( fauxToolCall ( "block" , {}), { stopReason :"toolUse" }), 681]); 682const submission = await parent . submit ({ type :"input" , content :"go" }, context ); 683await until (() => blocker . running > 0 ); 684const before = ( await berry . harness . snapshot ( Conversations , context )) ! ; 685const [ childTid ] = Object . keys ( before ). filter (( key ) => key !== tid ); 686assertEquals (( await ( await berry . conversation ( childTid )) ! . agent ( context )). model , worker ); 687assertEquals ( 688( await ( await berry . conversation ( childTid )) ! . agent ( context )). thinkingLevel , 689"low" , 690); 691const resumed :( readonly Message [])[] = []; 692await within ( berry . close ()); 693faux . setResponses ( Array . from ({ length :10 }, () => ( request ) => { 694resumed . push ( request . messages ); 695const message = last ( request . messages ); 696assert ( message . role === "toolResult" ); 697return fauxAssistantMessage ( 698message . toolName === "block" ?"recovered child answer" :"done" , 699); 700})); 701berry = await open (); 702await berry . install ( blocker . extension ); 703berry . harness . resume (); 704const receipt = ( await berry . harness . submission ( submission . id , context )) ! ; 705assertEquals (( await within ( receipt . wait ( context ))). status , "done" ); 706await within ( berry . harness . waitForIdle ( context )); 707assert ( 708( await ( await berry . conversation ( tid )) ! . agent ( context )). extensions . some (( ext ) => 709ext . name === "subagent" 710), 711); 712const after = ( await berry . harness . snapshot ( Conversations , context )) ! ; 713assertEquals ( Object . keys ( after ). sort (), Object . keys ( before ). sort ()); 714const child = ( await berry . conversation ( childTid )) ! ; 715assertEquals (( await child . agent ( context )). model , worker ); 716assertEquals (( await child . agent ( context )). thinkingLevel , "low" ); 717assertEquals (( await ( await berry . conversation ( tid )) ! . agent ( context )). model , model ); 718assertEquals (( await ( await berry . conversation ( tid )) ! . agent ( context )). thinkingLevel , "high" ); 719const entries = ( await child . entries ({ order :"ascending" }, 256 , undefined , context )). items ; 720assertEquals ( 721entries . flatMap (( entry ) => entry . model ?? []). filter (( m ) => m . role === "user" ). length , 7221 , 723); 724const result = lastToolResult ( resumed . at ( - 1 ) ! ); 725if ( mode === "direct" ) { 726assertEquals ( result . text . split ( "\n" )[ 0 ], "recovered child answer" ); 727assertStringIncludes ( result . text , childTid ); 728assert ( ! result . isError ); 729assertEquals ( resumed . length , 2 ); 730} else { 731assert ( result . isError ); 732assertStringIncludes ( result . text . toLowerCase (), "interrupted" ); 733assertEquals ( resumed . length , 1 ); 734assertEquals (( await berry . harness . snapshot ( LiveDoc , child . id , context ))?. run , undefined ); 735} 736} finally { 737await berry . close (); 738} 739}); 740} 741 742Deno . test ( "subagent guidance is editable prose with enabled models always present" , async () => { 743await usingharness = await setup ( undefined , { 744models :[{ id :"parent" }, { id :"worker" }], 745}); 746const { berry, model} = harness ; 747const worker = { provider :"faux" , modelId :"worker" }; 748await berry . enableModels ([ model , worker ]); 749const def = berry . promptProseDefault ( "subagent" ); 750assert ( def ?. trim ()); 751 752const tid = await berry . create ({ extensions :{ subagent :{} }, model}); 753const [ first ] = await turn ( harness , tid , []); 754const seen = sectionsSeen ( first )[ 0 ]; 755assertStringIncludes ( seen . subagent ! , def ! ); 756assertStringIncludes ( seen . subagent ! , "Enabled models:" ); 757assertStringIncludes ( seen . subagent ! , "worker" ); 758 759await berry . setSystemPrompt ({ 760userName :"Sam" , 761promptProse :{ subagent :"Custom guidance for $USER." }, 762}); 763const [ overrode ] = await turn ( harness , tid , []); 764const patch = sectionsSeen ( overrode ). at ( - 1 ) ! ; 765assertStringIncludes ( patch . subagent ! , "Custom guidance for Sam." ); 766assertStringIncludes ( patch . subagent ! , "Enabled models:" ); 767assertStringIncludes ( patch . subagent ! , "worker" ); 768assert ( ! patch . subagent ! . includes ( def ! )); 769 770await berry . setSystemPrompt ({ promptProse :{ subagent :"" } }); 771const [ blanked ] = await turn ( harness , tid , []); 772const cleared = sectionsSeen ( blanked ). at ( - 1 ) ! ; 773assertStringIncludes ( cleared . subagent ! , "Enabled models:" ); 774assertStringIncludes ( cleared . subagent ! , "worker" ); 775assert ( ! cleared . subagent ! . includes ( "Custom guidance" )); 776}); 777 778Deno . test ( "a background spawn returns at once and its report wakes an idle parent" , async () => { 779await usingharness = await setup (); 780const { berry, faux, model} = harness ; 781const blocker = blocking (); 782await berry . install ( blocker . extension ); 783const tid = await berry . create ({ extensions :{ subagent :{}, blocking :{} }, model}); 784const parent = ( await berry . conversation ( tid )) ! ; 785let spawned = false ; 786faux . setResponses ( Array . from ({ length :20 }, () => ( request ) => { 787const message = last ( request . messages ); 788const text = textOf ( message ); 789if ( message . role === "user" && text === "go" && ! spawned ) { 790spawned = true ; 791return fauxAssistantMessage ( 792fauxToolCall ( "codemode" , { 793code :`return await tools.subagent({task:"child work", model: ${ 794JSON . stringify ( model ) 795} , background:true})` , 796}), 797{ stopReason :"toolUse" }, 798); 799} 800if ( message . role === "user" && text === "child work" ) { 801return fauxAssistantMessage ( fauxToolCall ( "block" , {}), { stopReason :"toolUse" }); 802} 803if ( message . role === "toolResult" && message . toolName === "block" ) { 804return fauxAssistantMessage ( text ); 805} 806return fauxAssistantMessage ( "done" ); 807})); 808const submission = await parent . submit ({ type :"input" , content :"go" }, context ); 809await until (() => blocker . running > 0 ); 810// the spawn returns without waiting for the child, so the parent idles while it stays blocked 811await within ( parent . waitForIdle ( context )); 812assertEquals ( blocker . running , 1 ); 813assertEquals (( await within ( submission . wait ( context ))). status , "done" ); 814const index = ( await berry . harness . snapshot ( Conversations , context )) ! ; 815const [ child ] = Object . keys ( index ). filter (( key ) => index [ key ]. owner === tid ); 816assert ( child ); 817blocker . release . resolve ( "child answer" ); 818const reports = await within (( async () => { 819for (;;) { 820const items = ( await parent . entries ({ order :"ascending" }, 256 , undefined , context )). items ; 821const found = items . filter (( entry ) => entry . kind === "berry.subagent-report" ); 822if ( found . length > 0 ) return found ; 823await new Promise (( resolve ) => setTimeout ( resolve , 20 )); 824} 825})()); 826assertEquals ( reports . length , 1 ); 827const data = reports [ 0 ]. data as { tid :string ; task :string ; status :string ; answer ?:string }; 828assertEquals ( data . tid , child ); 829assertEquals ( data . task , "child work" ); 830assertEquals ( data . status , "completed" ); 831assertEquals ( data . answer , "child answer" ); 832const messages = ( await parent . context ( context )). messages ; 833assert ( messages . some (( message ) => textOf ( message ). includes ( "child answer" ))); 834}); 835 836Deno . test ( "the cap refuses a spawn and allows one after a stop" , async () => { 837await usingharness = await setup (); 838const { berry, faux, model} = harness ; 839const blocker = blocking (); 840await berry . install ( blocker . extension ); 841const tid = await berry . create ({ extensions :{ subagent :{}, blocking :{} }, model}); 842await berry . configureExtension ( tid , "subagent" , { models :[], limit :1 }); 843const parent = ( await berry . conversation ( tid )) ! ; 844const scripts = [ 845`return await tools.subagent({task:"one", model: ${ JSON . stringify ( model )} , background:true})` , 846`return await tools.subagent({task:"two", model: ${ JSON . stringify ( model )} , background:true})` , 847`return await tools.subagent_stop({})` , 848`return await tools.subagent({task:"two", model: ${ JSON . stringify ( model )} , background:true})` , 849]; 850faux . setResponses ( Array . from ({ length :30 }, () => ( request ) => { 851const message = last ( request . messages ); 852const text = textOf ( message ); 853if ( message . role === "user" && text === "go" ) { 854const code = scripts . shift () ?? "return 0;" ; 855return fauxAssistantMessage ( fauxToolCall ( "codemode" , { code}), { stopReason :"toolUse" }); 856} 857if ( message . role === "user" && ( text === "one" || text === "two" )) { 858return fauxAssistantMessage ( fauxToolCall ( "block" , {}), { stopReason :"toolUse" }); 859} 860return fauxAssistantMessage ( "done" ); 861})); 862const run = async () => { 863await within (( await parent . submit ({ type :"input" , content :"go" }, context )). wait ( context )); 864await within ( parent . waitForIdle ( context )); 865return lastToolResult (( await parent . context ( context )). messages ); 866}; 867const first = await run (); 868assert ( ! first . isError ); 869assertStringIncludes ( first . text , "running" ); 870await until (() => blocker . running > 0 ); 871const refused = await run (); 872assert ( refused . isError ); 873assertStringIncludes ( refused . text , "Subagent limit reached" ); 874assertStringIncludes ( refused . text , "1 running" ); 875assertStringIncludes ( refused . text , "subagent_wait" ); 876assertStringIncludes ( refused . text , "subagent_stop" ); 877const stopped = await run (); 878assert ( ! stopped . isError ); 879assertStringIncludes ( stopped . text , "stopped" ); 880const retry = await run (); 881assert ( ! retry . isError ); 882assertStringIncludes ( retry . text , "running" ); 883}); 884 885Deno . test ( "subagent_wait collects a settled child and times out without killing work" , async () => { 886await usingharness = await setup (); 887const { berry, faux, model} = harness ; 888const blocker = blocking (); 889await berry . install ( blocker . extension ); 890const tid = await berry . create ({ extensions :{ subagent :{}, blocking :{} }, model}); 891const parent = ( await berry . conversation ( tid )) ! ; 892const scripts :string [] = [ 893`const spawned = await tools.subagent({task:"waited", model: ${ 894JSON . stringify ( model ) 895} , background:true}); store("child", spawned.conversation); return spawned;` , 896`return await tools.subagent_wait({timeoutMs:50})` , 897`const child = load("child"); return await tools.subagent_wait({conversations:[child]})` , 898]; 899faux . setResponses ( Array . from ({ length :40 }, () => ( request ) => { 900const message = last ( request . messages ); 901const text = textOf ( message ); 902if ( message . role === "user" && text === "go" ) { 903const code = scripts . shift () ?? "return 0;" ; 904return fauxAssistantMessage ( fauxToolCall ( "codemode" , { code}), { stopReason :"toolUse" }); 905} 906if ( message . role === "user" && text === "waited" ) { 907return fauxAssistantMessage ( fauxToolCall ( "block" , {}), { stopReason :"toolUse" }); 908} 909if ( message . role === "toolResult" && message . toolName === "block" ) { 910return fauxAssistantMessage ( text ); 911} 912return fauxAssistantMessage ( "done" ); 913})); 914const run = async () => { 915await within (( await parent . submit ({ type :"input" , content :"go" }, context )). wait ( context )); 916await within ( parent . waitForIdle ( context )); 917return lastToolResult (( await parent . context ( context )). messages ); 918}; 919const spawned = await run (); 920assert ( ! spawned . isError ); 921await until (() => blocker . running > 0 ); 922const index = ( await berry . harness . snapshot ( Conversations , context )) ! ; 923const [ child ] = Object . keys ( index ). filter (( key ) => index [ key ]. owner === tid ); 924assert ( child ); 925// the wait times out while the child stays blocked, so the work survives it 926const timedOut = await run (); 927assert ( ! timedOut . isError ); 928assertStringIncludes ( timedOut . text , "running" ); 929assertStringIncludes ( timedOut . text , child ); 930assertEquals ( blocker . running , 1 ); 931blocker . release . resolve ( "waited answer" ); 932const collected = await run (); 933assert ( ! collected . isError ); 934assertStringIncludes ( collected . text , "done" ); 935assertStringIncludes ( collected . text , "waited answer" ); 936assertStringIncludes ( collected . text , child ); 937}); 938 939Deno . test ( "subagent_stop stops the child and leaves a note without starting a run" , async () => { 940await usingharness = await setup (); 941const { berry, faux, model} = harness ; 942const blocker = blocking (); 943await berry . install ( blocker . extension ); 944const tid = await berry . create ({ extensions :{ subagent :{}, blocking :{} }, model}); 945const parent = ( await berry . conversation ( tid )) ! ; 946const scripts = [ 947`return await tools.subagent({task:"stop me", model: ${ 948JSON . stringify ( model ) 949} , background:true})` , 950`return await tools.subagent_stop({})` , 951]; 952faux . setResponses ( Array . from ({ length :30 }, () => ( request ) => { 953const message = last ( request . messages ); 954const text = textOf ( message ); 955if ( message . role === "user" && text === "go" ) { 956const code = scripts . shift () ?? "return 0;" ; 957return fauxAssistantMessage ( fauxToolCall ( "codemode" , { code}), { stopReason :"toolUse" }); 958} 959if ( message . role === "user" && text === "stop me" ) { 960return fauxAssistantMessage ( fauxToolCall ( "block" , { name :"stop me" }), { 961stopReason :"toolUse" , 962}); 963} 964return fauxAssistantMessage ( "done" ); 965})); 966const run = async () => { 967await within (( await parent . submit ({ type :"input" , content :"go" }, context )). wait ( context )); 968await within ( parent . waitForIdle ( context )); 969return lastToolResult (( await parent . context ( context )). messages ); 970}; 971const spawned = await run (); 972assert ( ! spawned . isError ); 973await until (() => blocker . running > 0 ); 974const index = ( await berry . harness . snapshot ( Conversations , context )) ! ; 975const [ child ] = Object . keys ( index ). filter (( key ) => index [ key ]. owner === tid ); 976assert ( child ); 977const stopped = await run (); 978assert ( ! stopped . isError ); 979assertStringIncludes ( stopped . text , "stopped" ); 980assertStringIncludes ( stopped . text , child ); 981assertEquals ( blocker . running , 0 ); 982assertEquals ( blocker . cancelled , [ "stop me" ]); 983const entries = ( await parent . entries ({ order :"ascending" }, 256 , undefined , context )). items ; 984const notes = entries . filter (( entry ) => entry . kind === "berry.subagent-report" ); 985assertEquals ( notes . length , 1 ); 986const data = notes [ 0 ]. data as { tid :string ; task :string ; status :string }; 987assertEquals ( data . tid , child ); 988assertEquals ( data . task , "stop me" ); 989assertEquals ( data . status , "stopped" ); 990// the note does not wake the parent: no new run starts on its own 991const count = entries . length ; 992await new Promise (( resolve ) => setTimeout ( resolve , 100 )); 993await within ( parent . waitForIdle ( context )); 994assertEquals (( await berry . harness . snapshot ( LiveDoc , parent . id , context ))?. run , undefined ); 995const later = ( await parent . entries ({ order :"ascending" }, 256 , undefined , context )). items ; 996assertEquals ( later . length , count ); 997}); 998 999Deno . test ( "a background delegation survives a restart and reports exactly once" , async () => { 1000await usingtemporary = await tempDir (); 1001const directory = temporary . path ; 1002const faux = fauxProvider ({ models :[{ id :"parent" }, { id :"worker" }] }); 1003const models = createModels (); 1004models . setProvider ( faux . provider ); 1005const model = { provider :"faux" , modelId :"parent" }; 1006const worker = { provider :"faux" , modelId :"worker" }; 1007const open = () => 1008openNodeSqliteStorage ( ` ${ directory } /session.sqlite` ). then (( storage ) => 1009openBerry ( storage , models ) 1010); 1011let berry = await open (); 1012const blocker = blocking (); 1013try { 1014await berry . install ( blocker . extension ); 1015await berry . enableModels ([ model , worker ]); 1016const tid = await berry . create ({ extensions :{ subagent :{}, blocking :{} }, model}); 1017const parent = ( await berry . conversation ( tid )) ! ; 1018const respond = () => ( request :{ messages :readonly Message [] }) => { 1019const message = last ( request . messages ); 1020const text = textOf ( message ); 1021if ( message . role === "user" && text === "go" ) { 1022return fauxAssistantMessage ( 1023fauxToolCall ( "codemode" , { 1024code :`return await tools.subagent({task:"restart work", model: ${ 1025JSON . stringify ( worker ) 1026} , background:true})` , 1027}), 1028{ stopReason :"toolUse" }, 1029); 1030} 1031if ( message . role === "user" && text === "restart work" ) { 1032return fauxAssistantMessage ( fauxToolCall ( "block" , {}), { stopReason :"toolUse" }); 1033} 1034if ( message . role === "toolResult" && message . toolName === "block" ) { 1035return fauxAssistantMessage ( text ); 1036} 1037return fauxAssistantMessage ( "done" ); 1038}; 1039faux . setResponses ( Array . from ({ length :20 }, respond )); 1040await parent . submit ({ type :"input" , content :"go" }, context ); 1041await until (() => blocker . running > 0 ); 1042const before = ( await berry . harness . snapshot ( Conversations , context )) ! ; 1043const [ childTid ] = Object . keys ( before ). filter (( key ) => key !== tid ); 1044assert ( childTid ); 1045await within ( berry . close ()); 1046// after a restart the interrupted block settles and the child answers anew 1047faux . setResponses ( 1048Array . from ({ length :20 }, () => ( request :{ messages :readonly Message [] }) => { 1049const message = last ( request . messages ); 1050if ( message . role === "toolResult" && message . toolName === "block" ) { 1051return fauxAssistantMessage ( "restarted answer" ); 1052} 1053return fauxAssistantMessage ( "done" ); 1054}), 1055); 1056berry = await open (); 1057await berry . install ( blocker . extension ); 1058berry . harness . resume (); 1059const parentAfter = ( await berry . conversation ( tid )) ! ; 1060await within ( berry . harness . waitForIdle ( context )); 1061const after = ( await berry . harness . snapshot ( Conversations , context )) ! ; 1062assertEquals ( Object . keys ( after ). sort (), Object . keys ( before ). sort ()); 1063const entries = 1064( await parentAfter . entries ({ order :"ascending" }, 256 , undefined , context )). items ; 1065const reports = entries . filter (( entry ) => entry . kind === "berry.subagent-report" ); 1066assertEquals ( reports . length , 1 ); 1067const data = reports [ 0 ]. data as { tid :string ; task :string ; status :string ; answer ?:string }; 1068assertEquals ( data . tid , childTid ); 1069assertEquals ( data . task , "restart work" ); 1070assertEquals ( data . status , "completed" ); 1071assertEquals ( data . answer , "restarted answer" ); 1072} finally { 1073await berry . close (); 1074} 1075}); 1076 1077Deno . test ( "continuing a subagent in the background reports the new answer and stops" , async () => { 1078await usingharness = await setup (); 1079const { berry, faux, model} = harness ; 1080const blocker = blocking (); 1081await berry . install ( blocker . extension ); 1082const tid = await berry . create ({ extensions :{ subagent :{}, blocking :{} }, model}); 1083const parent = ( await berry . conversation ( tid )) ! ; 1084const scripts = [ 1085`const first = await tools.subagent({task:"one", model: ${ 1086JSON . stringify ( model ) 1087} , background:true}); store("child", first.conversation); return first;` , 1088`const child = load("child"); return await tools.subagent({task:"two", conversation: child, background:true});` , 1089`const child = load("child"); return await tools.subagent({task:"three", conversation: child, background:true});` , 1090`const child = load("child"); return await tools.subagent_stop({conversations:[child]});` , 1091]; 1092faux . setResponses ( Array . from ({ length :40 }, () => ( request ) => { 1093const message = last ( request . messages ); 1094const text = textOf ( message ); 1095if ( message . role === "user" && text === "go" ) { 1096const code = scripts . shift () ?? "return 0;" ; 1097return fauxAssistantMessage ( fauxToolCall ( "codemode" , { code}), { stopReason :"toolUse" }); 1098} 1099if ( message . role === "user" && ( text === "one" || text === "two" )) { 1100return fauxAssistantMessage ( text === "one" ?"ans1" :"ans2" ); 1101} 1102if ( message . role === "user" && text === "three" ) { 1103return fauxAssistantMessage ( fauxToolCall ( "block" , { name :"three" }), { 1104stopReason :"toolUse" , 1105}); 1106} 1107if ( message . role === "toolResult" && message . toolName === "block" ) { 1108return fauxAssistantMessage ( text ); 1109} 1110return fauxAssistantMessage ( "done" ); 1111})); 1112const run = async () => { 1113await within (( await parent . submit ({ type :"input" , content :"go" }, context )). wait ( context )); 1114await within ( parent . waitForIdle ( context )); 1115return lastToolResult (( await parent . context ( context )). messages ); 1116}; 1117const reportWith = ( answer :string ) => 1118within (( async () => { 1119for (;;) { 1120const items = ( await parent . entries ({ order :"ascending" }, 256 , undefined , context )). items ; 1121const match = items . find (( entry ) => 1122entry . kind === "berry.subagent-report" && 1123( entry . data as { answer ?:string }). answer === answer 1124); 1125if ( match ) return match ; 1126await new Promise (( resolve ) => setTimeout ( resolve , 20 )); 1127} 1128})()); 1129const spawned = await run (); 1130assert ( ! spawned . isError ); 1131const first = await reportWith ( "ans1" ); 1132assertEquals (( first . data as { task :string }). task , "one" ); 1133const childTid = ( first . data as { tid :string }). tid ; 1134assertEquals (( await berry . harness . snapshot ( Conversations , context )) ! [ childTid ]. title , "one" ); 1135const continued = await run (); 1136assert ( ! continued . isError ); 1137assertStringIncludes ( continued . text , "running" ); 1138const second = await reportWith ( "ans2" ); 1139assertEquals (( second . data as { task :string }). task , "two" ); 1140assertEquals (( await berry . harness . snapshot ( Conversations , context )) ! [ childTid ]. title , "two" ); 1141assertEquals (( second . data as { tid :string }). tid , ( first . data as { tid :string }). tid ); 1142const live = await run (); 1143assert ( ! live . isError ); 1144await until (() => blocker . running > 0 ); 1145const index = ( await berry . harness . snapshot ( Conversations , context )) ! ; 1146const [ child ] = Object . keys ( index ). filter (( key ) => index [ key ]. owner === tid ); 1147assert ( child ); 1148const stopped = await run (); 1149assert ( ! stopped . isError ); 1150assertStringIncludes ( stopped . text , "stopped" ); 1151assertEquals ( blocker . running , 0 ); 1152assertEquals ( blocker . cancelled , [ "three" ]); 1153assertEquals (( await berry . harness . snapshot ( Conversations , context )) ! [ childTid ]. title , "three" ); 1154const entries = ( await parent . entries ({ order :"ascending" }, 256 , undefined , context )). items ; 1155const note = entries . find (( entry ) => 1156entry . kind === "berry.subagent-report" && 1157( entry . data as { status ?:string }). status === "stopped" 1158); 1159assert ( note ); 1160assertEquals (( note ! . data as { tid :string }). tid , child ); 1161assertEquals (( note ! . data as { task :string }). task , "three" ); 1162});