char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
b4fc6cf
main
1import type { Context , JsonValue } from "@earendil-works/chord" ; 2import { homedir } from "node:os" ; 3import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context" ; 4import { diffRevisions , type Op } from "@earendil-works/chord/delta" ; 5import { 6type AssistantMessage , 7clampThinkingLevel , 8type Context as ModelContext , 9type ImageContent , 10lazyStream , 11type Models , 12type ModelsSimpleStreamOptions , 13type ModelThinkingLevel , 14type MutableModels , 15type Provider , 16type TextContent , 17type ToolCall , 18Type , 19validateToolArguments , 20} from "@earendil-works/pi-ai" ; 21import { 22type Agent , 23AgentDoc , 24type AgentState , 25type ConversationId , 26createRegistry , 27type Cursor , 28DEFAULT_COMPACTION_POLICY , 29defineDoc , 30defineExtension as defineDurableExtension , 31defineTool , 32type EntryId , 33type EntryRecord , 34type Extension as DurableExtension , 35GenerationTask , 36Harness , 37InboxDoc , 38LiveDoc , 39type ModelRef , 40ProviderDoc , 41section , 42type Storage , 43type SubmissionId , 44type ToolExecutionApi , 45type Tx , 46} from "@earendil-works/pi-durable" ; 47import { CODEMODE_SOURCE_GRAMMAR } from "@earendil-works/pi-codemode/source" ; 48import { 49type Channel , 50type ChannelHandler , 51type Command , 52type CompactionCap , 53type Configuration , 54configurationEvents , 55type ContextHook , 56type ConversationApi , 57defineExtension , 58type Extension , 59type Handler , 60handlerArguments , 61type PayloadHook , 62type PromptProse , 63type PromptRender , 64type SectionRender , 65tightestCap , 66type Tool , 67type ToolContext , 68} from "@berry/api" ; 69import { imageResult } from "@berry/api/content.ts" ; 70import { MAX_MESSAGE_BYTES , userContent } from "@berry/api/images.ts" ; 71import { directory , type Environment , LOCAL } from "@berry/api/environment.ts" ; 72import { 73applyStoreWrite , 74DESCRIPTION , 75type NestedTool , 76renderCatalog , 77runScript , 78type StoreWrite , 79} from "./codemode.ts" ; 80import { EventEntry , StartEntry } from "./entries.ts" ; 81import { recordSettled , SettledDoc } from "./settled.ts" ; 82import { startTool } from "./tool-timing.ts" ; 83import { 84type Binding , 85type BindingEvent , 86configuredEnvironments , 87ENVIRONMENT , 88ENVIRONMENT_NAME , 89environmentExtension , 90type Environments , 91type EnvironmentSettings , 92} from "./environment.ts" ; 93import { projectFor , type ProjectInput , workspaceFor , workspacePath } from "@berry/api/projects.ts" ; 94import { ProjectOrder , Projects , resolveProject , validateRoots } from "./projects.ts" ; 95import { folders } from "./folders.ts" ; 96import { references } from "./references.ts" ; 97import { DEFAULT_SYSTEM_PROMPT , renderGuidelines , type SystemPromptSettings } from "./prompts.ts" ; 98import type { Feed } from "./pubsub.ts" ; 99import { SUBAGENT , type Subagent , subagentExtension } from "./subagent.ts" ; 100import { tid } from "./tid.ts" ; 101import { ShellDoc , shellTask } from "./shell.ts" ; 102import { conversationRegistry } from "./registry.ts" ; 103 104const context = BACKGROUND_CONTEXT ; 105 106export { EventEntry , StartEntry }; 107 108export type ConversationInfo = { 109/** The durable conversation it began as. */ 110id :number ; 111created :number ; 112/** A user-chosen sidebar name, otherwise the first message is used. */ 113title ?:string ; 114/** A fork's parent, and the entry of it the fork continues from. */ 115parent ?:{ tid :string ; at :number }; 116owner ?:string ; 117/** The branches `/tree` has made of it since, each a durable conversation that continues from one of its entries. */ 118branches ?:{ id :number ; at :number ; created :number }[]; 119/** The branch it continues on, when that is not the one it began as. */ 120head ?:number ; 121/** When it was put out of the way, out of the list of conversations. */ 122archived ?:number ; 123/** The newest message acknowledged by a frontend, shared across devices. */ 124readThrough ?:number ; 125}; 126 127/** A stored choice of extensions, choosing by `name` the ones it chose by a name in `formerly`. */ 128function adopt ( agent :AgentState , formerly :readonly string [], name :string ) { 129const renamed = ( names :readonly string []) => [ 130 ...new Set ( names . map (( chosen ) => ( formerly . includes ( chosen ) ?name :chosen ))), 131]; 132const stale = ( names ?:readonly string []) => names ?. some (( chosen ) => formerly . includes ( chosen )); 133const selection = agent . extensions ; 134if ( Array . isArray ( selection )) { 135if ( stale ( selection )) agent . extensions = renamed ( selection ); 136} else if ( selection !== undefined ) { 137if ( stale ( selection . add )) selection . add = renamed ( selection . add ! ); 138if ( stale ( selection . remove )) selection . remove = renamed ( selection . remove ! ); 139} 140} 141 142/** The durable conversation a conversation continues on. */ 143export const headOf = ( info :ConversationInfo ) => ( info . head ?? info . id ) as ConversationId ; 144 145/** Every branch of a conversation, the one it continues on first. */ 146const branchesOf = ( info :ConversationInfo ) => 147[ 148 ...new Set ([ headOf ( info ), info . id , ...( info . branches ?? []). map (( branch ) => branch . id )]), 149] as ConversationId []; 150 151/** A change to a conversation's agent; `null` clears a field. */ 152export type AgentChange = { 153readonly model ?:ModelRef | null ; 154readonly thinkingLevel ?:ModelThinkingLevel | null ; 155/** Exactly these extensions, in order. */ 156readonly extensions ?:readonly string []; 157}; 158 159export const Conversations = defineDoc < Record < string , ConversationInfo >>({ 160kind :"berry.conversations" , 161version :1 , 162scope :"session" , 163initial :() => ({}), 164checkpointWhen :() => true , 165}); 166 167/** The harness's own settings, shared by every conversation. */ 168export type BerrySettings = { 169/** The models conversations may use, out of those the logged-in providers offer. */ 170models :{ provider :string ; modelId :string }[]; 171/** The machines conversations may work on besides this one, by name. */ 172environments ?:Record < string , EnvironmentSettings >; 173allowLocalEnvironment ?:boolean ; 174systemPrompt ?:SystemPromptSettings ; 175}; 176 177export const Settings = defineDoc < BerrySettings >({ 178kind :"berry.settings" , 179version :1 , 180scope :"session" , 181initial :() => ({ 182models :[], 183allowLocalEnvironment :true , 184systemPrompt :{ ...DEFAULT_SYSTEM_PROMPT }, 185}), 186checkpointWhen :() => true , 187}); 188 189// deno-lint-ignore no-explicit-any 190type AnyExtension = Extension < any , any >; 191// deno-lint-ignore no-explicit-any 192type AnyTool = Tool < any , any >; 193// deno-lint-ignore no-explicit-any 194type AnyCommand = Command < any , any >; 195// deno-lint-ignore no-explicit-any 196type AnyHandler = Handler < any >; 197// deno-lint-ignore no-explicit-any 198type AnyChannelHandler = ChannelHandler < any >; 199 200/** The calls running in one load of an extension, so a replaced load is disposed only once they end. */ 201type Calls = { count :number ; drained :PromiseWithResolvers < void > | null }; 202const idle = () :Calls => ({ count :0 , drained :null }); 203 204async function running < T > ( of :Loaded , work :() => Promise < T >) :Promise < T > { 205of . calls . count ++ ; 206try { 207return await work (); 208} finally { 209if ( -- of . calls . count === 0 ) of . calls . drained ?. resolve (); 210} 211} 212 213type Loaded = { 214readonly extension :AnyExtension ; 215readonly providers :readonly Provider []; 216readonly replacedProviders :ReadonlyMap < string , Provider >; 217readonly compactionCaps :readonly (() => CompactionCap | undefined )[]; 218readonly tools :readonly AnyTool []; 219readonly handlers :readonly AnyHandler []; 220readonly channels :readonly AnyChannelHandler []; 221/** Channels open to this load, which close with it. */ 222readonly open :Set < Channel >; 223readonly prompts :readonly { name :string ; render :SectionRender < JsonValue > }[]; 224readonly promptProse ?:PromptProse ; 225readonly configuration ?:Configuration < JsonValue , JsonValue >; 226readonly conversation ?:( conv :ConversationApi < JsonValue , JsonValue >) => void ; 227readonly disposers :readonly (() => void | Promise < void >)[]; 228readonly calls :Calls ; 229}; 230 231type Contribution = { 232readonly tools :AnyTool []; 233readonly prompts :PromptRender []; 234readonly payloadHooks :PayloadHook []; 235readonly contextHooks :ContextHook []; 236readonly commands :AnyCommand []; 237}; 238 239/** The durable tool invocation every nested call runs inside of. */ 240type Frame = { readonly api :ToolExecutionApi ; readonly context :Context }; 241 242type CallOptions = { 243readonly signal ?:AbortSignal ; 244readonly output ?:( chunk :string ) => void ; 245readonly progress ?:( details :JsonValue ) => void ; 246}; 247 248export class BerryError extends Error {} 249 250/** An extension's failure, for the user to read. */ 251const surfaced = ( error :unknown , prefix = "" ) => 252new BerryError ( prefix + ( error instanceof Error ?error . message :String ( error )), { 253cause :error , 254}); 255 256export type CreateOptions = { 257/** Selected extensions, in order, with their configuration values. */ 258readonly extensions :Readonly < Record < string , JsonValue >>; 259readonly model ?:ModelRef ; 260readonly thinkingLevel ?:ModelThinkingLevel ; 261}; 262 263const NEVER = new AbortController (). signal ; 264 265function toContent ( value :JsonValue | undefined ) :( TextContent | ImageContent )[] { 266if ( value === undefined ) return []; 267if ( typeof value === "string" ) return [{ type :"text" , text :value }]; 268const image = imageResult ( value ). value ; 269if ( image && ! Array . isArray ( value )) { 270return [{ type :"image" , data :image . data , mimeType :image . mimeType }]; 271} 272return [{ type :"text" , text :JSON . stringify ( value , null , 2 ) }]; 273} 274 275const exposure = ( tool :AnyTool ) => tool . expose ?? "codemode" ; 276 277const declaration = ( of :Loaded , tool :AnyTool ) => ({ 278name :tool . name , 279extension :of . extension . name , 280description :tool . description , 281parameters :tool . parameters , 282 ...( tool . returns === undefined ?{} :{ returns :tool . returns }), 283direct :exposure ( tool ) === "both" , 284}); 285 286/** How an extension's last load went. */ 287export type ExtensionStatus = { 288readonly name :string ; 289/** The source directory when known, or null for an extension without a unique source. */ 290readonly directory ?:string | null ; 291readonly description :string | null ; 292readonly status :"loaded" | "failed" ; 293/** Why the last load failed; the version before it, if any, still runs. */ 294readonly error :string | null ; 295}; 296 297/** What the process running berry offers its self-modification tools. */ 298export type Host = { 299/** Where berry's source is. */ 300readonly root :string ; 301/** Where extensions on disk are discovered and watched. */ 302readonly extensionDirectories ?:readonly string []; 303/** What `deno check` says of the code on disk, or `null` when it passes. */ 304check () :Promise < string | null >; 305/** Stop gracefully, and come back running the code on disk. */ 306restart () :void ; 307extensions () :readonly ExtensionStatus []; 308/** Load an extension's files again. */ 309reload ( name :string ) :Promise < ExtensionStatus >; 310}; 311 312/** The harness's own extension: codemode, and with a host the /reload command. Its state is the store. */ 313export const Builtin = defineExtension < Record < string , JsonValue >, StoreWrite >({ 314name :"berry" , 315alwaysOn :true , 316initial :() => ({}), 317apply :applyStoreWrite , 318setup () {}, 319}); 320 321/** The user's `/reload`: every extension on disk, or the one named. */ 322function reloadCommand ( host :Host ) :AnyCommand { 323return { 324name :"reload" , 325description :"reload all extensions or one extension by name" , 326async execute ( args ) { 327const names = args . trim () ?[ args . trim ()] :host . extensions (). map (( status ) => status . name ); 328const statuses = []; 329for ( const name of names ) statuses . push ( await host . reload ( name )); 330const failed = statuses . filter (( status ) => status . status === "failed" ); 331if ( failed . length > 0 ) { 332throw new BerryError ( failed . map (({ name, error}) => ` ${ name } : ${ error } ` ). join ( "\n\n" )); 333} 334return `reloaded ${ names . join ( ", " )} ` ; 335}, 336}; 337} 338 339function guide ( root :string , directories :readonly string [] = [ ` ${ root } /extensions` ]) :string { 340return [ 341`the assistant's operating environment is berry. its source is at ${ root } , a deno project.` , 342`- extensions live in ${ directories . join ( " and " )} , each under <name>/: ` + 343"mod.ts (`defineExtension` from @berry/api), an optional " + 344"web.tsx (renderers, from @berry/api/web.ts), and tests using @berry/api/testing.ts. " + 345"an extension imports only its own directory's files and bare specifiers." , 346"- saving an extension's files auto-reloads it once `deno check` passes." , 347"- events are kept forever: a reducer must keep folding every event shape it has ever recorded." , 348"- changes outside extension directories need the header's restart button, which checks the code first." , 349]. join ( "\n" ); 350} 351 352export async function openBerry ( 353storage :Storage , 354models :MutableModels , 355host ?:Host , 356given ?:Environments , 357) { 358const machines = given ?? 359configuredEnvironments ( async () => 360( await harness . snapshot ( Settings , context ))?. environments ?? {} 361); 362const environments :Environments = { 363async open ( name , cwd , during ) { 364await requireEnvironmentAllowed ( name ); 365return await machines . open ( name , cwd , during ); 366}, 367close :() => machines . close (), 368}; 369const registry = createRegistry (); 370const loaded = new Map < string , Loaded >(); 371const folds = new Map < string , { loaded :Loaded ; upTo :number ; state :JsonValue }>(); 372const tids = new Map < number , string >(); 373const providerSessions = new Map < string , ConversationId >(); 374/** Live state feeds; a reinstall changes state without a commit. */ 375const stateFeeds = new Set <() => void >(); 376 377const own :Loaded = { 378extension :Builtin , 379providers :[], 380replacedProviders :new Map (), 381compactionCaps :[], 382tools :[], 383handlers :[], 384channels :[], 385open :new Set (), 386prompts :[], 387disposers :[], 388calls :idle (), 389 ...( host ?{ conversation :( conv ) => conv . command ( reloadCommand ( host )) } :{}), 390}; 391loaded . set ( Builtin . name , own ); 392const shell = shellTask ( async ( command , runtime , during , progress ) => { 393const id = runtime . conversationId ; 394const of = ( await selected ( id )). find (( of ) => of . extension . name === "code" ); 395const env = await environmentOf ( id , during ); 396const name = env . platform === "win32" ?"powershell" :"bash" ; 397if ( ! of ) throw new BerryError ( "Shell commands require the code extension" ); 398const tool = [ ...of . tools , ...( await contribution ( id , of )). tools ] 399. findLast (( tool ) => tool . name === name ); 400if ( ! tool ) throw new BerryError ( `The code extension has no ${ name } tool for this conversation` ); 401let state = await stateOf ( id , of ); 402const value = await running ( of , async () => 403await tool . execute ({ command}, { 404tid :await tidOf ( id ), 405get state () { 406return state ; 407}, 408 env, 409signal :runtime . signal , 410emit :async ( event :JsonValue ) => { 411await runtime . commit ( async ( tx ) => { 412await tx . appendEntry ( id , { kind :EventEntry . kind , data :{ ext :"code" , event} }); 413}, during ); 414state = await stateOf ( id , of ); 415}, 416output :() => {}, 417 progress, 418})); 419return { value, text :tool . text ?tool . text ( value ) :JSON . stringify ( value ) ?? "(no output)" }; 420}, () => harness ); 421const builtin = defineDurableExtension ({ 422name :Builtin . name , 423tasks :[ shell ], 424tools :[ 425defineTool ({ 426name :"codemode" , 427description :DESCRIPTION , 428parameters :Type . Object ({ code :Type . String ({ description :"Raw JavaScript source." }) }), 429// capable models write the script as raw text instead of a json-escaped string 430constrainedSampling :{ 431type :"grammar" , 432variants :{ openai_lark :CODEMODE_SOURCE_GRAMMAR }, 433}, 434execute :async ({ code}, api , context ) => { 435await startTool ( api , context ); 436const id = api . conversationId ; 437const frame = { api, context}; 438const result = await runScript ( code , { 439tools :await nested ( id , frame ), 440store :await stateOf ( id , own ) as Record < string , JsonValue >, 441signal :context . abortSignal ?? NEVER , 442// a rejection only means the invocation ended first 443progress :( details ) => void api . details ( details , context ). catch (() => {}), 444}); 445if ( result . storeWrites ) await commitEvent ( frame , id , Builtin . name , result . storeWrites ); 446return { output :result . content , isError :result . isError , details :result . details }; 447}, 448}), 449], 450sections :[ 451section ( "lede" , async () => { 452const { userName} = await systemPrompt (); 453return "we are a multispecialist software dev & sysadmin team. you are assisting the user" + 454( userName ?` ( ${ userName } )` :"" ); 455}, { tag :false }), 456section ( "guidelines" , async () => { 457const { generalGuidelines, userName} = await systemPrompt (); 458return renderGuidelines ( generalGuidelines , userName ); 459}, { tag :false }), 460 ...( host 461 ?[ 462section ( "berry" , ({ env}) => 463( env as Environment | undefined )?. name === LOCAL 464 ?guide ( host . root , host . extensionDirectories ) 465 :undefined ), 466] 467 :[]), 468section ( 469"codemode" , 470async ( input ) => renderCatalog ( await declarations ( input . conversationId )), 471{ tag :false }, 472), 473], 474}); 475registry . install ( builtin ); 476 477// model reads go through the tightest compaction cap, so generation compacts at one 478// threshold whatever the model's window; the stored providers keep their own windows 479const effectiveModels :Models = new Proxy ( models , { 480get ( target , prop ) { 481if ( prop === "streamSimple" || prop === "completeSimple" ) { 482return ( ...[ model , request , options ] :Parameters < Models [ typeof prop ]>) => { 483const source = lazyStream ( model , async () => { 484const id = await providerConversation ( options ?. sessionId ); 485return await target . streamSimple ( 486model , 487await contextRequest ( id , model , request , options ), 488payloadOptions ( id , await cacheSession ( model , id , options )), 489); 490}); 491// pi-ai 1.1.0's transient-error classifier misses raw EOF messages. 492const normalizeError = ( message :AssistantMessage ) :AssistantMessage => 493message . stopReason === "error" && 494/ \b(?:unexpected eof|error reading a body from connection)\b / i . test ( 495message . errorMessage ?? "" , 496) 497 ?{ ...message , errorMessage :`Network error: ${ message . errorMessage } ` } 498 :message ; 499const stream = lazyStream ( model , () => 500Promise . resolve ({ 501async * [ Symbol . asyncIterator ]() { 502for await ( const event of source ) { 503yield event . type === "error" 504 ?{ ...event , error :normalizeError ( event . error ) } 505 :event ; 506} 507}, 508result :async () => normalizeError ( await source . result ()), 509})); 510return prop === "completeSimple" ?stream . result () :stream ; 511}; 512} 513if ( 514prop === "getModel" || prop === "getModelOfType" || prop === "getModels" || 515prop === "getModelsOfType" || prop === "getAllModels" || prop === "getAvailable" || 516prop === "getAvailableOfType" || prop === "getAllAvailable" 517) { 518const read = Reflect . get ( target , prop ) as ( ...args :never []) => unknown ; 519return ( ...args :never []) => { 520const found = read . apply ( target , args ); 521const cap = effectiveCap (); 522if ( cap === undefined ) return found ; 523if ( found instanceof Promise ) { 524return found . then (( value ) => 525Array . isArray ( value ) 526 ?value . map (( model ) => cappedModel ( model , cap )) 527 :cappedModel ( value , cap ) 528); 529} 530return Array . isArray ( found ) 531 ?found . map (( model ) => cappedModel ( model , cap )) 532 :cappedModel ( found , cap ); 533}; 534} 535const value = Reflect . get ( target , prop ); 536return typeof value === "function" ?value . bind ( target ) :value ; 537}, 538}); 539 540const harness = await Harness . open ( storage , { 541// the proxied catalog keeps generation honest about the cap without touching stored providers 542models :effectiveModels , 543registry :conversationRegistry ( registry , resolveTools ), 544// every conversation selects the always-on extensions; their stored choices only add to them 545settings :{ 546retry :{ enabled :true , maxRetries :3 , baseDelayMs :2_000 , maxAgentDelayMs :60_000 }, 547get compaction () { 548const cap = effectiveCap (); 549return cap === undefined ?{} :{ backgroundTokens :cap . backgroundTokens }; 550}, 551get extensions () { 552return registry . snapshot (). installed (). filter (( ext ) => 553ext . name === SUBAGENT || loaded . get ( ext . name )?. extension . alwaysOn 554); 555}, 556}, 557env :({ conversationId}, during ) => environmentOf ( conversationId , during ), 558onReport :( error ) => console . error ( "[berry]" , error ), 559conversationCreated :async ( tx , record ) => { 560const index = await tx . doc ( Conversations ); 561const tidFor = ( id :ConversationId ) => 562Object . entries ( index ). find (([, info ]) => branchesOf ( info ). includes ( id ))?.[ 0 ]; 563const parent = record . parent && tidFor ( record . parent . conversationId ); 564const owner = record . owner && tidFor ( record . owner . conversationId ); 565index [ tid ()] = { 566id :record . id , 567created :Date . now (), 568 ...( parent === undefined ?{} :{ 569parent :{ tid :parent , at :record . parent ! . at }, 570readThrough :record . parent ! . at , 571}), 572 ...( owner === undefined ?{} :{ owner}), 573}; 574if ( record . parent === undefined ) { 575await tx . appendEntry ( StartEntry , record . id , {}); 576await tx . doc ( SettledDoc , record . id ); 577} 578// a fork starts with its parent's choice as of the fork point, which may name extensions by what they were 579if ( record . parent !== undefined ) { 580const agent = await tx . doc ( AgentDoc , record . id ); 581for ( const { extension} of loaded . values ()) { 582adopt ( agent , extension . formerly ?? [], extension . name ); 583} 584} 585}, 586}, context ); 587const stopSettled = recordSettled ( harness ); 588// watchDoc never creates a document; the conversations topic needs this one from the start 589await harness . commit ( async ( tx ) => { 590await tx . doc ( Conversations ); 591const settings = await tx . doc ( Settings ); 592settings . systemPrompt = { ...DEFAULT_SYSTEM_PROMPT , ...settings . systemPrompt }; 593const legacy = settings . systemPrompt as SystemPromptSettings & { codingGuidelines ?:unknown }; 594if ( typeof legacy . codingGuidelines === "string" && legacy . promptProse ?. code === undefined ) { 595const text = legacy . codingGuidelines ; 596legacy . promptProse = { 597 ...( legacy . promptProse ?? {}), 598code :text . trim () ?`guidelines:\n ${ text } ` :text , 599}; 600} 601delete legacy . codingGuidelines ; 602await tx . doc ( Projects ); 603await tx . doc ( ProjectOrder ); 604}, context ); 605 606async function systemPrompt () :Promise < SystemPromptSettings > { 607return ( await harness . snapshot ( Settings , context ))?. systemPrompt ?? DEFAULT_SYSTEM_PROMPT ; 608} 609 610/** Editable prose for `name`, with `$USER` replaced and blank prose omitted. */ 611async function resolveProse ( 612name :string , 613registered :PromptProse | undefined , 614) :Promise < string | undefined > { 615if ( registered === undefined ) return undefined ; 616const { userName, promptProse} = await systemPrompt (); 617const raw = Object . hasOwn ( promptProse ?? {}, name ) ?promptProse ! [ name ] :registered . default ; 618return raw . trim () ?raw . replaceAll ( "$USER" , () => userName ) :undefined ; 619} 620 621/** The level `model` runs `requested` at: a conversation's level is always one its model supports. */ 622function supported ( model :ModelRef , requested :ModelThinkingLevel ) :ModelThinkingLevel { 623const known = models . getModel ( model . provider , model . modelId ); 624return known ?clampThinkingLevel ( known , requested ) :requested ; 625} 626 627async function requireEnabled ( model :ModelRef ) { 628const settings = await harness . snapshot ( Settings , context ); 629const enabled = settings ?. models . some (( m ) => 630m . provider === model . provider && m . modelId === model . modelId 631); 632if ( ! enabled ) { 633throw new BerryError ( ` ${ model . provider } / ${ model . modelId } is not enabled in settings` ); 634} 635} 636 637async function idOf ( tid :string ) :Promise < ConversationId | undefined > { 638const index = await harness . snapshot ( Conversations , context ); 639const info = index ?.[ tid ]; 640return info && headOf ( info ); 641} 642 643async function existing ( tid :string ) :Promise < ConversationId > { 644const id = await idOf ( tid ); 645if ( id === undefined ) throw new BerryError ( `No conversation ${ tid } ` ); 646return id ; 647} 648 649async function conversationOf ( tid :string ) { 650const id = await idOf ( tid ); 651return id === undefined ?undefined :await harness . conversation ( id , context ); 652} 653 654/** The name an extension goes by now, given any it went by. */ 655function current ( name :string ) :string { 656if ( loaded . has ( name )) return name ; 657return [ ...loaded . values ()]. find (( of ) => of . extension . formerly ?. includes ( name ))?. extension 658. name ?? 659name ; 660} 661 662function selection ( names :readonly string []) { 663const snapshot = registry . snapshot (); 664const chosen = names . map ( current ). filter (( name ) => ! loaded . get ( name )?. extension . alwaysOn ); 665const add = chosen . map (( name ) => { 666const installed = snapshot . extension ( name ); 667if ( installed === undefined ) throw new BerryError ( `No extension named ${ name } ` ); 668return installed ; 669}); 670return { 671 add, 672// old conversations inherit delegation; explicit choices override that default 673 ...( chosen . includes ( SUBAGENT ) ?{} :{ remove :[ subagent . extension ] }), 674}; 675} 676 677// Read from committed state only: a failed creating commit may hand its ID out again. 678async function tidOf ( id :ConversationId ) :Promise < string > { 679if ( ! tids . has ( id )) { 680const index = await harness . snapshot ( Conversations , context ); 681for ( const [ tid , info ] of Object . entries ( index ?? {})) { 682for ( const id of branchesOf ( info )) tids . set ( id , tid ); 683} 684} 685const found = tids . get ( id ); 686if ( found === undefined ) throw new BerryError ( `Conversation ${ id } has no TID` ); 687return found ; 688} 689 690async function conversation ( id :ConversationId ) { 691const found = await harness . conversation ( id , context ); 692if ( found === undefined ) throw new BerryError ( `No conversation ${ id } ` ); 693return found ; 694} 695 696/** The branch of `tid` whose history holds its entry `at`, the one it continues on if that does. */ 697async function locate ( tid :string , at :number ) { 698const info = ( await harness . snapshot ( Conversations , context ))?.[ tid ]; 699for ( const id of info ?branchesOf ( info ) :[]) { 700const branch = await conversation ( id ); 701const query = { 702order :"descending" , 703minEntryId :at as EntryId , 704maxEntryId :at as EntryId , 705} as const ; 706if (( await branch . entries ( query , 1 , undefined , context )). items . length > 0 ) return branch ; 707} 708return undefined ; 709} 710 711/** 712* Continue `tid` from its entry `at`: on a branch of it that ends there, or else on a new one, which keeps the agent 713* it has now. A busy conversation stays where it is. 714*/ 715async function branch ( tid :string , at :number ) { 716const info = ( await harness . snapshot ( Conversations , context ))?.[ tid ]; 717if ( info === undefined ) throw new BerryError ( `No conversation ${ tid } ` ); 718const head = headOf ( info ); 719const ending = await ( async () => { 720for ( const id of branchesOf ( info )) { 721const query = { order :"descending" } as const ; 722const [ tip ] = ( await ( await conversation ( id )). entries ( query , 1 , undefined , context )). items ; 723if ( tip ?. id === at ) return id ; 724} 725})(); 726if ( ending === head ) return ; 727if ( 728( await harness . snapshot ( LiveDoc , head , context ))?. run || 729( await harness . snapshot ( ShellDoc , head , context ))?. run 730) { 731throw new BerryError ( ` ${ tid } is busy: let it finish, or abort it, first` ); 732} 733if ( ending !== undefined ) { 734return await harness . commit ( async ( tx ) => { 735const moved = ( await tx . doc ( Conversations ))[ tid ]; 736if ( ending === moved . id ) delete moved . head ; 737else moved . head = ending ; 738}, context ); 739} 740const source = await locate ( tid , at ); 741if ( source === undefined ) throw new BerryError ( `No entry ${ at } in ${ tid } ` ); 742const agent = await harness . snapshot ( AgentDoc , head , context ); 743await source . fork ( at as EntryId , { 744ownership :{ kind :"ownerless" }, 745init :async ( tx , id ) => { 746// conversationCreated took the branch for a conversation of its own 747const index = await tx . doc ( Conversations ); 748for ( const [ other , { id :of }] of Object . entries ( index )) { 749if ( of === id ) delete index [ other ]; 750} 751const moved = index [ tid ]; 752moved . branches = [ ...( moved . branches ?? []), { id, at, created :Date . now () }]; 753moved . head = id ; 754const copy = await tx . doc ( AgentDoc , id ); 755for ( const key of Object . keys ( copy )) delete copy [ key as keyof typeof copy ]; 756Object . assign ( copy , structuredClone ( agent ?? {})); 757}, 758}, context ); 759} 760 761/** 762* A feed of `tid` that follows it onto whichever branch it continues on, opened by `open`: a move reads as the diff 763* between the branches' values. 764*/ 765async function follow ( 766tid :string , 767open :( id :ConversationId ) => Promise < Feed | undefined >, 768) :Promise < Feed | undefined > { 769let id = await idOf ( tid ); 770let inner = id === undefined ?undefined :await open ( id ); 771if ( inner === undefined ) return undefined ; 772let value = inner . value ; 773let listener :(( value :unknown , ops :readonly Op []) => Promise < void >) | undefined ; 774const forward = ( feed :Feed ) => 775feed . start ( async ( next , ops ) => { 776// a branch moved off may still deliver what it had pending 777if ( feed !== inner ) return ; 778value = next ; 779await listener ?.( value , ops ); 780}); 781let moving = Promise . resolve (); 782const move = () => { 783moving = moving . then ( async () => { 784const next = await idOf ( tid ); 785if ( next === undefined || next === id || listener === undefined ) return ; 786const feed = await open ( next ); 787if ( feed === undefined ) return ; 788const previous = inner ! ; 789id = next ; 790inner = feed ; 791const ops = diffRevisions ( value as JsonValue , feed . value as JsonValue ); 792value = feed . value ; 793// the move's ops go out before anything the new branch's feed delivers, which is never inline 794forward ( feed ); 795await listener ( value , ops ); 796await previous . stop (); 797}). catch (( error ) => console . error ( `[berry] following ${ tid } :` , error )); 798}; 799const unsubscribe = harness . subscribeCommits (( publication ) => { 800const moved = publication . changes . some (( change ) => 801change . type === "document" && change . record . kind === Conversations . definition . kind 802); 803if ( moved ) queueMicrotask ( move ); 804}); 805return { 806get value () { 807return value ; 808}, 809start ( next ) { 810listener = next ; 811forward ( inner ! ); 812}, 813async stop () { 814listener = undefined ; 815unsubscribe (); 816await moving ; 817await inner ! . stop (); 818}, 819}; 820} 821 822/** Conversations that chose an extension by a name it went by choose it by its name now. */ 823async function rename ( extension :AnyExtension ) { 824const formerly = extension . formerly ?? []; 825if ( formerly . length === 0 ) return ; 826const index = await harness . snapshot ( Conversations , context ) ?? {}; 827const stale :ConversationId [] = []; 828for ( const id of Object . values ( index ). flatMap ( branchesOf )) { 829const agent = structuredClone ( await harness . snapshot ( AgentDoc , id , context ) ?? {}); 830const before = JSON . stringify ( agent ); 831adopt ( agent , formerly , extension . name ); 832if ( JSON . stringify ( agent ) !== before ) stale . push ( id ); 833} 834if ( stale . length === 0 ) return ; 835await harness . commit ( async ( tx ) => { 836for ( const id of stale ) adopt ( await tx . doc ( AgentDoc , id ), formerly , extension . name ); 837}, context ); 838} 839 840function fold ( extension :AnyExtension , state :JsonValue , entry :EntryRecord ) :JsonValue { 841const own = ( ext :string ) => ext === extension . name || extension . formerly ?. includes ( ext ); 842if ( EventEntry . is ( entry ) && own ( entry . data . ext ) && extension . apply ) { 843state = extension . apply ( state , entry . data . event ); 844} 845return extension . observe ?extension . observe ( state , entry ) :state ; 846} 847 848async function stateOf ( id :ConversationId , of :Loaded , tx ?:Tx ) :Promise < JsonValue > { 849const key = ` ${ id } : ${ of . extension . name } ` ; 850const cached = folds . get ( key ); 851let { upTo, state} = cached ?. loaded === of 852 ?cached 853 :{ upTo :0 , state :of . extension . initial () }; 854const history = tx ?undefined :await conversation ( id ); 855const query = { order :"ascending" , minEntryId :( upTo + 1 ) as EntryId } as const ; 856let cursor :Cursor | undefined ; 857do { 858const page = tx 859 ?await tx . scanEntries ({ conversationId :id , ...query }, 256 , cursor ) 860 :await history ! . entries ( query , 256 , cursor , context ); 861for ( const entry of page . items ) { 862state = fold ( of . extension , state , entry ); 863upTo = entry . id ; 864} 865cursor = page . next ; 866} while ( cursor !== undefined ); 867const latest = folds . get ( key ); 868if ( latest ?. loaded !== of || latest . upTo < upTo ) folds . set ( key , { loaded :of , upTo, state}); 869return state ; 870} 871 872async function contribution ( id :ConversationId , of :Loaded ) :Promise < Contribution > { 873const state = await stateOf ( id , of ); 874const env = of . conversation ?await environmentOf ( id ). catch (() => undefined ) :undefined ; 875 876const result :Contribution = { 877tools :[], 878prompts :[], 879commands :[], 880payloadHooks :[], 881contextHooks :[], 882}; 883of . conversation ?.({ 884tid :await tidOf ( id ), 885 state, 886 env, 887tool :( tool ) => { 888if ( tool . name === SUBAGENT || ( tool . name === "codemode" && exposure ( tool ) !== "codemode" )) { 889throw new BerryError ( 890` ${ of . extension . name } : tool ${ tool . name } is reserved for the harness` , 891); 892} 893result . tools . push ( tool as AnyTool ); 894}, 895prompt :( render ) => void result . prompts . push ( render ), 896onPayload :( hook ) => void result . payloadHooks . push ( hook ), 897onContext :( hook ) => void result . contextHooks . push ( hook ), 898command :( command ) => void result . commands . push ( command ), 899}); 900return result ; 901} 902 903async function providerConversation ( sessionId ?:string ) :Promise < ConversationId | undefined > { 904if ( sessionId === undefined ) return undefined ; 905// pi-durable exposes the conversation to providers through its stable session identity. 906if ( ! providerSessions . has ( sessionId )) { 907const index = await harness . snapshot ( Conversations , context ) ?? {}; 908for ( const id of new Set ( Object . values ( index ). flatMap ( branchesOf ))) { 909const provider = await harness . snapshot ( ProviderDoc , id , context ); 910if ( provider ) providerSessions . set ( provider . sessionId , id ); 911} 912} 913return providerSessions . get ( sessionId ); 914} 915 916async function cacheSession ( 917model :Parameters < ContextHook >[ 1 ], 918id :ConversationId | undefined , 919options ?:ModelsSimpleStreamOptions , 920) :Promise < ModelsSimpleStreamOptions | undefined > { 921// codex pools a websocket per session and continues it with previous_response_id, so its session 922// stays per branch, and payloadOptions patches its cache key in the body instead 923if ( id === undefined || model . api === "openai-codex-responses" ) return options ; 924const tid = await tidOf ( id ). catch (() => undefined ); 925return tid === undefined ?options :{ ...options , sessionId :tid }; 926} 927 928async function contextRequest ( 929id :ConversationId | undefined , 930model :Parameters < ContextHook >[ 1 ], 931request :ModelContext , 932options ?:ModelsSimpleStreamOptions , 933) :Promise < ModelContext > { 934let messages = request . messages ; 935if ( id !== undefined ) { 936await requireEnvironmentAllowed (( await bindingOf ( id )). environment ); 937const extensions = await selected ( id ); 938let copied = false ; 939for ( const of of extensions ) { 940const { contextHooks} = await contribution ( id , of ); 941if ( contextHooks . length === 0 ) continue ; 942if ( ! copied ) { 943messages = structuredClone ( messages ); 944copied = true ; 945} 946await running ( of , async () => { 947for ( const hook of contextHooks ) { 948const next = await hook ( messages , model ); 949if ( next !== undefined ) messages = [ ...next ]; 950} 951}); 952} 953options ?. signal ?. throwIfAborted (); 954} 955// anthropic refuses a failed tool result that is not all text, and so every later request with it 956// in the history; one with images goes as a success instead, its text still saying it failed 957if ( model . api === "anthropic-messages" ) { 958messages = messages . map (( message ) => 959message . role === "toolResult" && message . isError && 960message . content . some (( block ) => block . type === "image" ) 961 ?{ ...message , isError :false } 962 :message 963); 964} 965return messages === request . messages ?request :{ ...request , messages}; 966} 967 968function payloadOptions ( 969id :ConversationId | undefined , 970options ?:ModelsSimpleStreamOptions , 971) :ModelsSimpleStreamOptions { 972return { 973 ...options , 974onPayload :async ( payload , model ) => { 975const original = await options ?. onPayload ?.( payload , model ); 976if ( original !== undefined ) payload = original ; 977if ( id === undefined ) return payload ; 978for ( const of of await selected ( id )) { 979const { payloadHooks} = await contribution ( id , of ); 980await running ( of , async () => { 981for ( const hook of payloadHooks ) { 982const next = await hook ( payload , model ); 983if ( next !== undefined ) payload = next ; 984} 985}); 986} 987// a provider whose session cannot be the conversation's (codex, see cacheSession) keeps the 988// conversation's key for its prompt cache here, so a rewind's new branch does not lose it 989const tid = await tidOf ( id ). catch (() => undefined ); 990if ( 991tid !== undefined && typeof payload === "object" && payload !== null && 992! Array . isArray ( payload ) && 993typeof ( payload as Record < string , unknown >). prompt_cache_key === "string" 994) { 995payload = { ...payload , prompt_cache_key :tid }; 996} 997return payload ; 998}, 999}; 1000} 1001 1002async function selected ( id :ConversationId ) :Promise < Loaded []> { 1003const agent = await ( await conversation ( id )). agent ( context ); 1004return agent . extensions . flatMap (({ name}) => loaded . get ( name ) ?? []); 1005} 1006 1007async function resolveTools ( agent :Agent , id :ConversationId ) :Promise < Agent > { 1008const tools = new Map < string , Agent [ "tools" ][ number ]>(); 1009const sources = new Map < string , AnyTool >(); 1010for ( const extension of agent . extensions ) { 1011const own = new Map (( extension . tools ?? []). map (( tool ) => [ tool . name , tool ])); 1012const of = loaded . get ( extension . name ); 1013const available = new Map ( of ?. tools . map (( tool ) => [ tool . name , tool ])); 1014if ( of ) { 1015for ( const tool of ( await contribution ( id , of )). tools ) { 1016available . set ( tool . name , tool ); 1017own . delete ( tool . name ); 1018if ( exposure ( tool ) !== "codemode" ) own . set ( tool . name , compileTool ( of , tool )); 1019} 1020} 1021for ( const [ name , tool ] of own ) { 1022tools . set ( name , tool ); 1023sources . delete ( name ); 1024const source = available . get ( name ); 1025if ( source ) sources . set ( name , source ); 1026} 1027} 1028if ([ ...sources . values ()]. some (( tool ) => tool . enabled )) { 1029const env = await environmentOf ( id ). catch (() => undefined ); 1030for ( const [ name , tool ] of sources ) { 1031if ( tool . enabled && ! await tool . enabled ({ env})) tools . delete ( name ); 1032} 1033} 1034const filter = ( await harness . snapshot ( AgentDoc , id , context ))?. tools ; 1035const offered = filter === undefined 1036 ?[ ...tools . values ()] 1037 :Array . isArray ( filter ) 1038 ?[ ...new Set ( filter )]. flatMap (( name ) => tools . get ( name ) ?? []) 1039 :[ ...tools . values ()]. filter (( tool ) => ! filter . remove . includes ( tool . name )); 1040return { ...agent , tools :offered }; 1041} 1042 1043/** Codemode-callable tools of the selected extensions; a later extension's tool replaces an earlier one. */ 1044async function callable ( id :ConversationId ) { 1045const byName = new Map < string , { loaded :Loaded ; tool :AnyTool }>(); 1046for ( const of of await selected ( id )) { 1047const own = [ 1048 ...of . tools , 1049 ...( await contribution ( id , of )). tools , 1050]; 1051for ( const tool of own ) byName . set ( tool . name , { loaded :of , tool}); 1052} 1053for ( const [ name , { tool}] of byName ) { 1054if ( exposure ( tool ) === "model" ) byName . delete ( name ); 1055} 1056if ([ ...byName . values ()]. some (({ tool}) => tool . enabled )) { 1057const env = await environmentOf ( id ). catch (() => undefined ); 1058for ( const [ name , { tool}] of byName ) { 1059if ( tool . enabled && ! await tool . enabled ({ env})) byName . delete ( name ); 1060} 1061} 1062return byName ; 1063} 1064 1065/** Commands of the selected extensions; a later extension's command replaces an earlier one. */ 1066async function commandsOf ( id :ConversationId ) { 1067const byName = new Map < string , { loaded :Loaded ; command :AnyCommand }>(); 1068for ( const of of await selected ( id )) { 1069for ( const command of ( await contribution ( id , of )). commands ) { 1070byName . set ( command . name , { loaded :of , command}); 1071} 1072} 1073if ([ ...byName . values ()]. some (({ command}) => command . enabled )) { 1074const env = await environmentOf ( id ). catch (() => undefined ); 1075for ( const [ name , { command}] of byName ) { 1076if ( command . enabled && ! await command . enabled ({ env})) byName . delete ( name ); 1077} 1078} 1079return byName ; 1080} 1081 1082async function writeEvent ( id :ConversationId , ext :string , event :JsonValue , content ?:string ) { 1083const target = await conversation ( id ); 1084return await target . submit ({ 1085type :"write" , 1086entry :{ 1087kind :EventEntry . kind , 1088data :{ ext, event}, 1089 ...( content === undefined ?{} :{ 1090model :[{ role :"user" , content, timestamp :Date . now () }], 1091}), 1092}, 1093}, context ); 1094} 1095 1096async function delegationOffered ( id :ConversationId ) { 1097const agent = await ( await conversation ( id )). agent ( context ); 1098return agent . extensions . some (( ext ) => ext . name === SUBAGENT ); 1099} 1100 1101async function declarations ( id :ConversationId ) { 1102const tools = [ ...( await callable ( id )). values ()]. map (({ loaded :of , tool}) => 1103declaration ( of , tool ) 1104); 1105if ( await delegationOffered ( id )) { 1106tools . push ( ...subagent . scripted . map (({ declaration}) => declaration )); 1107} 1108return tools ; 1109} 1110 1111/** What a script may call, each tool in its own extension's context, with arguments validated like the model's. */ 1112async function nested ( id :ConversationId , frame :Frame ) :Promise < NestedTool []> { 1113const tools :NestedTool [] = [ ...( await callable ( id )). values ()]. map (({ loaded :of , tool}) => ({ 1114 ...declaration ( of , tool ), 1115call :( args , options ) => { 1116const call = { 1117type :"toolCall" , 1118id :frame . api . callId , 1119name :tool . name , 1120arguments :args , 1121} as const ; 1122// deno-lint-ignore no-explicit-any 1123return invoke ( of , tool , id , validateToolArguments ( tool , call as any ), frame , options ); 1124}, 1125})); 1126if ( await delegationOffered ( id )) { 1127for ( const { declaration, call} of subagent . scripted ) { 1128tools . push ({ 1129 ...declaration , 1130call :( args , options ) => { 1131const toolCall = { 1132type :"toolCall" , 1133id :frame . api . callId , 1134name :declaration . name , 1135arguments :args as ToolCall [ "arguments" ], 1136} as const ; 1137const validated = validateToolArguments ( declaration , toolCall ); 1138return call ( validated , frame . api , frame . context , options ); 1139}, 1140}); 1141} 1142} 1143return tools ; 1144} 1145 1146async function commitEvent ( frame :Frame , id :ConversationId , ext :string , event :JsonValue ) { 1147await frame . api . commit ( 1148( tx ) => tx . appendEntry ( id , { kind :EventEntry . kind , data :{ ext, event} }), 1149frame . context , 1150); 1151} 1152 1153async function invoke ( 1154of :Loaded , 1155tool :AnyTool , 1156id :ConversationId , 1157args :unknown , 1158frame :Frame , 1159options :CallOptions , 1160) :Promise < JsonValue | undefined > { 1161let state = await stateOf ( id , of ); 1162// the harness's env option builds one for every call 1163const env = frame . api . env as Environment | undefined ; 1164if ( env === undefined ) throw new BerryError ( "the call has no environment" ); 1165if ( tool . enabled && ! await tool . enabled ({ env})) { 1166throw new BerryError ( `Tool ${ tool . name } is not enabled in this environment` ); 1167} 1168const ctx :ToolContext < JsonValue , JsonValue > = { 1169tid :await tidOf ( id ), 1170get state () { 1171return state ; 1172}, 1173 env, 1174signal :options . signal ?? frame . context . abortSignal ?? NEVER , 1175emit :async ( event ) => { 1176await commitEvent ( frame , id , of . extension . name , event ); 1177state = await stateOf ( id , of ); 1178}, 1179output :( chunk ) => options . output ?.( chunk ), 1180progress :( details ) => options . progress ?.( details ), 1181}; 1182return await running ( of , async () => await tool . execute ( args , ctx )); 1183} 1184 1185/** Once the load has no call running; a replaced load takes no new ones. */ 1186function drained ( of :Loaded ) :Promise < void > { 1187if ( of . calls . count === 0 ) return Promise . resolve (); 1188of . calls . drained ??= Promise . withResolvers (); 1189return of . calls . drained . promise ; 1190} 1191 1192function compileTool ( of :Loaded , tool :AnyTool ) { 1193return defineTool ({ 1194name :tool . name , 1195description :tool . description , 1196parameters :tool . parameters , 1197execute :async ( args , api , context ) => { 1198await startTool ( api , context ); 1199const value = await invoke ( of , tool , api . conversationId , args , { api, context}, { 1200output :( chunk ) => api . output ( chunk ), 1201// a rejection only means the invocation ended first 1202progress :( details ) => void api . details ( details , context ). catch (() => {}), 1203}); 1204return { 1205output :tool . text ?[{ type :"text" , text :tool . text ( value ) }] :toContent ( value ), 1206}; 1207}, 1208}); 1209} 1210 1211function compile ( of :Loaded ) { 1212const { name} = of . extension ; 1213return defineDurableExtension ({ 1214 name, 1215tools :of . tools . filter (( tool ) => exposure ( tool ) !== "codemode" ). map (( tool ) => 1216compileTool ( of , tool ) 1217), 1218sections :[ 1219section ( name , async ( input ) => { 1220const prose = await resolveProse ( name , of . promptProse ); 1221const { prompts} = await contribution ( input . conversationId , of ); 1222const env = input . env as Environment | undefined ; 1223const texts = await Promise . all ( prompts . map (( render ) => render ({ env}))); 1224const all = [ prose , ...texts ]. filter (( t ) => t !== undefined ); 1225return all . length === 0 ?undefined :all . join ( "\n\n" ); 1226}), 1227 ...of . prompts . map (( prompt ) => 1228section ( ` ${ name } _ ${ prompt . name } ` , async ( input ) => 1229await prompt . render ({ 1230tid :await tidOf ( input . conversationId ), 1231state :await stateOf ( input . conversationId , of ), 1232env :input . env as Environment | undefined , 1233})) 1234), 1235], 1236}); 1237} 1238 1239/** The tightest compaction cap any load reports; a failing getter reads as no cap. */ 1240function effectiveCap () :CompactionCap | undefined { 1241const seen :( CompactionCap | undefined )[] = []; 1242for ( const of of loaded . values ()) { 1243for ( const get of of . compactionCaps ) { 1244try { 1245seen . push ( get ()); 1246} catch ( error ) { 1247console . error ( `[berry] compaction cap of ${ of . extension . name } :` , error ); 1248} 1249} 1250} 1251return tightestCap ( seen ); 1252} 1253 1254/** A catalog model with its effective context window under a cap, smaller windows untouched. */ 1255function cappedModel ( model :unknown , cap :CompactionCap ) :unknown { 1256if ( typeof model !== "object" || model === null ) return model ; 1257const window = ( model as { contextWindow ?:unknown }). contextWindow ; 1258const limit = cap . maxContextTokens + DEFAULT_COMPACTION_POLICY . reserveTokens ; 1259if ( typeof window !== "number" || ! ( window > limit )) return model ; 1260return { ...( model as Record < string , unknown >), contextWindow :limit }; 1261} 1262 1263function releaseProviders ( of :Loaded ) { 1264for ( const provider of of . providers ) { 1265if ( models . getProvider ( provider . id ) !== provider ) continue ; 1266const original = of . replacedProviders . get ( provider . id ); 1267if ( original ) models . setProvider ( original ); 1268else models . deleteProvider ( provider . id ); 1269} 1270} 1271 1272async function dispose ( of :Loaded ) { 1273for ( const channel of of . open ) channel . close (); 1274for ( const disposer of of . disposers ) { 1275try { 1276await disposer (); 1277} catch ( error ) { 1278console . error ( `[berry] disposing ${ of . extension . name } :` , error ); 1279} 1280} 1281} 1282 1283/** An extension's state in one branch as a feed: a snapshot, then diffs whenever history or the extension changes. */ 1284async function stateFeed ( 1285id :ConversationId , 1286name :string , 1287configuration = false , 1288) :Promise < Feed | undefined > { 1289const initial = loaded . get ( name ); 1290if ( initial === undefined || ( configuration && ! initial . configuration )) return undefined ; 1291const read = async ( of :Loaded ) => { 1292const state = await stateOf ( id , of ); 1293return configuration ?of . configuration ?. values ( state ) ?? null :state ; 1294}; 1295let value = await read ( initial ); 1296let listener :(( value :JsonValue , ops :readonly Op []) => Promise < void >) | undefined ; 1297let dirty = false ; 1298let refreshing = false ; 1299 1300async function refresh () { 1301refreshing = true ; 1302try { 1303while ( dirty && listener ) { 1304dirty = false ; 1305const of = loaded . get ( name ); 1306if ( of === undefined ) continue ; 1307try { 1308const next = await read ( of ); 1309if ( next === value ) continue ; 1310const ops = diffRevisions ( value , next ); 1311if ( ops . length === 0 ) continue ; 1312value = next ; 1313await listener ?.( value , ops ); 1314} catch ( error ) { 1315console . error ( `[berry] state of ${ name } in ${ id } :` , error ); 1316} 1317} 1318} finally { 1319refreshing = false ; 1320} 1321} 1322const poke = () => { 1323dirty = true ; 1324if ( ! refreshing ) void refresh (); 1325}; 1326 1327// commit listeners must not call Session APIs inline 1328const unsubscribe = harness . subscribeCommits (( publication ) => { 1329const touched = publication . changes . some (( change ) => 1330change . type === "entry" && change . value . conversationId === id 1331); 1332if ( touched ) queueMicrotask ( poke ); 1333}); 1334stateFeeds . add ( poke ); 1335return { 1336get value () { 1337return value ; 1338}, 1339start ( next ) { 1340listener = next ; 1341poke (); 1342}, 1343stop () { 1344listener = undefined ; 1345unsubscribe (); 1346stateFeeds . delete ( poke ); 1347return Promise . resolve (); 1348}, 1349}; 1350} 1351 1352/** Load a berry extension, as the durable extension it compiles to unless the harness gives its own. */ 1353async function install ( 1354extension :AnyExtension , 1355durable ?:DurableExtension , 1356) :Promise < void > { 1357const providers :Provider [] = []; 1358const compactionCaps :(() => CompactionCap | undefined )[] = []; 1359const tools :AnyTool [] = []; 1360const handlers :AnyHandler [] = []; 1361const channels :AnyChannelHandler [] = []; 1362const prompts :{ name :string ; render :SectionRender < JsonValue > }[] = []; 1363const disposers :(() => void | Promise < void >)[] = []; 1364let configuration :Configuration < JsonValue , JsonValue > | undefined ; 1365let promptProse :PromptProse | undefined ; 1366const conversation = extension . setup ({ 1367provider :( provider ) => void providers . push ( provider ), 1368auth :( provider , signal ) => models . getAuth ( provider , { signal}), 1369tool :( tool ) => void tools . push ( tool ), 1370handle :( handler ) => void handlers . push ( handler as AnyHandler ), 1371channel :( handler ) => void channels . push ( handler as AnyChannelHandler ), 1372prompt :( name , render ) => void prompts . push ({ name, render}), 1373promptProse :( given ) => void ( promptProse = given ), 1374compactionCap :( getter ) => void compactionCaps . push ( getter ), 1375configuration :( given ) => 1376void ( configuration = given as unknown as Configuration < JsonValue , JsonValue >), 1377onDispose :( disposer ) => void disposers . push ( disposer ), 1378}); 1379const replacedProviders = new Map < string , Provider >(); 1380const next :Loaded = { 1381 extension, 1382 providers, 1383 replacedProviders, 1384 compactionCaps, 1385 tools, 1386 handlers, 1387 channels, 1388open :new Set (), 1389 prompts, 1390 disposers, 1391calls :idle (), 1392 ...( conversation ?{ conversation} :{}), 1393 ...( configuration ?{ configuration} :{}), 1394 ...( promptProse ?{ promptProse} :{}), 1395}; 1396// the model resolves same-named tools to the later extension, which would shadow the harness's own 1397if ( tools . some (( tool ) => tool . name === "codemode" && exposure ( tool ) !== "codemode" )) { 1398await dispose ( next ); 1399throw new BerryError ( ` ${ extension . name } : the model's codemode tool is the harness's own` ); 1400} 1401if ( tools . some (( tool ) => tool . name === SUBAGENT )) { 1402await dispose ( next ); 1403throw new BerryError ( ` ${ extension . name } : the subagent tool is the harness's own` ); 1404} 1405const previous = loaded . get ( extension . name ); 1406const claimed = new Set < string >(); 1407for ( const provider of providers ) { 1408if ( 1409claimed . has ( provider . id ) || 1410[ ...loaded . values ()]. some (( of ) => 1411of !== previous && of . providers . some (( owned ) => owned . id === provider . id ) 1412) 1413) { 1414await dispose ( next ); 1415throw new BerryError ( ` ${ extension . name } : provider ${ provider . id } is already registered` ); 1416} 1417claimed . add ( provider . id ); 1418} 1419if ( previous ) releaseProviders ( previous ); 1420for ( const provider of providers ) { 1421const original = models . getProvider ( provider . id ); 1422if ( original ) replacedProviders . set ( provider . id , original ); 1423models . setProvider ( provider ); 1424} 1425loaded . set ( extension . name , next ); 1426registry . install ( durable ?? compile ( next )); 1427await rename ( extension ); 1428if ( providers . length ) { 1429const refreshed = await models . refresh ({ 1430providers :providers . map (( provider ) => provider . id ), 1431}); 1432for ( const [ id , error ] of refreshed . errors ) { 1433console . error ( `[berry] refreshing ${ id } :` , error ); 1434} 1435} 1436for ( const refresh of stateFeeds ) refresh (); 1437// calls the old load started finish on it, and may still need what it disposes 1438if ( previous ) void drained ( previous ). then (() => dispose ( previous )); 1439} 1440 1441/** Where a conversation works now. */ 1442async function bindingOf ( id :ConversationId , tx ?:Tx ) :Promise < Binding > { 1443return await stateOf ( id , loaded . get ( ENVIRONMENT ) ! , tx ) as Binding ; 1444} 1445 1446async function requireEnvironmentAllowed ( name :string ) { 1447if ( 1448name === LOCAL && 1449( await harness . snapshot ( Settings , context ))?. allowLocalEnvironment === false 1450) { 1451throw new BerryError ( "The local environment is disabled in settings" ); 1452} 1453} 1454 1455async function environmentOf ( id :ConversationId , during :Context = context ) { 1456const { environment, cwd} = await bindingOf ( id ); 1457return await environments . open ( environment , cwd , during ); 1458} 1459 1460/** Move a conversation to another machine, which only an idle one may do. */ 1461async function rebind ( id :ConversationId , events :readonly BindingEvent []) { 1462// mid-run, calls the model made for one machine would reach the other 1463await harness . commit ( async ( tx ) => { 1464if (( await tx . doc ( LiveDoc , id )). run || ( await tx . doc ( ShellDoc , id )). run ) { 1465throw new BerryError ( "busy: let it finish, or abort it, before changing its environment" ); 1466} 1467for ( const event of events ) { 1468await tx . appendEntry ( id , { kind :EventEntry . kind , data :{ ext :ENVIRONMENT , event} }); 1469} 1470}, context ); 1471} 1472 1473await install ( environmentExtension ( environments )); 1474const subagent = subagentExtension ({ 1475 harness, 1476 tidOf, 1477enabled :async () => ( await harness . snapshot ( Settings , context ))?. models ?? [], 1478workers :async ( id ) => await stateOf ( id , loaded . get ( SUBAGENT ) ! ) as Subagent , 1479async title ( tx , id , task ) { 1480const info = Object . values ( await tx . doc ( Conversations )) 1481. find (( info ) => branchesOf ( info ). includes ( id )); 1482if ( info ) info . title = task . replace ( / \s+ / g , " " ). trim (); 1483}, 1484 supported, 1485async prose () { 1486return await resolveProse ( SUBAGENT , loaded . get ( SUBAGENT )?. promptProse ); 1487}, 1488async resolve ( tid ) { 1489const info = ( await harness . snapshot ( Conversations , context ))?.[ tid ]; 1490return info && 1491{ id :headOf ( info ), ...( info . owner === undefined ?{} :{ owner :info . owner }) }; 1492}, 1493async prepare ( api , model , thinkingLevel , during ) { 1494const agent = await api . agent ( during ); 1495const extensions = agent . extensions . filter (( ext ) => ext . name !== SUBAGENT ); 1496const events :{ ext :string ; event :JsonValue }[] = []; 1497for ( const { name} of extensions ) { 1498const of = loaded . get ( name ); 1499if ( ! of ?. configuration || name === ENVIRONMENT ) continue ; 1500const state = await stateOf ( api . conversationId , of ); 1501const values = of . configuration . values ( state ); 1502const seeds = await configurationEvents ( of . configuration , values , of . extension . initial ()); 1503for ( const event of seeds ) events . push ({ ext :name , event}); 1504} 1505const env = api . env as Environment ; 1506events . push ({ 1507ext :ENVIRONMENT , 1508event :{ type :"bind" , environment :env . name , cwd :env . cwd }, 1509}); 1510return { 1511 model, 1512thinkingLevel :supported ( model , thinkingLevel ?? agent . thinkingLevel ), 1513extensions :extensions 1514. filter (( ext ) => ! loaded . get ( ext . name )?. extension . alwaysOn ) 1515. map (( ext ) => ext . name ), 1516 events, 1517}; 1518}, 1519}); 1520await install ( subagent . workers , subagent . extension ); 1521 1522async function stateAt ( tid :string , name :string , at ?:EntryId ) :Promise < JsonValue | undefined > { 1523const id = at === undefined ?await idOf ( tid ) :( await locate ( tid , at ))?. id ; 1524const of = loaded . get ( name ); 1525if ( id === undefined || of === undefined ) return undefined ; 1526if ( at === undefined ) return await stateOf ( id , of ); 1527let state = of . extension . initial (); 1528let cursor ; 1529do { 1530const page = await ( await conversation ( id )). entries ( 1531{ order :"ascending" , maxEntryId :at }, 1532256 , 1533cursor , 1534context , 1535); 1536for ( const entry of page . items ) state = fold ( of . extension , state , entry ); 1537cursor = page . next ; 1538} while ( cursor !== undefined ); 1539return state ; 1540} 1541 1542return { 1543 harness, 1544 1545async changeQueuedInput ( tid :string , id :SubmissionId , text :string | null ) { 1546return await harness . commit ( async ( tx ) => { 1547const info = ( await tx . doc ( Conversations ))[ tid ]; 1548if ( info === undefined ) return "not_found" as const ; 1549const conversationId = headOf ( info ); 1550// public Tx omits submission-by-id; this read still holds the harness's commit line. 1551const record = await storage . submission ( id , context ); 1552if ( record ?. conversationId !== conversationId || record . type !== "input" ) { 1553return "not_found" as const ; 1554} 1555if ( record . status !== "queued" ) return "not_queued" as const ; 1556const inbox = await tx . doc ( InboxDoc , conversationId ); 1557const index = inbox . items . findIndex (( item ) => item . id === id ); 1558const item = inbox . items [ index ]; 1559if ( item === undefined ) return "not_queued" as const ; 1560if ( item . mode === "write" ) return "not_found" as const ; 1561if ( text === null ) { 1562tx . settleSubmission ( id , { status :"unanswered" , reason :"aborted" }); 1563inbox . items . splice ( index , 1 ); 1564} else { 1565const images = typeof item . content === "string" 1566 ?[] 1567 :item . content . filter (( block ) => block . type === "image" ); 1568if ( ! text . trim () && images . length === 0 ) return "empty" as const ; 1569const content = userContent ( text , images ); 1570if ( new TextEncoder (). encode ( JSON . stringify ({ content})). length > MAX_MESSAGE_BYTES ) { 1571return "too_large" as const ; 1572} 1573item . content = content ; 1574} 1575return "ok" as const ; 1576}, context ); 1577}, 1578 1579/** Load an extension, replacing the installed one with its name. A failing setup changes nothing. */ 1580async install ( extension :AnyExtension ) :Promise < void > { 1581if ( 1582extension . name === Builtin . name || extension . name === ENVIRONMENT || 1583extension . name === SUBAGENT 1584) { 1585throw new BerryError ( ` ${ extension . name } is the harness's own` ); 1586} 1587await install ( extension ); 1588}, 1589 1590/** Remove an extension: conversations that select it go without it, until one by its name is installed again. */ 1591uninstall ( name :string ) { 1592const previous = loaded . get ( name ); 1593if ( previous === undefined || previous === own || name === ENVIRONMENT || name === SUBAGENT ) { 1594return ; 1595} 1596loaded . delete ( name ); 1597releaseProviders ( previous ); 1598const installed = registry . snapshot (). extension ( name ); 1599if ( installed ) registry . uninstall ( installed ); 1600for ( const refresh of stateFeeds ) refresh (); 1601void drained ( previous ). then (() => dispose ( previous )); 1602}, 1603 1604extensions :() => 1605[ ...loaded . values ()]. flatMap (( of ) => 1606of === own || of . extension . name === ENVIRONMENT || of . extension . name === SUBAGENT 1607 ?[] 1608 :[ of . extension ] 1609), 1610 1611/** The harness's own extensions that the ui shows as extensions: those with configuration. */ 1612builtins :() :readonly Pick < 1613AnyExtension , 1614"name" | "description" | "alwaysOn" | "defaultEnabled" | "sortOrder" | "formerly" 1615>[] => [ 1616loaded . get ( ENVIRONMENT ) ! . extension , 1617loaded . get ( SUBAGENT ) ! . extension , 1618], 1619 1620/** What a conversation starting with the extension is asked, as a json schema; `null` when nothing. */ 1621configurationParameters :( name :string ) => loaded . get ( name )?. configuration ?. parameters ?? null , 1622 1623/** The tightest compaction cap any installed extension reports, if any. */ 1624compactionCap :() => effectiveCap (), 1625 1626/** Model catalog as generation sees it: effective windows under the tightest cap. */ 1627 effectiveModels, 1628 1629async saveProject ( id :string | undefined , input :ProjectInput ) :Promise < string > { 1630const projects = await harness . snapshot ( Projects , context ) ?? {}; 1631if ( id !== undefined && ! Object . hasOwn ( projects , id )) { 1632throw new BerryError ( `No project ${ id } ` ); 1633} 1634const key = id ?? crypto . randomUUID (); 1635try { 1636const project = await resolveProject ( 1637input , 1638id ?projects [ id ] :undefined , 1639environments , 1640context , 1641); 1642await harness . commit ( async ( tx ) => { 1643const index = await tx . doc ( Projects ); 1644if ( id !== undefined && ! Object . hasOwn ( index , id )) { 1645throw new BerryError ( `No project ${ id } ` ); 1646} 1647validateRoots ( index , key , project ); 1648const previous = index [ key ]; 1649if ( input . moveConversations && previous ) { 1650const removed = previous . roots . filter (( root ) => 1651! project . roots . some (( saved ) => 1652saved . environment === root . environment && 1653workspacePath ( saved . cwd , saved . platform ) === workspacePath ( root . cwd , root . platform ) 1654) 1655); 1656if ( removed . length ) { 1657const destination = project . roots [ project . defaultRoot ]; 1658const moving :ConversationId [] = []; 1659for ( const info of Object . values ( await tx . doc ( Conversations ))) { 1660const conversationId = headOf ( info ); 1661const binding = await bindingOf ( conversationId , tx ); 1662if ( binding . cwd === null && binding . environment !== LOCAL ) continue ; 1663const location = { 1664environment :binding . environment , 1665cwd :binding . cwd ?? homedir (), 1666}; 1667if ( projectFor ( index , location ) !== key ) continue ; 1668const workspace = workspaceFor ( previous , location ); 1669if ( ! workspace || ! removed . includes ( workspace )) continue ; 1670if ( 1671( await tx . doc ( LiveDoc , conversationId )). run || 1672( await tx . doc ( ShellDoc , conversationId )). run 1673) { 1674throw new BerryError ( 1675"busy: let affected conversations finish, or abort them, before moving their workspaces" , 1676); 1677} 1678moving . push ( conversationId ); 1679} 1680const event :BindingEvent = { 1681type :"bind" , 1682environment :destination . environment , 1683cwd :destination . cwd , 1684}; 1685for ( const conversationId of moving ) { 1686await tx . appendEntry ( conversationId , { 1687kind :EventEntry . kind , 1688data :{ ext :ENVIRONMENT , event}, 1689}); 1690} 1691} 1692} 1693index [ key ] = project ; 1694}, context ); 1695return key ; 1696} catch ( error ) { 1697throw surfaced ( error ); 1698} 1699}, 1700 1701async deleteProject ( id :string ) { 1702await harness . commit ( async ( tx ) => { 1703const projects = await tx . doc ( Projects ); 1704if ( ! Object . hasOwn ( projects , id )) throw new BerryError ( `No project ${ id } ` ); 1705delete projects [ id ]; 1706}, context ); 1707}, 1708 1709async folders ( 1710environment :string , 1711cwd :string | null , 1712prefix :string , 1713during :Context = context , 1714) { 1715try { 1716const env = await environments . open ( environment , cwd , during ); 1717return await folders ( env , prefix , during ); 1718} catch ( error ) { 1719throw surfaced ( error ); 1720} 1721}, 1722 1723/** Paths a reference could extend to from `cwd`, before any conversation works there. */ 1724async references ( 1725environment :string , 1726cwd :string | null , 1727prefix :string , 1728during :Context = context , 1729) { 1730try { 1731const env = await environments . open ( environment , cwd , during ); 1732return await references ( env , prefix , during ); 1733} catch ( error ) { 1734throw surfaced ( error ); 1735} 1736}, 1737 1738async create ( options :CreateOptions ) :Promise < string > { 1739if ( options . model ) await requireEnabled ( options . model ); 1740const selected = selection ( Object . keys ( options . extensions )); 1741const seeds = await Promise . all ( 1742Object . entries ( options . extensions ). map ( async ([ given , values ]) => { 1743const name = current ( given ); 1744const of = loaded . get ( name ); 1745try { 1746const events = of ?. configuration 1747 ?await configurationEvents ( of . configuration , values , of . extension . initial ()) 1748 :[]; 1749return [ name , events ] as const ; 1750} catch ( error ) { 1751throw surfaced ( error , ` ${ name } : ` ); 1752} 1753}), 1754); 1755const thinkingLevel = options . model 1756 ?supported ( options . model , options . thinkingLevel ?? "high" ) 1757 :options . thinkingLevel ; 1758const created = await harness . createConversation ({ 1759ownership :{ kind :"ownerless" }, 1760agent :{ 1761extensions :selected , 1762 ...( options . model ?{ model :options . model } :{}), 1763 ...( thinkingLevel ?{ thinkingLevel} :{}), 1764}, 1765init :async ( tx , id ) => { 1766for ( const [ ext , events ] of seeds ) { 1767for ( const event of events ) { 1768await tx . appendEntry ( id , { kind :EventEntry . kind , data :{ ext, event} }); 1769} 1770} 1771}, 1772}, context ); 1773return await tidOf ( created . id ); 1774}, 1775 1776conversation :conversationOf , 1777 1778state :stateAt , 1779 1780/** The environment `tid` is bound to, now or as of its entry `at`. */ 1781async environment ( tid :string , at ?:EntryId ) :Promise < Environment > { 1782const binding = await stateAt ( tid , ENVIRONMENT , at ) as Binding | undefined ; 1783if ( binding === undefined ) throw new BerryError ( `No entry ${ at } in ${ tid } ` ); 1784return await environments . open ( binding . environment , binding . cwd , context ); 1785}, 1786 1787async configure ( tid :string , change :AgentChange ) { 1788const target = await conversationOf ( tid ); 1789if ( target === undefined ) throw new BerryError ( `No conversation ${ tid } ` ); 1790if ( change . model ) await requireEnabled ( change . model ); 1791const extensions = change . extensions && selection ( change . extensions ); 1792// a model change carries the level over as far as the new model allows 1793const agent = await target . agent ( context ); 1794const model = change . model === undefined ?agent . model :change . model ?? undefined ; 1795const requested = change . thinkingLevel === undefined 1796 ?agent . thinkingLevel 1797 :change . thinkingLevel ?? "off" ; 1798const thinkingLevel = model && ( change . model !== undefined || change . thinkingLevel ) 1799 ?supported ( model , requested ) 1800 :change . thinkingLevel ; 1801await target . configure ({ 1802 ...( change . model === undefined ?{} :{ model :change . model }), 1803 ...( thinkingLevel === undefined ?{} :{ thinkingLevel}), 1804 ...( extensions === undefined ?{} :{ extensions}), 1805}, context ); 1806}, 1807 1808/** An extension's state as a feed, following the conversation from branch to branch. */ 1809watchState :( tid :string , name :string ) => follow ( tid , ( id ) => stateFeed ( id , name )), 1810 1811watchConfiguration :( tid :string , name :string ) => 1812follow ( tid , ( id ) => stateFeed ( id , name , true )), 1813 1814/** The durable view of a conversation as a feed, following it from branch to branch. */ 1815watch :( tid :string ) => 1816follow ( tid , async ( id ) => await ( await harness . conversation ( id , context ))?. watch ( context )), 1817 1818 locate, 1819 1820/** Acknowledge only the message the frontend saw, never a newer completed turn. */ 1821async markRead ( tid :string , entryId :number ) { 1822const found = await locate ( tid , entryId ); 1823if ( found === undefined ) throw new BerryError ( `No entry ${ entryId } in ${ tid } ` ); 1824await harness . commit ( async ( tx ) => { 1825const entry = await tx . entry ( entryId as EntryId ); 1826if ( entry ?. kind !== "pi.user" && entry ?. kind !== "pi.assistant" ) { 1827throw new BerryError ( `Entry ${ entryId } is not a message` ); 1828} 1829const info = ( await tx . doc ( Conversations ))[ tid ]; 1830if ( info === undefined ) throw new BerryError ( `No conversation ${ tid } ` ); 1831info . readThrough = Math . max ( info . readThrough ?? 0 , entryId ); 1832}, context ); 1833}, 1834 1835/** Set a sidebar name, or restore the message preview with an empty name. */ 1836async rename ( tid :string , title :string | null ) { 1837await harness . commit ( async ( tx ) => { 1838const info = ( await tx . doc ( Conversations ))[ tid ]; 1839if ( info === undefined ) throw new BerryError ( `No conversation ${ tid } ` ); 1840const name = title ?. trim (); 1841if ( name ) info . title = name ; 1842else delete info . title ; 1843}, context ); 1844}, 1845 1846/** Put a conversation out of the way, or bring it back. */ 1847async archive ( tid :string , archived :boolean ) { 1848await harness . commit ( async ( tx ) => { 1849const info = ( await tx . doc ( Conversations ))[ tid ]; 1850if ( info === undefined ) throw new BerryError ( `No conversation ${ tid } ` ); 1851if ( ! archived ) delete info . archived ; 1852else info . archived ??= Date . now (); 1853}, context ); 1854}, 1855 1856 branch, 1857 1858/** 1859* Have the agent write its message `at` afresh: continue `tid` from just before it, on a run without new input, 1860* which pi-durable starts as its own `startRun` does. 1861*/ 1862async regenerate ( tid :string , at :number ) { 1863const source = await locate ( tid , at ); 1864if ( source === undefined ) throw new BerryError ( `No entry ${ at } in ${ tid } ` ); 1865const one = async ( query :{ minEntryId ?:EntryId ; maxEntryId :EntryId }) => 1866( await source . entries ({ order :"descending" , ...query }, 1 , undefined , context )). items [ 0 ]; 1867const entry = await one ({ minEntryId :at as EntryId , maxEntryId :at as EntryId }); 1868if ( entry ?. kind !== "pi.assistant" ) { 1869throw new BerryError ( `Entry ${ at } is not the agent's message` ); 1870} 1871const before = await one ({ maxEntryId :( at - 1 ) as EntryId }); 1872if ( before === undefined ) throw new BerryError ( "Nothing comes before this message" ); 1873await branch ( tid , before . id ); 1874const id = await existing ( tid ); 1875await harness . commit ( async ( tx ) => { 1876const live = await tx . doc ( LiveDoc , id ); 1877if ( live . run || ( await tx . doc ( ShellDoc , id )). run ) { 1878throw new BerryError ( ` ${ tid } is busy: let it finish, or abort it, first` ); 1879} 1880const taskId = await tx . createTask ( GenerationTask , {}, { 1881ownership :{ kind :"conversation" }, 1882conversationId :id , 1883}); 1884live . run = { taskId, inputs :[] }; 1885}, context ); 1886}, 1887 1888/** A host-originated event: a write submission, placed at the next boundary while busy. */ 1889async emit ( tid :string , ext :string , event :JsonValue ) { 1890const id = await existing ( tid ); 1891return await writeEvent ( id , ext , event ); 1892}, 1893 1894async configureExtension ( tid :string , name :string , change :Record < string , JsonValue >) { 1895const id = await existing ( tid ); 1896const of = ( await selected ( id )). find (( of ) => of . extension . name === name ); 1897if ( ! of ?. configuration ) throw new BerryError ( ` ${ name } has no enabled configuration` ); 1898const configuration = of . configuration ; 1899try { 1900for ( const key of Object . keys ( change )) { 1901if ( ! Object . hasOwn ( configuration . parameters . properties , key )) { 1902throw new Error ( `unknown configuration field: ${ key } ` ); 1903} 1904} 1905const state = await stateOf ( id , of ); 1906const events = await configurationEvents ( configuration , { 1907 ...configuration . values ( state ), 1908 ...change , 1909}, state ); 1910const moving = name === ENVIRONMENT && 1911( events as readonly BindingEvent []). some (( event ) => 1912event . environment !== ( state as Binding ). environment 1913); 1914if ( moving ) await rebind ( id , events as readonly BindingEvent []); 1915else { 1916const submissions = await Promise . all ( 1917events . map (( event ) => writeEvent ( id , name , event )), 1918); 1919await Promise . all ( submissions . map (( submission ) => submission . wait ( context ))); 1920} 1921return configuration . values ( await stateOf ( id , of )); 1922} catch ( error ) { 1923throw surfaced ( error ); 1924} 1925}, 1926 1927async commands ( tid :string ) { 1928const id = await existing ( tid ); 1929return [ ...( await commandsOf ( id )). values ()]. map (({ loaded, command}) => ({ 1930name :command . name , 1931extension :loaded . extension . name , 1932description :command . description , 1933})); 1934}, 1935 1936/** Hand a channel the frontend opened to the extension it names; refusals surface as `BerryError`. */ 1937async channel ( 1938tid :string , 1939extension :string , 1940name :string , 1941args :JsonValue | undefined , 1942channel :Channel , 1943signal :AbortSignal , 1944) :Promise < void > { 1945const id = await existing ( tid ); 1946const of = ( await selected ( id )). find (( of ) => of . extension . name === extension ); 1947if ( of === undefined ) throw new BerryError ( ` ${ extension } is not in this conversation` ); 1948const handler = of . channels . findLast (( handler ) => handler . name === name ); 1949if ( handler === undefined ) throw new BerryError ( ` ${ extension } has no channel named ${ name } ` ); 1950try { 1951const validated = handlerArguments ( handler . parameters , args ); 1952const state = await stateOf ( id , of ); 1953const opened = await environmentOf ( id ). then ( 1954( env ) => ({ env}), 1955( error :unknown ) => ({ error}), 1956); 1957of . open . add ( channel ); 1958signal . addEventListener ( "abort" , () => of . open . delete ( channel ), { once :true }); 1959await handler . open ( validated , channel , { 1960 tid, 1961 state, 1962get env () { 1963if ( "error" in opened ) throw opened . error ; 1964return opened . env ; 1965}, 1966 signal, 1967}); 1968} catch ( error ) { 1969throw surfaced ( error ); 1970} 1971}, 1972 1973/** Answer an extension's frontend with one of its handlers; its failures surface as `BerryError`. */ 1974async request ( 1975tid :string , 1976extension :string , 1977name :string , 1978args :JsonValue | undefined , 1979signal :AbortSignal = NEVER , 1980) :Promise < JsonValue > { 1981const id = await existing ( tid ); 1982const of = ( await selected ( id )). find (( of ) => of . extension . name === extension ); 1983if ( of === undefined ) throw new BerryError ( ` ${ extension } is not in this conversation` ); 1984const handler = of . handlers . findLast (( handler ) => handler . name === name ); 1985if ( handler === undefined ) throw new BerryError ( ` ${ extension } has no handler named ${ name } ` ); 1986try { 1987const validated = handlerArguments ( handler . parameters , args ); 1988const state = await stateOf ( id , of ); 1989// a handler that does not use the environment still answers while it cannot be reached 1990const opened = await environmentOf ( id ). then ( 1991( env ) => ({ env}), 1992( error :unknown ) => ({ error}), 1993); 1994return await running ( of , async () => 1995await handler . execute ( validated , { 1996 tid, 1997 state, 1998get env () { 1999if ( "error" in opened ) throw opened . error ; 2000return opened . env ; 2001}, 2002 signal, 2003}) ?? null ); 2004} catch ( error ) { 2005throw surfaced ( error ); 2006} 2007}, 2008 2009/** Run a command; its failures are the user's to read, so they surface as `BerryError`. */ 2010async command ( tid :string , name :string , args :string ) :Promise < JsonValue | undefined > { 2011const id = await existing ( tid ); 2012const found = ( await commandsOf ( id )). get ( name ); 2013if ( found === undefined ) throw new BerryError ( `No command / ${ name } ` ); 2014const { loaded :of , command} = found ; 2015// a command that does not use the environment still runs while it cannot be reached 2016const opened = await environmentOf ( id ). then ( 2017( env ) => ({ env}), 2018( error :unknown ) => ({ error}), 2019); 2020const envOf = () => { 2021if ( "error" in opened ) throw opened . error ; 2022return opened . env ; 2023}; 2024try { 2025return await command . execute ( args , { 2026 tid, 2027state :await stateOf ( id , of ), 2028get env () { 2029return envOf (); 2030}, 2031emit :async ( event , options ) => { 2032const submission = await writeEvent ( id , of . extension . name , event , options ?. content ); 2033await submission . wait ( context ); 2034}, 2035async cd ( path , options ) { 2036const env = envOf (); 2037const cwd = await directory ( env , path , context ); 2038if ( options ?. inheritProject ) { 2039await harness . commit ( async ( tx ) => { 2040const projects = await tx . doc ( Projects ); 2041const projectId = projectFor ( projects , { environment :env . name , cwd :env . cwd }); 2042if ( projectId === null ) return ; 2043const project = projects [ projectId ]; 2044if ( 2045project . roots . some (( root ) => 2046root . environment === env . name && 2047workspacePath ( root . cwd , root . platform ) === workspacePath ( cwd , env . platform ) 2048) 2049) return ; 2050const updated = { 2051 ...project , 2052roots :[ ...project . roots , { environment :env . name , cwd, platform :env . platform }], 2053}; 2054validateRoots ( projects , projectId , updated ); 2055projects [ projectId ] = updated ; 2056}, context ); 2057} 2058const binding = await bindingOf ( id ); 2059if ( cwd !== binding . cwd ) { 2060const event :BindingEvent = { type :"bind" , environment :binding . environment , cwd}; 2061await writeEvent ( id , ENVIRONMENT , event ); 2062} 2063return cwd ; 2064}, 2065compact :async ( instructions ) => { 2066await ( await conversation ( id )). compact ( instructions , context ); 2067}, 2068}); 2069} catch ( error ) { 2070throw surfaced ( error ); 2071} 2072}, 2073 2074 tidOf, 2075 2076async shell ( tid :string , command :string , excludeFromContext = false ) { 2077if ( ! command . trim ()) throw new BerryError ( "Expected a shell command after !" ); 2078const id = await existing ( tid ); 2079if ( ! ( await selected ( id )). some (( of ) => of . extension . name === "code" )) { 2080throw new BerryError ( "Shell commands require the code extension" ); 2081} 2082const taskId = await harness . commit ( async ( tx ) => { 2083const live = await tx . doc ( LiveDoc , id ); 2084const state = await tx . doc ( ShellDoc , id ); 2085if ( live . run || state . run ) { 2086throw new BerryError ( "Busy: let it finish, or abort it, before running a shell command" ); 2087} 2088const taskId = await tx . createTask ( shell , { command, excludeFromContext}, { 2089ownership :{ kind :"conversation" }, 2090conversationId :id , 2091}); 2092state . run = { 2093 taskId, 2094 command, 2095 excludeFromContext, 2096status :"running" , 2097result :null , 2098details :null , 2099error :null , 2100startedAt :Date . now (), 2101durationMs :null , 2102}; 2103return taskId ; 2104}, context ); 2105harness . resume (); 2106return taskId ; 2107}, 2108 2109watchShell :( tid :string ) => 2110follow ( tid , async ( id ) => { 2111await harness . commit ( async ( tx ) => { 2112await tx . doc ( ShellDoc , id ); 2113}, context ); 2114return await harness . watchDoc ( ShellDoc , id , context ); 2115}), 2116 2117/** Replace the enabled models; a model listed twice counts once. */ 2118async enableModels ( models :readonly ModelRef []) { 2119const unique = [ ...new Map ( models . map (( m ) => [ ` ${ m . provider } / ${ m . modelId } ` , m ])). values ()]; 2120await harness . commit ( async ( tx ) => { 2121( await tx . doc ( Settings )). models = unique . map (({ provider, modelId}) => ({ 2122 provider, 2123 modelId, 2124})); 2125}, context ); 2126}, 2127 2128async setAllowLocalEnvironment ( allowed :boolean ) { 2129await harness . commit ( async ( tx ) => { 2130( await tx . doc ( Settings )). allowLocalEnvironment = allowed ; 2131}, context ); 2132}, 2133 2134/** What an extension's prose is unless settings override it; `null` when it registers none. */ 2135promptProseDefault :( name :string ) => loaded . get ( name )?. promptProse ?.default ?? null , 2136 2137async setSystemPrompt ( patch :Partial < SystemPromptSettings >) { 2138await harness . commit ( async ( tx ) => { 2139const settings = await tx . doc ( Settings ); 2140const current = { ...DEFAULT_SYSTEM_PROMPT , ...settings . systemPrompt }; 2141settings . systemPrompt = { 2142 ...current , 2143 ...patch , 2144promptProse :{ ...current . promptProse , ...patch . promptProse }, 2145}; 2146}, context ); 2147}, 2148 2149/** Replace the machines conversations may work on besides this one. */ 2150async setEnvironments ( configured :Readonly < Record < string , EnvironmentSettings >>) { 2151for ( const [ name , { ssh, deno}] of Object . entries ( configured )) { 2152if ( name === LOCAL || ! ENVIRONMENT_NAME . test ( name )) { 2153throw new BerryError ( ` ${ JSON . stringify ( name )} cannot name an environment` ); 2154} 2155// the destination is an argument of its own, so this is only ssh mistaking it for an option 2156if ( ! ssh . trim () || ssh . startsWith ( "-" )) { 2157throw new BerryError ( ` ${ name } : ${ JSON . stringify ( ssh )} is not an ssh destination` ); 2158} 2159if ( deno !== undefined && ! deno . trim ()) throw new BerryError ( ` ${ name } : deno is empty` ); 2160} 2161await harness . commit ( async ( tx ) => { 2162( await tx . doc ( Settings )). environments = Object . fromEntries ( 2163Object . entries ( configured ). map (([ name , { ssh, deno}]) => [ 2164name , 2165{ ssh :ssh . trim (), ...( deno === undefined ?{} :{ deno :deno . trim () }) }, 2166]), 2167); 2168}, context ); 2169}, 2170 2171async close () { 2172await stopSettled (); 2173await harness . close ( context ); 2174// a browser session, mcp servers, and each machine are independent, so none waits out another 2175await Promise . allSettled ( 2176[ ...loaded . values ()]. map ( async ( of ) => { 2177releaseProviders ( of ); 2178await dispose ( of ); 2179}), 2180); 2181await environments . close (); 2182}, 2183}; 2184} 2185 2186export type Berry = Awaited < ReturnType < typeof openBerry >>;