char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
056cf36
main
1import type { Context , JsonValue } from "@earendil-works/chord" ; 2import { BACKGROUND_CONTEXT , withAbortSignal } from "@earendil-works/chord/context" ; 3import { type ModelThinkingLevel , Type } from "@earendil-works/pi-ai" ; 4import { 5AgentDoc , 6AssistantEntry , 7type ConversationId , 8type Cursor , 9defineDoc , 10defineExtension as defineDurableExtension , 11defineTask , 12defineTool , 13type Harness , 14type ModelRef , 15section , 16type TaskId , 17type TaskOutcome , 18type ToolExecutionApi , 19type Tx , 20} from "@earendil-works/pi-durable" ; 21import { defineExtension } from "@berry/api" ; 22import { EventEntry } from "./entries.ts" ; 23import { startTool } from "./tool-timing.ts" ; 24 25export const SUBAGENT = "subagent" ; 26export const SUBAGENT_DESCRIPTION = "delegate tasks to blocking subagents" ; 27 28const PROSE_DEFAULT = "subagents:\n" + [ 29"work locally by default; delegate only for parallelizable work, independent review, " + 30"cheaper-model tasks, or noisy exploration you want kept out of your context" , 31"subagents share the working tree: don't run parallel workers that edit overlapping files " + 32"or mutate VCS state" , 33"when continuing a `conversation`, omit `model` and `thinkingLevel` so its prompt cache " + 34"stays warm" , 35"background subagents report back on their own—don't poll `subagent_wait` unless you are " + 36"blocked on the result, and `subagent_stop` work you no longer need" , 37]. map (( g ) => `- ${ g } ` ). join ( "\n" ); 38 39const Call = defineDoc <{ delegation ?:{ id :TaskId < string >; child :ConversationId } }>({ 40kind :"berry.delegation-call" , 41version :1 , 42scope :"task" , 43initial :() => ({}), 44checkpointWhen :() => true , 45}); 46 47/** How a subagent's turn ended. */ 48type Answered = { status :"completed" ; answer :string } | { status :"failed" ; error :string }; 49type Settled = Answered | { status :"stopped" }; 50 51/** The input a delegation sends its subagent, which a retry finds again by its request ID. */ 52const turn = ( task :{ id :TaskId < string >; input :{ task :string } }) => ({ 53type :"input" as const , 54content :task . input . task , 55requestId :`delegation: ${ task . id } ` , 56}); 57 58/** A background delegation's report to its parent: the entry the web shows, and what the model reads. */ 59const reportOf = ( tid :string , task :string , outcome :Settled ) => ({ 60kind :"berry.subagent-report" , 61data :{ tid, task, ...outcome }, 62content :outcome . status === "completed" 63 ?`Background subagent ${ tid } finished its task.\nTask: ${ task } \n\n ${ outcome . answer } \n\n(conversation ${ tid } ; pass it as conversation to continue it)` 64 :outcome . status === "failed" 65 ?`Background subagent ${ tid } failed: ${ outcome . error } \nTask: ${ task } \n\n(conversation ${ tid } )` 66 :`Background subagent ${ tid } stopped.\nTask: ${ task } \n\n(conversation ${ tid } )` , 67}); 68 69const terminal = ( outcome :Answered ) => ({ 70status :"terminal" as const , 71outcome :outcome . status === "completed" 72 ?{ status :"completed" as const , result :outcome . answer } 73 :{ status :"failed" as const , error :{ message :outcome . error } }, 74}); 75 76type SubagentArgs = { 77task :string ; 78/** The subagent conversation to continue, by its TID. */ 79conversation ?:string ; 80model ?:ModelRef ; 81thinkingLevel ?:ModelThinkingLevel ; 82/** Run without waiting, reporting back when finished. */ 83background ?:boolean ; 84}; 85 86type Seed = { 87model :ModelRef ; 88thinkingLevel :ModelThinkingLevel ; 89extensions :string []; 90events :{ ext :string ; event :JsonValue }[]; 91}; 92 93/** What a conversation delegates to: its worker models (none means those enabled in settings), its 94* background limit, and the background delegations it started. */ 95export type Subagent = { 96models :ModelRef []; 97/** How many background delegations may run at once. */ 98limit :number ; 99/** Background delegations this conversation started, by their child conversation id. */ 100children :Record < string , Child >; 101}; 102type Child = { 103task :string ; 104/** The delegation task driving it, by id; absent before ids were tracked. */ 105delegation ?:string ; 106model :ModelRef ; 107startedAt :number ; 108settled ?:{ status :Settled [ "status" ]; answer ?:string ; error ?:string }; 109}; 110type SubagentEvent = 111| { type :"models" ; models :ModelRef [] } 112| { type :"limit" ; limit :number } 113| { 114type :"spawned" ; 115conversationId :number ; 116task :string ; 117delegation ?:string ; 118model :ModelRef ; 119startedAt :number ; 120} 121| { 122type :"settled" ; 123conversationId :number ; 124status :Settled [ "status" ]; 125answer ?:string ; 126error ?:string ; 127}; 128 129const keyOf = ( model :ModelRef ) => ` ${ model . provider } / ${ model . modelId } ` ; 130 131type CallOptions = { 132signal ?:AbortSignal ; 133progress ?( details :JsonValue ) :void ; 134}; 135 136/** Delegation, blocking or in the background, with one owned task and transcript per invocation. */ 137export function subagentExtension ( host :{ 138harness :Harness ; 139/** The models enabled in settings. */ 140enabled () :Promise < readonly ModelRef []>; 141workers ( id :ConversationId ) :Promise < Subagent >; 142title ( tx :Tx , id :ConversationId , task :string ) :Promise < void >; 143/** Editable delegation guidance, with `$USER` replaced and blank prose omitted. */ 144prose () :Promise < string | undefined >; 145/** A new subagent's conversation, on `model`. */ 146prepare ( 147api :ToolExecutionApi , 148model :ModelRef , 149thinkingLevel :ModelThinkingLevel | undefined , 150context :Context , 151) :Promise < Seed >; 152/** The level `model` runs `requested` at. */ 153supported ( model :ModelRef , requested :ModelThinkingLevel ) :ModelThinkingLevel ; 154/** A conversation by its TID, with the TID of the conversation that owns it. */ 155resolve ( tid :string ) :Promise <{ id :ConversationId ; owner ?:string } | undefined >; 156tidOf ( id :ConversationId ) :Promise < string >; 157}) { 158async function offered ( id :ConversationId ) { 159const { models} = await host . workers ( id ); 160return models . length > 0 ?models :await host . enabled (); 161} 162 163/** A subagent's turn: the first in a conversation it owns, or one more in a conversation it continues. */ 164const Delegation = defineTask < 165{ task :string ; child ?:ConversationId }, 166{ phase :"run" } | { phase :"report" ; outcome :Answered ; child :ConversationId }, 167string 168>({ 169name :"berry.delegation" , 170version :1 , 171initial :() => ({ phase :"run" }), 172phases :{ 173async run ( task , runtime , context ) { 174let child = task . input . child ; 175if ( child === undefined ) { 176await runtime . commit ( async ( tx ) => { 177const record = ( await tx . scanConversations ({ ownerTaskId :task . id }, 1 )). items [ 0 ]; 178if ( ! record ) throw new Error ( "Delegation has no child conversation" ); 179child = record . id ; 180}, context ); 181} 182const conversation = ( await runtime . conversation ( child ! , context )) ! ; 183const settled = await ( await conversation . submit ( turn ( task ), context )). wait ( context ); 184// the child's answer lives in its own conversation, so read it where any entry is visible; 185// a background settle lands in the same commit, before the report it will send 186await runtime . commit ( async ( tx ) => { 187let outcome :Answered ; 188if ( settled . status !== "done" || settled . type !== "input" ) { 189outcome = { 190status :"failed" , 191error :settled . status === "unanswered" ?settled . reason :settled . status , 192}; 193} else { 194const message = ( await tx . entry ( AssistantEntry , settled . answer ))?. model ?.[ 0 ]; 195if ( message ?. role === "assistant" ) { 196const answer = message . content . flatMap (( part ) => 197part . type === "text" ?[ part . text ] :[] 198); 199outcome = { status :"completed" , answer :answer . join ( "\n" ) }; 200} else if ( task . background ) { 201outcome = { status :"failed" , error :"Subagent has no answer" }; 202} else throw new Error ( "Subagent has no answer" ); 203} 204if ( ! task . background ) return terminal ( outcome ); 205await tx . appendEntry ( EventEntry , runtime . conversationId , { 206data :{ ext :SUBAGENT , event :{ type :"settled" , conversationId :child ! , ...outcome } }, 207}); 208return { status :"running" , checkpoint :{ phase :"report" , outcome, child :child ! } }; 209}, context ); 210}, 211async report ( task , runtime , context ) { 212const { outcome, child} = task . state . checkpoint ; 213const { content, ...entry } = reportOf ( await host . tidOf ( child ), task . input . task , outcome ); 214const parent = ( await runtime . conversation ( runtime . conversationId , context )) ! ; 215await parent . submit ({ 216type :"input" , 217 content, 218 entry, 219whenBusy :"steer" , 220requestId :`subagent-report: ${ task . id } ` , 221}, context ); 222await runtime . commit (() => terminal ( outcome ), context ); 223}, 224}, 225async abort ( task , runtime , context ) { 226// a continued conversation is owned by its first delegation: nothing else stops its turn when 227// only this call is cancelled, as when a script ends without awaiting it 228const { child} = task . input ; 229if ( child !== undefined ) { 230try { 231let pending = false ; 232await runtime . commit ( async ( tx ) => { 233const status = ( await tx . submissionByRequest ( child , turn ( task ). requestId ))?. status ; 234pending = status === "queued" || status === "placed" ; 235}, context ); 236if ( pending ) { 237const conversation = ( await runtime . conversation ( child , context )) ! ; 238const submission = await conversation . submit ( turn ( task ), context ); 239if (( await submission . abort ( context )) === "already_placed" ) { 240await conversation . abort ( context ); 241} 242} 243} catch ( error ) { 244// a stop still leaves its note and settles 245if ( ! task . background ) throw error ; 246} 247} 248if ( ! task . background ) { 249await runtime . commit ( 250() => ({ status :"terminal" , outcome :{ status :"aborted" } }), 251context , 252); 253return ; 254} 255// a child the delegation created was stopped with it, as work it owns 256let stopped = child ; 257if ( stopped === undefined ) { 258await runtime . commit ( async ( tx ) => { 259stopped = ( await tx . scanConversations ({ ownerTaskId :task . id }, 1 )). items [ 0 ]?. id ; 260}, context ). catch (() => {}); 261} 262const tid = stopped === undefined 263 ?String ( task . id ) 264 :await host . tidOf ( stopped ). catch (() => String ( stopped )); 265const { content, ...entry } = reportOf ( tid , task . input . task , { status :"stopped" }); 266await runtime . commit ( async ( tx ) => { 267await tx . appendEntry ( runtime . conversationId , { 268 ...entry , 269model :[{ role :"user" , content, timestamp :Date . now () }], 270}); 271if ( stopped !== undefined ) { 272await tx . appendEntry ( EventEntry , runtime . conversationId , { 273data :{ 274ext :SUBAGENT , 275event :{ type :"settled" , conversationId :stopped , status :"stopped" }, 276}, 277}); 278} 279return { status :"terminal" , outcome :{ status :"aborted" } }; 280}, context ); 281}, 282}); 283 284/** The configuration of which models a conversation delegates to, which the harness folds as an extension's. */ 285const workers = defineExtension < Subagent , SubagentEvent >({ 286name :SUBAGENT , 287description :SUBAGENT_DESCRIPTION , 288sortOrder :30 , 289initial :() => ({ models :[], limit :4 , children :{} }), 290apply :( state , event ) => { 291switch ( event . type ) { 292case "models" : 293return { ...state , models :event . models }; 294case "limit" : 295return { ...state , limit :event . limit }; 296case "spawned" :{ 297const { type :_ , conversationId, ...child } = event ; 298return { ...state , children :{ ...state . children , [ conversationId ] :child } }; 299} 300case "settled" :{ 301const { type :_ , conversationId, ...settled } = event ; 302const child = state . children [ conversationId ]; 303if ( ! child ) return state ; 304return { 305 ...state , 306children :{ ...state . children , [ conversationId ] :{ ...child , settled} }, 307}; 308} 309} 310}, 311setup ( ext ) { 312ext . promptProse ({ default :PROSE_DEFAULT }); 313ext . configuration ({ 314parameters :Type . Object ({ 315models :Type . Array ( Type . String (), { 316title :"worker models" , 317default :[], 318description :"permitted subagent models (empty = all globally-enabled models)" , 319}), 320limit :Type . Integer ({ 321title :"background subagent limit" , 322default :4 , 323minimum :1 , 324description :"how many background delegations may run at once" , 325}), 326}), 327values :({ models, limit}) => ({ models :models . map ( keyOf ), limit}), 328async events ({ models :keys , limit}, state ) { 329const known = new Map ( 330[ ...state . models , ...await host . enabled ()]. map (( model ) => [ keyOf ( model ), model ]), 331); 332const models = [ ...new Set ( keys )]. map (( key ) => { 333const model = known . get ( key ); 334if ( ! model ) throw new Error ( ` ${ key } is not enabled in settings` ); 335return { provider :model . provider , modelId :model . modelId }; 336}); 337return [{ type :"models" , models}, { type :"limit" , limit}]; 338}, 339}); 340}, 341}); 342 343async function invoke ( 344args :SubagentArgs , 345api :ToolExecutionApi , 346context :Context , 347options :CallOptions , 348reuse :boolean , 349) :Promise < 350| { conversation :string ; status :"running" } 351| { conversation :string ; status :"done" ; answer :string } 352> { 353const signal = options . signal ?? context . abortSignal ; 354const { task, background= false } = args ; 355const during = options . signal ?withAbortSignal ( options . signal , context ) :context ; 356let delegation :{ id :TaskId < string >; child :ConversationId } | undefined ; 357try { 358signal ?. throwIfAborted (); 359await api . commit ( async ( tx ) => { 360if (( await tx . conversation ( api . conversationId ))?. owner ) { 361throw new Error ( "Only top-level agents can delegate" ); 362} 363if ( reuse ) { 364const stored = ( await tx . doc ( Call , api . taskId )). delegation ; 365if ( stored ) delegation = { id :stored . id , child :stored . child }; 366} 367}, during ); 368if ( ! delegation ) { 369const offers = await offered ( api . conversationId ); 370const require = ( model :ModelRef ) => { 371if ( ! offers . some (( m ) => keyOf ( m ) === keyOf ( model ))) { 372throw new Error ( ` ${ keyOf ( model )} is not enabled for delegation` ); 373} 374}; 375const { conversation, model, thinkingLevel} = args ; 376const limit = background ?( await host . workers ( api . conversationId )). limit :undefined ; 377let target :{ seed :Seed } | { child :ConversationId }; 378if ( conversation === undefined ) { 379if ( ! model ) throw new Error ( "model is required to start a subagent" ); 380require ( model ); 381target = { seed :await host . prepare ( api , model , thinkingLevel , during ) }; 382} else { 383const found = await host . resolve ( conversation ); 384if ( found ?. owner !== await host . tidOf ( api . conversationId )) { 385throw new Error ( ` ${ conversation } is not one of this conversation's subagents` ); 386} 387target = { child :found . id }; 388} 389await api . commit ( async ( tx ) => { 390// the cap holds within the commit that would exceed it 391if ( limit !== undefined ) { 392let live = 0 ; 393let cursor :Cursor | undefined ; 394do { 395const page = await tx . scanTasks ( 396{ conversationId :api . conversationId , kind :"berry.delegation" , background :true }, 397100 , 398cursor , 399); 400live += page . items . filter (( record ) => record . state . status !== "terminal" ). length ; 401cursor = page . next ; 402} while ( cursor !== undefined ); 403if ( live >= limit ) { 404throw new Error ( 405`Subagent limit reached ( ${ limit } running); use subagent_wait or subagent_stop first.` , 406); 407} 408} 409// a background delegation belongs to the conversation, not this call, so it outlives 410// the call, the script, and the parent's abort without keeping the parent busy 411const id = await tx . createTask ( 412Delegation , 413"child" in target ?{ task, child :target . child } :{ task}, 414background 415 ?{ 416ownership :{ kind :"conversation" }, 417conversationId :api . conversationId , 418background :true , 419} 420 :{ ownership :{ kind :"task" , taskId :api . taskId } }, 421); 422const child = "child" in target 423 ?target . child 424 :( await tx . createConversation ({ ownership :{ kind :"task" , taskId :id } })). id ; 425const agent = await tx . doc ( AgentDoc , child ); 426const worker = "seed" in target ?target . seed . model :model ?? agent . model ; 427if ( ! worker ) throw new Error ( ` ${ conversation } has no model` ); 428require ( worker ); 429if ( "seed" in target ) { 430const { seed} = target ; 431agent . model = seed . model ; 432agent . thinkingLevel = seed . thinkingLevel ; 433agent . extensions = { 434add :seed . extensions . filter (( name ) => name !== SUBAGENT ), 435remove :[ SUBAGENT ], 436}; 437for ( const event of seed . events ) { 438await tx . appendEntry ( EventEntry , child , { data :event }); 439} 440} else if ( model || thinkingLevel ) { 441agent . model = { provider :worker . provider , modelId :worker . modelId }; 442agent . thinkingLevel = host . supported ( 443worker , 444thinkingLevel ?? agent . thinkingLevel ?? "off" , 445); 446} 447if ( background ) { 448await tx . appendEntry ( EventEntry , api . conversationId , { 449data :{ 450ext :SUBAGENT , 451event :{ 452type :"spawned" , 453conversationId :child , 454 task, 455delegation :String ( id ), 456model :{ provider :worker . provider , modelId :worker . modelId }, 457startedAt :Date . now (), 458}, 459}, 460}); 461} 462await host . title ( tx , child , task ); 463delegation = { id, child}; 464if ( reuse ) ( await tx . doc ( Call , api . taskId )). delegation = delegation ; 465}, during ); 466} 467const { id, child} = delegation ! ; 468const tid = await host . tidOf ( child ); 469const details = { tid, conversationId :child , ...( background ?{ status :"running" } :{}) }; 470if ( options . progress ) options . progress ( details ); 471else await api . details ( details , during ); 472if ( background ) return { conversation :tid , status :"running" }; 473const { state :{ outcome} } = await api . waitForTask ( id , during ); 474if ( outcome . status !== "completed" ) { 475throw new Error ( 476`Subagent failed: ${ "error" in outcome ? outcome . error ?. message : outcome . status } \n\n` + 477`(subagent conversation ${ tid } ; pass it as conversation to continue)` , 478); 479} 480return { conversation :tid , status :"done" , answer :outcome . result }; 481} finally { 482// a nested call's cancellation stops its work, not merely its wait; a background 483// spawn returning early is the whole point, so it never aborts its child 484if ( ! background && delegation && signal ?. aborted && ! context . abortSignal ?. aborted ) { 485await host . harness . abortTask ( delegation . id , BACKGROUND_CONTEXT ); 486await host . harness . waitForTask ( delegation . id , BACKGROUND_CONTEXT ); 487} 488} 489} 490 491/** This conversation's background subagents by TID: those named, or every live one. */ 492async function targets ( api :ToolExecutionApi , conversations :string [] | undefined ) { 493const { children} = await host . workers ( api . conversationId ); 494const found :[ string , Child ][] = []; 495if ( conversations === undefined ) { 496for ( const [ id , child ] of Object . entries ( children )) { 497if ( ! child . settled ) found . push ([ await host . tidOf ( Number ( id ) as ConversationId ), child ]); 498} 499return found ; 500} 501const owner = await host . tidOf ( api . conversationId ); 502for ( const tid of conversations ) { 503const resolved = await host . resolve ( tid ); 504const child = resolved ?. owner === owner ?children [ resolved . id ] :undefined ; 505if ( ! child ) throw new Error ( ` ${ tid } is not one of this conversation's background subagents` ); 506found . push ([ tid , child ]); 507} 508return found ; 509} 510 511const waited = ( conversation :string , settled :NonNullable < Child [ "settled" ]>) => 512settled . status === "completed" 513 ?{ conversation, status :"done" , answer :settled . answer ?? "" } 514 :settled . status === "failed" 515 ?{ conversation, status :"failed" , error :settled . error ?? "failed" } 516 :{ conversation, status :"stopped" }; 517 518const settledOf = ( outcome :TaskOutcome < string >) :Settled => 519outcome . status === "completed" 520 ?{ status :"completed" , answer :outcome . result } 521 :outcome . status === "aborted" 522 ?{ status :"stopped" } 523 :{ status :"failed" , error :outcome . error ?. message ?? outcome . reason ?? outcome . status }; 524 525const tool = defineTool ({ 526name :SUBAGENT , 527description : 528"Do the task yourself by default; delegate only when the benefit outweighs handoff overhead. " + 529"Delegate a self-contained task to a worker model, or follow up in a subagent's conversation. " + 530"No parent transcript or further delegation; shared files. Returns the final answer and the " + 531"subagent's conversation." , 532parameters :Type . Object ({ 533task :Type . String ({ 534minLength :1 , 535description :"Task and all needed context, or a follow-up to a continued conversation." , 536}), 537conversation :Type . Optional ( Type . String ({ 538minLength :1 , 539description :"Subagent conversation to continue, from an earlier result." , 540})), 541model :Type . Optional ( Type . Object ({ 542provider :Type . String ({ minLength :1 }), 543modelId :Type . String ({ minLength :1 }), 544}, { 545description : 546"Enabled worker model; required unless continuing, which keeps the subagent's." , 547})), 548thinkingLevel :Type . Optional ( Type . Union ([ 549Type . Literal ( "off" ), 550Type . Literal ( "minimal" ), 551Type . Literal ( "low" ), 552Type . Literal ( "medium" ), 553Type . Literal ( "high" ), 554Type . Literal ( "xhigh" ), 555Type . Literal ( "max" ), 556], { 557description :"Inherits the parent's, or a continued subagent's, when omitted; clamped to " + 558"worker support." , 559})), 560background :Type . Optional ( Type . Boolean ({ 561description :"Start in the background and return at once; the report arrives as a message." , 562})), 563}), 564replay :"safe" , 565executionMode :"parallel" , 566execute :async ( args , api , context ) => { 567await startTool ( api , context ); 568const result = await invoke ( args , api , context , {}, true ); 569const text = result . status === "running" 570 ?`spawned subagent ${ result . conversation } in the background; ` + 571"its report arrives as a message when it finishes" 572 :` ${ result . answer } \n\n` + 573`(subagent conversation ${ result . conversation } ; pass it as conversation to continue)` ; 574return { output :[{ type :"text" , text}] }; 575}, 576}); 577return { 578extension :defineDurableExtension ({ 579name :SUBAGENT , 580tasks :[ Delegation ], 581tools :[ tool ], 582sections :[ 583section ( SUBAGENT , async ({ conversationId}) => { 584const prose = await host . prose (); 585const lines = [ 586"Enabled models:" , 587 ...( await offered ( conversationId )). map (( model ) => JSON . stringify ( model )), 588]; 589const { children, limit} = await host . workers ( conversationId ); 590const live = Object . entries ( children ). filter (([, child ]) => ! child . settled ) 591. sort (([, a ], [, b ]) => a . startedAt - b . startedAt ); 592if ( live . length > 0 ) lines . push ( `Background subagents ( ${ live . length } / ${ limit } ):` ); 593// no elapsed time: this is the system prompt, whose every change costs a cold prompt cache 594for ( const [ id , child ] of live ) { 595const [ first ] = child . task . split ( "\n" ); 596lines . push ( 597`- ${ await host . tidOf ( Number ( id ) as ConversationId )} : ` + 598` ${ first . length > 80 ? ` ${ first . slice ( 0 , 80 )} …` : first } ` + 599`( ${ keyOf ( child . model )} , running)` , 600); 601} 602const dynamic = lines . join ( "\n" ); 603return prose ?` ${ prose } \n ${ dynamic } ` :dynamic ; 604}, { tag :false }), 605], 606}), 607 workers, 608/** What scripts may call: delegation, and the waiting and stopping the model is never offered directly. */ 609scripted :[{ 610declaration :{ 611name :tool . name , 612extension :SUBAGENT , 613description :tool . description , 614parameters :tool . parameters , 615returns :Type . Object ({ 616conversation :Type . String (), 617status :Type . Union ([ Type . Literal ( "done" ), Type . Literal ( "running" )]), 618answer :Type . Optional ( Type . String ()), 619}), 620direct :true , 621}, 622call :( args :SubagentArgs , api :ToolExecutionApi , context :Context , options :CallOptions ) => 623invoke ( args , api , context , options , false ), 624}, { 625declaration :{ 626name :"subagent_wait" , 627extension :SUBAGENT , 628description :"Wait for background subagents to finish and collect their answers; " + 629"a timeout only ends the wait, never the work." , 630parameters :Type . Object ({ 631conversations :Type . Optional ( Type . Array ( Type . String ({ minLength :1 }), { 632description :"Background subagents to wait for; default every live one." , 633})), 634timeoutMs :Type . Optional ( Type . Number ({ 635minimum :1 , 636description :"How long to wait before returning running ones; default 600000." , 637})), 638}), 639returns :Type . Array ( Type . Object ({ 640conversation :Type . String (), 641status :Type . Union ([ 642Type . Literal ( "done" ), 643Type . Literal ( "running" ), 644Type . Literal ( "failed" ), 645Type . Literal ( "stopped" ), 646]), 647answer :Type . Optional ( Type . String ()), 648error :Type . Optional ( Type . String ()), 649})), 650direct :false , 651}, 652call :async ( 653args :{ conversations ?:string []; timeoutMs ?:number }, 654api :ToolExecutionApi , 655context :Context , 656) => { 657const wanted = await targets ( api , args . conversations ); 658const timed = withAbortSignal ( AbortSignal . timeout ( args . timeoutMs ?? 600_000 ), context ); 659return await Promise . all ( wanted . map ( async ([ tid , child ]) => { 660if ( child . settled ) return waited ( tid , child . settled ); 661// a spawn from before ids were tracked has nothing to wait on 662if ( child . delegation === undefined ) return { conversation :tid , status :"running" }; 663const id = Number ( child . delegation ) as TaskId < string >; 664try { 665return waited ( tid , settledOf (( await api . waitForTask ( id , timed )). state . outcome )); 666} catch ( error ) { 667if ( context . abortSignal ?. aborted ) throw error ; 668// the timeout may have raced the end of the work 669const known = await api . getTask ( id , context ). catch (() => undefined ); 670return known ?. state . status === "terminal" 671 ?waited ( tid , settledOf ( known . state . outcome )) 672 :{ conversation :tid , status :"running" }; 673} 674})); 675}, 676}, { 677declaration :{ 678name :"subagent_stop" , 679extension :SUBAGENT , 680description :"Stop background subagents; their note arrives without starting a run." , 681parameters :Type . Object ({ 682conversations :Type . Optional ( Type . Array ( Type . String ({ minLength :1 }), { 683description :"Background subagents to stop; default every live one." , 684})), 685}), 686returns :Type . Array ( Type . Object ({ 687conversation :Type . String (), 688status :Type . Literal ( "stopped" ), 689})), 690direct :false , 691}, 692call :async ( args :{ conversations ?:string [] }, api :ToolExecutionApi , context :Context ) => { 693const wanted = await targets ( api , args . conversations ); 694const ids = wanted . flatMap (([, child ]) => 695child . settled || child . delegation === undefined 696 ?[] 697 :[ Number ( child . delegation ) as TaskId < string >] 698); 699for ( const id of ids ) await host . harness . abortTask ( id , context ); 700await Promise . all ( ids . map (( id ) => host . harness . waitForTask ( id , context ))); 701return wanted . map (([ conversation ]) => ({ conversation, status :"stopped" })); 702}, 703}], 704}; 705}