101 lines
4.3 KiB
JavaScript
101 lines
4.3 KiB
JavaScript
// agent-swarm batched worker runner — copy into the `script` field of the workflow tool.
|
|
// args: {
|
|
// wave: number,
|
|
// tasks: [{ id: string, write: string[] }],
|
|
// schema: object, // paste assets/report.schema.json
|
|
// concurrency?: number // <= free active-child slots; default 6
|
|
// }
|
|
//
|
|
// The runner starts tasks in chunks instead of one giant fan-out. This matters
|
|
// because `parallel` is not a queue and the runtime active-child cap is
|
|
// host-specific; a single spawn-limit throw inside an unbounded fan-out can
|
|
// abort the whole wave. Each `agent()` call is guarded so a scheduling failure
|
|
// becomes a structured result for that task, and completed chunks are kept.
|
|
|
|
const REPORT = args.schema;
|
|
if (!REPORT) throw new Error("args.schema is required (paste assets/report.schema.json)");
|
|
|
|
const CONCURRENCY = Math.max(
|
|
1,
|
|
Math.min(Number(args.concurrency) || 6, 8)
|
|
);
|
|
|
|
const promptFor = (t, attempt) => [
|
|
`You are agent ${t.id} in a coordinated swarm.`,
|
|
`Read .swarm/brief.md, .swarm/tasks/${t.id}.md, and .swarm/contracts.md first.`,
|
|
`Write only within: ${(t.write || []).join(", ") || "(read-only task)"}, plus your own report, review, and scratch under .swarm/reports/, .swarm/reviews/, and .swarm/tmp/${t.id}/.`,
|
|
`Do not edit other tasks' files or reports; do not run installs, repo-wide format/lint/codegen, migrations, dev servers, or git writes.`,
|
|
`Run the acceptance checks and record exact commands and results.`,
|
|
`Write .swarm/reports/${t.id}.json and return the same object.`,
|
|
`If a load-bearing unknown or a needed file outside scope blocks you, return status "blocked" with blocked_question; do not guess.`,
|
|
`If you hit a compile/test failure caused by a sibling task's in-flight file, wait briefly and retry; report status "partial" with the exact external error rather than editing outside your scope.`,
|
|
attempt > 1
|
|
? `A previous attempt produced no usable report. Re-read the task and fix the cause or report the exact blocker.`
|
|
: ""
|
|
].filter(Boolean).join("\n");
|
|
|
|
const failed = (t, why, scheduling = false) => ({
|
|
task_id: t.id,
|
|
status: "failed",
|
|
summary: why,
|
|
files_changed: [],
|
|
evidence: scheduling ? [`scheduling failure: ${why}`] : [],
|
|
acceptance: [],
|
|
issues: [why],
|
|
assumptions: [],
|
|
spawn_error: scheduling
|
|
});
|
|
|
|
async function runOne(t) {
|
|
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
try {
|
|
const r = await agent(promptFor(t, attempt), {
|
|
label: t.id,
|
|
phase: `wave ${args.wave}`,
|
|
schema: REPORT
|
|
});
|
|
if (r && r.status !== "failed") return r;
|
|
log(`${t.id}: attempt ${attempt} produced no usable report`);
|
|
} catch (error) {
|
|
const message = String(error && error.message ? error.message : error);
|
|
// A spawn/limit error is a scheduling problem: keep the batch, report it.
|
|
// Any other throw (bad hook option, unsupported schema) should still fail
|
|
// loudly so the script is fixed instead of silently swallowing a mistake.
|
|
if (/(limit|spawn|could not start|active child|capacity|start a child)/i.test(message)) {
|
|
log(`${t.id}: scheduling error: ${message}`);
|
|
return failed(t, `scheduling error: ${message}`, true);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
return failed(t, "no usable report after 2 attempts");
|
|
}
|
|
|
|
phase(`wave ${args.wave}`);
|
|
log(`Dispatching ${args.tasks.length} task(s) in batches of ${CONCURRENCY}`);
|
|
|
|
const results = [];
|
|
for (let start = 0; start < args.tasks.length; start += CONCURRENCY) {
|
|
const batch = args.tasks.slice(start, start + CONCURRENCY);
|
|
log(`Batch ${Math.floor(start / CONCURRENCY) + 1}: ${batch.map((t) => t.id).join(", ")}`);
|
|
const batchResults = await parallel(batch.map((t) => () => runOne(t)));
|
|
results.push(...batchResults);
|
|
}
|
|
|
|
const spawnErrors = results.filter((r) => r && r.spawn_error).length;
|
|
if (spawnErrors > 0) {
|
|
log(`${spawnErrors} task(s) hit a spawn/scheduling error. Dispatch only the unstarted tasks with a smaller concurrency.`);
|
|
}
|
|
|
|
return {
|
|
wave: args.wave,
|
|
concurrency: CONCURRENCY,
|
|
total: results.length,
|
|
done: results.filter((r) => r && r.status === "done").length,
|
|
partial: results.filter((r) => r && r.status === "partial").length,
|
|
blocked: results.filter((r) => r && r.status === "blocked").length,
|
|
failed: results.filter((r) => !r || r.status === "failed").length,
|
|
spawn_errors: spawnErrors,
|
|
results
|
|
};
|