141 lines
12 KiB
TypeScript
141 lines
12 KiB
TypeScript
/** P3 exit contract: one branch, two child answers, and one landing across three crash boundaries. */
|
|
import { BACKGROUND_CONTEXT as context, awaitWithContext } from '@earendil-works/chord/context';
|
|
import { createModels } from '@earendil-works/pi-ai/models';
|
|
import { Harness, createRegistry, defineExtension, defineTool } from '@earendil-works/pi-durable';
|
|
import { obj } from '@earendil-works/pi-schema';
|
|
import { loadExtensions, fs, exec } from './extensions';
|
|
import { openStorage } from './sqlite';
|
|
import { conversationChains } from './environment';
|
|
import { scriptedModel, testReplay, type Rule } from './scripted-model';
|
|
const assert = (ok: any, message: string) => { if (!ok) throw new Error(message); };
|
|
testReplay();
|
|
if (globalThis.p3Mode !== 'unit') {
|
|
const workerId = (transcript: any) => Number(/Started subagent (\d+)/.exec(JSON.stringify(transcript.messages))?.[1]);
|
|
const rules: Rule[] = [
|
|
{ id: 'mainAgent-branch', role: 'mainAgent', match: { kind: 'user', text: 'launch scenario' }, answer: { tools: [{ name: 'branch', args: { repo: '/src/demo', isolated: true, prompt: 'begin' } }] } },
|
|
{ id: 'mainAgent-probe', role: 'mainAgent', 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: 'mainAgent-receipt', role: 'mainAgent', 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 } }] } },
|
|
{ id: 'land-worker', role: 'coordinator', match: { kind: 'user', text: 'approve' }, answer: transcript => ({ tools: [{ name: 'subagent', args: { action: 'land', id: workerId(transcript) } }] }) },
|
|
{ id: 'submit', role: 'coordinator', match: { text: 'Landed commits' }, answer: { tools: [{ name: 'workspace', args: { action: 'submit', divergence: 'None' } }] } },
|
|
{ id: 'coordinator-wait', role: 'coordinator', match: { text: '' }, answer: { text: 'Wait for child report.' } },
|
|
{ id: 'worker-write', role: 'worker', match: { kind: 'user', text: 'build feature' }, answer: { tools: [{ name: 'write-feature', args: {} }] } },
|
|
{ id: 'worker-commit', role: 'worker', match: { text: 'feature written' }, answer: { tools: [{ name: 'commit-feature', args: {} }] } },
|
|
{ id: 'worker-done', role: 'worker', match: { text: 'feature committed' }, answer: { text: 'worker complete' } },
|
|
{ id: 'reviewer-approve', role: 'reviewer', match: { kind: 'user', text: 'review feature' }, answer: { tools: [{ name: 'review-feature', args: {} }] } },
|
|
{ id: 'reviewer-done', role: 'reviewer', match: { text: 'feature verified' }, answer: { text: 'approve' } },
|
|
];
|
|
const models = createModels();
|
|
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:mainAgent', 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);
|
|
registry.install = (extension: any) => install(extension.name !== 'workspaces' ? extension : { ...extension, tasks: extension.tasks.map((task: any) => task.definition.name !== 'workspaces.lander' ? task : { definition: { ...task.definition, phases: { ...task.definition.phases, land: async (record: any, runtime: any, ctx: any) => {
|
|
const commit = runtime.commit.bind(runtime);
|
|
return task.definition.phases.land(record, { ...runtime, commit: async (...args: any[]) => {
|
|
const result = await commit(...args);
|
|
const index = await harness.snapshot(globalThis.p3Workspaces.doc, root.id, context);
|
|
if (index.queue.some((entry: any) => entry.landed)) {
|
|
await fs.write('/forward-recorded.tmp', JSON.stringify(index.queue));
|
|
await fs.rename('/forward-recorded.tmp', '/forward-recorded');
|
|
await awaitWithContext(new Promise(() => {}), ctx);
|
|
}
|
|
return result;
|
|
} }, ctx);
|
|
} } } }) });
|
|
}
|
|
const git = async (cwd: string, ...args: string[]) => {
|
|
const result = await exec(['/bin/git.wasm', ...args], { cwd, env: [['GIT_AUTHOR_NAME', 'Test'], ['GIT_AUTHOR_EMAIL', 'test@example.com'], ['GIT_COMMITTER_NAME', 'Test'], ['GIT_COMMITTER_EMAIL', 'test@example.com']] });
|
|
assert(result.code === 0, `World git ${args}: ${JSON.stringify(result)}`);
|
|
return result;
|
|
};
|
|
const tools = defineExtension({ name: 'p3-tools', tools: [
|
|
defineTool({ name: 'write-feature', description: 'Write the isolated feature', parameters: obj({}), replay: 'safe', execute: async (_args, api, ctx) => {
|
|
const cwd = (await api.agent(ctx)).cwd;
|
|
assert(cwd.startsWith('/worktrees/') && await fs.read(`${cwd}/README.md`) === 'P3 demo\n', 'Worker must start in a prepared checkout');
|
|
if (globalThis.p3Mode === 'kill-worker') {
|
|
await fs.write('/worker-running', 'yes');
|
|
await awaitWithContext(new Promise(() => {}), ctx);
|
|
}
|
|
await fs.write(`${cwd}/feature.txt`, 'P3 feature\n');
|
|
return { output: [{ type: 'text', text: 'feature written' }] };
|
|
} }),
|
|
defineTool({ name: 'commit-feature', description: 'Commit the isolated feature', parameters: obj({}), replay: 'safe', execute: async (_args, api, ctx) => {
|
|
const cwd = (await api.agent(ctx)).cwd;
|
|
await git(cwd, 'add', 'feature.txt');
|
|
if ((await git(cwd, 'status', '--porcelain')).stdout.trim()) await git(cwd, 'commit', '--no-gpg-sign', '-m', 'P3 feature');
|
|
return { output: [{ type: 'text', text: 'feature committed' }] };
|
|
} }),
|
|
defineTool({ name: 'review-feature', description: 'Verify the worker commit before landing', parameters: obj({}), replay: 'safe', execute: async (_args, _api, ctx) => {
|
|
const index = await harness.snapshot(globalThis.p3Workspaces.doc, root.id, ctx);
|
|
const worker = Object.values(index.items).find((item: any) => item.source.startsWith('/worktrees/')) as any;
|
|
assert(worker && (await git(worker.path, 'show', 'HEAD:feature.txt')).stdout === 'P3 feature\n', 'Reviewer must approve the worker commit');
|
|
return { output: [{ type: 'text', text: 'feature verified' }] };
|
|
} }),
|
|
] });
|
|
const loader = await loadExtensions(harness, registry, storage, models, context, [tools]);
|
|
assert(!loader.state().error, `Extension load: ${loader.state().error}`);
|
|
try {
|
|
if (globalThis.p3Mode === 'load-all') {
|
|
assert((await root.agent(context)).tools.some((tool: any) => tool.name === 'branch'), 'Every extension must load');
|
|
} else {
|
|
const mainAgent = (await harness.conversation(await globalThis.p3Subagents.mainAgent('mainAgent', context), context))!;
|
|
harness.resume();
|
|
const probe = globalThis.p3Mode.startsWith('branch');
|
|
if (globalThis.p3Mode === 'branch' || globalThis.p3Mode.startsWith('kill-')) {
|
|
await (await mainAgent.submit({ type: 'input', content: probe ? 'launch branch' : 'launch scenario', requestId: 'p3-launch' }, context)).wait(context);
|
|
const entries = (await mainAgent.context(context)).entries;
|
|
assert(!JSON.stringify(entries).includes('"isError":true'), `Branch tool failed: ${JSON.stringify(entries)}`);
|
|
}
|
|
if (globalThis.p3Mode.startsWith('kill-')) await new Promise(() => {});
|
|
const deadline = Date.now() + 60000;
|
|
const wait = async (predicate: () => Promise<boolean>, message: string) => {
|
|
while (!await predicate()) {
|
|
assert(Date.now() < deadline, `${message}: ${JSON.stringify(Deno.core.ops.op_sql('all', "SELECT id, kind, status, record FROM tasks WHERE status != 'terminal'", []))}`);
|
|
await new Promise(resolve => setTimeout(resolve, 0));
|
|
}
|
|
};
|
|
const replies = async () => (await mainAgent.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.includes('P3 role:coordinator'), 'Branch must use coordinator definition');
|
|
assert((await conversationChains(harness, context)(coordinator.id)).includes(String(mainAgent.id)), 'D63 must include mainAgent');
|
|
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');
|
|
}
|
|
await harness.waitForIdle(context);
|
|
await wait(async () => Deno.core.ops.op_sql('all', "SELECT id FROM tasks WHERE status NOT IN ('terminal', 'faulted')", []).length === 0, 'Background tasks did not settle');
|
|
const entries = (await coordinator.context(context)).entries;
|
|
if (probe) assert(received.length === 1 && received[0].data.text === 'branch complete' && !received[0].data.failed, 'Expected one branch answer');
|
|
else {
|
|
const reports = entries.filter((entry: any) => entry.kind === 'subagents.report');
|
|
assert(reports.length === 2 && new Set(reports.map((entry: any) => entry.data.sender)).size === 2, `Expected one report per child: ${JSON.stringify(reports)}`);
|
|
for (const answer of ['worker complete', 'approve']) assert(reports.filter((entry: any) => JSON.stringify(entry).includes(answer)).length === 1, `Duplicate child answer: ${answer}`);
|
|
const index = await harness.snapshot(globalThis.p3Workspaces.doc, root.id, context);
|
|
const worker = Object.values(index.items).find((item: any) => item.source.startsWith('/worktrees/')) as any;
|
|
assert(worker?.state === 'removed', 'Worker checkout must be released');
|
|
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 mainAgentEntries = (await mainAgent.context(context)).entries;
|
|
const landed = Object.values(index.results)[0] as any;
|
|
const cards = mainAgentEntries.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)}`);
|
|
assert(entries.filter((entry: any) => entry.kind === 'workspaces.lander-report').length === 1, 'Expected one lander report');
|
|
}
|
|
const faults = Deno.core.ops.op_sql('all', "SELECT id, record FROM tasks WHERE status = 'faulted'", []);
|
|
assert(faults.length === 0, `Faulted tasks: ${JSON.stringify(faults)}`);
|
|
}
|
|
} finally { await loader.close(); await harness.close(context); }
|
|
}
|