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