Merge branch 'v3' into v3-t85

This commit is contained in:
2026-11-11 22:51:01 +00:00
142 changed files with 110842 additions and 8097 deletions
+2 -2
View File
@@ -37,7 +37,7 @@ CARGO_TARGET_DIR=target/harness nix develop -c cargo test -p sausage --test harn
The lock file pins npm dependencies. The bundle command requires Nushell, GNU patch, and npm.
It applies `patches/pi-durable-1.5.0.patch` before esbuild runs.
This is Ultimate's patch at `7ec0b4f`, without the Node execution environment hunks (D65).
This is Standing agent's patch at `7ec0b4f`, without the Node execution environment hunks (D65).
The patch adds transactional `beforeInput`, input markers, scheduler hold/release, and the failed-run boundary fix.
The step verifies an existing application before another bundle build. A mismatched or partially applied patch fails the command.
`npm ci` restores the unpatched dependency. The next bundle command applies the patch again.
@@ -85,5 +85,5 @@ The signal client sends `Input::Stop`. The harness closes its watches, credentia
The host then stops its process services and HTTP listener.
Console output still streams assistant messages, without replay of the historical transcript.
`harness_foundation.rs` runs the three Ultimate input tests and the scheduler phase-boundary test through bundled V8 modules.
`harness_foundation.rs` runs the three Standing agent input tests and the scheduler phase-boundary test through bundled V8 modules.
It also tests the channel with closed stdin, CLI EOF and SIGTERM, submission scans, and the patched bundle hooks.
+9 -9
View File
@@ -1,4 +1,4 @@
/** All 19 Ultimate pause cases, executed against world snapshot modules. */
/** All 19 Standing agent pause cases, executed against world snapshot modules. */
import {expect,it,until,setImmediate} from "./automations-test-support";
import {fixture} from "./automations-pause-fixture";
import {BACKGROUND_CONTEXT as context} from "@earendil-works/chord/context";
@@ -245,9 +245,9 @@ it('pauses on the first rate-limited answer: the run waits in its retry, fails n
},
])
const { root, harness } = await f.open()
const ultimate = await harness.createConversation({ ownership: { kind: 'ownerless' } }, context)
const standing = await harness.createConversation({ ownership: { kind: 'ownerless' } }, context)
await harness.commit(async (tx) => {
;(await tx.doc(Standing, ROOT_CONVERSATION_ID)).ultimate = ultimate.id
;(await tx.doc(Standing, ROOT_CONVERSATION_ID))['$standing'] = standing.id
}, context)
const submission = await root.submit({ type: 'input', content: 'go' }, context)
await until(async () => (await f.action('state')).limit !== undefined)
@@ -263,7 +263,7 @@ it('pauses on the first rate-limited answer: the run waits in its retry, fails n
expect(generation.record.state.status).not.toBe('terminal')
expect((await harness.snapshot(LiveDoc, root.id, context))?.run).toBeDefined()
const cards = async () =>
(await ultimate.context(context)).entries.filter((entry) => entry.kind === 'pause.limit')
(await standing.context(context)).entries.filter((entry) => entry.kind === 'pause.limit')
await until(async () => (await cards()).length === 1)
expect((await cards())[0].data).toMatchObject({ error: limit, conversationId: root.id })
@@ -420,9 +420,9 @@ it('plays a rate-limit pause by itself once, N minutes in, only when the user sw
},
])
const { root, harness } = await f.open()
const ultimate = await harness.createConversation({ ownership: { kind: 'ownerless' } }, context)
const standing = await harness.createConversation({ ownership: { kind: 'ownerless' } }, context)
await harness.commit(async (tx) => {
;(await tx.doc(Standing, ROOT_CONVERSATION_ID)).ultimate = ultimate.id
;(await tx.doc(Standing, ROOT_CONVERSATION_ID))['$standing'] = standing.id
}, context)
const submission = await root.submit({ type: 'input', content: 'go' }, context)
await until(async () => (await f.action('state')).limit !== undefined)
@@ -443,14 +443,14 @@ it('plays a rate-limit pause by itself once, N minutes in, only when the user sw
expect(responses).toBe(2)
// Once: the pause is over, so a second due timer finds nothing to play.
expect(await f.autoPlay(since! + 31 * 60_000)).toBe(false)
const played = (await ultimate.context(context)).entries.filter(
const played = (await standing.context(context)).entries.filter(
(entry) => entry.kind === 'pause.autoplay',
)
expect(played).toHaveLength(1)
expect(played[0].data).toMatchObject({ since, at: since! + 30 * 60_000, minutes: 30 })
const notes = (await root.context(context)).entries.filter((entry) => entry.kind === 'pause.note')
expect(notes[0].data).toMatchObject({ auto: true })
expect(notes[0].model?.[0]?.content).toMatch(/continued after Ultimate played by itself\.$/)
expect(notes[0].model?.[0]?.content).toMatch(/continued after Standing agent played by itself\.$/)
// The setting stays for the next limit.
expect((await f.action('state')).autoPlay).toBe(30)
})
@@ -495,7 +495,7 @@ const at = (kind: string, phase: string) => ({ kind, state: { checkpoint: { phas
it('treats message deliveries and routes as passive waiters', () => {
expect(passive(at('board.delivery', 'deliver'))).toBe(true)
expect(passive(at('board.delivery', 'reply'))).toBe(true)
expect(passive(at('ultimate.subagent-reporter', 'deliver'))).toBe(true)
expect(passive(at('standing.subagent-reporter', 'deliver'))).toBe(true)
expect(passive(at('board.route', 'observe'))).toBe(true)
expect(passive(at('conversations.summary', 'generate'))).toBe(false)
})
+1 -1
View File
@@ -1,4 +1,4 @@
/** Pause cases use snapshot modules and isolated durable storage, as Ultimate's fixture does. */
/** Pause cases use snapshot modules and isolated durable storage, as Standing agent's fixture does. */
import { BACKGROUND_CONTEXT as context } from '@earendil-works/chord/context'
import { obj } from '@earendil-works/pi-schema'
import { createModels } from '@earendil-works/pi-ai/models'
+1 -1
View File
@@ -1,4 +1,4 @@
/** The five Ultimate calendar cases execute inside deno_core's V8. */
/** The five Standing agent calendar cases execute inside deno_core's V8. */
import {describe, expect, it} from "./automations-test-support";
const {describeSchedule,nextAt,parseSchedule}=await import(`${globalThis.automationBase}/automations/schedule.ts`);
const time = Date.parse('2026-01-05T08:00:00Z')
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,4 +1,4 @@
/** Minimal assertions for the imported Ultimate cases, without Node or Vitest. */
/** Minimal assertions for the imported Standing agent cases, without Node or Vitest. */
export const cases: {name:string;run:()=>any}[]=[]
export const it=(name:string,run:()=>any)=>cases.push({name,run})
export const describe=(_name:string,run:()=>any)=>run()
+7702 -1061
View File
File diff suppressed because it is too large Load Diff
+2467 -200
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+28 -9
View File
@@ -5,6 +5,7 @@ import { fauxProvider, fauxAssistantMessage } from '@earendil-works/pi-ai/provid
import { Harness, AgentDoc, createRegistry } from '@earendil-works/pi-durable'
import { openStorage } from './sqlite'
import { loadExtensions, fs } from './extensions'
import { parse } from 'yaml'
const assert = (ok:any,message:string)=>{if(!ok)throw new Error(message);Deno.core.ops.op_output(`ok - ${message}\n`)}
const models=createModels(), faux=fauxProvider({tokensPerSecond:100000});models.setProvider(faux.provider)
const proxyTemplate={...models.getModels('faux')[0],provider:'openai',baseUrl:'https://proxy.invalid/v1'}
@@ -18,17 +19,35 @@ const loader=await loadExtensions(harness,registry,storage,models,context)
try {
assert(!loader.state().error,`loader: ${loader.state().error ?? 'loaded'}`)
const {selection,conversations,State,metadata}=globalThis.core
const config = parse(await fs.read('/etc/sausage/config.yaml'))
config.standing='worker'
config.roster=[{name:'worker',description:'Worker',model:'faux/faux-1',thinking:'low',tools:['read','write'],prompt:'Worker body'}]
const save = ()=>fs.write('/etc/sausage/config.yaml',JSON.stringify(config))
await save()
const worker=await selection.start({agent:'worker',cwd:'/project/sub'},context)
let agent=await worker.agent(context)
assert(agent.model.modelId==='faux-1' && agent.thinkingLevel==='low','worker fast family and thinking')
assert(agent.tools.map(t=>t.name).join(',')==='read,write','worker tools and project overlay')
assert(agent.instructions.includes('Worker body') && agent.instructions.includes('Project body'),'definition body and overlay')
assert(agent.model.modelId==='faux-1' && agent.thinkingLevel==='low','worker roster model and thinking')
assert(agent.tools.map(t=>t.name).join(',')==='read,write','worker roster tools')
assert(agent.instructions.indexOf('## Sausage and the world') < agent.instructions.indexOf('Worker body'),'global prompt precedes role prompt')
const sections:any={}
for(const section of agent.sections) sections[section.key]=await section.render({conversationId:worker.id,agent},context)
assert(sections.project_context.includes('Root instructions') && sections.project_context.includes('Nested instructions'),'cwd and parent AGENTS.md')
assert(!sections.project_context.includes('Outside instructions'),'instructions stop at repo root')
assert(sections.skills.includes('builtin-skill') && sections.skills.includes('project-skill'),'builtin and project skills')
assert(sections.cwd==='/project/sub' && sections.tools.includes('read'),'cwd and tools sections')
const checkPrompt = (ctx: any, expected: string) => {
const prompt=JSON.stringify(ctx.messages.filter((message: any)=>message.role==='system'))
for (const route of ['whoami','conversations/ID/entries?after=N','events?after=N','models','extensions','credential-requests','/proc/processes','/proc/extensions','jaq']) assert(prompt.includes(route),`fresh system prompt includes ${route}`)
assert(prompt.includes(expected),'system prompt uses current global prompt')
assert(prompt.indexOf(expected)<prompt.indexOf('Worker body'),'system prompt places global prompt before role prompt')
}
faux.setResponses([(ctx)=>{checkPrompt(ctx,'## Sausage and the world');return fauxAssistantMessage('fresh')}])
harness.resume()
await (await worker.submit({type:'input',content:'fresh'},context)).wait(context)
config.prompt='Edited global prompt\\n'+config.prompt
await save()
faux.setResponses([(ctx)=>{checkPrompt(ctx,'Edited global prompt');return fauxAssistantMessage('edited')}])
await (await worker.submit({type:'input',content:'edited'},context)).wait(context)
const seed=await harness.commit(tx=>tx.appendEntry(worker.id,{kind:'test.seed'}),context)
const child=await worker.fork(seed.id,{ownership:{kind:'ownerless'}},context)
const unrelated=await harness.createConversation({ownership:{kind:'ownerless'}},context)
@@ -46,7 +65,7 @@ try {
const receipt=await child.submit({type:'input',content:'resume'},context)
assert(JSON.stringify([...await conversations.concluded(context)])===JSON.stringify([unrelated.id]),'input placement atomically resumes ancestors')
harness.resume();await receipt.wait(context)
await fs.write('/src/sausage/agents/models.json',JSON.stringify({default:{model:'faux/faux-1'},fast:{model:'faux/faux-1',thinking:'high'}}))
config.roster[0].thinking='high';await save()
faux.setResponses([fauxAssistantMessage('next')])
await (await worker.submit({type:'input',content:'next'},context)).wait(context)
agent=await worker.agent(context)
@@ -54,22 +73,22 @@ try {
// A second faux model proves selection changes, not only the thinking level.
const provider=models.getProvider('faux'), template=models.getModels('faux')[0]
models.setProvider({...provider,getModels:()=>[template,{...template,id:'second'}]})
await fs.write('/src/sausage/agents/models.json',JSON.stringify({default:{model:'faux/faux-1'},fast:{model:'faux/second'}}))
config.roster[0].model='faux/second';await save()
faux.setResponses([fauxAssistantMessage('second')])
await (await worker.submit({type:'input',content:'second'},context)).wait(context)
assert((await harness.snapshot(AgentDoc,worker.id,context)).model.modelId==='second','edited family changes next model without restart')
await fs.write('/src/sausage/agents/models.json',JSON.stringify({default:{model:'faux/faux-1'},fast:{model:'openai/new'}}))
assert((await harness.snapshot(AgentDoc,worker.id,context)).model.modelId==='second','edited roster changes next model without restart')
config.roster[0].model='openai/new';await save()
faux.setResponses([fauxAssistantMessage('proxy')])
await (await worker.submit({type:'input',content:'proxy'},context)).wait(context)
assert(models.getModel('openai','new').baseUrl===proxyTemplate.baseUrl && models.getModel('openai','new').cost.input===0 && models.getProvider('openai').auth===proxyAuth,'new model retains proxy transport and auth')
await fs.write('/src/sausage/agents/models.json',JSON.stringify({default:{model:'faux/faux-1'},fast:{model:'anthropic/new'}}))
config.roster[0].model='anthropic/new';await save()
faux.setResponses([fauxAssistantMessage('anthropic')])
await (await worker.submit({type:'input',content:'anthropic'},context)).wait(context)
assert(models.getModel('anthropic','new').baseUrl==='https://proxy.invalid/apis/anthropic','new Anthropic model retains its own provider transport')
globalThis.modelPin={provider:'faux',modelId:'faux-1'}
faux.setResponses([fauxAssistantMessage('pin')])
await (await worker.submit({type:'input',content:'pin'},context)).wait(context)
assert((await worker.agent(context)).model.provider==='faux','models.pin overrides family selection')
assert((await worker.agent(context)).model.provider==='faux','models.pin overrides roster selection')
await harness.commit(tx=>conversations.setStanding(tx,worker.id,true),context)
let refused=false;try{await harness.commit(tx=>conversations.concludeIn(tx,[worker.id]),context)}catch{refused=true}
assert(refused,'standing conversation cannot conclude')
+7001 -356
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -35,7 +35,7 @@ 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 primary=(await harness.conversation(await globalThis.dreamSubagents.standing('standing',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')
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -42,7 +42,7 @@ Event emission updates the feed inside the same transaction.
Notices become `sausage.notice` events.
The host records each distinct notice once per facet load.
This task does not add Ultimate's build or supervisor startup events.
This task does not add Standing agent's build or supervisor startup events.
## Tests
File diff suppressed because it is too large Load Diff
+8 -2
View File
@@ -1,5 +1,6 @@
// World extension snapshots, staged services, lifecycle, and last-good recovery.
import * as durable from '@earendil-works/pi-durable';
import * as yaml from 'yaml';
import * as tools from '@earendil-works/pi-durable/tools';
import * as ai from '@earendil-works/pi-ai';
import * as modelApi from '@earendil-works/pi-ai/models';
@@ -68,7 +69,7 @@ export async function git(cwd:string,...args:string[]) {
return result.stdout.trim();
}
export function registerModules() {
const modules = {'@earendil-works/sausage-events':events,'@earendil-works/pi-schema':schema,'@earendil-works/pi-durable':durable,'@earendil-works/pi-durable/tools':tools,'@earendil-works/pi-ai':ai,'@earendil-works/pi-ai/models':modelApi,'@earendil-works/chord':chord,'@earendil-works/chord/context':contextApi};
const modules = {'yaml':yaml,'@earendil-works/sausage-events':events,'@earendil-works/pi-schema':schema,'@earendil-works/pi-durable':durable,'@earendil-works/pi-durable/tools':tools,'@earendil-works/pi-ai':ai,'@earendil-works/pi-ai/models':modelApi,'@earendil-works/chord':chord,'@earendil-works/chord/context':contextApi};
globalThis.__sausage = {modules};
ops.op_modules(Object.fromEntries(Object.entries(modules).map(([name,value])=>[name,Object.keys(value)])));
}
@@ -209,7 +210,12 @@ export async function loadExtensions(harness:any, registry:any, storage:any, mod
const rid = ops.op_watch([`${repo}/.git/refs/heads/main`,`${repo}/.git/packed-refs`,`${repo}/settings.json`]);
await enqueue();
const pending = (async()=> {while(active){await ops.op_watch_next(rid);if(active)await enqueue();}})();
return {reload:enqueue, state:()=>({...state}), async invoke(name:string,key:string,args:any,signal:AbortSignal,conversationId?:number) {
return {reload:enqueue, state:()=>({...state}), async standing() {
if (!providers.has('models') || !providers.has('subagents')) return undefined;
const {standing:name}=await providers.get('models').discover(repo);
const conversationId=await providers.get('subagents').standing(name,context);
return {name,conversationId};
}, async invoke(name:string,key:string,args:any,signal:AbortSignal,conversationId?:number) {
const facet = facets.find(f=>f.extension.name===name);
const action = facet?.actions?.[key];
if (!action) throw Object.assign(new Error('Unknown user action'),{status:404});
+4 -3
View File
@@ -20614,7 +20614,7 @@ async function openStorage(path2) {
}
// input.ts
async function serve(harness2, context, afterSubmit, api) {
async function serve(harness2, context, afterSubmit, api, defaultConversation) {
const pending = /* @__PURE__ */ new Set();
const track2 = (work) => {
const task = work.catch((error) => Deno.core.ops.op_output(`${String(error)}
@@ -20637,8 +20637,9 @@ async function serve(harness2, context, afterSubmit, api) {
track2(api.handle(input));
continue;
}
const conversation = await harness2.conversation(input.conversation, context);
if (!conversation) throw new Error(`Unknown conversation: ${input.conversation}`);
const id = defaultConversation ? await defaultConversation() : input.conversation;
const conversation = await harness2.conversation(id, context);
if (!conversation) throw new Error(`Unknown conversation: ${id}`);
const submission2 = await conversation.submit({
type: "input",
content: input.content,
+1 -1
View File
@@ -1,4 +1,4 @@
// Ultimate's input-placement cases, scheduler hold, and the headless channel contract.
// Standing agent's input-placement cases, scheduler hold, and the headless channel contract.
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';
+35 -35
View File
@@ -1,4 +1,4 @@
/** The seven Ultimate goal cases use overlaid documents and the loaded server modules. */
/** The seven Standing agent goal cases use overlaid documents and the loaded server modules. */
import type { ConversationId, GenerationHooks, Tx } from '@earendil-works/pi-durable'
import { BACKGROUND_CONTEXT } from '@earendil-works/chord/context'
import { expect, it } from './goal-questions-test-support'
@@ -6,7 +6,7 @@ const { reminderTask } = await import(`${globalThis.goalBase}/goal/reminder.ts`)
const {default:goalExtension,act,actions,end,Goals,openGoal,start,TRIES} = await import(`${globalThis.goalBase}/goal/server.ts`)
import type { Host } from './extensions.ts'
const ULTIMATE = 5 as ConversationId
const STANDING = 5 as ConversationId
const screenshot = { type: 'image', data: 'aGk=', mimeType: 'image/png' }
/** Like the harness's document overlay, a proxy at every level: structuredClone refuses it. */
@@ -18,17 +18,17 @@ const overlay = <T extends object>(value: T): T =>
},
})
/** A transaction over overlaid documents and a few entries of @ultimate's conversation; @ultimate is conversation 5. */
/** A transaction over overlaid documents and a few entries of @standing's conversation; @standing is conversation 5. */
function fakeTx(
newer: { id: number; conversationId: number; kind?: string; model: object[] }[] = [],
) {
const docs = new Map<string, object>()
const entries = [
...newer,
{ id: 30, conversationId: ULTIMATE, model: [{ role: 'user', content: 'no images' }] },
{ id: 30, conversationId: STANDING, model: [{ role: 'user', content: 'no images' }] },
{
id: 20,
conversationId: ULTIMATE,
conversationId: STANDING,
model: [{ role: 'toolResult', content: [{ type: 'text', text: 'shot' }, screenshot] }],
},
{ id: 10, conversationId: 9, model: [{ role: 'user', content: [screenshot] }] },
@@ -47,7 +47,7 @@ function fakeTx(
if (!docs.has(key))
docs.set(
key,
def.kind === 'ultimate.standing-agents' ? { ultimate: ULTIMATE } : def.initial(),
def.kind === 'standing.standing-agents' ? { '$standing': STANDING } : def.initial(),
)
return overlay(docs.get(key)!)
},
@@ -120,27 +120,27 @@ it('keeps one goal open at a time and ends it once', async () => {
it('blocks a criterion after repeated failed tries and finishes with the outcome', async () => {
const tx = fakeTx()
await expect(act(tx, ULTIMATE, { action: 'met', n: 1 }, 0)).rejects.toThrow('No hands-off goal')
await expect(act(tx, STANDING, { action: 'met', n: 1 }, 0)).rejects.toThrow('No hands-off goal')
await start(tx, 'Goal', 0)
await act(tx, ULTIMATE, { action: 'criteria', criteria: ['Loads in 1s', 'Tests pass'] }, 0)
await expect(act(tx, ULTIMATE, { action: 'met', n: 3 }, 0)).rejects.toThrow('No criterion 3')
for (let i = 1; i < TRIES; i++) await act(tx, ULTIMATE, { action: 'failed', n: 1 }, 0)
await act(tx, STANDING, { action: 'criteria', criteria: ['Loads in 1s', 'Tests pass'] }, 0)
await expect(act(tx, STANDING, { action: 'met', n: 3 }, 0)).rejects.toThrow('No criterion 3')
for (let i = 1; i < TRIES; i++) await act(tx, STANDING, { action: 'failed', n: 1 }, 0)
expect((await openGoal(tx))!.criteria[0]).toMatchObject({ status: 'open', tries: TRIES - 1 })
const last = await act(tx, ULTIMATE, { action: 'failed', n: 1 }, 0)
const last = await act(tx, STANDING, { action: 'failed', n: 1 }, 0)
expect(last.answer).toContain('blocked')
expect((await openGoal(tx))!.criteria[0].status).toBe('blocked')
// Rewording keeps what was learned about an unchanged criterion.
await act(tx, ULTIMATE, { action: 'criteria', criteria: ['Loads in 1s', 'Lint passes'] }, 0)
await act(tx, STANDING, { action: 'criteria', criteria: ['Loads in 1s', 'Lint passes'] }, 0)
expect((await openGoal(tx))!.criteria.map((item) => item.status)).toEqual(['blocked', 'open'])
await expect(act(tx, ULTIMATE, { action: 'finish', summary: 'x' }, 0)).rejects.toThrow('outcome')
await expect(act(tx, STANDING, { action: 'finish', summary: 'x' }, 0)).rejects.toThrow('outcome')
const done = await act(
tx,
ULTIMATE,
STANDING,
{ action: 'finish', outcome: 'blocked', summary: 'Stuck' },
4,
)
expect(done.ended).toMatchObject({
ended: { by: 'ultimate', outcome: 'blocked', summary: 'Stuck' },
ended: { by: 'standing', outcome: 'blocked', summary: 'Stuck' },
})
expect(await openGoal(tx)).toBeUndefined()
})
@@ -149,11 +149,11 @@ it('records decisions with the screenshots of its own conversation only', async
const tx = fakeTx()
await start(tx, 'Goal', 0)
await expect(
act(tx, ULTIMATE, { action: 'decide', question: 'Go?', picked: 'Yes' }, 0),
act(tx, STANDING, { action: 'decide', question: 'Go?', picked: 'Yes' }, 0),
).rejects.toThrow('reason')
await act(
tx,
ULTIMATE,
STANDING,
{
action: 'decide',
question: 'Approve?',
@@ -165,7 +165,7 @@ it('records decisions with the screenshots of its own conversation only', async
1,
)
await expect(
act(tx, ULTIMATE, { action: 'decide', question: 'q', picked: 'p', reason: 'r', images: 10 }, 2),
act(tx, STANDING, { action: 'decide', question: 'q', picked: 'p', reason: 'r', images: 10 }, 2),
).rejects.toThrow('has no images')
expect((await openGoal(tx))!.decisions).toEqual([
{ at: 1, question: 'Approve?', picked: 'Yes', reason: 'Meets 1', conversation: 7, images: 20 },
@@ -173,17 +173,17 @@ it('records decisions with the screenshots of its own conversation only', async
})
it('never takes an earlier turn\'s screenshot for images: "latest"', async () => {
const tx = fakeTx([{ id: 40, kind: 'pi.user', conversationId: ULTIMATE, model: [] }])
const tx = fakeTx([{ id: 40, kind: 'pi.user', conversationId: STANDING, model: [] }])
await start(tx, 'Goal', 0)
const decide = { action: 'decide', question: 'q', picked: 'p', reason: 'r' } as const
await expect(act(tx, ULTIMATE, { ...decide, images: 'latest' }, 1)).rejects.toThrow(
await expect(act(tx, STANDING, { ...decide, images: 'latest' }, 1)).rejects.toThrow(
'no images in this turn',
)
await act(tx, ULTIMATE, { ...decide, images: 20 }, 2)
await act(tx, STANDING, { ...decide, images: 20 }, 2)
expect((await openGoal(tx))!.decisions).toMatchObject([{ images: 20 }])
})
it("notes the open goal on @ultimate's runs only, and lets only @ultimate keep it", async () => {
it("notes the open goal on @standing's runs only, and lets only @standing keep it", async () => {
const tx = fakeTx()
const extension = goalExtension({
doc: () => undefined,
@@ -193,10 +193,10 @@ it("notes the open goal on @ultimate's runs only, and lets only @ultimate keep i
const hooks = extension.hooks![0].handlers as GenerationHooks
const before = (conversationId: number, kind = 'pi.user') =>
hooks.beforeInput({ kind }, { tx, conversationId } as never, BACKGROUND_CONTEXT)
expect(await before(ULTIMATE)).toBeUndefined()
expect(await before(STANDING)).toBeUndefined()
await start(tx, 'Make it fast', 0)
expect(await before(9)).toBeUndefined()
const note = (await before(ULTIMATE))!.entries![0]
const note = (await before(STANDING))!.entries![0]
expect(note.kind).toBe('goal.note')
expect(note.model![0].content).toContain(
'Hands-off goal 1, granted by the user, who is away: Make it fast',
@@ -209,19 +209,19 @@ it("notes the open goal on @ultimate's runs only, and lets only @ultimate keep i
{ conversationId, commit: (fn: (tx: Tx) => unknown) => fn(tx) } as never,
undefined as never,
)
await expect(run(9, { action: 'met', n: 1 })).rejects.toThrow('Only @ultimate')
expect(await run(ULTIMATE, { action: 'criteria', criteria: ['Fast'] })).toEqual({
await expect(run(9, { action: 'met', n: 1 })).rejects.toThrow('Only the standing agent')
expect(await run(STANDING, { action: 'criteria', criteria: ['Fast'] })).toEqual({
content: [{ type: 'text', text: 'Goal 1 has 1 criteria.' }],
})
expect((await tx.doc(Goals, 1 as ConversationId)).goals[0].criteria).toHaveLength(1)
// Finishing ends the goal and creates the report's delivery in the same commit; the note stops.
await run(ULTIMATE, { action: 'finish', outcome: 'met', summary: 'Done' })
await run(STANDING, { action: 'finish', outcome: 'met', summary: 'Done' })
expect(tx.tasks).toEqual([
{ name: 'goal.deliver', input: expect.objectContaining({ to: ULTIMATE, kind: 'goal.report' }) },
{ name: 'goal.deliver', input: expect.objectContaining({ to: STANDING, kind: 'goal.report' }) },
{ name: 'goal.reminder', input: { goal: 1 } },
])
expect(await before(ULTIMATE)).toBeUndefined()
expect(await before(STANDING)).toBeUndefined()
})
it('keeps a reminder while the goal is open: paused while waiting on the user, gone once ended', async () => {
@@ -232,7 +232,7 @@ it('keeps a reminder while the goal is open: paused while waiting on the user, g
const run = (args: object) =>
tool.execute(
args as never,
{ conversationId: ULTIMATE, commit: (fn: (tx: Tx) => unknown) => fn(tx) } as never,
{ conversationId: STANDING, commit: (fn: (tx: Tx) => unknown) => fn(tx) } as never,
undefined as never,
)
await actions.start({ text: 'Make it fast' }, { host } as never)
@@ -240,7 +240,7 @@ it('keeps a reminder while the goal is open: paused while waiting on the user, g
// its key instead of making a second one, and pauses it.
await service.create({
name: 'Goal reminder · goal 1',
subscribers: ['@ultimate'],
subscribers: ['@standing'],
key: 'goal.reminder:1',
})
;(await openGoal(tx))!.waiting = { reason: 'Which account?', at: 0 }
@@ -249,7 +249,7 @@ it('keeps a reminder while the goal is open: paused while waiting on the user, g
{
id: 'A1',
name: 'Goal reminder · goal 1',
subscribers: ['@ultimate'],
subscribers: ['@standing'],
key: 'goal.reminder:1',
enabled: false,
},
@@ -261,10 +261,10 @@ it('keeps a reminder while the goal is open: paused while waiting on the user, g
// The user renamed it: it stays linked by its id.
ledger.get('A1')!.name = 'Night check'
// Its fire tells @ultimate what to check; other inputs do not.
// Its fire tells @standing what to check; other inputs do not.
const hooks = extension.hooks![0].handlers as GenerationHooks
const note = async (input: object) =>
(await hooks.beforeInput(input as never, { tx, conversationId: ULTIMATE }, BACKGROUND_CONTEXT))!
(await hooks.beforeInput(input as never, { tx, conversationId: STANDING }, BACKGROUND_CONTEXT))!
.entries![0].model![0].content as string
expect(await note({ kind: 'automations.fired', data: { id: 'A1' } })).toContain(
'This is the goal reminder',
@@ -315,7 +315,7 @@ it('grants nothing through a start card still queued after the user ended its go
expect(
await hooks.beforeInput(
{ kind: 'goal.started' },
{ tx, conversationId: ULTIMATE },
{ tx, conversationId: STANDING },
BACKGROUND_CONTEXT,
),
).toBeUndefined()
+2 -2
View File
@@ -30,8 +30,8 @@ export async function fixture(clock: any = {}) {
const settled=async()=>{await until(async()=>(await harness.commit((tx:any)=>tx.scanTasks({},1000),context)).items.every((t:any)=>t.state.status==='terminal'));await harness.waitForIdle(context)}
const entries=async(id:number)=>(await harness.commit((tx:any)=>tx.scanEntries({conversationId:id},1000),context)).items.toSorted((a:any,b:any)=>a.id-b.id)
const type=async(id:number,content:string)=>{const c=await harness.conversation(id,context);await(await c.submit({type:'input',content},context)).wait(context)}
const ultimate=async()=>{const c=await harness.createConversation({ownership:{kind:'ownerless'}},context);await c.configure({model:{provider:'faux',modelId:'faux-1'}},context);await harness.commit(async(tx:any)=>{(await tx.doc(Standing,root.id)).ultimate=c.id},context);return c.id}
const standing=async()=>{const c=await harness.createConversation({ownership:{kind:'ownerless'}},context);await c.configure({model:{provider:'faux',modelId:'faux-1'}},context);await harness.commit(async(tx:any)=>{(await tx.doc(Standing,root.id))['$standing']=c.id},context);return c.id}
const ask=async(id:number,args:any)=>{await type(id,`SEND ${JSON.stringify({to:'user',...args})}`);await settled();const result=(await entries(id)).findLast((e:any)=>e.kind==='pi.tool-result')?.model?.[0];return {error:result?.isError?result.content.map((p:any)=>p.text).join(''):undefined,message:(await log()).messages.findLast((m:any)=>m.from===id&&m.to==='user')}}
const action=(conversationId?:number)=>({host:globalThis.testHost,conversationId,signal:new AbortController().signal})
return {base,faux,loader,host:globalThis.testHost,opened:{harness,root,close:async()=>{await loader.close();await harness.close(context);await storage.close()}},ultimate,type,ask,answer:(args:any)=>question.actions.answer(args,action()),dismiss:(messageId:number)=>question.actions.dismiss({messageId},action()),log,entries,observed,settled,hear:(fn:any)=>heard=fn}
return {base,faux,loader,host:globalThis.testHost,opened:{harness,root,close:async()=>{await loader.close();await harness.close(context);await storage.close()}},standing,type,ask,answer:(args:any)=>question.actions.answer(args,action()),dismiss:(messageId:number)=>question.actions.dismiss({messageId},action()),log,entries,observed,settled,hear:(fn:any)=>heard=fn}
}
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,4 +1,4 @@
/** Minimal assertions for the imported Ultimate cases, without Node or Vitest. */
/** Minimal assertions for the imported Standing agent cases, without Node or Vitest. */
export const cases: {name:string;run:()=>any}[]=[]
export const it=(name:string,run:()=>any)=>cases.push({name,run})
export const describe=(_name:string,run:()=>any)=>run()
+1 -1
View File
@@ -1,4 +1,4 @@
/** Ultimate goal and question-card cases execute in the world loader. */
/** Standing agent goal and question-card cases execute in the world loader. */
import {fixture} from './goal-questions-fixture'
import {cases,cleanups} from './goal-questions-test-support'
const initial=await fixture()
+1 -1
View File
@@ -9,7 +9,7 @@ it('fires one goal reminder through automations',async()=>{
const timer={at:now+delay,callback};timers.add(timer);return()=>timers.delete(timer)
}})
try {
const id=await f.ultimate();const {harness}=f.opened
const id=await f.standing();const {harness}=f.opened
const goal=await import(/* @vite-ignore */ `${f.base}/goal/server.ts`)
const {Automations}=await import(/* @vite-ignore */ `${f.base}/automations/server.ts`)
await goal.actions.start({text:'Ship the goal'},{host:f.host})
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -14,11 +14,12 @@ const curl=async(path:string,body?:any)=>{
assert(result.code===0,`curl failed: ${result.stderr}`);
return JSON.parse(result.stdout);
};
const id=await f.ultimate();
const id=await f.standing();
const question=await f.ask(id,{text:'Choose.',questions:[{text:'Name',kind:'text',ref:'1'}]});
assert(question.message,'Question fixture');
const listed=await curl('/conversations');
assert(listed.conversations.some((c:any)=>c.id===id && c.agent && !c.running),'Conversation list');
assert(listed.standing.name==='standing'&&listed.standing.conversationId===id,'Conversation list identifies the configured standing conversation');
const initial=await curl(`/conversations/${id}/entries`);
const after=initial.entries.at(-1).id;
const answer=await curl('/extensions/questions/actions/answer',{args:{messageId:question.message.id,answers:[{question:'Name',answer:'API choice'}]}});
@@ -26,7 +27,7 @@ assert(!answer.error,JSON.stringify(answer));
await f.settled();
assert(JSON.stringify(await f.entries(id)).includes('API choice'),'Question answer reaches asker');
assert((await curl('/models')).models.length>0,'Models');
assert((await curl('/families')).families.default,'Families');
assert((await curl('/families')).error,'The obsolete families route is absent');
assert((await curl('/extensions')).good,'Extension status');
assert(Array.isArray((await curl('/credential-requests')).requests),'Credential requests');
assert((await curl('/unknown')).error,'Unknown route');
+3 -6
View File
@@ -1,6 +1,6 @@
// 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 { Metadata } 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:()=>({})});
@@ -51,6 +51,7 @@ export function harnessApi(harness:any, models:any, extensions:any, context:any)
const reply=(status:number,body:any)=>{send(input.id,{status,body});send(input.id,null);};
try {
if(input.method==='GET' && path==='/conversations') {
const standing=await extensions.standing?.() ?? null;
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);
@@ -64,17 +65,13 @@ export function harnessApi(harness:any, models:any, extensions:any, context:any)
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;
reply(200,{conversations,standing});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) {
+4 -3
View File
@@ -1,5 +1,5 @@
// EOF and Stop drain submissions. Shutdown cancels invocations and preserves durable tasks for restart.
export async function serve(harness: any, context: any, afterSubmit?: () => Promise<void>, api?: any) {
export async function serve(harness: any, context: any, afterSubmit?: () => Promise<void>, api?: any, defaultConversation?: () => Promise<number>) {
const pending = new Set<Promise<void>>();
const track = (work: Promise<void>) => {
const task = work.catch(error => Deno.core.ops.op_output(`${String(error)}\n`)).finally(() => pending.delete(task));
@@ -22,8 +22,9 @@ export async function serve(harness: any, context: any, afterSubmit?: () => Prom
track(api.handle(input));
continue;
}
const conversation = await harness.conversation(input.conversation, context);
if (!conversation) throw new Error(`Unknown conversation: ${input.conversation}`);
const id = defaultConversation ? await defaultConversation() : input.conversation;
const conversation = await harness.conversation(id, context);
if (!conversation) throw new Error(`Unknown conversation: ${id}`);
const submission = await conversation.submit({
type: 'input', content: input.content, requestId: input.requestId,
}, context);
+13 -18
View File
@@ -1,7 +1,7 @@
// Durable root conversation and console. SQLite stays outside the world.
// The standing conversation serves the console and chat. SQLite stays outside the world.
import { BACKGROUND_CONTEXT as context } from '@earendil-works/chord/context';
import { proxyModels } from './proxy-models';
import { Harness, AgentDoc, createRegistry } from '@earendil-works/pi-durable';
import { Harness, createRegistry } from '@earendil-works/pi-durable';
import { CodingTools } from './environment';
import { openStorage } from './sqlite';
import { environment, conversationChains } from './environment';
@@ -9,10 +9,8 @@ import { CredentialRequests, startRequests } from './credentials';
import { serve } from './input';
import { harnessApi } from './harness-api';
import { loadExtensions } from './extensions';
const proxy = Deno.core.ops.op_model_proxy();
if (!proxy?.base_url || !proxy?.api_key) throw new Error('The llm-proxy credential is required');
const models = await proxyModels(proxy);
const model = models.getModel('openai', 'gpt-6.1-sol');
const proxy = await Deno.core.ops.op_model_proxy();
const models = await proxyModels(proxy, () => Deno.core.ops.op_model_proxy());
const registry = createRegistry();
registry.install(CodingTools);
registry.install(CredentialRequests);
@@ -22,14 +20,13 @@ const harness = await Harness.open(storage, {
env: async (target: any) => environment(target.cwd ?? '/', await chain(target.conversationId)),
}, context);
const chain = conversationChains(harness, context);
const root = await harness.root(context, { agent: { model: { provider: 'openai', modelId: model.id }, thinkingLevel: 'low', cwd: '/' } });
// T32 stored this exact built-in list in the console root. Use registry defaults now.
const storedAgent = await harness.snapshot(AgentDoc, root.id, context);
if (Array.isArray(storedAgent?.extensions) && storedAgent.extensions.length === 2 &&
[CodingTools, CredentialRequests].every(extension => storedAgent.extensions.includes(extension.name))) {
await root.configure({ extensions: null }, context);
}
const watch = await root.watch(context);
await harness.root(context, {agent:{cwd:'/src/sausage'}});
const stopRequests = await startRequests(harness, context);
const extensions = await loadExtensions(harness, registry, storage, models, context, [CodingTools, CredentialRequests]);
const standing = await extensions.standing();
if (!standing) throw new Error('The world needs the models and subagents extensions for its standing agent');
const conversation = (await harness.conversation(standing.conversationId, context))!;
const watch = await conversation.watch(context);
const emitted = new Map<number, string>();
let partial = '';
const text = (message: any) => message?.content?.filter((b: any) => b.type === 'text').map((b: any) => b.text).join('') ?? '';
@@ -52,17 +49,15 @@ for (const entry of watch.value.entries) {
if (entry.kind === 'pi.assistant') emitted.set(entry.id, text(entry.model?.[0]));
}
if (globalThis.consoleEnabled) watch.start((event: any) => { if (event.kind === 'value') render(event.value); });
const stopRequests = await startRequests(harness, context);
const extensions = await loadExtensions(harness, registry, storage, models, context, [CodingTools, CredentialRequests]);
const api = harnessApi(harness, models, extensions, context);
harness.resume();
await serve(harness, context, globalThis.consoleEnabled ? async () => {
await harness.waitForIdle(context);
const final = await root.watch(context);
const final = await conversation.watch(context);
render(final.value);
await final.stop();
Deno.core.ops.op_output('\n');
} : undefined, api);
} : undefined, api, async () => (await extensions.standing())!.conversationId);
await api.close();
await extensions.close();
await stopRequests();
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -59,7 +59,7 @@ const {SubmissionLanded} = await import(`${base}/lineage/refs.ts`);
const {Tally, abstract} = await import(`${base}/lineage/server.ts`);
const {normalizeMemoryOp, parseMemoryOps, applyMemoryOps, readMemory, memorizeTask} = await import(`${base}/lineage/memory.ts`);
const {Seen, mainOf} = await import(`${base}/lineage/catchup.ts`);
const primaryId = await globalThis.subagentService.standing('ultimate', context);
const primaryId = await globalThis.subagentService.standing('standing', context);
const primary = (await harness.conversation(primaryId, context))!;
const entries = async (conversation: any) => (await conversation.context(context)).entries;
const replies = async () => (await entries(primary)).filter((e: any) => e.kind === 'board.reply');
+7012 -367
View File
File diff suppressed because it is too large Load Diff
+12 -12
View File
@@ -12,10 +12,10 @@ testReplay();
if (globalThis.p3Mode !== 'unit') {
const workerId = (transcript: any) => Number(/Started subagent (\d+)/.exec(JSON.stringify(transcript.messages))?.[1]);
const rules: Rule[] = [
{ id: 'ultimate-branch', role: 'ultimate', match: { kind: 'user', text: 'launch scenario' }, answer: { tools: [{ name: 'branch', args: { repo: '/src/demo', isolated: true, prompt: 'begin' } }] } },
{ id: 'ultimate-probe', role: 'ultimate', match: { kind: 'user', text: 'launch branch' }, answer: { tools: [{ name: 'branch', args: { repo: '/src/demo', prompt: 'branch probe' } }] } },
{ id: 'standing-branch', role: 'standing', match: { kind: 'user', text: 'launch scenario' }, answer: { tools: [{ name: 'branch', args: { repo: '/src/demo', isolated: true, prompt: 'begin' } }] } },
{ id: 'standing-probe', role: 'standing', match: { kind: 'user', text: 'launch branch' }, answer: { tools: [{ name: 'branch', args: { repo: '/src/demo', prompt: 'branch probe' } }] } },
{ id: 'coordinator-probe', role: 'coordinator', match: { kind: 'user', text: 'branch probe' }, answer: { text: 'branch complete' } },
{ id: 'ultimate-receipt', role: 'ultimate', match: { text: '' }, answer: { text: 'Receipt noted.' } },
{ id: 'standing-receipt', role: 'standing', match: { text: '' }, answer: { text: 'Receipt noted.' } },
{ id: 'coordinator-echo', role: 'coordinator', match: { kind: 'user', text: 'Receipt noted.' }, answer: { text: 'Acknowledged.' } },
{ id: 'start-worker', role: 'coordinator', match: { kind: 'user', text: 'begin' }, answer: { tools: [{ name: 'subagent', args: { action: 'start', agent: 'worker', isolate: 'worktree', task: 'build feature', background: true } }] } },
{ id: 'start-reviewer', role: 'coordinator', match: { kind: 'user', text: 'worker complete' }, answer: { tools: [{ name: 'subagent', args: { action: 'start', agent: 'reviewer', isolate: 'none', task: 'review feature', background: true } }] } },
@@ -32,7 +32,7 @@ if (globalThis.p3Mode !== 'unit') {
models.setProvider(scriptedModel(rules, rule => Deno.core.ops.op_output(`scripted rule: ${rule}\n`)));
const storage = await openStorage(globalThis.storagePath), registry = createRegistry();
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' }, instructions: 'P3 role:ultimate', cwd: '/src/demo' } });
const root = await harness.root(context, { agent: { model: { provider: 'faux', modelId: 'faux-1' }, instructions: 'P3 role:standing', cwd: '/src/demo' } });
// Stop after the durable lander commit records landed.sha, before its event/report commit.
if (globalThis.p3Mode === 'kill-forward') {
const install = registry.install.bind(registry);
@@ -85,12 +85,12 @@ if (globalThis.p3Mode !== 'unit') {
if (globalThis.p3Mode === 'load-all') {
assert((await root.agent(context)).tools.some((tool: any) => tool.name === 'branch'), 'Every extension must load');
} else {
const ultimate = (await harness.conversation(await globalThis.p3Subagents.standing('ultimate', context), context))!;
const standing = (await harness.conversation(await globalThis.p3Subagents.standing('standing', context), context))!;
harness.resume();
const probe = globalThis.p3Mode.startsWith('branch');
if (globalThis.p3Mode === 'branch' || globalThis.p3Mode.startsWith('kill-')) {
await (await ultimate.submit({ type: 'input', content: probe ? 'launch branch' : 'launch scenario', requestId: 'p3-launch' }, context)).wait(context);
const entries = (await ultimate.context(context)).entries;
await (await standing.submit({ type: 'input', content: probe ? 'launch branch' : 'launch scenario', requestId: 'p3-launch' }, context)).wait(context);
const entries = (await standing.context(context)).entries;
assert(!JSON.stringify(entries).includes('"isError":true'), `Branch tool failed: ${JSON.stringify(entries)}`);
}
if (globalThis.p3Mode.startsWith('kill-')) await new Promise(() => {});
@@ -101,12 +101,12 @@ if (globalThis.p3Mode !== 'unit') {
await new Promise(resolve => setTimeout(resolve, 0));
}
};
const replies = async () => (await ultimate.context(context)).entries.filter((entry: any) => entry.kind === 'board.reply');
const replies = async () => (await standing.context(context)).entries.filter((entry: any) => entry.kind === 'board.reply');
await wait(async () => (await replies()).length > 0, 'No branch reply');
const received = await replies();
const coordinator = (await harness.conversation(received[0].data.from, context))!;
assert((await coordinator.agent(context)).instructions === 'P3 role:coordinator', 'Branch must use coordinator definition');
assert((await conversationChains(harness, context)(coordinator.id)).includes(String(ultimate.id)), 'D63 must include ultimate');
assert((await coordinator.agent(context)).instructions.includes('P3 role:coordinator'), 'Branch must use coordinator definition');
assert((await conversationChains(harness, context)(coordinator.id)).includes(String(standing.id)), 'D63 must include standing');
if (!probe) {
await wait(async () => Object.keys((await harness.snapshot(globalThis.p3Workspaces.doc, root.id, context)).results).length === 1, 'No landing');
await wait(async () => (await harness.snapshot(globalThis.p3Workspaces.doc, root.id, context)).queue.length === 0, 'Lander did not report');
@@ -125,9 +125,9 @@ if (globalThis.p3Mode !== 'unit') {
assert(!await fs.stat(worker.path).then(() => true, () => false), 'Released checkout must not exist');
assert(Object.values(index.results).every((result: any) => result.outcome === 'landed'), `Landing failed: ${JSON.stringify(index)}`);
assert((await globalThis.p3Host.eventLog.recent({ events: [{ kind: 'event.workspaces.landed' }] })).length === 1, 'Expected one landed event');
const ultimateEntries = (await ultimate.context(context)).entries;
const standingEntries = (await standing.context(context)).entries;
const landed = Object.values(index.results)[0] as any;
const cards = ultimateEntries.filter((entry: any) => entry.kind === 'board.message' && entry.data.text.startsWith('[lander] Submission') && entry.data.text.includes(' landed as '));
const cards = standingEntries.filter((entry: any) => entry.kind === 'board.message' && entry.data.text.startsWith('[lander] Submission') && entry.data.text.includes(' landed as '));
assert(cards.length === 1 && cards[0].data.from === coordinator.id && cards[0].data.text.includes(landed.sha), `Expected one landing card through messages: ${JSON.stringify(cards)}`);
const repliesNow = await replies();
assert(repliesNow.length === 1 && !repliesNow[0].data.failed && new Set(repliesNow.map((entry: any) => entry.data.id)).size === repliesNow.length, `Duplicate board.reply: ${JSON.stringify(repliesNow)}`);
+1 -1
View File
@@ -1697,7 +1697,7 @@ trap - EXIT
printf 'P4_PHASE_2_OK
'`,
`set -e
undo list --conversation 1 > /tmp/p4-undo.json
undo list > /tmp/p4-undo.json
box ls > /tmp/p4-boxes
printf 'P4_PHASE_3_OK
'`
+1 -1
View File
@@ -56,7 +56,7 @@ box rm "$id"
trap - EXIT
printf 'P4_PHASE_2_OK\n'`,
`set -e
undo list --conversation 1 > /tmp/p4-undo.json
undo list > /tmp/p4-undo.json
box ls > /tmp/p4-boxes
printf 'P4_PHASE_3_OK\n'`,
];
+17 -1
View File
@@ -7,7 +7,8 @@
"dependencies": {
"@earendil-works/chord": "1.5.0",
"@earendil-works/pi-ai": "1.5.0",
"@earendil-works/pi-durable": "1.5.0"
"@earendil-works/pi-durable": "1.5.0",
"yaml": "2.9.1"
},
"devDependencies": {
"esbuild": "0.28.2"
@@ -1682,6 +1683,21 @@
"optional": true
}
}
},
"node_modules/yaml": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz",
"integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==",
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
},
"funding": {
"url": "https://github.com/sponsors/eemeli"
}
}
}
}
+11 -6
View File
@@ -1,11 +1,16 @@
{
"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 && esbuild dream-test.ts --bundle --format=esm --platform=browser --outfile=dream-test-bundle.js && esbuild p4-test.ts --bundle --format=esm --platform=browser --outfile=p4-test-bundle.js && esbuild harness-api-test.ts --bundle --format=esm --platform=browser --outfile=harness-api-test-bundle.js && esbuild proxy-models-test.ts --bundle --format=esm --platform=browser --outfile=proxy-models-test-bundle.js" },
"dependencies": {
"@earendil-works/pi-durable": "1.5.0",
"@earendil-works/pi-ai": "1.5.0",
"@earendil-works/chord": "1.5.0"
"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 && esbuild p4-test.ts --bundle --format=esm --platform=browser --outfile=p4-test-bundle.js && esbuild harness-api-test.ts --bundle --format=esm --platform=browser --outfile=harness-api-test-bundle.js && esbuild proxy-models-test.ts --bundle --format=esm --platform=browser --outfile=proxy-models-test-bundle.js"
},
"devDependencies": { "esbuild": "0.28.2" }
"dependencies": {
"@earendil-works/chord": "1.5.0",
"@earendil-works/pi-ai": "1.5.0",
"@earendil-works/pi-durable": "1.5.0",
"yaml": "2.9.1"
},
"devDependencies": {
"esbuild": "0.28.2"
}
}
File diff suppressed because it is too large Load Diff
+12 -12
View File
@@ -1,4 +1,4 @@
/** The six Ultimate plan/todos cases, plus real-loader turns and branch reset. */
/** The six Standing agent plan/todos cases, plus real-loader turns and branch reset. */
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';
@@ -113,37 +113,37 @@ try {
assert((await harness.snapshot(Started,branch.id,context)).started,'Branch started');
// Todos cases 1-4 retain the original small transaction fixture.
const ULTIMATE=5;
const fakeTx=()=>{const docs=new Map();return {doc:async({definition:def}:any,id:number)=>{const key=`${def.kind}:${id}`;if(!docs.has(key))docs.set(key,def.kind==='ultimate.standing-agents'?{ultimate:ULTIMATE}:def.initial());return docs.get(key);}};};
const STANDING=5;
const fakeTx=()=>{const docs=new Map();return {doc:async({definition:def}:any,id:number)=>{const key=`${def.kind}:${id}`;if(!docs.has(key))docs.set(key,def.kind==='standing.standing-agents'?{'$standing':STANDING}:def.initial());return docs.get(key);}};};
const list=async(tx:any)=>(await tx.doc(Todos,1)).items;
{
const tx=fakeTx();equal((await add(tx,' Deploy fleet ','ultimate')).text,'Deploy fleet');
const tx=fakeTx();equal((await add(tx,' Deploy fleet ','standing')).text,'Deploy fleet');
assert((await add(tx,'Rotate the key','user')).id===2,'Sequential todo ids');
await rejects(()=>add(tx,' ','user'),'nonempty');await rejects(()=>add(tx,'x','lander'),'owner');equal((await list(tx)).map((todo:any)=>todo.id),[1,2]);
}
{
const tx=fakeTx();for(let i=0;i<DONE_LIMIT+2;i++)await add(tx,`t${i}`,'ultimate');
const tx=fakeTx();for(let i=0;i<DONE_LIMIT+2;i++)await add(tx,`t${i}`,'standing');
assert(typeof(await mark(tx,1,true)).done==='number','Tick');assert((await mark(tx,1,false)).done===undefined,'Reopen');
for(const todo of (await list(tx)).slice(0,-1))todo.done=todo.id;
await mark(tx,DONE_LIMIT+2,true);equal((await list(tx)).map((todo:any)=>todo.id),Array.from({length:DONE_LIMIT},(_,i)=>i+3));await rejects(()=>mark(tx,999,true),'No to-do 999');
}
{
const tx=fakeTx(), content=async()=>(await noteFor(tx,ULTIMATE,0))?.entries[0].model[0].content;
assert(await content()===undefined,'Empty note');await add(tx,'Mine','ultimate');await add(tx,'Theirs','user');
const tx=fakeTx(), content=async()=>(await noteFor(tx,STANDING,0))?.entries[0].model[0].content;
assert(await content()===undefined,'Empty note');await add(tx,'Mine','standing');await add(tx,'Theirs','user');
equal(await content(),"Open to-dos (todo tool: done when finished):\nYours:\n- 1: Mine\nThe user's:\n- 2: Theirs");assert(await content()===undefined,'Unchanged note');
await mark(tx,1,true);await mark(tx,2,true);equal(await content(),'No to-dos are open.');assert(await content()===undefined,'Unchanged empty note');
}
{
const tx=fakeTx(), extension=todosModule.default(), tool=extension.tools[0];
const run=(conversationId:number,args:any)=>tool.execute(args,{conversationId,commit:(fn:any)=>fn(tx)},context);
await rejects(()=>run(9,{action:'list'}),'Only @ultimate');equal(await run(ULTIMATE,{action:'add',text:'Ship it',owner:'user'}),{output:[{type:'text',text:'Added 1 for user: Ship it'}]});
equal(await run(ULTIMATE,{action:'list'}),{output:[{type:'text',text:"The user's:\n- 1: Ship it"}]});
await rejects(()=>run(9,{action:'list'}),'Only the standing agent');equal(await run(STANDING,{action:'add',text:'Ship it',owner:'user'}),{output:[{type:'text',text:'Added 1 for user: Ship it'}]});
equal(await run(STANDING,{action:'list'}),{output:[{type:'text',text:"The user's:\n- 1: Ship it"}]});
const before=(conversationId:number)=>extension.hooks[0].handlers.beforeInput({}, {tx,conversationId},context);
assert(await before(9)===undefined,'Nonstanding note');assert(await before(ULTIMATE)!==undefined,'Standing note');
assert(await before(9)===undefined,'Nonstanding note');assert(await before(STANDING)!==undefined,'Standing note');
}
// The real world also calls todo through the loaded factory and places its note.
await harness.commit(async(tx:any)=>{(await tx.doc(Standing,root.id)).ultimate=root.id;},context);
assert(text(await call({action:'add',text:'Real world',owner:'ultimate'},root.id,'todo')).includes('Added 1'),'Loaded todo tool');
await harness.commit(async(tx:any)=>{(await tx.doc(Standing,root.id))['$standing']=root.id;},context);
assert(text(await call({action:'add',text:'Real world',owner:'standing'},root.id,'todo')).includes('Added 1'),'Loaded todo tool');
assert(text(await call({action:'list'},root.id,'todo')).includes('Real world'),'Loaded todo list');
assert((await root.context(context)).entries.some((entry:any)=>entry.kind==='todos.note'),'Real beforeInput note');
await fs.write('/saved',JSON.stringify({plan:await harness.snapshot(Plan,root.id,context),index:await harness.snapshot(TaskIndex,root.id,context),todos:await harness.snapshot(Todos,root.id,context)}));
File diff suppressed because it is too large Load Diff
+14 -1
View File
@@ -1,6 +1,8 @@
/** Exercise both real pi-ai transports and their catalog prices, without provider SDK mocks. */
import { proxyModels } from './proxy-models';
const models = await proxyModels(globalThis.testProxy);
import { fs } from './extensions';
const readProxy=()=>Deno.core.ops.op_model_proxy();
const models = await proxyModels(await readProxy(),readProxy);
for (const [provider, id] of [['openai', 'gpt-6.1-sol'], ['anthropic', 'claude-opus-5-5']]) {
const model = models.getModel(provider, id);
if (model.cost.input !== 2 || model.cost.cacheWrite !== 3) throw new Error('catalog prices lost');
@@ -15,3 +17,14 @@ for (const [provider, api] of [['openai', 'openai-responses'], ['anthropic', 'an
const model = fallback.getModels(provider)[0];
if (model.api !== api || model.cost.input !== 0) throw new Error('offline transport fallback failed');
}
await fs.write('/etc/sausage/config.yaml',JSON.stringify({proxy:{base_url:`${globalThis.testProxy.base_url}/edited`,api_key:'next-key'}}));
const auth=await models.getAuth(models.getModel('openai','gpt-6.1-sol'));
if(auth.auth.apiKey!=='next-key'||auth.auth.baseUrl!==`${globalThis.testProxy.base_url}/edited/v1`) throw new Error('Proxy edits did not reach the next request');
await fs.write('/etc/sausage/config.yaml',JSON.stringify({proxy:{base_url:'',api_key:''}}));
const empty=await proxyModels(await readProxy(),readProxy);
let failure='';
try {
const result=await empty.completeSimple(empty.getModels('openai')[0],{messages:[{role:'user',content:'Missing key',timestamp:Date.now()}]});
failure=result.errorMessage??'';
} catch(error) {failure=String(error)}
if(!failure.includes('/etc/sausage/config.yaml')) throw new Error(`Missing proxy settings must name the config file: ${failure}`);
+14 -9
View File
@@ -3,15 +3,17 @@ import { createModels } from '@earendil-works/pi-ai/models';
import { openaiProvider } from '@earendil-works/pi-ai/providers/openai';
import { anthropicProvider } from '@earendil-works/pi-ai/providers/anthropic';
export async function proxyModels(proxy: {base_url: string, api_key: string}) {
export async function proxyModels(proxy: {base_url: string, api_key: string}, readProxy = async () => proxy) {
const models = createModels();
const base = proxy.base_url.replace(/\/$/, '');
let catalog: any[] = [];
try {
const response = await fetch(`${base}/v1/models`, {
headers: {Authorization: `Bearer ${proxy.api_key}`}, signal: AbortSignal.timeout(5000),
});
if (response.ok) catalog = (await response.json()).data ?? [];
if (proxy.base_url && proxy.api_key) {
const response = await fetch(`${base}/v1/models`, {
headers: {Authorization: `Bearer ${proxy.api_key}`}, signal: AbortSignal.timeout(5000),
});
if (response.ok) catalog = (await response.json()).data ?? [];
}
} catch { /* An unavailable catalog must not prevent durable recovery. */ }
for (const [provider, api, path, fallback] of [
[openaiProvider(), 'openai-responses', '/v1/responses', 'gpt-6.1-sol'],
@@ -37,10 +39,13 @@ export async function proxyModels(proxy: {base_url: string, api_key: string}) {
};
});
models.setProvider({...provider, baseUrl: entries[0].baseUrl, getModels: () => entries as any,
auth: {apiKey: {name: 'Sausage proxy', resolve: async () => ({
auth: {apiKey: proxy.api_key, headers: {'Shopify-Usage-Tag': JSON.stringify(['pi', 'sausage'])}},
source: 'llm-proxy',
})}},
auth: {apiKey: {name: 'Sausage proxy', resolve: async () => {
const current = await readProxy();
if (!current.base_url || !current.api_key) throw new Error('Set proxy.base_url and proxy.api_key in /etc/sausage/config.yaml before a model request');
return {auth:{apiKey:current.api_key,
baseUrl:current.base_url.replace(/\/$/,'') + entries[0].baseUrl.slice(base.length),
headers:{'Shopify-Usage-Tag':JSON.stringify(['pi','sausage'])}},source:'/etc/sausage/config.yaml'};
}}},
});
}
return models;
+8 -8
View File
@@ -1,5 +1,5 @@
/** Ultimate server question cases, with a real world fixture. */
/** Standing agent server question cases, with a real world fixture. */
import { BACKGROUND_CONTEXT as context } from '@earendil-works/chord/context'
import {expect,it,describe,afterEach} from './goal-questions-test-support'
import {fixture as openFixture} from './goal-questions-fixture'
@@ -36,7 +36,7 @@ const decisions = [
describe('questions on a message', () => {
it('are stored as the tool validated them, only on reply-expected messages to the user', async () => {
const f = await fixture()
const id = await f.ultimate()
const id = await f.standing()
const sent = await f.ask(id, { text: 'Decide these.', questions: decisions })
expect(sent.error).toBeUndefined()
expect(sent.message).toMatchObject({ state: 'delivered', questions: decisions })
@@ -55,7 +55,7 @@ describe('questions on a message', () => {
it('refuses questions the card could not answer, naming the question', async () => {
const f = await fixture()
const id = await f.ultimate()
const id = await f.standing()
const refused = async (questions: unknown, reason: string) => {
const { error } = await f.ask(id, { text: 'Decide.', questions })
expect(error).toContain(reason)
@@ -118,7 +118,7 @@ describe('questions on a message', () => {
it('answers in the message numbering for every kind, with a note after the answers', async () => {
const f = await fixture()
const id = await f.ultimate()
const id = await f.standing()
const { message } = await f.ask(id, { text: 'Decide these.', questions: decisions })
expect(userActions).toEqual(['answer', 'dismiss'])
await expect(
@@ -166,7 +166,7 @@ describe('questions on a message', () => {
it('sends a note on its own, and free text for a message without questions', async () => {
const f = await fixture()
const id = await f.ultimate()
const id = await f.standing()
const { message } = await f.ask(id, { text: 'Decide these.', questions: decisions })
await expect(f.answer({ messageId: message!.id, answers: [], note: ' ' })).rejects.toThrow(
'Invalid answers',
@@ -193,9 +193,9 @@ describe('questions on a message', () => {
)
})
it("keeps @ultimate's open message open until its card answers it, naming it by its questions", async () => {
it("keeps @standing's open message open until its card answers it, naming it by its questions", async () => {
const f = await fixture()
const id = await f.ultimate()
const id = await f.standing()
const { message } = await f.ask(id, { text: 'Decide these.', questions: decisions })
await f.type(
f.opened.root.id,
@@ -224,7 +224,7 @@ describe('questions on a message', () => {
it('dismisses one open question with a one-way note that starts no run, leaving the others open', async () => {
const f = await fixture()
const id = await f.ultimate()
const id = await f.standing()
const first = (await f.ask(id, { text: 'Keep it running?', questions: [decisions[2]] }))
.message!
const second = (await f.ask(id, { text: 'Which layout?' })).message!
File diff suppressed because it is too large Load Diff
+26 -14
View File
@@ -5,6 +5,7 @@ import { fauxProvider, fauxAssistantMessage, fauxToolCall } from '@earendil-work
import { Harness, createRegistry, defineExtension, defineTool, LiveDoc, InboxDoc } from '@earendil-works/pi-durable';
import { obj } from '@earendil-works/pi-schema';
import { loadExtensions, fs } from './extensions';
import { parse } from 'yaml';
import { conversationChains } from './environment';
import { openStorage } from './sqlite';
const assert = (value:any,message:string)=>{if(!value)throw new Error(message);};
@@ -17,7 +18,7 @@ const gate=Promise.withResolvers<void>(), parentGate=Promise.withResolvers<void>
const held=defineExtension({name:'hold',tools:[defineTool({name:'hold',description:'Wait for test gate',parameters:obj({}),replay:'safe',execute:async(_args,api,ctx)=>{holding.add(api.conversationId);await awaitWithContext(api.conversationId===root.id?parentGate.promise:gate.promise,ctx);return {output:[{type:'text',text:'released'}]};}})]});
if(globalThis.subagentsMode==='kill') {
const install=registry.install.bind(registry);
registry.install=(extension:any)=>install(extension.name!=='subagents'?extension:{...extension,tasks:extension.tasks.map((task:any)=>task.definition.name!=='ultimate.subagent-reporter'?task:{definition:{...task.definition,phases:{...task.definition.phases,report:async(_task:any,_runtime:any,ctx:any)=>{await fs.write('/report-ready','yes');await awaitWithContext(new Promise(()=>{}),ctx);}}}})});
registry.install=(extension:any)=>install(extension.name!=='subagents'?extension:{...extension,tasks:extension.tasks.map((task:any)=>task.definition.name!=='standing.subagent-reporter'?task:{definition:{...task.definition,phases:{...task.definition.phases,report:async(_task:any,_runtime:any,ctx:any)=>{await fs.write('/report-ready','yes');await awaitWithContext(new Promise(()=>{}),ctx);}}}})});
}
const route=(transcript:any):any=>{
faux.appendResponses([route]);
@@ -57,17 +58,18 @@ try {
assert(await service.standing('scout',context)===standing,'Standing refresh must reuse its conversation');
const standingRecord=await harness.commit((tx:any)=>tx.conversation(standing),context);
assert(!standingRecord.owner,'Standing must be ownerless');
const scoutFile=await fs.read('/src/sausage/agents/scout.md');
await fs.write('/src/sausage/agents/scout.md',scoutFile.replace('Scout instructions','Updated instructions'));
const config=parse(await fs.read('/etc/sausage/config.yaml'));
const save=()=>fs.write('/etc/sausage/config.yaml',JSON.stringify(config));
config.roster[0].prompt='Updated instructions';await save();
await service.standing('scout',context);
assert((await (await harness.conversation(standing,context))!.agent(context)).instructions==='Updated instructions','Standing refresh must reread instructions');
assert((await (await harness.conversation(standing,context))!.agent(context)).instructions.includes('Updated instructions'),'Standing refresh must reread instructions');
const answer=await call({action:'start',agent:'scout',task:'facts'});
assert(text(answer)==='Returned model: faux/faux-1\nChild: facts',`Foreground answer: ${JSON.stringify(answer)}`);
const [first]=await children();
assert(JSON.stringify(await chain(first.conversationId))===JSON.stringify([String(first.conversationId),String(root.id)]),'Anchor ownership chain');
const record=await harness.commit((tx:any)=>tx.conversation(first.conversationId),context);
const anchor=await harness.getTask(record!.owner!.taskId,context);
assert(anchor?.kind==='ultimate.subagent-anchor','Child owner must be anchor');
assert(anchor?.kind==='standing.subagent-anchor','Child owner must be anchor');
const child=(await harness.conversation(first.conversationId,context))!;
assert(!(await child.agent(context)).tools.some((tool:any)=>tool.name==='subagent'),'Tool denials');
assert((await root.agent(context)).tools.some((tool:any)=>tool.name==='subagent'),'Parent tools remain unchanged');
@@ -106,26 +108,36 @@ try {
assert(!(await harness.snapshot(LiveDoc,third.conversationId,context))?.run,'Stop must end child');
assert((await reports()).length===2,'Stop must suppress aborted report');
assert(text(await call({action:'start',agent:'scout',task:'no',isolate:'worktree'})).includes('requires the workspaces extension'),'Worktree seam');
await fs.write('/src/sausage/agents/worker.md','---\nname: worker\ndescription: Worker\nfamily: fast\ntools: []\n---\nWorker body');
await fs.mkdir('/src/sausage/.sausage');await fs.mkdir('/src/sausage/.sausage/agents');
await fs.write('/src/sausage/.sausage/agents/worker.md','---\nname: worker\naddTools: [hold]\n---\nOverlay body');
await fs.write('/src/sausage/agents/models.json',JSON.stringify({default:{model:'faux/faux-1'},fast:{model:'faux/faux-1',thinking:'low'}}));
config.roster.push({name:'worker',description:'Worker',model:'faux/faux-1',thinking:'low',tools:[],addTools:['hold'],prompt:'Worker body'});await save();
await call({action:'start',agent:'worker',task:'family one'});
const familyChild=(await children()).at(-1)!;
const firstAgent=await (await harness.conversation(familyChild.conversationId,context))!.agent(context);
assert(firstAgent.model?.modelId==='faux-1'&&firstAgent.thinkingLevel==='low','Worker must resolve fast family');
assert(firstAgent.tools.map((tool:any)=>tool.name).join(',')==='hold','Project overlay must add a tool');
assert(firstAgent.instructions.includes('Worker body')&&firstAgent.instructions.includes('Overlay body'),'Child must use overlaid instructions');
assert(firstAgent.model?.modelId==='faux-1'&&firstAgent.thinkingLevel==='low','Worker must resolve roster model and thinking');
assert(firstAgent.tools.map((tool:any)=>tool.name).join(',')==='hold','Roster addTools must add a tool');
assert(firstAgent.instructions.includes('Worker body'),'Child must use roster instructions');
const provider=models.getProvider('faux')!, template=models.getModels('faux')[0];
models.setProvider({...provider,getModels:()=>[template,{...template,id:'second'}]});
await fs.write('/src/sausage/agents/models.json',JSON.stringify({default:{model:'faux/faux-1'},fast:{model:'faux/second',thinking:'high'}}));
Object.assign(config.roster[1],{model:'faux/second',thinking:'high'});await save();
await call({action:'start',agent:'worker',task:'family two'});
const secondFamilyChild=(await children()).at(-1)!;
const secondAgent=await (await harness.conversation(secondFamilyChild.conversationId,context))!.agent(context);
assert(secondAgent.model?.modelId==='second'&&secondAgent.thinkingLevel==='high','Next child must use edited family without restart');
assert(secondAgent.model?.modelId==='second'&&secondAgent.thinkingLevel==='high','Next child must use edited roster without restart');
globalThis.childPin={provider:'faux',modelId:'faux-1'};
await call({action:'start',agent:'worker',task:'pin'});
const pinnedChild=(await children()).at(-1)!;
assert((await (await harness.conversation(pinnedChild.conversationId,context))!.agent(context)).model?.modelId==='faux-1','models.pin must apply to subagents');
globalThis.childPin=undefined;
config.standing='renamed-scout';
Object.assign(config.roster[0],{name:config.standing,model:'faux/second'});await save();
assert(await service.standing(config.standing,context)===standing,'Rename must retain the standing conversation');
faux.setResponses([(transcript:any)=>{
const prompt=JSON.stringify(transcript.messages.filter((m:any)=>m.role==='system'));
assert(prompt.includes('You are @renamed-scout.'),'Renamed standing prompt must use the configured name');
return fauxAssistantMessage('I am @renamed-scout.');
}]);
const primary=(await harness.conversation(standing,context))!;
await (await primary.submit({type:'input',content:'Who are you?'},context)).wait(context);
assert((await primary.agent(context)).model?.modelId==='second','Standing model edit applies at its next run');
assert(JSON.stringify((await primary.context(context)).entries).includes('I am @renamed-scout.'),'Same conversation answers with the new name');
}
} finally {gate.resolve();parentGate.resolve();await loader.close();await harness.close(context);}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -38,8 +38,8 @@ const create = async (title: string) => harness.commit(async (tx: any) => {
Object.assign(await tx.doc(Metadata, record.id), {title});
return record.id;
}, context);
let primary = (await harness.snapshot(Standing, root.id, context)).ultimate;
if (!primary) {primary = await create('@ultimate'); await harness.commit(async (tx: any) => {(await tx.doc(Standing, root.id)).ultimate = primary}, context)}
let primary = (await harness.snapshot(Standing, root.id, context))['$standing'];
if (!primary) {primary = await create('@standing'); await harness.commit(async (tx: any) => {(await tx.doc(Standing, root.id))['$standing'] = primary}, context)}
const submit = async (id: number, text: string, requestId?: string) => (await (await harness.conversation(id, context))!.submit({type: 'input', content: text, requestId}, context)).wait(context);
const requests = async (id: number) => (await storage.scanSubmissions({conversationId: id}, 100, undefined, context)).items;
const retries = async (id: number) => (await requests(id)).filter((r: any) => r.requestId?.startsWith('watchdog-retry:'));
@@ -70,7 +70,7 @@ try {
await until(async () => (await reports(branch)).length === 1);
await harness.waitForIdle(context);
assert((await retries(branch)).length === 1, 'Expected one retry');
assert((await reports(branch))[0].to === primary, 'The message must reach @ultimate');
assert((await reports(branch))[0].to === primary, 'The message must reach @standing');
assert((await reports(branch))[0].text.includes('After one automatic retry'), 'Report must describe its retry');
await until(async () => (await service.failures(0)).length === 1);
const history = await service.failures(0);
File diff suppressed because it is too large Load Diff
+44
View File
@@ -0,0 +1,44 @@
//! Fresh reads of the user-owned world configuration. Listener edits apply at host startup.
use sausage_vfs::{Layer, Open};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
pub const PATH: &str = "/etc/sausage/config.yaml";
#[derive(Default, Deserialize, Serialize)]
#[serde(default)]
pub struct Proxy {
pub base_url: String,
pub api_key: String,
}
#[derive(Deserialize)]
pub struct Listener {
pub address: std::net::SocketAddr,
pub origin: String,
}
impl Default for Listener {
fn default() -> Self {
Self {
address: "127.0.0.1:8090".parse().unwrap(),
origin: "http://localhost:8090".into(),
}
}
}
#[derive(Default, Deserialize)]
#[serde(default)]
pub struct Config {
pub proxy: Proxy,
pub listener: Listener,
}
impl Config {
pub async fn read(root: &Arc<dyn Layer>) -> anyhow::Result<Self> {
let file = match root.open(PATH, Open::default()).await {
Ok(file) => file,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
return Ok(Self::default());
}
Err(error) => return Err(error.into()),
};
serde_yaml_ng::from_slice(&file.read(0, 16 * 1024 * 1024).await?)
.map_err(|error| anyhow::anyhow!("Cannot read {PATH}: {error}"))
}
}
+12 -91
View File
@@ -66,97 +66,18 @@ pub(super) fn op_credential_requests(state: &mut OpState) -> Result<serde_json::
work().map_err(|_| JsErrorBox::generic("Cannot read credential requests"))
}
pub(super) fn proxy(cred: &Cred) -> anyhow::Result<Option<Value>> {
use crate::cred::{
items::{Field, Item},
types::{Caller, FieldType},
};
let caller = Caller {
pid: 0,
program: "host".into(),
conversations: vec![],
};
let item = cred
.item_list()?
.into_iter()
.find(|item| item.name == "llm-proxy");
let item = match item {
Some(item) => item,
None => {
let (Ok(base), Ok(key)) = (
std::env::var("PI_PROXY_BASE_URL"),
std::env::var("PI_PROXY_API_KEY"),
) else {
return Ok(None);
};
let mut store = cred.access()?;
let master = store.master.clone();
let tx = store
.db
.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
let mut item = crate::cred::items::insert_new(
&tx,
Item {
id: String::new(),
name: "llm-proxy".into(),
version: 0,
free_use: false,
rules: vec![],
validate_js: None,
transform_js: "export default (ctx) => ctx.request".into(),
fields: vec![
Field {
field_id: String::new(),
name: "base_url".into(),
public: true,
value: Some(base),
needs_value: false,
field_type: FieldType::Text,
key_type: None,
},
Field {
field_id: String::new(),
name: "api_key".into(),
public: false,
value: None,
needs_value: true,
field_type: FieldType::Text,
key_type: None,
},
],
},
&caller,
Some("host bootstrap"),
)?;
let field = &item
.fields
.iter()
.find(|f| f.name == "api_key")
.unwrap()
.field_id;
let sealed =
crate::cred::seal::seal(&master, &format!("{}|{field}", item.id), key.as_bytes())?;
tx.execute(
"INSERT INTO fields VALUES(?1,?2,?3)",
rusqlite::params![item.id, field, sealed],
)?;
item.fields
.iter_mut()
.find(|f| f.name == "api_key")
.unwrap()
.needs_value = false;
crate::cred::items::save(&tx, &item)?;
tx.commit()?;
item
}
};
Ok(Some(cred.credential(&item, true)?))
}
#[op2]
#[serde]
pub(super) fn op_model_proxy(state: &mut OpState) -> Result<serde_json::Value, JsErrorBox> {
proxy(state.borrow::<Arc<Cred>>())
.map(|v| v.unwrap_or(Value::Null))
.map_err(|_| JsErrorBox::generic("Cannot read llm-proxy credential"))
pub(super) async fn op_model_proxy(
state: Rc<RefCell<OpState>>,
) -> Result<serde_json::Value, JsErrorBox> {
let root = state
.borrow()
.borrow::<super::environment::Environment>()
.root
.clone();
let config = super::config::Config::read(&root)
.await
.map_err(|error| JsErrorBox::generic(error.to_string()))?;
serde_json::to_value(config.proxy).map_err(|error| JsErrorBox::generic(error.to_string()))
}
+1 -1
View File
@@ -81,7 +81,7 @@ impl ModuleLoader for Loader {
referrer: &str,
_: ResolutionKind,
) -> Result<ModuleSpecifier, JsErrorBox> {
if specifier.starts_with("@earendil-works/") {
if specifier == "yaml" || specifier.starts_with("@earendil-works/") {
return ModuleSpecifier::parse(&format!("sausage:{specifier}"))
.map_err(|e| JsErrorBox::generic(e.to_string()));
}
+3 -5
View File
@@ -1,4 +1,5 @@
//! Durable V8 harness, host-side SQLite storage, and world tool operations.
pub mod config;
mod credentials;
mod environment;
mod http;
@@ -157,7 +158,7 @@ pub async fn run_module_with_input(
receiver: input::Receiver,
source: &str,
) -> anyhow::Result<()> {
let proxy = credentials::proxy(&cred)?;
let proxy = serde_json::to_value(config::Config::read(&world.root).await?.proxy)?;
let loader = std::rc::Rc::new(loader::Loader {
root: world.root.clone(),
modules: Default::default(),
@@ -239,10 +240,7 @@ pub async fn run_module_with_input(
.await;
result.map_err(|error| {
let mut text = error.to_string();
if let Some(key) = proxy
.as_ref()
.and_then(|v| v.get("api_key"))
.and_then(Value::as_str)
if let Some(key) = proxy.get("api_key").and_then(Value::as_str)
&& !key.is_empty()
{
text = text.replace(key, "[redacted]");
+31 -12
View File
@@ -14,14 +14,14 @@ struct Cli {
#[derive(Subcommand)]
enum Command {
/// Start a world: the harness, its services and a console.
Run {
/// Create a world with default tools and /etc/sausage/config.yaml.
Init {
world: PathBuf,
#[arg(long)]
userland: Option<PathBuf>,
#[command(flatten)]
api: sausage::api::Options,
},
/// Start a world: the harness, its services and a console.
Run { world: PathBuf },
/// Run a shell in a world. With `-c`, run one command and exit with its status.
Sh {
world: PathBuf,
@@ -39,14 +39,33 @@ async fn main() -> anyhow::Result<()> {
sausage::process::configure_cache(cache)?;
}
match cli.command {
Command::Run {
world,
userland,
api,
} => {
let bundle =
userland.or_else(|| std::env::var_os("SAUSAGE_USERLAND").map(PathBuf::from));
sausage::api::run_with_userland(&world, bundle.as_deref(), api).await
Command::Init { world, userland } => {
let bundle = userland
.or_else(|| std::env::var_os("SAUSAGE_USERLAND").map(PathBuf::from))
.ok_or_else(|| {
anyhow::anyhow!(
"Default tools unavailable. Set SAUSAGE_USERLAND or pass --userland."
)
})?;
sausage::world::World::init(&world, &bundle).await?;
Ok(())
}
Command::Run { world } => {
anyhow::ensure!(
world.join("world").is_dir(),
"No world at {}. Run sausage init {} first.",
world.display(),
world.display()
);
let opened = sausage::world::World::open(&world)?;
let config = sausage::harness::config::Config::read(&opened.root).await?;
let origin = url::Url::parse(&config.listener.origin)?;
let api = sausage::api::Options {
api_listen: config.listener.address,
api_rp_id: origin.host_str().unwrap_or("localhost").into(),
api_origin: config.listener.origin,
};
sausage::api::run_with_userland(&world, None, api).await
}
Command::Sh {
world,
+70 -1
View File
@@ -3,7 +3,7 @@
//! Layout of a world directory:
//! - `world/`: the virtual root `/` that every guest sees. Agents change everything in it.
//! - `host/`: host state outside the sandbox (harness storage, later the vault). No guest sees it.
use sausage_vfs::{Device, Generated, Host, Layer, Memory, Mounts, Process};
use sausage_vfs::{Device, Generated, Host, Layer, Memory, Mounts, Open, Process};
use std::{
os::unix::fs::PermissionsExt,
path::{Path, PathBuf},
@@ -43,6 +43,75 @@ fn copy_tree(source: &Path, destination: &Path) -> std::io::Result<()> {
}
impl World {
/// Initialize a fresh world and commit its plain bundled source on main.
pub async fn init(dir: &Path, bundle: &Path) -> anyhow::Result<Self> {
anyhow::ensure!(
!dir.join("world").exists(),
"World already exists: {}",
dir.display()
);
let world = Self::open_with_userland(dir, Some(bundle))?;
let path = crate::harness::config::PATH;
let config = world.root.open(path, Open::default()).await?;
let mut text = String::from_utf8(config.read(0, 16 * 1024 * 1024).await?)?;
for (field, variable) in [
("base_url", "PI_PROXY_BASE_URL"),
("api_key", "PI_PROXY_API_KEY"),
] {
if let Ok(value) = std::env::var(variable) {
text = text.replace(
&format!("{field}: \"\""),
&format!("{field}: {}", serde_json::to_string(&value)?),
);
}
}
let file = world
.root
.open(
path,
Open {
write: true,
truncate: true,
..Default::default()
},
)
.await?;
file.write(0, text.into_bytes()).await?;
let exec = crate::process::exec(Self {
root: world.root.clone(),
host_dir: world.host_dir.clone(),
processes: world.processes.clone(),
})?;
for args in [
vec!["init", "-b", "main"],
vec!["add", "."],
vec!["commit", "--no-gpg-sign", "-m", "Initial world source"],
] {
let argv = [
vec!["/bin/git.wasm".to_owned()],
args.into_iter().map(str::to_owned).collect(),
]
.concat();
let mut output = Vec::new();
let code = exec
.exec(
crate::exec::Command::Argv(argv),
crate::exec::Options {
cwd: "/src/sausage".into(),
..Default::default()
},
&mut |_, bytes| output.extend_from_slice(bytes),
)
.await?;
anyhow::ensure!(
code == 0,
"Cannot initialize source repository: {}",
String::from_utf8_lossy(&output)
);
}
Ok(world)
}
/// Open (and create if missing) the world directory `dir`, including `/proc` and `/run` mounts.
pub fn open(dir: &Path) -> anyhow::Result<Self> {
Self::open_with_userland(dir, None)
+11 -9
View File
@@ -1,4 +1,4 @@
//! Native HTTP smoke test for host pages and the listener's CLI options.
//! Host pages remain reachable. World config replaces listener flags on the CLI.
use anyhow::Result;
use sausage::{
api::{Listener, Options},
@@ -44,14 +44,16 @@ fn listener_serves_host_pages() -> Result<()> {
.output()?;
assert!(help.status.success());
let help = std::str::from_utf8(&help.stdout)?;
for option in [
"--api-listen",
"127.0.0.1:8090",
"--api-rp-id",
"--api-origin",
"http://localhost:8090",
] {
assert!(help.contains(option));
for option in ["--api-listen", "--api-rp-id", "--api-origin"] {
assert!(!help.contains(option));
}
let missing = dir.path().join("missing-world");
let run = std::process::Command::new(env!("CARGO_BIN_EXE_sausage"))
.arg("run")
.arg(&missing)
.output()?;
assert!(!run.status.success());
assert!(std::str::from_utf8(&run.stderr)?.contains("sausage init"));
assert!(!missing.join("world").exists());
Ok(())
}
+6 -4
View File
@@ -79,10 +79,12 @@ fn setup(directory: &Path) -> Result<()> {
&Path::new(env!("CARGO_MANIFEST_DIR")).join("../extensions"),
&repo.join("extensions"),
)?;
std::fs::create_dir_all(repo.join("agents"))?;
std::fs::write(
repo.join("agents/models.json"),
r#"{"default":{"model":"faux/faux-1"}}"#,
crate::common::write_config(
directory,
"standing",
serde_json::json!([
{"name":"standing","description":"Primary","model":"faux/faux-1","prompt":"Primary agent"}
]),
)?;
ensure!(
Command::new("git")
+17 -1
View File
@@ -8,6 +8,22 @@ use std::{
};
pub static BUILD: Mutex<()> = Mutex::new(());
/// Keep the real shared prompt in every roster fixture, with an ephemeral approval port.
pub fn write_config(dir: &Path, standing: &str, roster: serde_json::Value) -> anyhow::Result<()> {
let mut config: serde_json::Value =
serde_yaml_ng::from_str(include_str!("../../../config/config.yaml"))?;
config["standing"] = serde_json::json!(standing);
config["roster"] = roster;
config["listener"]["address"] = serde_json::json!("127.0.0.1:0");
let folder = dir.join("world/etc/sausage");
std::fs::create_dir_all(&folder)?;
std::fs::write(
folder.join("config.yaml"),
serde_yaml_ng::to_string(&config)?,
)?;
Ok(())
}
pub fn copy_tree(source: &Path, destination: &Path) -> anyhow::Result<()> {
std::fs::create_dir_all(destination)?;
for entry in std::fs::read_dir(source)? {
@@ -39,7 +55,7 @@ pub fn bundle_key(repo: &Path) -> anyhow::Result<String> {
"host",
"vfs",
"userland",
"agents",
"config",
"skills",
"extensions",
"Cargo.toml",
+67 -13
View File
@@ -44,6 +44,70 @@ fn write(root: &Path, path: &str, text: &str) -> anyhow::Result<()> {
std::fs::write(path, text)?;
Ok(())
}
#[tokio::test(flavor = "current_thread")]
async fn init_seeds_config_tools_and_one_source_commit() -> anyhow::Result<()> {
let dir = tempfile::tempdir()?;
let bundle = dir.path().join("bundle");
let target = crate::common::build_git();
std::fs::create_dir_all(bundle.join("bin"))?;
std::fs::copy(
target.join("wasm32-wasip2/release/git.wasm"),
bundle.join("bin/git.wasm"),
)?;
write(&bundle, "src/sausage/README.md", "World source")?;
write(
&bundle,
"etc/sausage/config.yaml",
include_str!("../../../config/config.yaml"),
)?;
let destination = dir.path().join("new-world");
let initialized = std::process::Command::new(env!("CARGO_BIN_EXE_sausage"))
.arg("init")
.arg(&destination)
.arg("--userland")
.arg(&bundle)
.env("PI_PROXY_BASE_URL", "https://proxy.test")
.env("PI_PROXY_API_KEY", "fixture-key")
.stdin(std::process::Stdio::null())
.output()?;
assert!(
initialized.status.success(),
"{}",
String::from_utf8_lossy(&initialized.stderr)
);
let world = World::open(&destination)?;
assert!(destination.join("world/bin/git").exists());
let config = sausage::harness::config::Config::read(&world.root).await?;
assert_eq!(config.listener.origin, "http://localhost:8090");
assert_eq!(config.proxy.base_url, "https://proxy.test");
assert_eq!(config.proxy.api_key, "fixture-key");
let source = destination.join("world/src/sausage");
for (args, expected) in [
(vec!["symbolic-ref", "HEAD"], "refs/heads/main"),
(vec!["rev-list", "--count", "HEAD"], "1"),
] {
let result = std::process::Command::new("git")
.args(args)
.current_dir(&source)
.output()?;
assert!(
result.status.success(),
"{}",
String::from_utf8_lossy(&result.stderr)
);
assert_eq!(String::from_utf8(result.stdout)?.trim(), expected);
}
assert!(
World::init(&destination, &bundle)
.await
.err()
.unwrap()
.to_string()
.contains("already exists")
);
Ok(())
}
#[tokio::test(flavor = "current_thread")]
async fn core_extensions() -> anyhow::Result<()> {
let repo = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
@@ -66,14 +130,9 @@ async fn core_extensions() -> anyhow::Result<()> {
)?;
}
write(
&source,
"agents/worker.md",
"---\nname: worker\ndescription: Worker\nfamily: fast\ntools: [read]\n---\nWorker body",
)?;
write(
&source,
"agents/models.json",
r#"{"default":{"model":"faux/faux-1"},"fast":{"model":"faux/faux-1","thinking":"low"}}"#,
&disk,
"etc/sausage/config.yaml",
include_str!("../../../config/config.yaml"),
)?;
write(
&source,
@@ -93,11 +152,6 @@ export default host=>{host.implement({name:'models.pin'},{pin:async()=>globalThi
write(&disk, "project/.git/HEAD", "ref: refs/heads/main")?;
write(&disk, "project/AGENTS.md", "Root instructions")?;
write(&disk, "project/sub/AGENTS.md", "Nested instructions")?;
write(
&disk,
"project/.sausage/agents/worker.md",
"---\nname: worker\naddTools: [write]\n---\nProject body",
)?;
write(
&disk,
"project/.sausage/skills/project/SKILL.md",
+17 -27
View File
@@ -50,33 +50,23 @@ fn prepare(dir: &Path) -> anyhow::Result<()> {
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 mut roster: Vec<serde_json::Value> = ["standing", "coordinator", "worker", "reviewer"].into_iter().map(|role| serde_json::json!({
"name":role, "description":format!("P3 {role}"), "model":"faux/faux-1",
"isolate":"none", "resident":role == "standing", "prompt":format!("P3 role:{role}"),
"denyTools":if role == "worker" || role == "reviewer" {vec!["subagent"]} else {vec![]}
})).collect();
let defaults: serde_json::Value =
serde_yaml_ng::from_str(include_str!("../../../config/config.yaml"))?;
let mut pass = defaults["roster"]
.as_array()
.unwrap()
.iter()
.find(|entry| entry["name"] == "dream-pass")
.unwrap()
.clone();
pass["model"] = serde_json::json!("faux/faux-1");
roster.push(pass);
crate::common::write_config(dir, "standing", serde_json::json!(roster))?;
let demo = disk.join("src/demo");
std::fs::create_dir_all(&demo)?;
std::fs::write(demo.join("README.md"), "P3 demo\n")?;
+10 -4
View File
@@ -146,7 +146,7 @@ async fn proxy_child() -> anyhow::Result<()> {
};
let dir = std::path::Path::new(&dir);
let source = format!(
"globalThis.proxyOnly = true; const proxy = Deno.core.ops.op_model_proxy(); if (proxy.base_url !== 'https://proxy.invalid' || proxy.api_key !== 'faux-private-token') throw new Error('Wrong proxy item');\n{}",
"globalThis.proxyOnly = true; const proxy = await Deno.core.ops.op_model_proxy(); if (proxy.base_url !== 'https://proxy.invalid' || proxy.api_key !== 'faux-private-token') throw new Error('Wrong proxy config');\n{}",
include_str!("../../js/durable-test-bundle.js")
);
run_module(
@@ -158,7 +158,7 @@ async fn proxy_child() -> anyhow::Result<()> {
let error = run_module(
World::open(dir)?,
sausage::process::exec(World::open(dir)?)?,
"throw new Error(Deno.core.ops.op_model_proxy().api_key)",
"throw new Error((await Deno.core.ops.op_model_proxy()).api_key)",
)
.await
.unwrap_err();
@@ -168,8 +168,14 @@ async fn proxy_child() -> anyhow::Result<()> {
}
#[test]
fn proxy_bootstrap_once() -> anyhow::Result<()> {
fn proxy_config_ignores_environment() -> anyhow::Result<()> {
let dir = std::env::temp_dir().join(format!("sausage-durable-proxy-{}", std::process::id()));
let settings = dir.join("world/etc/sausage");
std::fs::create_dir_all(&settings)?;
std::fs::write(
settings.join("config.yaml"),
"proxy:\n base_url: https://proxy.invalid\n api_key: faux-private-token\n",
)?;
for start in 0..2 {
let status = std::process::Command::new(std::env::current_exe()?)
.args(["--exact", "durable::proxy_child", "--nocapture"])
@@ -199,7 +205,7 @@ fn proxy_bootstrap_once() -> anyhow::Result<()> {
.iter()
.filter(|i| i.name == "llm-proxy")
.count(),
1
0
);
for file in [
"host/cred.sqlite",
+31 -15
View File
@@ -26,21 +26,37 @@ fn prepare(dir: &Path) -> anyhow::Result<()> {
repo.join("extensions/probe/server.ts"),
"export default host => { globalThis.testHost = host; globalThis.subagentService = host.need({name:'subagents'}); globalThis.messageService = host.need({name:'messages'}); globalThis.lineageService = host.need({name:'lineage'}); host.implement({name:'conversations'}, {summaryModel: async () => globalThis.summaryRef, concludedIn: async () => new Set(), generatedTitle: async () => {}, setStanding: async () => {}, setAutomaticSummaries: async () => {}}); return {name:'probe'}; }",
)?;
std::fs::write(
repo.join("agents/models.json"),
r#"{"default":{"model":"faux/faux-1"}}"#,
)?;
std::fs::write(
repo.join("agents/scout.md"),
"---\nname: scout\ndescription: Search\nresident: true\ndenyTools: [subagent]\nisolate: none\n---\nScout instructions\n",
)?;
std::fs::write(
repo.join("agents/ultimate.md"),
"---\nname: ultimate\ndescription: Primary\nresident: true\nisolate: none\n---\nPrimary agent\n",
)?;
std::fs::write(
repo.join("agents/coordinator.md"),
"---\nname: coordinator\ndescription: Coordinate\nisolate: none\n---\nCoordinator agent\n",
crate::common::write_config(
dir,
"standing",
serde_json::json!([
{
"name": "scout",
"description": "Search",
"resident": true,
"denyTools": [
"subagent"
],
"isolate": "none",
"model": "faux/faux-1",
"prompt": "Scout instructions"
},
{
"name": "standing",
"description": "Primary",
"resident": true,
"isolate": "none",
"model": "faux/faux-1",
"prompt": "Primary agent"
},
{
"name": "coordinator",
"description": "Coordinate",
"isolate": "none",
"model": "faux/faux-1",
"prompt": "Coordinator agent"
}
]),
)?;
for args in [
vec!["init", "-b", "main"],
+31 -15
View File
@@ -76,21 +76,37 @@ fn prepare(dir: &Path) -> anyhow::Result<()> {
repo.join("extensions/probe/server.ts"),
"export default host => { globalThis.testHost = host; globalThis.subagentService = host.need({name:'subagents'}); globalThis.messageService = host.need({name:'messages'}); globalThis.lineageService = host.need({name:'lineage'}); host.implement({name:'conversations'}, {summaryModel: async () => globalThis.summaryRef, concludedIn: async () => new Set(), generatedTitle: async () => {}, setStanding: async () => {}, setAutomaticSummaries: async () => {}}); return {name:'probe'}; }",
)?;
std::fs::write(
repo.join("agents/models.json"),
r#"{"default":{"model":"faux/faux-1"}}"#,
)?;
std::fs::write(
repo.join("agents/scout.md"),
"---\nname: scout\ndescription: Search\nresident: true\ndenyTools: [subagent]\nisolate: none\n---\nScout instructions\n",
)?;
std::fs::write(
repo.join("agents/ultimate.md"),
"---\nname: ultimate\ndescription: Primary\nresident: true\nisolate: none\n---\nPrimary agent\n",
)?;
std::fs::write(
repo.join("agents/coordinator.md"),
"---\nname: coordinator\ndescription: Coordinate\nisolate: none\n---\nCoordinator agent\n",
crate::common::write_config(
dir,
"standing",
serde_json::json!([
{
"name": "scout",
"description": "Search",
"resident": true,
"denyTools": [
"subagent"
],
"isolate": "none",
"model": "faux/faux-1",
"prompt": "Scout instructions"
},
{
"name": "standing",
"description": "Primary",
"resident": true,
"isolate": "none",
"model": "faux/faux-1",
"prompt": "Primary agent"
},
{
"name": "coordinator",
"description": "Coordinate",
"isolate": "none",
"model": "faux/faux-1",
"prompt": "Coordinator agent"
}
]),
)?;
for args in [
vec!["init", "-b", "main"],
+43 -4
View File
@@ -114,12 +114,51 @@ fn channel_with_closed_stdin() -> anyhow::Result<()> {
#[test]
fn cli_stays_up_after_stdin_eof() -> anyhow::Result<()> {
let dir = tempfile::tempdir()?;
World::open(dir.path())?;
let disk = dir.path().join("world");
let target = crate::common::build_git();
std::fs::create_dir_all(disk.join("bin"))?;
for binary in ["git", "tar"] {
std::fs::copy(
target.join(format!("wasm32-wasip2/release/{binary}.wasm")),
disk.join(format!("bin/{binary}.wasm")),
)?;
}
let repo = disk.join("src/sausage");
let source = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../extensions");
for extension in ["core", "models", "subagents"] {
crate::common::copy_tree(
&source.join(extension),
&repo.join("extensions").join(extension),
)?;
}
crate::common::write_config(
dir.path(),
"console",
serde_json::json!([
{"name":"console","description":"Standing console","model":"openai/gpt-6.1-sol","prompt":"Keep the user conversation"}
]),
)?;
for args in [
vec!["init", "-b", "main"],
vec!["add", "."],
vec!["commit", "--no-gpg-sign", "-m", "EOF fixture"],
] {
assert!(
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")
.status()?
.success()
);
}
let mut child = Command::new(env!("CARGO_BIN_EXE_sausage"))
.arg("run")
.arg(dir.path())
.args(["--api-listen", "127.0.0.1:0"])
.env("PI_PROXY_BASE_URL", "https://proxy.invalid")
.env("PI_PROXY_API_KEY", "faux-token")
.env_remove("SAUSAGE_USERLAND")
.stdin(Stdio::null())
.spawn()?;
@@ -134,7 +173,7 @@ fn cli_stays_up_after_stdin_eof() -> anyhow::Result<()> {
r.get::<_, i64>(0)
})
.unwrap_or(0)
> 0
> 1
{
break;
}
+31 -15
View File
@@ -60,21 +60,37 @@ fn prepare(dir: &Path) -> anyhow::Result<()> {
source.join("extensions/core/agents.ts"),
repo.join("extensions/core/agents.ts"),
)?;
std::fs::write(
repo.join("agents/models.json"),
r#"{"default":{"model":"faux/faux-1"}}"#,
)?;
std::fs::write(
repo.join("agents/scout.md"),
"---\nname: scout\ndescription: Search\nresident: true\ndenyTools: [subagent]\nisolate: none\n---\nScout instructions\n",
)?;
std::fs::write(
repo.join("agents/ultimate.md"),
"---\nname: ultimate\ndescription: Primary\nresident: true\nisolate: none\n---\nPrimary agent\n",
)?;
std::fs::write(
repo.join("agents/coordinator.md"),
"---\nname: coordinator\ndescription: Coordinate\nmodel: faux/coordinator\nisolate: none\n---\nCoordinator agent\n",
crate::common::write_config(
dir,
"standing",
serde_json::json!([
{
"name": "scout",
"description": "Search",
"resident": true,
"denyTools": [
"subagent"
],
"isolate": "none",
"model": "faux/faux-1",
"prompt": "Scout instructions"
},
{
"name": "standing",
"description": "Primary",
"resident": true,
"isolate": "none",
"model": "faux/faux-1",
"prompt": "Primary agent"
},
{
"name": "coordinator",
"description": "Coordinate",
"isolate": "none",
"model": "faux/coordinator",
"prompt": "Coordinator agent"
}
]),
)?;
for args in [
vec!["init", "-b", "main"],
+8 -9
View File
@@ -10,7 +10,7 @@ fn bundle() -> anyhow::Result<PathBuf> {
let key = crate::common::bundle_key(&repo)?;
let stamp = bundle.join(".source-key");
if !bundle.join("bin/sh.wasm").exists()
|| !bundle.join("src/sausage/agents/models.json").exists()
|| !bundle.join("etc/sausage/config.yaml").exists()
|| std::fs::read_to_string(&stamp).ok().as_deref() != Some(key.as_str())
{
if stamp.exists() {
@@ -33,12 +33,11 @@ async fn bundle_copy_and_real_shell() -> anyhow::Result<()> {
let repo = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..");
let bundle = bundle()?;
let dir = tempfile::tempdir()?;
let world = World::open_with_userland(dir.path(), Some(&bundle))?;
let world = World::init(dir.path(), &bundle).await?;
assert!(world.root.stat("/bin/sh.wasm", true).await?.size > 0);
let snapshot = dir.path().join("world/src/sausage");
for path in [
"agents/models.json",
"agents/ultimate.md",
"config/config.yaml",
"skills/README.md",
"extensions/README.md",
] {
@@ -58,11 +57,11 @@ async fn bundle_copy_and_real_shell() -> anyhow::Result<()> {
Ok(String::from_utf8(output.stdout)?.trim().to_owned())
};
let message = git(&snapshot, &["log", "-1", "--format=%s"])?;
let revision = message.strip_prefix("Bundle source ").unwrap();
for path in ["agents", "skills", "extensions"] {
assert_eq!(message, "Initial world source");
for path in ["config", "skills", "extensions"] {
assert_eq!(
git(&snapshot, &["rev-parse", &format!("HEAD:{path}")])?,
git(&repo, &["rev-parse", &format!("{revision}:{path}")])?,
git(&repo, &["rev-parse", &format!("HEAD:{path}")])?,
"bundled {path} must match the source commit"
);
}
@@ -197,7 +196,7 @@ async fn bundle_copy_and_real_shell() -> anyhow::Result<()> {
);
let history = String::from_utf8(history)?;
assert_eq!(history.lines().count(), 1);
assert!(history.contains("Bundle source "));
assert!(history.contains("Initial world source"));
let mut status = Vec::new();
assert_eq!(
exec.exec(
@@ -312,7 +311,7 @@ impl sausage::exec::Exec for LoaderTiming {
async fn bundled_extensions_load() -> anyhow::Result<()> {
let bundle = bundle()?;
let dir = tempfile::tempdir()?;
let world = World::open_with_userland(dir.path(), Some(&bundle))?;
let world = World::init(dir.path(), &bundle).await?;
let root = world.root.clone();
let exec = sausage::process::exec(World {
root: world.root.clone(),
+6 -23
View File
@@ -61,29 +61,12 @@ fn prepare(dir: &Path, all_extensions: bool) -> anyhow::Result<()> {
repo.join("extensions/probe/server.ts"),
"export default host => { globalThis.p3Models=host.need({name:'models'}); globalThis.p3Subagents=host.need({name:'subagents'}); globalThis.p3Workspaces=host.need({name:'workspaces'}); globalThis.p3Host=host; 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"}}"#,
)?;
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 {
""
}
),
)?;
}
let roster: Vec<serde_json::Value> = ["standing", "coordinator", "worker", "reviewer"].into_iter().map(|role| serde_json::json!({
"name":role, "description":format!("P3 {role}"), "model":"faux/faux-1",
"isolate":"none", "resident":role == "standing", "prompt":format!("P3 role:{role}"),
"denyTools":if role == "worker" || role == "reviewer" {vec!["subagent"]} else {vec![]}
})).collect();
crate::common::write_config(dir, "standing", serde_json::json!(roster))?;
std::fs::write(
repo.join("settings.json"),
r#"{"lander":{"checks":["sh -c ': > /check-running; until test -f /check-release; do :; done; test -f feature.txt'"]}}"#,
+15 -7
View File
@@ -73,6 +73,19 @@ fn prepare(dir: &Path) -> anyhow::Result<PathBuf> {
}
World::open_with_userland(dir, Some(&bundle))?;
let repo = dir.join("world/src/sausage");
for extension in ["core", "models", "subagents"] {
crate::common::copy_tree(
&source.join("extensions").join(extension),
&repo.join("extensions").join(extension),
)?;
}
crate::common::write_config(
dir,
"console",
serde_json::json!([
{"name":"console","description":"P4 console","model":"openai/gpt-6.1-sol","prompt":"Build the requested programs","tools":["bash"]}
]),
)?;
fs::create_dir_all(repo.join("extensions/p4"))?;
fs::write(
repo.join("extensions/p4/server.ts"),
@@ -102,18 +115,13 @@ impl Drop for Running {
let _ = self.0.wait();
}
}
fn round(dir: &Path, bundle: &Path, number: usize) -> anyhow::Result<()> {
fn round(dir: &Path, number: usize) -> anyhow::Result<()> {
let stdout = dir.join(format!("round-{number}.stdout"));
let stderr = dir.join(format!("round-{number}.stderr"));
let mut child = Running(
Command::new(env!("CARGO_BIN_EXE_sausage"))
.arg("run")
.arg(dir)
.arg("--userland")
.arg(bundle)
.args(["--api-listen", "127.0.0.1:0"])
.env("PI_PROXY_BASE_URL", "https://proxy.invalid")
.env("PI_PROXY_API_KEY", "faux-token")
.env_remove("CREDENTIALS_DIRECTORY")
.stdin(Stdio::piped())
.stdout(fs::File::create(&stdout)?)
@@ -229,7 +237,7 @@ fn agent_builds_rust_and_node_twice() -> anyhow::Result<()> {
let bundle = prepare(dir.path())?;
let mut first_hits = 0;
for number in 1..=2 {
let outcome = round(dir.path(), &bundle, number);
let outcome = round(dir.path(), number);
let cleanup = boxes_removed(dir.path(), &bundle);
if outcome.is_err() {
for stream in ["stdout", "stderr"] {
+6
View File
@@ -96,6 +96,12 @@ async fn provider_transport_and_prices() -> anyhow::Result<()> {
Ok(())
});
let dir = tempfile::tempdir()?;
let config = dir.path().join("world/etc/sausage");
std::fs::create_dir_all(&config)?;
std::fs::write(
config.join("config.yaml"),
serde_json::to_vec(&json!({"proxy":proxy}))?,
)?;
let source = format!(
"globalThis.testProxy = {proxy};\n{}",
include_str!("../../js/proxy-models-test-bundle.js")
+16 -7
View File
@@ -51,13 +51,22 @@ fn prepare(dir: &Path) -> anyhow::Result<()> {
source.join("extensions/core/agents.ts"),
repo.join("extensions/core/agents.ts"),
)?;
std::fs::write(
repo.join("agents/models.json"),
r#"{"default":{"model":"faux/faux-1"}}"#,
)?;
std::fs::write(
repo.join("agents/scout.md"),
"---\nname: scout\ndescription: Search\nresident: true\ndenyTools: [subagent]\nisolate: none\n---\nScout instructions\n",
crate::common::write_config(
dir,
"scout",
serde_json::json!([
{
"name": "scout",
"description": "Search",
"resident": true,
"denyTools": [
"subagent"
],
"isolate": "none",
"model": "faux/faux-1",
"prompt": "Scout instructions"
}
]),
)?;
for args in [
vec!["init", "-b", "main"],
+31 -15
View File
@@ -62,21 +62,37 @@ fn prepare(dir: &Path) -> anyhow::Result<()> {
source.join("extensions/conversations/api.ts"),
repo.join("extensions/conversations/api.ts"),
)?;
std::fs::write(
repo.join("agents/models.json"),
r#"{"default":{"model":"faux/faux-1"}}"#,
)?;
std::fs::write(
repo.join("agents/scout.md"),
"---\nname: scout\ndescription: Search\nresident: true\ndenyTools: [subagent]\nisolate: none\n---\nScout instructions\n",
)?;
std::fs::write(
repo.join("agents/ultimate.md"),
"---\nname: ultimate\ndescription: Primary\nresident: true\nisolate: none\n---\nPrimary agent\n",
)?;
std::fs::write(
repo.join("agents/coordinator.md"),
"---\nname: coordinator\ndescription: Coordinate\nisolate: none\n---\nCoordinator agent\n",
crate::common::write_config(
dir,
"standing",
serde_json::json!([
{
"name": "scout",
"description": "Search",
"resident": true,
"denyTools": [
"subagent"
],
"isolate": "none",
"model": "faux/faux-1",
"prompt": "Scout instructions"
},
{
"name": "standing",
"description": "Primary",
"resident": true,
"isolate": "none",
"model": "faux/faux-1",
"prompt": "Primary agent"
},
{
"name": "coordinator",
"description": "Coordinate",
"isolate": "none",
"model": "faux/faux-1",
"prompt": "Coordinator agent"
}
]),
)?;
for args in [
vec!["init", "-b", "main"],
+6 -8
View File
@@ -70,14 +70,12 @@ fn prepare(dir: &Path) -> anyhow::Result<()> {
repo.join("extensions/probe/server.ts"),
"export default host => { globalThis.workspaceService=host.need({name:'workspaces'}); globalThis.workspaceHost=host; 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"}}"#,
)?;
std::fs::write(
repo.join("agents/worker.md"),
"---\nname: worker\ndescription: Implement\nisolate: worktree\n---\nImplement\n",
crate::common::write_config(
dir,
"worker",
serde_json::json!([
{"name":"worker","description":"Implement","model":"faux/faux-1","isolate":"worktree","prompt":"Implement"}
]),
)?;
std::fs::write(
repo.join("settings.json"),