blob: af667e1d17feb1199ecb08f46077724c92cea3af [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { pathToFileURL } from 'node:url';
import type { ExperimentSpec } from './experiment.js';
import { summarizeFleet, type FleetPolicy, type FleetState } from './fleet.js';
import { FleetCoordinator, MemoryFleetPersistence, type FleetTransport } from './fleet-store.js';
import { FleetWorker, type FleetCellOutput } from './fleet-worker.js';
/** Replayable virtual-time fault simulation, exercising the real coordinator and worker loop. */
export async function simulateFleet(seed: number) {
if (!Number.isSafeInteger(seed) || seed < 0 || seed > 0xffffffff)
throw new Error('seed must be uint32');
let randomState = seed;
const random = () => {
randomState = (Math.imul(1664525, randomState) + 1013904223) >>> 0;
return randomState;
};
const spec: ExperimentSpec = {
schemaVersion: 'maka.eval.v1',
id: 'fleet-simulation',
benchmark: { id: 'mock-benchmark', version: '1', config: {} },
executor: { kind: 'mock', config: {} },
execution: { maxConcurrentTaskGroups: 3 },
subjects: ['a', 'b', 'c'].map((id) => ({ id, kind: 'external', credentials: [], config: {} })),
tasks: Array.from({ length: 20 }, (_, index) => ({
id: `task-${index}`,
input: 'mock',
config: {},
})),
repetitions: 2,
budget: {},
verifier: {},
};
const policy: FleetPolicy = {
runId: `seed-${seed}`,
environmentId: 'mock-manifest-v1',
groupCpus: 6,
groupMemoryMb: 1024,
leaseMs: 12,
recoveryGraceMs: 15,
retryBackoffMs: 2,
maxAttemptsPerCell: 20,
};
let now = 0;
const persistence = new MemoryFleetPersistence();
let coordinator = await FleetCoordinator.open({ persistence, spec, policy, now: () => now });
const trace: string[] = [];
let order = 0;
const events: { at: number; order: number; action: () => void }[] = [];
let crashed = false;
const lost = new Set<string>();
const committed = new Map<string, string>();
const workers = Array.from({ length: 3 }, (_, index) => {
const id = `vm-${index}/boot-1`;
const disconnected = () => (now >= 55 && now < 100) || (index === 0 && now >= 8 && now < 38);
const transport: FleetTransport = {
async command(command) {
if (disconnected()) throw new SimulatedLinkError();
const roll = random();
if (now < 240 && roll % 31 === 0) {
trace.push(`${now} ${id} ${command.kind} dropped-before`);
throw new SimulatedLinkError();
}
const reply = await coordinator.command(command);
const once = `${id}/${command.kind}`;
// Guaranteed lost claim/report acknowledgements, plus seeded background faults.
if (
now < 240 &&
((['claim', 'report'].includes(command.kind) && !lost.has(once)) || roll % 37 === 0)
) {
lost.add(once);
trace.push(`${now} ${id} ${command.kind} dropped-after`);
throw new SimulatedLinkError();
}
return reply;
},
async putArtifact(bytes) {
if (disconnected()) throw new SimulatedLinkError();
return coordinator.putArtifact(bytes);
},
};
return new FleetWorker(
{
id,
environmentId: policy.environmentId,
cpus: 6,
memoryMb: 1024,
groupSlots: 1,
},
transport,
(work, emit) =>
new Promise<void>((resolve) => {
trace.push(`${now} ${id} start ${work.assignment.id} cells=${work.cells.length}`);
let remaining = work.cells.length;
if (!remaining) {
resolve();
return;
}
for (const cell of work.cells) {
const delay = 2 + (random() % 19);
events.push({
at: now + delay,
order: order++,
action: () => {
if (index === 2 && crashed) return;
const output = simulationOutput();
// Valid outcomes with absent metering must remain selectable.
if (random() % 5 === 0) {
output.report.usage = 'missing';
output.report.result = {
...output.report.result,
usage: null,
costUsd: null,
status: 'indeterminate',
};
}
emit(cell.attemptId, output);
trace.push(`${now} ${id} completed ${cell.attemptId}`);
if (--remaining === 0) resolve();
},
});
}
}),
);
});
for (now = 0; now < 3000; now++) {
if (now === 20) {
crashed = true;
trace.push(`${now} vm-2 process-crash`);
}
if (now === 55) {
await coordinator.command({ kind: 'pause' });
trace.push(`${now} coordinator-offline`);
}
if (now === 100) {
await coordinator.close();
coordinator = await FleetCoordinator.open({ persistence, spec, policy, now: () => now });
trace.push(`${now} coordinator-restarted`);
}
events.sort((a, b) => a.at - b.at || a.order - b.order);
while (events[0]?.at <= now) events.shift()!.action();
// Drain the finite execute/catch/finally chain, without timers or real sleeps.
for (let i = 0; i < 8; i++) await Promise.resolve();
for (const [index, worker] of workers.entries()) {
if (index === 2 && crashed) continue;
try {
await worker.poll();
} catch (error) {
if (!(error instanceof SimulatedLinkError)) throw error;
}
}
await coordinator.command({ kind: 'tick' });
const state = await coordinator.snapshot();
try {
assertFleetInvariants(state);
for (const [id, evidence] of committed) {
const attempt = state.attempts.find((a) => a.id === id);
if (!attempt || JSON.stringify(attempt) !== evidence)
throw new Error('committed result changed or disappeared');
}
for (const attempt of state.attempts) {
if (attempt.disposition === 'selected') committed.set(attempt.id, JSON.stringify(attempt));
}
} catch (error) {
throw new Error(
`simulation invariant failed: seed=${seed} tick=${now}\n${trace.slice(-30).join('\n')}`,
{ cause: error },
);
}
if (summarizeFleet(state).settled)
return { seed, elapsedTicks: now, trace, state, summary: summarizeFleet(state) };
}
throw new Error(`simulation did not settle: seed=${seed}\n${trace.slice(-30).join('\n')}`);
}
/** Cross-cutting safety oracle, checked after every simulated tick. */
export function assertFleetInvariants(state: FleetState) {
const fail = (message: string): never => {
throw new Error(`fleet invariant: ${message}`);
};
const active = state.assignments.filter((a) => a.status === 'active');
if (active.length > state.spec.execution.maxConcurrentTaskGroups) fail('global capacity');
if (new Set(active.map((a) => a.groupId)).size !== active.length) fail('group ownership');
for (const worker of state.workers) {
if (
active.filter((a) => a.workerId === worker.description.id).length >
worker.description.groupSlots
)
fail('worker capacity');
}
for (const cellId of state.groups.flatMap((g) => g.cellIds)) {
const attempts = state.attempts.filter((a) => a.cellId === cellId);
if (attempts.filter((a) => a.disposition === 'selected').length > 1) fail('duplicate result');
if (attempts.length > state.policy.maxAttemptsPerCell) fail('retry budget');
if (attempts.some((a, i) => a.sequence !== i + 1)) fail('attempt ordering');
}
if (new Set(state.attempts.map((a) => a.id)).size !== state.attempts.length)
fail('attempt identity');
for (const assignment of state.assignments) {
const group = state.groups.find((g) => g.id === assignment.groupId);
if (
!group ||
assignment.attemptIds.some((id) => {
const attempt = state.attempts.find((a) => a.id === id);
return (
!attempt ||
attempt.assignmentId !== assignment.id ||
!group.cellIds.includes(attempt.cellId)
);
})
)
fail('assignment membership');
}
for (const attempt of state.attempts.filter((a) => a.disposition === 'selected')) {
if (
!attempt.report ||
attempt.report.verification !== 'valid' ||
attempt.report.cleanup !== 'confirmed' ||
attempt.report.environmentFailure !== null ||
!['completed', 'subject_failed'].includes(attempt.report.execution)
)
fail('selected evidence');
}
}
export function simulationOutput(): FleetCellOutput {
return {
report: {
result: {
status: 'completed',
score: 1,
usage: {
inputTokens: 1,
outputTokens: 1,
cacheReadTokens: 0,
cacheWriteTokens: 0,
reasoningTokens: 0,
totalTokens: 2,
},
costUsd: 0.01,
durationMs: 1,
failureReason: null,
artifacts: [],
},
execution: 'completed',
verification: 'valid',
usage: 'complete',
cleanup: 'confirmed',
environmentFailure: null,
},
artifacts: [new TextEncoder().encode('mock subject artifact')],
};
}
class SimulatedLinkError extends Error {}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const seed = Number(process.argv[2] ?? 1);
const result = await simulateFleet(seed);
process.stdout.write(
`${JSON.stringify(
{ seed, elapsedTicks: result.elapsedTicks, ...result.summary, trace: result.trace },
null,
2,
)}\n`,
);
}