Merge v3 messages compatibility fix into v3-t51

This commit is contained in:
2026-11-07 14:43:07 +00:00
7 changed files with 32117 additions and 8 deletions
File diff suppressed because it is too large Load Diff
+93
View File
@@ -0,0 +1,93 @@
/** P3 exit scaffold: real world git, scripted coordination, and worker SIGKILL recovery. */
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 { 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 rules: Rule[] = [
{ id: 'start-worker', role: 'coordinator', match: { kind: 'user', text: 'begin' }, answer: { tools: [{ name: 'subagent', args: { action: 'start', agent: 'worker', isolate: 'none', 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: 'coordinator-done', role: 'coordinator', match: { kind: 'user', text: 'approve' }, answer: { text: 'scenario complete' } },
{ 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:ultimate', cwd: '/src/demo' } });
const git = async (...args: string[]) => {
const result = await exec(['/bin/git.wasm', ...args], { cwd: '/src/demo', 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 demo feature', parameters: obj({}), replay: 'safe', execute: async (_args, _api, ctx) => {
if (globalThis.p3Mode === 'kill-worker') {
await fs.write('/worker-running', 'yes');
await awaitWithContext(new Promise(() => {}), ctx);
}
await fs.write('/src/demo/feature.txt', 'P3 feature\n');
return { output: [{ type: 'text', text: 'feature written' }] };
} }),
defineTool({ name: 'commit-feature', description: 'Commit the demo feature', parameters: obj({}), replay: 'safe', execute: async () => {
await git('add', 'feature.txt');
// A replay after a committed tool side effect must not create a second commit.
if ((await git('status', '--porcelain')).stdout.trim()) await git('commit', '--no-gpg-sign', '-m', 'P3 feature');
return { output: [{ type: 'text', text: 'feature committed' }] };
} }),
defineTool({ name: 'review-feature', description: 'Verify the committed demo feature', parameters: obj({}), replay: 'safe', execute: async () => {
assert(await fs.read('/src/demo/feature.txt') === 'P3 feature\n', 'Reviewer lost feature');
const result = await git('show', 'HEAD:feature.txt');
assert(result.stdout === 'P3 feature\n', 'Reviewer did not read the committed feature');
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, including messages');
} else {
if (globalThis.p3Mode === 'kill-worker') {
const coordinator = await globalThis.p3Models.start({ agent: 'coordinator', cwd: '/src/demo' }, context);
await fs.write('/coordinator', String(coordinator.id));
await coordinator.submit({ type: 'input', content: 'begin', requestId: 'p3-begin' }, context);
}
harness.resume();
if (globalThis.p3Mode === 'kill-worker') await new Promise(() => {});
const coordinator = (await harness.conversation(Number(await fs.read('/coordinator')), context))!;
// waitForIdle does not wait for background children. Poll the durable outcome first.
const deadline = Date.now() + 30000;
while (!JSON.stringify((await coordinator.context(context)).entries).includes('scenario complete')) {
if (Date.now() > deadline) throw new Error(`P3 did not finish: ${JSON.stringify((await coordinator.context(context)).entries)}`);
await new Promise(resolve => setTimeout(resolve, 0));
}
await harness.waitForIdle(context);
const entries = (await coordinator.context(context)).entries;
const reports = entries.filter((entry: any) => entry.kind === 'subagents.report');
assert(reports.length === 2, `Expected two child reports: ${JSON.stringify(entries)}`);
assert(new Set(reports.map((report: any) => report.data.sender)).size === 2, 'Expected one report from each child');
for (const answer of ['worker complete', 'approve']) assert(reports.filter((report: any) => JSON.stringify(report).includes(answer)).length === 1, `Expected exactly one report for ${answer}`);
assert(JSON.stringify(entries).includes('scenario complete'), 'Coordinator did not finish');
const faults = Deno.core.ops.op_sql('all', "SELECT id FROM tasks WHERE status = 'faulted'", []);
assert(faults.length === 0, `Faulted tasks: ${JSON.stringify(faults)}`);
await fs.write('/p3-idle', 'yes');
}
} finally { await loader.close(); await harness.close(context); }
}
// PENDING T49: @ultimate calls branch to create the coordinator, then receives one board.reply.
// PENDING T52: worker isolate:worktree, subagent land, workspace submit, and worktree release.
// Kill point 2 (ignored until T52): SIGKILL while the lander check waits for /check-release.
// Restart, release the marker, and run to idle.
// Kill point 3 (ignored until T52): SIGKILL after the target fast-forward log entry.
// Restart and run to idle. Assert one target commit, workspaces.landed event, and landing card.
+1 -1
View File
@@ -1,7 +1,7 @@
{
"private": true,
"type": "module",
"scripts": { "bundle": "nu apply-patch.nu && esbuild main.ts --bundle --format=esm --platform=browser --outfile=bundle.js && esbuild test.ts --bundle --format=esm --platform=browser --outfile=test-bundle.js && esbuild durable-test.ts --bundle --format=esm --platform=browser --outfile=durable-test-bundle.js && esbuild foundation-test.ts --bundle --format=esm --platform=browser --outfile=foundation-test-bundle.js && esbuild extensions-test.ts --bundle --format=esm --platform=browser --outfile=extensions-test-bundle.js && esbuild events-test.ts --bundle --format=esm --platform=browser --outfile=events-test-bundle.js && esbuild subagents-test.ts --bundle --format=esm --platform=browser --outfile=subagents-test-bundle.js && esbuild core-ext-test.ts --bundle --format=esm --platform=browser --outfile=core-ext-test-bundle.js && esbuild plan-todos-test.ts --bundle --format=esm --platform=browser --outfile=plan-todos-test-bundle.js && esbuild p2-test.ts --bundle --format=esm --platform=browser --outfile=p2-test-bundle.js && esbuild chain-test.ts --bundle --format=esm --platform=browser --outfile=chain-test-bundle.js && esbuild undo-harness-test.ts --bundle --format=esm --platform=browser --outfile=undo-harness-test-bundle.js && esbuild messages-test.ts --bundle --format=esm --platform=browser --outfile=messages-test-bundle.js && esbuild automations-test.ts --bundle --format=esm --platform=browser --outfile=automations-test-bundle.js" },
"scripts": { "bundle": "nu apply-patch.nu && esbuild main.ts --bundle --format=esm --platform=browser --outfile=bundle.js && esbuild test.ts --bundle --format=esm --platform=browser --outfile=test-bundle.js && esbuild durable-test.ts --bundle --format=esm --platform=browser --outfile=durable-test-bundle.js && esbuild foundation-test.ts --bundle --format=esm --platform=browser --outfile=foundation-test-bundle.js && esbuild extensions-test.ts --bundle --format=esm --platform=browser --outfile=extensions-test-bundle.js && esbuild events-test.ts --bundle --format=esm --platform=browser --outfile=events-test-bundle.js && esbuild subagents-test.ts --bundle --format=esm --platform=browser --outfile=subagents-test-bundle.js && esbuild core-ext-test.ts --bundle --format=esm --platform=browser --outfile=core-ext-test-bundle.js && esbuild plan-todos-test.ts --bundle --format=esm --platform=browser --outfile=plan-todos-test-bundle.js && esbuild p2-test.ts --bundle --format=esm --platform=browser --outfile=p2-test-bundle.js && esbuild chain-test.ts --bundle --format=esm --platform=browser --outfile=chain-test-bundle.js && esbuild undo-harness-test.ts --bundle --format=esm --platform=browser --outfile=undo-harness-test-bundle.js && esbuild messages-test.ts --bundle --format=esm --platform=browser --outfile=messages-test-bundle.js && esbuild p3-test.ts --bundle --format=esm --platform=browser --outfile=p3-test-bundle.js && esbuild automations-test.ts --bundle --format=esm --platform=browser --outfile=automations-test-bundle.js" },
"dependencies": {
"@earendil-works/pi-durable": "1.5.0",
"@earendil-works/pi-ai": "1.5.0",
+38
View File
@@ -0,0 +1,38 @@
/** Stateless role/input rules for deterministic faux-model replay in integration tests. */
import { fauxProvider, fauxAssistantMessage, fauxToolCall } from '@earendil-works/pi-ai/providers/faux';
export type Answer = { text?: string; tools?: { name: string; args: any }[] };
export type Rule = { id: string; role: string; match: { kind?: string; text: string }; answer: Answer };
export const textOf = (content: any): string => typeof content === 'string' ? content : (content ?? []).filter((part: any) => part.type === 'text').map((part: any) => part.text).join('\n');
export function answerFor(rules: Rule[], transcript: any) {
const system = JSON.stringify(transcript.messages.filter((message: any) => message.role === 'system'));
const role = /P3 role:([a-z]+)/.exec(system)?.[1];
const last = transcript.messages.findLast((message: any) => message.role !== 'system');
const rule = rules.find(rule => rule.role === role && (!rule.match.kind || rule.match.kind === last?.role) && textOf(last?.content).includes(rule.match.text));
if (!rule) throw new Error(`No scripted rule for ${role}: ${JSON.stringify(last)}`);
return { rule: rule.id, answer: rule.answer };
}
export function scriptedModel(rules: Rule[], report: (rule: string) => void) {
const faux = fauxProvider({ tokensPerSecond: 100000 });
const respond = (transcript: any): any => {
faux.appendResponses([respond]);
const selected = answerFor(rules, transcript);
report(selected.rule);
const calls = selected.answer.tools ?? [];
return fauxAssistantMessage([
...(selected.answer.text ? [{ type: 'text' as const, text: selected.answer.text }] : []),
...calls.map((call, index) => fauxToolCall(call.name, call.args, { id: `${selected.rule}-${index}` })),
], { stopReason: calls.length ? 'toolUse' : 'stop', timestamp: 0 });
};
faux.setResponses([respond]);
return faux.provider;
}
/** Unit test: history length and a provider restart must not change rule selection or the answer. */
export function testReplay() {
const rules: Rule[] = [{ id: 'worker-write', role: 'worker', match: { kind: 'user', text: 'build' }, answer: { tools: [{ name: 'write', args: { path: 'feature.txt' } }] } }];
const system = { role: 'system', content: 'P3 role:worker' }, input = { role: 'user', content: 'build' };
const first = answerFor(rules, { messages: [system, input] });
const replay = answerFor(JSON.parse(JSON.stringify(rules)), { messages: [system, { role: 'assistant', content: [] }, input] });
if (JSON.stringify(first) !== JSON.stringify(replay)) throw new Error('Scripted replay changed the answer');
const toolRules: Rule[] = [{ id: 'worker-done', role: 'worker', match: { text: 'committed' }, answer: { text: 'done' } }];
if (answerFor(toolRules, { messages: [system, { role: 'toolResult', content: [{ type: 'text', text: 'committed' }] }] }).rule !== 'worker-done') throw new Error('Tool result rule did not match');
}