@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 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 outcomes = await Promise.all(
3674
- subtasks.map(async (subtask, i) => {
3675
- const agent = opts.agents[i % opts.agents.length];
3676
- const wt = worktrees[i];
3677
- opts.events?.onWorkerStart?.(subtask, agent.config.name);
3678
- const outcome = await runWorker(
3679
- subtask,
3680
- agent,
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);