Files
sausage/extensions/dream/server.ts
T

59 lines
3.9 KiB
TypeScript

/** Dream requests and prepared handoffs in the current world. No storage chapters or host restart. */
import { BACKGROUND_CONTEXT as context } from '@earendil-works/chord/context'
import { defineDoc, defineExtension, defineTool, ROOT_CONVERSATION_ID as ROOT } from '@earendil-works/pi-durable'
import { arr, obj, opt, str } from '@earendil-works/pi-schema'
import { Fired } from '../automations/refs.ts'
import { Standing, Started } from '../subagents/api.ts'
import { BuiltInDoc, PauseDoc, DREAM_ASK, ensureAutomation, fired } from './built-ins.ts'
import { deliver, deliverTask } from './delivery.ts'
export const DreamApi={name:'dream'}
export const Next=defineDoc<any>({kind:'dream.next',version:1,scope:'conversation',history:'latest',fork:'initial',checkpointWhen:()=>false,initial:()=>({})})
const Receipts=defineDoc<Record<string,any>>({kind:'dream.tool-receipts',version:1,scope:'task',checkpointWhen:()=>false,initial:()=>({})})
const plain=(value:any)=>JSON.parse(JSON.stringify(value))
export function prepare(args:any,now:number) {
if(!Array.isArray(args?.learned)||args.learned.length!==3) throw new Error('learned must be 3 lines')
const text=(value:any,name:string,max:number)=>{
if(typeof value!=='string'||!value.trim()) throw new Error(`${name} must be nonempty text`)
if(value.trim().length>max) throw new Error(`${name} must be at most ${max} characters`)
return value.trim()
}
return {learned:args.learned.map((line:any)=>text(line,'learned',200)),handoff:text(args.handoff,'handoff',12000),at:now}
}
export default function dream(host:any) {
const queue=host.resource('dream.ensure',()=>({last:Promise.resolve()}))
const ensure=()=>{
const run=queue.last.then(()=>ensureAutomation(host))
queue.last=run.catch(()=>undefined)
return run.catch((error:any)=>host.notice(`Dream automations: ${String(error)}`))
}
host.onOpen(ensure)
host.react('ultimate-started',[Started],(_tx:any,{data}:any)=>{
// Creation commits run after this reactor transaction completes.
if(data.kind==='standing'&&data.agent==='ultimate') void ensure()
},{from:'now'})
host.react('requested',[Fired],(tx:any,{data}:any)=>fired(host,tx,data.id,data.fire),{from:'now'})
async function change(tx:any,args:any) {
const ultimate=(await tx.doc(Standing,ROOT)).ultimate
if(ultimate===undefined) throw new Error('@ultimate has no conversation yet')
if(args.action==='start') {
await deliver(host,tx,ultimate,{type:'input',requestId:`dream.requested:${crypto.randomUUID()}`,kind:'dream.requested',data:{},content:`The user requested a dream. ${DREAM_ASK}`})
return {requested:true}
}
if(args.action!=='prepare') throw new Error(`Unknown dream action ${args.action}`)
const prepared=prepare(args,Date.now())
;(await tx.doc(Next,ROOT)).prepared=prepared
await deliver(host,tx,ultimate,{type:'write',requestId:`dream.prepared:${crypto.randomUUID()}`,kind:'dream.prepared',data:prepared,content:`Prepared dream handoff:\n${prepared.learned.join('\n')}\n\n${prepared.handoff}\nContinue in this world. No chapter switch is available.`})
return prepared
}
host.implement(DreamApi,{operate:(args:any)=>host.harness().commit((tx:any)=>change(tx,args),context)})
return defineExtension({name:'dream',tasks:[deliverTask(host)],tools:[defineTool({name:'dream',description:'Request a read-only dream branch, or prepare three learned lines and a handoff in the current world. No chapter switch.',parameters:obj({action:str(),learned:opt(arr(str())),handoff:opt(str())}),execute:async(args,api,ctx)=>{
const result=await api.commit(async(tx:any)=>{
const receipts=await tx.doc(Receipts,api.taskId)
if(receipts[api.callId]===undefined) receipts[api.callId]=await change(tx,args)
return plain(receipts[api.callId])
},ctx)
return {content:[{type:'text',text:JSON.stringify(result)}]}
}})]})
}
export const docs=[Next,BuiltInDoc,PauseDoc]