char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
f5275c6
main
1import { assert , assertEquals , assertStringIncludes } from "@std/assert" ; 2import { BACKGROUND_CONTEXT as context } from "@earendil-works/chord/context" ; 3import { type Message } from "@earendil-works/pi-ai" ; 4import { fauxAssistantMessage , fauxToolCall } from "@earendil-works/pi-ai/providers/faux" ; 5import { defineExtension } from "@berry/api" ; 6import { until } from "@berry/util" ; 7import { lastToolResult , script , setup , toolsOffered , turn } from "./fixture.ts" ; 8 9const images = ( messages :readonly Message []) => 10messages . flatMap (( message ) => 11message . role === "toolResult" 12 ?message . content . flatMap (( block ) => block . type === "image" ?[ block . data ] :[]) 13 :[] 14); 15 16Deno . test ( "selected context hooks run in order on every request, compaction's too, without changing what is stored" , async () => { 17await usingharness = await setup (); 18const { berry, faux, model} = harness ; 19for ( const name of [ "first" , "second" , "unused" , "refuse" ]) { 20await berry . install ( defineExtension ({ 21 name, 22initial :() => null , 23setup () { 24return ( conv ) => 25conv . onContext ( async ( messages , requestedModel ) => { 26await Promise . resolve (); 27if ( name === "unused" ) throw new Error ( "An unselected hook ran" ); 28if ( name === "refuse" ) throw new Error ( "context refused" ); 29assertEquals ( requestedModel . id , faux . getModel (). id ); 30// the second sees what the first returned 31return messages . map (( message ) => 32message . role !== "user" ?message :{ 33 ...message , 34content :name === "first" ?"filtered" :` ${ message . content } second` , 35} 36); 37}); 38}, 39})); 40} 41const tid = await berry . create ({ extensions :{ first :{}, second :{} }, model}); 42const conversation = ( await berry . conversation ( tid )) ! ; 43const longInput = "x" . repeat ( 100_000 ); 44const [ seen ] = await turn ( harness , tid , []); 45assertEquals ( seen . findLast (( message ) => message . role === "user" )?. content , "filtered second" ); 46assertEquals ( toolsOffered ( seen ), [ "codemode" ]); 47 48faux . setResponses ([ fauxAssistantMessage ( "done" )]); 49await ( await conversation . submit ({ type :"input" , content :longInput }, context )). wait ( context ); 50await conversation . waitForIdle ( context ); 51let summarized :readonly Message [] = []; 52faux . setResponses ([( request ) => { 53summarized = request . messages ; 54return fauxAssistantMessage ( "A summary" ); 55}]); 56await berry . harness . waitForTask ( await conversation . compact ( undefined , context ), context ); 57assertEquals ( 58summarized . findLast (( message ) => message . role === "user" )?. content , 59"filtered second" , 60); 61const stored = await conversation . context ( context ); 62assertEquals ( 63stored . messages . findLast (( message ) => message . role === "user" )?. content , 64longInput , 65); 66 67// a failing hook ends the request without calling the provider 68const refused = await berry . create ({ extensions :{ refuse :{} }, model}); 69const calls = faux . state . callCount ; 70assertEquals ( await turn ( harness , refused , []), []); 71assertEquals ( faux . state . callCount , calls ); 72const entries = ( await ( await berry . conversation ( refused )) ! . entries ( 73{ order :"ascending" }, 74100 , 75undefined , 76context , 77)) 78. items ; 79const failure = entries . flatMap (( entry ) => entry . model ?? []). findLast (( message ) => 80message . role === "assistant" 81); 82assert ( failure ?. role === "assistant" ); 83assertEquals ( failure . stopReason , "error" ); 84assertStringIncludes ( failure . errorMessage ?? "" , "context refused" ); 85}); 86 87Deno . test ( "command context appends passively with its event, and busy writes wait for the boundary" , async () => { 88await usingharness = await setup (); 89const { berry, model, faux, gate} = harness ; 90await berry . install ( defineExtension < string [], string >({ 91name :"reports" , 92initial :() => [], 93apply :( state , event ) => [ ...state , event ], 94setup () { 95return ( conv ) => 96conv . command ({ 97name :"report" , 98description :"" , 99async execute ( content , ctx ) { 100await ctx . emit ( content , { content}); 101}, 102}); 103}, 104})); 105const tid = await berry . create ({ extensions :{ reports :{}, gate :{} }, model}); 106const conversation = ( await berry . conversation ( tid )) ! ; 107const [ before ] = await turn ( harness , tid , []); 108const previous = ( await conversation . context ( context )). messages ; 109const calls = faux . state . callCount ; 110await berry . command ( tid , "report" , "first edit" ); 111assertEquals ( faux . state . callCount , calls ); 112const stored = ( await conversation . context ( context )). messages ; 113assertEquals ( stored . slice ( 0 , previous . length ), previous ); 114assertEquals ( stored . at ( - 1 )?. content , "first edit" ); 115 116const [ after ] = await turn ( harness , tid , []); 117assertEquals ( after . slice ( 0 , stored . length ), stored ); 118assertEquals ( after . filter (( m ) => m . role === "system" ), before . filter (( m ) => m . role === "system" )); 119 120let next :readonly Message [] = []; 121faux . setResponses ([ 122fauxAssistantMessage ( fauxToolCall ( "hold" , {}), { stopReason :"toolUse" }), 123( request ) => ( next = request . messages , fauxAssistantMessage ( "done" )), 124]); 125const running = await conversation . submit ({ type :"input" , content :"go" }, context ); 126await gate . entered . promise ; 127const reporting = berry . command ( tid , "report" , "second edit" ); 128try { 129await until ( async () => 130( await berry . harness . inspect ( context )). submissions . some (( s ) => 131s . conversationId === conversation . id && s . type === "write" 132) 133); 134assertEquals ( await berry . state ( tid , "reports" ), [ "first edit" ]); 135assert ( 136! ( await conversation . context ( context )). messages . some (( m ) => m . content === "second edit" ), 137); 138} finally { 139gate . release . resolve ( "released" ); 140} 141await reporting ; 142await running . wait ( context ); 143await conversation . waitForIdle ( context ); 144assertEquals ( await berry . state ( tid , "reports" ), [ "first edit" , "second edit" ]); 145assertEquals ( next . slice ( 0 , stored . length ), stored ); 146assertEquals ( next . at ( - 1 )?. content , "second edit" ); 147assert ( next . findLastIndex (( m ) => m . role === "toolResult" ) < next . length - 1 ); 148}); 149 150Deno . test ( "a failed script's images reach anthropic in a result it accepts, and the history keeps the failure" , async () => { 151await usingharness = await setup ( undefined , { 152api :"anthropic-messages" , 153models :[{ id :"claude" , input :[ "text" , "image" ] }], 154}); 155const { berry, model} = harness ; 156const png = 157"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=" ; 158const tid = await berry . create ({ extensions :{}, model}); 159const [, sent ] = await turn ( harness , tid , [ 160script ( ` 161image({ type: "image", data: " ${ png } ", mimeType: "image/png" }); 162throw new Error("after the screenshot"); 163` ), 164]); 165const result = sent . findLast (( message ) => message . role === "toolResult" ); 166assert ( result ?. role === "toolResult" ); 167assertEquals ( result . isError , false ); 168assertEquals ( images ( sent ), [ png ]); 169assertStringIncludes ( lastToolResult ( sent ). text , "after the screenshot" ); 170const stored = await ( await berry . conversation ( tid )) ! . context ( context ); 171const kept = stored . messages . findLast (( message ) => message . role === "toolResult" ); 172assert ( kept ?. role === "toolResult" ); 173assertEquals ( kept . isError , true ); 174});