char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
77bec20
main
1import type { Environment , Process } from "@berry/api" ; 2import { BACKGROUND_CONTEXT , withAbortSignal } from "@earendil-works/chord/context" ; 3import { 4type JsonRpcMessage , 5McpConnectionClosedError , 6type McpTransport , 7parseJsonRpcMessage , 8} from "@earendil-works/pi-mcp" ; 9import type { Server } from "./config.ts" ; 10import { lines } from "./lines.ts" ; 11 12async function settleWithin ( promise :Promise < unknown >, ms :number ) { 13let timer :ReturnType < typeof setTimeout > | undefined ; 14try { 15await Promise . race ([ 16promise . catch (() => {}), 17new Promise < void >(( resolve ) => timer = setTimeout ( resolve , ms )), 18]); 19} finally { 20clearTimeout ( timer ); 21} 22} 23 24/** MCP framing over a process in the conversation's environment. */ 25export class EnvironmentStdio implements McpTransport { 26private readonly messages = new Set <( message :JsonRpcMessage ) => void >(); 27private readonly errors = new Set <( error :Error ) => void >(); 28private readonly closes = new Set <() => void >(); 29private readonly lifetime = new AbortController (); 30private process ?:Process ; 31private writer ?:WritableStreamDefaultWriter < Uint8Array >; 32private readers :ReadableStreamDefaultReader < Uint8Array >[] = []; 33private closed = false ; 34private closing ?:Promise < void >; 35private stderr = "" ; 36private exitCode ?:number | null ; 37private reason ?:Error ; 38private reading ?:Promise < unknown >; 39 40constructor ( 41private readonly env :Environment , 42private readonly config :Extract < Server , { command :string }>, 43signal :AbortSignal , 44) { 45signal . addEventListener ( "abort" , () => void this . close (), { once :true }); 46if ( signal . aborted ) this . closed = true ; 47} 48 49get failure () :Error | undefined { 50if ( ! this . reason ) return undefined ; 51return new McpConnectionClosedError ( 52[ 53this . reason . message , 54this . exitCode === undefined 55 ?"" 56 :this . exitCode === null 57 ?"terminated by signal" 58 :`exit code ${ this . exitCode } ` , 59this . stderr . trim (), 60]. filter ( Boolean ). join ( "\n" ), 61); 62} 63 64onMessage ( listener :( message :JsonRpcMessage ) => void ) { 65this . messages . add ( listener ); 66return () => void this . messages . delete ( listener ); 67} 68 69onError ( listener :( error :Error ) => void ) { 70this . errors . add ( listener ); 71return () => void this . errors . delete ( listener ); 72} 73 74onClose ( listener :() => void ) { 75this . closes . add ( listener ); 76return () => void this . closes . delete ( listener ); 77} 78 79private finish ( error :unknown ) { 80if ( this . closed ) return ; 81this . reason = error instanceof Error ?error :new Error ( String ( error )); 82this . closed = true ; 83for ( const listener of this . errors ) listener ( this . reason ); 84for ( const listener of this . closes ) listener (); 85this . lifetime . abort (); 86void this . close (); 87} 88 89async start () { 90if ( this . closed ) throw new McpConnectionClosedError (); 91if ( this . process ) throw new Error ( "MCP transport already started" ); 92const child = await this . env . spawn ( 93[ this . config . command , ...this . config . args ], 94{ cwd :this . config . cwd , env :this . config . env , group :true }, 95withAbortSignal ( this . lifetime . signal , BACKGROUND_CONTEXT ), 96); 97this . process = child ; 98if ( this . closed ) { 99child . kill ( "SIGKILL" ); 100await settleWithin ( 101Promise . allSettled ([ 102child . exited , 103child . stdout . cancel (), 104child . stderr . cancel (), 105child . stdin . abort (), 106]), 1071000 , 108); 109throw new McpConnectionClosedError (); 110} 111this . writer = child . stdin . getWriter (); 112this . readers = [ child . stdout . getReader (), child . stderr . getReader ()]; 113const errors = ( async () => { 114const reader = this . readers [ 1 ]; 115const decoder = new TextDecoder (); 116try { 117while ( true ) { 118const { done, value} = await reader . read (); 119if ( done ) break ; 120this . stderr = ( this . stderr + decoder . decode ( value , { stream :true })). slice ( - 4000 ); 121} 122this . stderr = ( this . stderr + decoder . decode ()). slice ( - 4000 ); 123} finally { 124reader . releaseLock (); 125} 126})(); 127const messages = ( async () => { 128const reader = this . readers [ 0 ]; 129try { 130for await ( const line of lines ( reader )) { 131let message :JsonRpcMessage ; 132try { 133message = parseJsonRpcMessage ( JSON . parse ( line )); 134} catch ( error ) { 135const normalized = error instanceof Error ?error :new Error ( String ( error )); 136for ( const listener of this . errors ) listener ( normalized ); 137continue ; 138} 139for ( const listener of this . messages ) listener ( message ); 140} 141} finally { 142reader . releaseLock (); 143} 144this . finish ( new McpConnectionClosedError ( "MCP server closed stdout" )); 145})(); 146this . reading = Promise . allSettled ([ errors , messages ]); 147void messages . catch (( error ) => this . finish ( error )); 148void errors . catch (( error ) => this . finish ( error )); 149void child . exited . then ( 150( code ) => this . exitCode = code , 151( error ) => this . finish ( error ), 152); 153} 154 155async send ( message :JsonRpcMessage ) { 156if ( this . closed || ! this . writer ) throw new McpConnectionClosedError (); 157await this . writer . write ( new TextEncoder (). encode ( JSON . stringify ( message ) + "\n" )); 158} 159 160close () :Promise < void > { 161return this . closing ??= ( async () => { 162if ( ! this . closed ) { 163this . closed = true ; 164for ( const listener of this . closes ) listener (); 165} 166const inputClosed = this . writer ?. close (). catch (() => {}); 167const child = this . process ; 168if ( ! child ) { 169this . lifetime . abort (); 170return ; 171} 172await settleWithin ( child . exited , 500 ); 173this . lifetime . abort (); 174child . kill (); 175await settleWithin ( child . exited , 1000 ); 176child . kill ( "SIGKILL" ); 177await settleWithin ( Promise . allSettled ([ child . exited , inputClosed , this . reading ]), 500 ); 178// escaped descendants may retain inherited pipes even after the process group is dead 179await settleWithin ( 180Promise . allSettled ([ 181 ...this . readers . map (( reader ) => reader . cancel (). catch (() => {})), 182this . writer ?. abort (). catch (() => {}), 183this . reading , 184]), 185500 , 186); 187})(); 188} 189}