Files
skills/agent-swarm/assets/wave.workflow.js
T
2026-10-27 18:50:15 +03:00

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
};