// The world chat API runs in the same harness as turns and extension actions. import { AgentDoc, LiveDoc, defineDoc } from '@earendil-works/pi-durable'; import { Metadata, fs } from './extensions'; import { eventLog } from './events'; const Concluded = defineDoc({kind:'conversations.concluded',version:1,scope:'session',checkpointWhen:()=>false,initial:()=>({ids:[]})}); const Started = defineDoc({kind:'board.started',version:1,scope:'conversation',history:'latest',fork:'initial',checkpointWhen:()=>false,initial:()=>({})}); const fail = (status:number,message:string):never => { throw Object.assign(new Error(message),{status}); }; const integer = (value:any, fallback?:number) => { if (value === null && fallback !== undefined) return fallback; const n=Number(value); if (!Number.isSafeInteger(n) || n<0) fail(400,'Invalid cursor or conversation'); return n; }; export function harnessApi(harness:any, models:any, extensions:any, context:any) { const streams = new MapPromise>(); const log = eventLog(()=>harness).eventLog; const send = (id:string,value:any) => Deno.core.ops.op_harness_reply(id,value); async function entries(conversation:any,after:number) { const found:any[]=[]; let cursor:any; do { const page=await conversation.entries({minEntryId:after+1,order:'ascending'},256,cursor,context); found.push(...page.items); cursor=page.next; } while(cursor); return found; } async function follow(input:any, read:(after:number)=>Promise, cursor:number) { // Observe before the snapshot. Serialize scans to close the snapshot/subscription race. let active=true, queue=Promise.resolve(); const scan=()=>queue=queue.then(async()=>{ if (!active) return; for (;;) { const page=await read(cursor); if(!active || !page.length) return; for (const entry of page) { cursor=entry.id; send(input.id,entry); } } }); const observation=harness.observeCommits({advance:(publication:any)=>{if(publication.changes.some((change:any)=>change.type==='entry'))void scan();},end:()=>{void stop();}}); const stop=async()=>{if(!active)return;active=false;observation.detach();await queue;send(input.id,null);streams.delete(input.id);}; streams.set(input.id,stop); send(input.id,{status:200,stream:true}); void Deno.core.ops.op_harness_closed(input.id).then(stop); await scan(); } async function handle(input:any) { const url=new URL(input.path,'http://host.sausage'); const path=url.pathname.slice('/harness'.length); const data=input.body??{}; const reply=(status:number,body:any)=>{send(input.id,{status,body});send(input.id,null);}; try { if(input.method==='GET' && path==='/conversations') { const records=await harness.commit(async(tx:any)=>{ const all:any[]=[];let cursor:any; do {const page=await tx.scanConversations({},256,cursor);all.push(...page.items);cursor=page.next;}while(cursor); return all; },context); const concluded=new Set((await harness.snapshot(Concluded,context))?.ids??[]); const conversations=await Promise.all(records.map(async(record:any)=>{ const agent=await harness.snapshot(AgentDoc,record.id,context); const metadata=await harness.snapshot(Metadata,record.id,context); const live=await harness.snapshot(LiveDoc,record.id,context); const started:any=await harness.snapshot(Started,record.id,context); return {...record,title:metadata?.title??'',agent,starter:started?.by??null,concluded:concluded.has(record.id),running:!!live?.run}; })); reply(200,{conversations});return; } if(input.method==='GET' && path==='/events') { const cursor=integer(url.searchParams.get('after'),0); await follow(input,async(after)=>log.recent({since:after,order:'ascending',limit:256}),cursor);return; } if(input.method==='GET' && path==='/extensions') {reply(200,extensions.state());return;} if(input.method==='GET' && path==='/families') { let families={};try{families=JSON.parse(await fs.read('/src/sausage/agents/models.json'));}catch(error){if((error as any).code!=='not_found')throw error;} reply(200,{families});return; } if(input.method==='GET' && path==='/models') {reply(200,{models:models.getAllModels().map(({id,name,provider,contextWindow,maxTokens,reasoning,cost}:any)=>({id,name,provider,contextWindow,maxTokens,reasoning,cost}))});return;} const action=/^\/extensions\/([^/]+)\/actions\/([^/]+)$/.exec(path); if(input.method==='POST' && action) { if(data.conversationId!==undefined && (!Number.isSafeInteger(data.conversationId)||data.conversationId<1)) fail(400,'Invalid conversation id'); const controller=new AbortController(); void Deno.core.ops.op_harness_closed(input.id).then(()=>controller.abort()); reply(200,{result:await extensions.invoke(decodeURIComponent(action[1]),decodeURIComponent(action[2]),data.args??{},controller.signal,data.conversationId)});return; } const match=/^\/conversations\/(\d+)\/(entries|stream|submit|stop)$/.exec(path); if(!match) fail(404,'Unknown harness route'); const conversation=await harness.conversation(integer(match![1]),context); if(!conversation) fail(404,'Unknown conversation'); const after=integer(url.searchParams.get('after'),0); if(input.method==='GET' && match![2]==='entries') {reply(200,{entries:await entries(conversation,after)});return;} if(input.method==='GET' && match![2]==='stream') {await follow(input,(cursor)=>entries(conversation,cursor),after);return;} if(input.method==='POST' && match![2]==='submit') { if(typeof data.requestId!=='string'||!data.requestId||typeof data.content!=='string'||!data.content.trim()) fail(400,'Expected content and requestId'); const submission=await conversation.submit({type:'input',content:data.content,requestId:data.requestId},context); reply(202,{submissionId:submission.id});return; } if(input.method==='POST' && match![2]==='stop') { for (;;) { const own=(await harness.inspect(context)).tasks.filter(({record}:any)=>record.conversationId===conversation.id&&!record.owner&&!record.background); if(!own.length) break; for(const {record} of own) {await harness.abortTask(record.id,context);await harness.waitForTask(record.id,context);} } reply(200,{ok:true});return; } fail(404,'Unknown harness route'); } catch(error) {reply((error as any).status??500,{error:(error as Error).message});} } return {handle,async close(){await Promise.all([...streams.values()].map(stop=>stop()));}}; }