Merge v3-t58 (T58)
# Conflicts: # host/js/package.json
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
# Dream
|
||||
|
||||
A dream is a read-only review in an ordinary branch. It does not change code, memory, settings, or storage automatically.
|
||||
The user decides which proposals to accept. Approved proposals become ordinary work.
|
||||
|
||||
## For @ultimate
|
||||
|
||||
A user request or a built-in automation can request a dream.
|
||||
If a dream branch is active, report that fact. Do not start another branch.
|
||||
Otherwise, write a handoff while the current context still contains the relevant evidence.
|
||||
Include open work, decisions, user corrections, memory slugs, and the next steps.
|
||||
Use `branch` with an explicit repository path and that handoff in its prompt.
|
||||
Ask the branch to follow this document.
|
||||
|
||||
Review the branch report with the user. Apply approved memory changes through the memory workflow.
|
||||
Start ordinary branches for approved code or instruction changes.
|
||||
|
||||
Use `dream` with `action: prepare`, exactly three `learned` lines, and a fresh `handoff` when the review finishes.
|
||||
Each learned line has at most 200 characters. The handoff has at most 12,000 characters.
|
||||
A later preparation replaces the previous prepared handoff.
|
||||
The preparation adds a passive `dream.prepared` entry. It does not switch storage or restart the host.
|
||||
There are no chapters, seeds, or Fall asleep actions.
|
||||
|
||||
## For the dream branch
|
||||
|
||||
Coordinate these six read-only passes with background `dream-pass` subagents:
|
||||
|
||||
- **memory:** Propose evidence-based additions, updates, or deletions. Preserve the memory about the user.
|
||||
- **failures:** Group repeated failures by cause. Propose small corrections.
|
||||
- **tools:** Examine supplied tool results and repeated manual steps. Do not invent trace statistics.
|
||||
- **corrections:** Identify repeated user corrections and requests.
|
||||
- **codify:** Identify lessons that belong in code, agent instructions, or a reusable procedure.
|
||||
- **project:** Identify repeated work that a project-specific program can simplify.
|
||||
|
||||
Give every pass the handoff and its relevant evidence. The handoff is the primary input, not proof by itself.
|
||||
P3 supplies no general history-query or trace database interface. Name missing evidence rather than invent a query.
|
||||
Each pass cites conversation IDs, commits, or explicit file paths and line numbers.
|
||||
|
||||
Check each report against its evidence. Return three learned lines, a journal, and one proposal per slug.
|
||||
Each proposal names its pass, title, size, risk, evidence, and any memory it replaces.
|
||||
Return the final answer normally. The messages service reports the branch answer to its starter.
|
||||
Do not edit files, apply proposals, or switch storage during a dream.
|
||||
|
||||
## Built-in automations
|
||||
|
||||
Dream creates two ordinary context automations after @ultimate exists:
|
||||
|
||||
- `Dream before compaction`: the context exceeds 70%.
|
||||
- `Dream after a pause`: the context exceeds 50% after eight hours without user input.
|
||||
|
||||
The automations wake @ultimate. Each fire also adds one passive `dream.requested` entry.
|
||||
The automation ID and fire number identify that entry across replay and reopen.
|
||||
Creation keys recover a lost receipt without a duplicate automation.
|
||||
A user can pause or delete an automation. Reopen preserves those choices.
|
||||
@@ -0,0 +1,40 @@
|
||||
/** Two ordinary context automations, with durable creation and deletion receipts. */
|
||||
import { BACKGROUND_CONTEXT as context } from '@earendil-works/chord/context'
|
||||
import { defineDoc, ROOT_CONVERSATION_ID as ROOT } from '@earendil-works/pi-durable'
|
||||
import { Automations, AutomationsApi } from '../automations/server.ts'
|
||||
import { Standing } from '../subagents/api.ts'
|
||||
import { deliver } from './delivery.ts'
|
||||
export const AUTOMATION_NAME='Dream before compaction'
|
||||
export const PAUSE_NAME='Dream after a pause'
|
||||
export const BuiltInDoc=defineDoc<{id?:string;deleted?:true}>({kind:'dream.after-compaction',version:1,scope:'conversation',history:'latest',fork:'initial',checkpointWhen:()=>false,initial:()=>({})})
|
||||
export const PauseDoc=defineDoc<{id?:string;deleted?:true}>({kind:'dream.after-pause',version:1,scope:'conversation',history:'latest',fork:'initial',checkpointWhen:()=>false,initial:()=>({})})
|
||||
const builtIns=[
|
||||
{doc:BuiltInDoc,name:AUTOMATION_NAME,condition:{metric:'context',of:'@ultimate',above:70}},
|
||||
{doc:PauseDoc,name:PAUSE_NAME,condition:{metric:'context',of:'@ultimate',above:50,idleFor:8*3600_000}},
|
||||
]
|
||||
export const DREAM_ASK='Dream now: follow "For @ultimate" in /src/sausage/extensions/dream/DREAM.md. Write your handoff into the dream branch prompt in this turn, while your context still holds everything. If a dream branch is already active, say so and do not start another.'
|
||||
export async function ensureAutomation(host:any) {
|
||||
const service=host.want(AutomationsApi)
|
||||
if(!service || (await host.harness().snapshot(Standing,ROOT,context))?.ultimate===undefined) return
|
||||
for(const {doc,name,condition} of builtIns) {
|
||||
const state=await host.harness().snapshot(doc,ROOT,context)
|
||||
if(state?.deleted) continue
|
||||
if(state?.id) {
|
||||
const list=await service.operate({action:'list'})
|
||||
if(!list.some((a:any)=>a.id===state.id)) await host.harness().commit(async(tx:any)=>{(await tx.doc(doc,ROOT)).deleted=true},context)
|
||||
} else {
|
||||
const made=await service.operate({action:'create',name,condition,subscribers:['@ultimate'],key:`dream:${doc.definition.kind}`})
|
||||
await host.harness().commit(async(tx:any)=>{(await tx.doc(doc,ROOT)).id=made.id},context)
|
||||
}
|
||||
}
|
||||
}
|
||||
export async function fired(host:any,tx:any,id:string,fire:number) {
|
||||
const builtIn=await Promise.all(builtIns.map(async(item)=>({item,id:(await tx.doc(item.doc,ROOT)).id})))
|
||||
const found=builtIn.find(item=>item.id===id)?.item
|
||||
const ultimate=(await tx.doc(Standing,ROOT)).ultimate
|
||||
if(!found || ultimate===undefined) return
|
||||
const automation=(await tx.doc(Automations)).automations[id]
|
||||
const pause=found.doc===PauseDoc && automation ? Date.now()-(automation.condition?.inputAt??automation.createdAt) : undefined
|
||||
const data={automation:{id,name:automation?.name??found.name,...(pause===undefined?{}:{pause})}}
|
||||
await deliver(host,tx,ultimate,{type:'write',requestId:`dream.requested:${id}:${fire}`,kind:'dream.requested',data,content:`Automation ${id} "${data.automation.name}" fired${pause===undefined?'':` after ${Math.round(pause/3600_000)} h away`}. ${DREAM_ASK}`})
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/** Durable dream inputs and passive notes, admitted once per request ID. */
|
||||
import { defineTask } from '@earendil-works/pi-durable'
|
||||
const terminal = {status:'terminal',outcome:{status:'completed',result:null}} as const
|
||||
export const deliverTask = (host:any) => defineTask<any,{phase:'submit'},null>({
|
||||
name:'dream.deliver', version:1, initial:()=>({phase:'submit'}),
|
||||
phases:{submit:async(task,runtime,ctx)=>{
|
||||
const {to,type,requestId,kind,data,content}=task.input
|
||||
const target=(await host.harness().conversation(to,ctx))!
|
||||
await target.submit(type==='input'
|
||||
? {type,requestId,content,entry:{kind,data},whenBusy:'followUp'}
|
||||
: {type,requestId,entry:{kind,data,model:[{role:'user',content,timestamp:Date.now()}]}},ctx)
|
||||
await runtime.commit(()=>terminal,ctx)
|
||||
}},
|
||||
abort:(_task,runtime,ctx)=>runtime.commit(()=>({status:'terminal',outcome:{status:'aborted'}}),ctx),
|
||||
})
|
||||
export const deliver = (host:any,tx:any,to:number,delivery:any) => tx.createTask(deliverTask(host),{to,...delivery},{conversationId:to,ownership:{kind:'conversation'},background:true})
|
||||
@@ -0,0 +1,58 @@
|
||||
/** 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]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,93 @@
|
||||
/** Real dream triggers, message reports, and built-in receipts across a harness reopen. */
|
||||
import { BACKGROUND_CONTEXT as context } from '@earendil-works/chord/context'
|
||||
import { createModels } from '@earendil-works/pi-ai/models'
|
||||
import { fauxProvider, fauxAssistantMessage, fauxToolCall } from '@earendil-works/pi-ai/providers/faux'
|
||||
import { Harness, createRegistry, defineExtension, defineTool } from '@earendil-works/pi-durable'
|
||||
import { obj } from '@earendil-works/pi-schema'
|
||||
import { loadExtensions } from './extensions'
|
||||
import { openStorage } from './sqlite'
|
||||
const assert=(ok:any,message:string)=>{if(!ok) throw new Error(message)}
|
||||
const until=async(test:()=>any)=>{const deadline=Date.now()+20000;while(!await test()){if(Date.now()>deadline)throw new Error(`Dream deadline: ${JSON.stringify((await entries()).filter((e:any)=>['pi.tool-result','board.reply'].includes(e.kind)))}`);await new Promise(r=>setTimeout(r,0))}}
|
||||
const models=createModels(),faux=fauxProvider({tokensPerSecond:100000});models.setProvider(faux.provider)
|
||||
const registry=createRegistry(),storage=await openStorage(globalThis.storagePath)
|
||||
const harness=await Harness.open(storage,{models,registry,settings:{retry:{enabled:false},get extensions(){return registry.snapshot().installed()}}},context)
|
||||
const root=await harness.root(context,{agent:{model:{provider:'faux',modelId:'faux-1'},cwd:'/src/demo'}})
|
||||
const route=(transcript:any):any=>{
|
||||
faux.appendResponses([route])
|
||||
const last=transcript.messages.findLast((m:any)=>m.role!=='system')
|
||||
const text=typeof last?.content==='string'?last.content:(last?.content??[]).filter((p:any)=>p.type==='text').map((p:any)=>p.text).join('')
|
||||
if(last?.role==='user'&&text.startsWith('CALL ')){const call=JSON.parse(text.slice(5));return fauxAssistantMessage([fauxToolCall(call.name,call.args)],{stopReason:'toolUse'})}
|
||||
if(last?.role==='user'&&text.includes('Dream journal pass')) return fauxAssistantMessage([fauxToolCall('subagent',{action:'start',agent:'dream-pass',task:'Review supplied evidence',background:true,isolate:'none'})],{stopReason:'toolUse'})
|
||||
if(last?.role==='user'&&text==='Review supplied evidence') return fauxAssistantMessage('Evidence #1: keep the review read-only.')
|
||||
if(last?.role==='user'&&/^(Reply|Message|Note) from/.test(text)) return fauxAssistantMessage('Received')
|
||||
if(last?.role==='toolResult'||text.includes('Evidence #1')) return fauxAssistantMessage('Dream journal: keep the review read-only. Evidence #1.')
|
||||
return fauxAssistantMessage('Received')
|
||||
}
|
||||
faux.setResponses([route])
|
||||
// The role requires the normal host read/bash tools. The scripted pass does not invoke them.
|
||||
const baseTools=defineExtension({name:'dream-test-tools',tools:['read','bash'].map(name=>defineTool({name,description:'Unused fixture tool',parameters:obj({}),execute:async()=>{throw new Error('Unexpected fixture tool invocation')}}))})
|
||||
const loader=await loadExtensions(harness,registry,storage,models,context,[baseTools])
|
||||
assert(!loader.state().error,`Extension load: ${loader.state().error}`)
|
||||
const base=`file:///run/sausage/extensions/${loader.state().good}/extensions`
|
||||
const {BuiltInDoc,PauseDoc,ensureAutomation,fired,AUTOMATION_NAME,PAUSE_NAME}=await import(`${base}/dream/built-ins.ts`)
|
||||
const {Next}=await import(`${base}/dream/server.ts`)
|
||||
const {Automations}=await import(`${base}/automations/server.ts`)
|
||||
const {ContextUsageDoc}=await import(`${base}/automations/triggers.ts`)
|
||||
const {Fired}=await import(`${base}/automations/refs.ts`)
|
||||
const {Messages}=await import(`${base}/messages/api.ts`)
|
||||
const primary=(await harness.conversation(await globalThis.dreamSubagents.standing('ultimate',context),context))!
|
||||
const entries=async()=> (await primary.context(context)).entries
|
||||
const ledger=async()=> (await harness.snapshot(Automations,context))?.automations??{}
|
||||
const requested=async()=> (await entries()).filter((e:any)=>e.kind==='dream.requested')
|
||||
const operation=(args:any)=>globalThis.dreamAutomations.operate(args)
|
||||
harness.resume()
|
||||
try {
|
||||
if(globalThis.dreamMode==='reopen') {
|
||||
await ensureAutomation(globalThis.dreamHost)
|
||||
const all=Object.values(await ledger()) as any[]
|
||||
assert(all.length===1&&all[0].name===PAUSE_NAME&&!all[0].enabled,'Reopen must preserve deletion and pause')
|
||||
assert((await requested()).length===2,'Reopen duplicated dream requests')
|
||||
assert((await harness.snapshot(Next,root.id,context)).prepared.handoff==='Replacement handoff','Prepared handoff must survive reopen')
|
||||
assert((await harness.snapshot(Messages,root.id,context)).messages.filter((m:any)=>m.re!==undefined&&m.text.includes('Dream journal')).length===1,'Reopen duplicated the dream report')
|
||||
} else {
|
||||
await until(async()=>Object.keys(await ledger()).length===2)
|
||||
await ensureAutomation(globalThis.dreamHost)
|
||||
const id=(await harness.snapshot(BuiltInDoc,root.id,context)).id
|
||||
const pause=(await harness.snapshot(PauseDoc,root.id,context)).id
|
||||
assert(Object.keys(await ledger()).length===2,'Repeated ensure duplicated built-ins')
|
||||
assert((await ledger())[id].name===AUTOMATION_NAME&&(await ledger())[id].condition.above===70,'Compaction trigger')
|
||||
assert((await ledger())[pause].condition.idleFor===8*3600_000&&(await ledger())[pause].condition.above===50,'Pause trigger')
|
||||
await harness.commit(async(tx:any)=>Object.assign(await tx.doc(ContextUsageDoc,primary.id),{percent:71,tokens:710,window:1000}),context)
|
||||
await until(async()=> (await requested()).filter((e:any)=>e.data.automation?.id===id).length===1)
|
||||
await harness.commit((tx:any)=>globalThis.dreamHost.emit(tx,Fired,{id,fire:1}),context)
|
||||
await harness.commit((tx:any)=>fired(globalThis.dreamHost,tx,id,1),context)
|
||||
await harness.waitForIdle(context)
|
||||
const deliveries=await harness.commit((tx:any)=>tx.scanTasks({kind:'dream.deliver'},100),context)
|
||||
for(const task of deliveries.items) await harness.waitForTask(task.id,context)
|
||||
assert((await requested()).length===1,'One fire must add exactly one dream.requested entry')
|
||||
const request=(await storage.scanSubmissions({conversationId:primary.id},100,undefined,context)).items.find((s:any)=>s.requestId===`dream.requested:${id}:1`)
|
||||
assert(request?.type==='write','The fire adds a passive write')
|
||||
await globalThis.dreamService.operate({action:'start'})
|
||||
await until(async()=> (await requested()).length===2)
|
||||
for(const args of [{learned:['one'],handoff:'x'},{learned:['a','b','c'],handoff:'x'.repeat(12001)}]) {
|
||||
let failed=false;try{await globalThis.dreamService.operate({action:'prepare',...args})}catch{failed=true}assert(failed,'Invalid preparation must fail')
|
||||
}
|
||||
await globalThis.dreamService.operate({action:'prepare',learned:['I learned one.','I keep two.','I propose three.'],handoff:'First handoff'})
|
||||
await globalThis.dreamService.operate({action:'prepare',learned:['a','b','c'],handoff:'Replacement handoff'})
|
||||
await until(async()=> (await entries()).filter((e:any)=>e.kind==='dream.prepared').length===2)
|
||||
await (await primary.submit({type:'input',content:`CALL ${JSON.stringify({name:'branch',args:{repo:'/src/demo',prompt:'Dream journal pass'}})}`},context)).wait(context)
|
||||
await until(async()=> (await entries()).some((e:any)=>e.kind==='board.reply'&&e.data.text.includes('Dream journal')))
|
||||
await harness.waitForIdle(context)
|
||||
const report=(await entries()).find((e:any)=>e.kind==='board.reply'&&e.data.text.includes('Dream journal'))
|
||||
const branch=(await harness.conversation(report.data.from,context))!
|
||||
const reports=(await branch.context(context)).entries.filter((e:any)=>e.kind==='subagents.report')
|
||||
assert(reports.length===1&&reports[0].data.agent==='dream-pass'&&JSON.stringify(reports).includes('Evidence #1'),`The dream-pass must report its evidence: ${JSON.stringify((await branch.context(context)).entries.filter((e:any)=>['pi.tool-result','subagents.report'].includes(e.kind)))}`)
|
||||
await operation({action:'pause',id:pause})
|
||||
await harness.commit(async(tx:any)=>{delete (await tx.doc(PauseDoc,root.id)).id},context)
|
||||
await ensureAutomation(globalThis.dreamHost)
|
||||
assert((await harness.snapshot(PauseDoc,root.id,context)).id===pause&&!(await ledger())[pause].enabled,'A lost receipt must adopt the paused automation')
|
||||
await operation({action:'delete',id})
|
||||
await ensureAutomation(globalThis.dreamHost)
|
||||
assert((await harness.snapshot(BuiltInDoc,root.id,context)).deleted&&Object.keys(await ledger()).length===1,'Deletion must persist')
|
||||
}
|
||||
} finally {await loader.close();await harness.close(context)}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": { "bundle": "nu apply-patch.nu && esbuild main.ts --bundle --format=esm --platform=browser --outfile=bundle.js && esbuild test.ts --bundle --format=esm --platform=browser --outfile=test-bundle.js && esbuild durable-test.ts --bundle --format=esm --platform=browser --outfile=durable-test-bundle.js && esbuild foundation-test.ts --bundle --format=esm --platform=browser --outfile=foundation-test-bundle.js && esbuild extensions-test.ts --bundle --format=esm --platform=browser --outfile=extensions-test-bundle.js && esbuild events-test.ts --bundle --format=esm --platform=browser --outfile=events-test-bundle.js && esbuild subagents-test.ts --bundle --format=esm --platform=browser --outfile=subagents-test-bundle.js && esbuild core-ext-test.ts --bundle --format=esm --platform=browser --outfile=core-ext-test-bundle.js && esbuild plan-todos-test.ts --bundle --format=esm --platform=browser --outfile=plan-todos-test-bundle.js && esbuild p2-test.ts --bundle --format=esm --platform=browser --outfile=p2-test-bundle.js && esbuild chain-test.ts --bundle --format=esm --platform=browser --outfile=chain-test-bundle.js && esbuild undo-harness-test.ts --bundle --format=esm --platform=browser --outfile=undo-harness-test-bundle.js && esbuild messages-test.ts --bundle --format=esm --platform=browser --outfile=messages-test-bundle.js && esbuild p3-test.ts --bundle --format=esm --platform=browser --outfile=p3-test-bundle.js && esbuild automations-test.ts --bundle --format=esm --platform=browser --outfile=automations-test-bundle.js && esbuild watchdog-test.ts --bundle --format=esm --platform=browser --outfile=watchdog-test-bundle.js && esbuild workspaces-test.ts --bundle --format=esm --platform=browser --outfile=workspaces-test-bundle.js && esbuild goal-questions-test.ts --bundle --format=esm --platform=browser --outfile=goal-questions-test-bundle.js" },
|
||||
"scripts": { "bundle": "nu apply-patch.nu && esbuild main.ts --bundle --format=esm --platform=browser --outfile=bundle.js && esbuild test.ts --bundle --format=esm --platform=browser --outfile=test-bundle.js && esbuild durable-test.ts --bundle --format=esm --platform=browser --outfile=durable-test-bundle.js && esbuild foundation-test.ts --bundle --format=esm --platform=browser --outfile=foundation-test-bundle.js && esbuild extensions-test.ts --bundle --format=esm --platform=browser --outfile=extensions-test-bundle.js && esbuild events-test.ts --bundle --format=esm --platform=browser --outfile=events-test-bundle.js && esbuild subagents-test.ts --bundle --format=esm --platform=browser --outfile=subagents-test-bundle.js && esbuild core-ext-test.ts --bundle --format=esm --platform=browser --outfile=core-ext-test-bundle.js && esbuild plan-todos-test.ts --bundle --format=esm --platform=browser --outfile=plan-todos-test-bundle.js && esbuild p2-test.ts --bundle --format=esm --platform=browser --outfile=p2-test-bundle.js && esbuild chain-test.ts --bundle --format=esm --platform=browser --outfile=chain-test-bundle.js && esbuild undo-harness-test.ts --bundle --format=esm --platform=browser --outfile=undo-harness-test-bundle.js && esbuild messages-test.ts --bundle --format=esm --platform=browser --outfile=messages-test-bundle.js && esbuild p3-test.ts --bundle --format=esm --platform=browser --outfile=p3-test-bundle.js && esbuild automations-test.ts --bundle --format=esm --platform=browser --outfile=automations-test-bundle.js && esbuild watchdog-test.ts --bundle --format=esm --platform=browser --outfile=watchdog-test-bundle.js && esbuild workspaces-test.ts --bundle --format=esm --platform=browser --outfile=workspaces-test-bundle.js && esbuild goal-questions-test.ts --bundle --format=esm --platform=browser --outfile=goal-questions-test-bundle.js && esbuild dream-test.ts --bundle --format=esm --platform=browser --outfile=dream-test-bundle.js" },
|
||||
"dependencies": {
|
||||
"@earendil-works/pi-durable": "1.5.0",
|
||||
"@earendil-works/pi-ai": "1.5.0",
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
//! World-loaded dream built-ins and messages survive a harness reopen.
|
||||
use sausage::{harness::run_module, world::World};
|
||||
use std::{path::Path, process::Command};
|
||||
const BUNDLE: &str = include_str!("../js/dream-test-bundle.js");
|
||||
fn git(repo: &Path, args: &[&str]) -> anyhow::Result<String> {
|
||||
let output = Command::new("git")
|
||||
.args(args)
|
||||
.current_dir(repo)
|
||||
.env("GIT_AUTHOR_NAME", "Test")
|
||||
.env("GIT_AUTHOR_EMAIL", "test@example.com")
|
||||
.env("GIT_COMMITTER_NAME", "Test")
|
||||
.env("GIT_COMMITTER_EMAIL", "test@example.com")
|
||||
.output()?;
|
||||
anyhow::ensure!(
|
||||
output.status.success(),
|
||||
"git {args:?}: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
Ok(String::from_utf8(output.stdout)?.trim().into())
|
||||
}
|
||||
fn copy_tree(source: &Path, destination: &Path) -> anyhow::Result<()> {
|
||||
std::fs::create_dir_all(destination)?;
|
||||
for entry in std::fs::read_dir(source)? {
|
||||
let entry = entry?;
|
||||
let target = destination.join(entry.file_name());
|
||||
if entry.file_type()?.is_dir() {
|
||||
copy_tree(&entry.path(), &target)?;
|
||||
} else {
|
||||
std::fs::copy(entry.path(), target)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn prepare(dir: &Path) -> anyhow::Result<()> {
|
||||
World::open(dir)?;
|
||||
let disk = dir.join("world");
|
||||
let repo = disk.join("src/sausage");
|
||||
let source = Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
|
||||
let target = Path::new(env!("CARGO_TARGET_TMPDIR")).parent().unwrap();
|
||||
let userland = source.join("userland");
|
||||
if !userland.join("cwd-init/init.o").exists() {
|
||||
anyhow::ensure!(
|
||||
Command::new("nu")
|
||||
.args(["--no-config-file", "cwd-init/build.nu"])
|
||||
.current_dir(&userland)
|
||||
.status()?
|
||||
.success(),
|
||||
"cwd-init build failed"
|
||||
);
|
||||
}
|
||||
anyhow::ensure!(
|
||||
Command::new("cargo")
|
||||
.args(["build", "--release", "-p", "git", "--manifest-path"])
|
||||
.arg(userland.join("Cargo.toml"))
|
||||
.args(["--target", "wasm32-wasip2", "--target-dir"])
|
||||
.arg(target)
|
||||
.args(["-j", "2", "--locked"])
|
||||
.current_dir(&userland)
|
||||
.status()?
|
||||
.success(),
|
||||
"world git build failed"
|
||||
);
|
||||
std::fs::create_dir_all(disk.join("bin"))?;
|
||||
std::fs::copy(
|
||||
target.join("wasm32-wasip2/release/git.wasm"),
|
||||
disk.join("bin/git.wasm"),
|
||||
)?;
|
||||
copy_tree(&source.join("extensions"), &repo.join("extensions"))?;
|
||||
std::fs::create_dir_all(repo.join("extensions/probe"))?;
|
||||
std::fs::write(
|
||||
repo.join("extensions/probe/server.ts"),
|
||||
"export default host => { globalThis.dreamHost=host; globalThis.dreamService=host.need({name:'dream'}); globalThis.dreamSubagents=host.need({name:'subagents'}); globalThis.dreamAutomations=host.need({name:'automations'}); return {name:'probe'}; }",
|
||||
)?;
|
||||
std::fs::create_dir_all(repo.join("agents"))?;
|
||||
std::fs::write(
|
||||
repo.join("agents/models.json"),
|
||||
r#"{"default":{"model":"faux/faux-1"},"fast":{"model":"faux/faux-1"}}"#,
|
||||
)?;
|
||||
for role in ["ultimate", "coordinator", "worker", "reviewer"] {
|
||||
let denial = if role == "worker" || role == "reviewer" {
|
||||
"denyTools: [subagent]\n"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
std::fs::write(
|
||||
repo.join(format!("agents/{role}.md")),
|
||||
format!(
|
||||
"---\nname: {role}\ndescription: P3 {role}\nisolate: none\n{denial}{}---\nP3 role:{role}\n",
|
||||
if role == "ultimate" {
|
||||
"resident: true\n"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
),
|
||||
)?;
|
||||
}
|
||||
std::fs::copy(
|
||||
source.join("agents/dream-pass.md"),
|
||||
repo.join("agents/dream-pass.md"),
|
||||
)?;
|
||||
let demo = disk.join("src/demo");
|
||||
std::fs::create_dir_all(&demo)?;
|
||||
std::fs::write(demo.join("README.md"), "P3 demo\n")?;
|
||||
for repo in [&repo, &demo] {
|
||||
git(repo, &["init", "-b", "main"])?;
|
||||
git(repo, &["add", "."])?;
|
||||
git(repo, &["commit", "--no-gpg-sign", "-m", "P3 fixture"])?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
async fn run(dir: &Path, mode: &str) -> anyhow::Result<()> {
|
||||
let world = World::open(dir)?;
|
||||
let exec = sausage::process::exec(World {
|
||||
root: world.root.clone(),
|
||||
host_dir: world.host_dir.clone(),
|
||||
processes: world.processes.clone(),
|
||||
})?;
|
||||
run_module(
|
||||
world,
|
||||
exec,
|
||||
&format!("globalThis.dreamMode={mode:?};\n{BUNDLE}"),
|
||||
)
|
||||
.await
|
||||
}
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn builtins_fire_and_report_across_reopen() -> anyhow::Result<()> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
prepare(dir.path())?;
|
||||
run(dir.path(), "first").await?;
|
||||
run(dir.path(), "reopen").await
|
||||
}
|
||||
Reference in New Issue
Block a user