@gaberrb/polypus 0.4.11 → 0.4.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +136 -50
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -205,6 +205,7 @@ var en = {
|
|
|
205
205
|
"swarm.needsAgents": "Swarm mode needs at least {min} configured agents (you have {have}). Add more with `polypus add-agent`, or use `polypus run` for a single agent.",
|
|
206
206
|
"swarm.status": "swarm agents=[{agents}] workspace={workspace}",
|
|
207
207
|
"swarm.bypassNote": "Workers run in bypass mode inside isolated git worktrees; branches are merged at the end.",
|
|
208
|
+
"swarm.cancelling": "cancelling swarm \u2014 finishing in-flight workers, then merging what committed\u2026",
|
|
208
209
|
"swarm.decomposed": "Decomposed into {n} subtask(s):",
|
|
209
210
|
"swarm.workerStart": "\u25B6 {id} started by {agent}",
|
|
210
211
|
"swarm.workerDone": "\u2713 {id} done",
|
|
@@ -464,6 +465,7 @@ var ptBR = {
|
|
|
464
465
|
"swarm.needsAgents": "O modo swarm precisa de pelo menos {min} agentes configurados (voc\xEA tem {have}). Adicione mais com `polypus add-agent`, ou use `polypus run` para um agente s\xF3.",
|
|
465
466
|
"swarm.status": "swarm agentes=[{agents}] workspace={workspace}",
|
|
466
467
|
"swarm.bypassNote": "Os workers rodam em modo bypass dentro de git worktrees isoladas; os branches s\xE3o mesclados no final.",
|
|
468
|
+
"swarm.cancelling": "cancelando o swarm \u2014 encerrando os workers em andamento e mesclando o que commitou\u2026",
|
|
467
469
|
"swarm.decomposed": "Dividido em {n} subtarefa(s):",
|
|
468
470
|
"swarm.workerStart": "\u25B6 {id} iniciada por {agent}",
|
|
469
471
|
"swarm.workerDone": "\u2713 {id} conclu\xEDda",
|
|
@@ -3632,7 +3634,7 @@ async function removeWorktree(git, wt) {
|
|
|
3632
3634
|
}
|
|
3633
3635
|
|
|
3634
3636
|
// src/core/agent/worker.ts
|
|
3635
|
-
async function runWorker(subtask, agent, wt, allow, deny, events) {
|
|
3637
|
+
async function runWorker(subtask, agent, wt, allow, deny, events, signal) {
|
|
3636
3638
|
const permissions = new PermissionEngine({
|
|
3637
3639
|
mode: "bypass",
|
|
3638
3640
|
policy: { workspace: wt.path, allow, deny },
|
|
@@ -3644,9 +3646,10 @@ async function runWorker(subtask, agent, wt, allow, deny, events) {
|
|
|
3644
3646
|
agent,
|
|
3645
3647
|
permissions,
|
|
3646
3648
|
promptContext: { workspace: wt.path, mode: "bypass", allow, briefing: subtask.brief },
|
|
3647
|
-
events
|
|
3649
|
+
events,
|
|
3650
|
+
signal
|
|
3648
3651
|
});
|
|
3649
|
-
const committed = await commitWorktree(wt, `polypus(${subtask.id}): ${subtask.title}`);
|
|
3652
|
+
const committed = result.reason === "cancelled" ? false : await commitWorktree(wt, `polypus(${subtask.id}): ${subtask.title}`);
|
|
3650
3653
|
return {
|
|
3651
3654
|
subtask,
|
|
3652
3655
|
agentName: agent.config.name,
|
|
@@ -3663,30 +3666,23 @@ async function runSwarm(opts) {
|
|
|
3663
3666
|
const lead = opts.agents[0];
|
|
3664
3667
|
if (!lead) throw new Error("Swarm requires at least one agent.");
|
|
3665
3668
|
const maxSubtasks = opts.maxSubtasks ?? Math.max(opts.agents.length, 2);
|
|
3669
|
+
const concurrency = Math.max(1, opts.concurrency ?? opts.agents.length);
|
|
3670
|
+
const idleTimeoutMs2 = opts.idleTimeoutMs ?? 0;
|
|
3666
3671
|
const git = await ensureRepo(opts.workspace);
|
|
3667
|
-
const subtasks = await decompose(lead, opts.task, maxSubtasks);
|
|
3672
|
+
const subtasks = await decompose(lead, opts.task, maxSubtasks, opts.signal);
|
|
3668
3673
|
opts.events?.onDecomposed?.(subtasks);
|
|
3669
3674
|
const worktrees = [];
|
|
3670
3675
|
for (const subtask of subtasks) {
|
|
3671
3676
|
worktrees.push(await createWorktree(git, subtask.id));
|
|
3672
3677
|
}
|
|
3673
|
-
const
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
wt,
|
|
3682
|
-
opts.allow,
|
|
3683
|
-
opts.deny,
|
|
3684
|
-
opts.events?.workerEvents?.(subtask)
|
|
3685
|
-
);
|
|
3686
|
-
opts.events?.onWorkerDone?.(outcome);
|
|
3687
|
-
return outcome;
|
|
3688
|
-
})
|
|
3689
|
-
);
|
|
3678
|
+
const runOne = (subtask, i) => guardedWorker(subtask, opts.agents[i % opts.agents.length], worktrees[i], {
|
|
3679
|
+
allow: opts.allow,
|
|
3680
|
+
deny: opts.deny,
|
|
3681
|
+
idleTimeoutMs: idleTimeoutMs2,
|
|
3682
|
+
signal: opts.signal,
|
|
3683
|
+
events: opts.events
|
|
3684
|
+
});
|
|
3685
|
+
const outcomes = await runPool(subtasks, concurrency, runOne);
|
|
3690
3686
|
const merges = [];
|
|
3691
3687
|
for (const outcome of outcomes) {
|
|
3692
3688
|
if (!outcome.committed) continue;
|
|
@@ -3704,13 +3700,69 @@ async function runSwarm(opts) {
|
|
|
3704
3700
|
}
|
|
3705
3701
|
return { subtasks, outcomes, merges };
|
|
3706
3702
|
}
|
|
3703
|
+
async function guardedWorker(subtask, agent, wt, opts) {
|
|
3704
|
+
const ac = new AbortController();
|
|
3705
|
+
const onAbort = () => ac.abort();
|
|
3706
|
+
if (opts.signal) {
|
|
3707
|
+
if (opts.signal.aborted) ac.abort();
|
|
3708
|
+
else opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
3709
|
+
}
|
|
3710
|
+
let idleTimer;
|
|
3711
|
+
const resetIdle = () => {
|
|
3712
|
+
if (opts.idleTimeoutMs <= 0) return;
|
|
3713
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
3714
|
+
idleTimer = setTimeout(() => ac.abort(), opts.idleTimeoutMs);
|
|
3715
|
+
idleTimer.unref?.();
|
|
3716
|
+
};
|
|
3717
|
+
const base = opts.events?.workerEvents?.(subtask);
|
|
3718
|
+
const events = {
|
|
3719
|
+
...base,
|
|
3720
|
+
onStep: (n) => {
|
|
3721
|
+
resetIdle();
|
|
3722
|
+
base?.onStep?.(n);
|
|
3723
|
+
}
|
|
3724
|
+
};
|
|
3725
|
+
opts.events?.onWorkerStart?.(subtask, agent.config.name);
|
|
3726
|
+
resetIdle();
|
|
3727
|
+
let outcome;
|
|
3728
|
+
try {
|
|
3729
|
+
outcome = await runWorker(subtask, agent, wt, opts.allow, opts.deny, events, ac.signal);
|
|
3730
|
+
} catch {
|
|
3731
|
+
outcome = {
|
|
3732
|
+
subtask,
|
|
3733
|
+
agentName: agent.config.name,
|
|
3734
|
+
branch: wt.branch,
|
|
3735
|
+
finished: false,
|
|
3736
|
+
committed: false,
|
|
3737
|
+
steps: 0
|
|
3738
|
+
};
|
|
3739
|
+
} finally {
|
|
3740
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
3741
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
3742
|
+
}
|
|
3743
|
+
opts.events?.onWorkerDone?.(outcome);
|
|
3744
|
+
return outcome;
|
|
3745
|
+
}
|
|
3746
|
+
async function runPool(items, limit, fn) {
|
|
3747
|
+
const results = new Array(items.length);
|
|
3748
|
+
let next = 0;
|
|
3749
|
+
const worker = async () => {
|
|
3750
|
+
for (; ; ) {
|
|
3751
|
+
const i = next++;
|
|
3752
|
+
if (i >= items.length) return;
|
|
3753
|
+
results[i] = await fn(items[i], i);
|
|
3754
|
+
}
|
|
3755
|
+
};
|
|
3756
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
3757
|
+
return results;
|
|
3758
|
+
}
|
|
3707
3759
|
var DECOMPOSE_SYSTEM = [
|
|
3708
3760
|
"You are a tech lead splitting a coding task into independent subtasks that can be done in parallel.",
|
|
3709
3761
|
'Return ONLY a JSON array. Each item: {"title": string, "brief": string}.',
|
|
3710
3762
|
"Make subtasks touch DIFFERENT files/areas to minimize merge conflicts.",
|
|
3711
3763
|
"Keep the list small (prefer 2-4 items). Each brief must be self-contained and actionable."
|
|
3712
3764
|
].join("\n");
|
|
3713
|
-
async function decompose(lead, task, maxSubtasks) {
|
|
3765
|
+
async function decompose(lead, task, maxSubtasks, signal) {
|
|
3714
3766
|
try {
|
|
3715
3767
|
const res = await lead.provider.chat({
|
|
3716
3768
|
messages: [
|
|
@@ -3720,7 +3772,8 @@ ${task}
|
|
|
3720
3772
|
|
|
3721
3773
|
Return at most ${maxSubtasks} subtasks as a JSON array.` }
|
|
3722
3774
|
],
|
|
3723
|
-
params: { temperature: 0 }
|
|
3775
|
+
params: { temperature: 0 },
|
|
3776
|
+
signal
|
|
3724
3777
|
});
|
|
3725
3778
|
const parsed = extractJsonArray(res.content);
|
|
3726
3779
|
if (parsed && parsed.length > 0) {
|
|
@@ -3746,6 +3799,28 @@ function extractJsonArray(text2) {
|
|
|
3746
3799
|
}
|
|
3747
3800
|
}
|
|
3748
3801
|
|
|
3802
|
+
// src/core/agent/concurrency.ts
|
|
3803
|
+
var OLLAMA_ENDPOINT_CONCURRENCY = 2;
|
|
3804
|
+
var DEFAULT_IDLE_TIMEOUT_MS = 3e5;
|
|
3805
|
+
function endpointKey(agent) {
|
|
3806
|
+
return `${agent.config.provider}:${agent.config.baseUrl ?? "default"}`;
|
|
3807
|
+
}
|
|
3808
|
+
function recommendConcurrency(agents) {
|
|
3809
|
+
const perEndpoint = /* @__PURE__ */ new Map();
|
|
3810
|
+
for (const a of agents) {
|
|
3811
|
+
perEndpoint.set(endpointKey(a), (perEndpoint.get(endpointKey(a)) ?? 0) + 1);
|
|
3812
|
+
}
|
|
3813
|
+
let total = 0;
|
|
3814
|
+
for (const [key, count] of perEndpoint) {
|
|
3815
|
+
total += key.startsWith("ollama:") ? Math.min(count, OLLAMA_ENDPOINT_CONCURRENCY) : count;
|
|
3816
|
+
}
|
|
3817
|
+
return Math.max(1, total);
|
|
3818
|
+
}
|
|
3819
|
+
function idleTimeoutMs() {
|
|
3820
|
+
const raw = Number(process.env.POLYPUS_SWARM_IDLE_TIMEOUT_MS);
|
|
3821
|
+
return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_IDLE_TIMEOUT_MS;
|
|
3822
|
+
}
|
|
3823
|
+
|
|
3749
3824
|
// src/ui/swarm-view.ts
|
|
3750
3825
|
var RESET2 = "\x1B[0m";
|
|
3751
3826
|
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
@@ -3909,6 +3984,35 @@ function pad(s, n) {
|
|
|
3909
3984
|
return s.length >= n ? s : s + " ".repeat(n - s.length);
|
|
3910
3985
|
}
|
|
3911
3986
|
|
|
3987
|
+
// src/ui/cancel.ts
|
|
3988
|
+
function listenForCancel(controller) {
|
|
3989
|
+
const stdin2 = process.stdin;
|
|
3990
|
+
if (!stdin2.isTTY) return { pause() {
|
|
3991
|
+
}, resume() {
|
|
3992
|
+
}, dispose() {
|
|
3993
|
+
} };
|
|
3994
|
+
const onData = (buf) => {
|
|
3995
|
+
if (buf.length === 1 && (buf[0] === 27 || buf[0] === 3)) controller.abort();
|
|
3996
|
+
};
|
|
3997
|
+
let active = false;
|
|
3998
|
+
const attach = () => {
|
|
3999
|
+
if (active) return;
|
|
4000
|
+
stdin2.setRawMode(true);
|
|
4001
|
+
stdin2.resume();
|
|
4002
|
+
stdin2.on("data", onData);
|
|
4003
|
+
active = true;
|
|
4004
|
+
};
|
|
4005
|
+
const detach = () => {
|
|
4006
|
+
if (!active) return;
|
|
4007
|
+
stdin2.off("data", onData);
|
|
4008
|
+
stdin2.setRawMode(false);
|
|
4009
|
+
stdin2.pause();
|
|
4010
|
+
active = false;
|
|
4011
|
+
};
|
|
4012
|
+
attach();
|
|
4013
|
+
return { pause: detach, resume: attach, dispose: detach };
|
|
4014
|
+
}
|
|
4015
|
+
|
|
3912
4016
|
// src/cli/commands/swarm.ts
|
|
3913
4017
|
var MIN_SWARM_AGENTS = 3;
|
|
3914
4018
|
function canSwarm(agentCount) {
|
|
@@ -3932,6 +4036,11 @@ async function runSwarmSession(task, config, opts = {}) {
|
|
|
3932
4036
|
pc7.dim(t("swarm.status", { agents: resolved.map((a) => a.config.name).join(", "), workspace }))
|
|
3933
4037
|
);
|
|
3934
4038
|
console.log(pc7.yellow(t("swarm.bypassNote") + "\n"));
|
|
4039
|
+
const controller = new AbortController();
|
|
4040
|
+
const cancel2 = listenForCancel(controller);
|
|
4041
|
+
controller.signal.addEventListener("abort", () => console.log(pc7.dim("\n" + t("swarm.cancelling"))), {
|
|
4042
|
+
once: true
|
|
4043
|
+
});
|
|
3935
4044
|
const view = new SwarmView(resolved[0].config.name);
|
|
3936
4045
|
view.start();
|
|
3937
4046
|
let result;
|
|
@@ -3943,6 +4052,9 @@ async function runSwarmSession(task, config, opts = {}) {
|
|
|
3943
4052
|
allow: config.permissions.allow,
|
|
3944
4053
|
deny: config.permissions.deny,
|
|
3945
4054
|
maxSubtasks: opts.maxSubtasks,
|
|
4055
|
+
concurrency: recommendConcurrency(resolved),
|
|
4056
|
+
idleTimeoutMs: idleTimeoutMs(),
|
|
4057
|
+
signal: controller.signal,
|
|
3946
4058
|
events: {
|
|
3947
4059
|
onDecomposed: (subtasks) => view.setSubtasks(subtasks),
|
|
3948
4060
|
onWorkerStart: (subtask, agentName) => view.workerStart(subtask.id, agentName),
|
|
@@ -3956,6 +4068,7 @@ async function runSwarmSession(task, config, opts = {}) {
|
|
|
3956
4068
|
});
|
|
3957
4069
|
} finally {
|
|
3958
4070
|
view.stop();
|
|
4071
|
+
cancel2.dispose();
|
|
3959
4072
|
}
|
|
3960
4073
|
console.log("");
|
|
3961
4074
|
console.log(pc7.bold("\n" + t("swarm.summary")));
|
|
@@ -4228,33 +4341,6 @@ async function executeTask(task, resolved, workspace, session, json = false, ver
|
|
|
4228
4341
|
function fmtTokens(n) {
|
|
4229
4342
|
return n >= 1e3 ? `${(n / 1e3).toFixed(1)}k` : String(n);
|
|
4230
4343
|
}
|
|
4231
|
-
function listenForCancel(controller) {
|
|
4232
|
-
const stdin2 = process.stdin;
|
|
4233
|
-
if (!stdin2.isTTY) return { pause() {
|
|
4234
|
-
}, resume() {
|
|
4235
|
-
}, dispose() {
|
|
4236
|
-
} };
|
|
4237
|
-
const onData = (buf) => {
|
|
4238
|
-
if (buf.length === 1 && (buf[0] === 27 || buf[0] === 3)) controller.abort();
|
|
4239
|
-
};
|
|
4240
|
-
let active = false;
|
|
4241
|
-
const attach = () => {
|
|
4242
|
-
if (active) return;
|
|
4243
|
-
stdin2.setRawMode(true);
|
|
4244
|
-
stdin2.resume();
|
|
4245
|
-
stdin2.on("data", onData);
|
|
4246
|
-
active = true;
|
|
4247
|
-
};
|
|
4248
|
-
const detach = () => {
|
|
4249
|
-
if (!active) return;
|
|
4250
|
-
stdin2.off("data", onData);
|
|
4251
|
-
stdin2.setRawMode(false);
|
|
4252
|
-
stdin2.pause();
|
|
4253
|
-
active = false;
|
|
4254
|
-
};
|
|
4255
|
-
attach();
|
|
4256
|
-
return { pause: detach, resume: attach, dispose: detach };
|
|
4257
|
-
}
|
|
4258
4344
|
async function confirmAction(req) {
|
|
4259
4345
|
if (req.kind === "write" && req.hunks && req.hunks.length > 0) {
|
|
4260
4346
|
renderDiff(req.hunks);
|