@gaberrb/polypus 0.1.0 โ 0.2.1
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/README.md +20 -0
- package/dist/index.js +205 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Polypus ๐
|
|
2
2
|
|
|
3
|
+
[](https://github.com/GaberRB/polypus/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@gaberrb/polypus)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
**๐ Site:** https://gaberrb.github.io/polypus/ ยท **๐ฆ npm:** [`@gaberrb/polypus`](https://www.npmjs.com/package/@gaberrb/polypus)
|
|
8
|
+
|
|
3
9
|
An agentic coding harness that makes **any** AI API generate and apply code โ
|
|
4
10
|
even models **without** native tool-calling. Bring your own keys: OpenRouter,
|
|
5
11
|
Ollama, Anthropic, or any OpenAI-compatible endpoint. Run one agent, or split a
|
|
@@ -221,6 +227,20 @@ npm run typecheck
|
|
|
221
227
|
npm test # vitest
|
|
222
228
|
```
|
|
223
229
|
|
|
230
|
+
## Contributing
|
|
231
|
+
|
|
232
|
+
Contributions are welcome โ but **open an issue first**. Issues are triaged and labeled
|
|
233
|
+
`accepted` before any PR; PRs without a linked accepted issue don't pass CI. See
|
|
234
|
+
[CONTRIBUTING.md](CONTRIBUTING.md) for the full flow (issue โ `accepted` โ branch โ PR โ review).
|
|
235
|
+
|
|
236
|
+
## Apoie o projeto
|
|
237
|
+
|
|
238
|
+
Se o Polypus te ajudou, vocรช pode apoiar o desenvolvimento via **PIX** ๐
|
|
239
|
+
|
|
240
|
+
> **Chave PIX (e-mail):** `gabrielriosbelmiro@gmail.com`
|
|
241
|
+
|
|
242
|
+
Qualquer valor รฉ muito bem-vindo e ajuda a manter o projeto vivo. Obrigado!
|
|
243
|
+
|
|
224
244
|
## Author
|
|
225
245
|
|
|
226
246
|
**Gabriel Rios**
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
+
import { createRequire } from "module";
|
|
5
6
|
import pc10 from "picocolors";
|
|
6
7
|
|
|
7
8
|
// src/cli/commands/add-agent.ts
|
|
@@ -121,6 +122,7 @@ var en = {
|
|
|
121
122
|
"run.stopped": "\u26A0 Stopped after {steps} steps without a finish signal. You can continue with another instruction.",
|
|
122
123
|
"run.confirm": "Allow {summary}?",
|
|
123
124
|
"run.reprompt": "\u21BB no tool call \u2014 reinforcing instructions (attempt {attempt})",
|
|
125
|
+
"run.autocorrect": "\u21BB tool failed \u2014 auto-correcting with extra context",
|
|
124
126
|
"run.cancelled": "\u25A0 cancelled",
|
|
125
127
|
// repl
|
|
126
128
|
"repl.welcome": "Polypus interactive session.",
|
|
@@ -308,6 +310,7 @@ var ptBR = {
|
|
|
308
310
|
"run.stopped": "\u26A0 Parou ap\xF3s {steps} passos sem sinal de conclus\xE3o. Voc\xEA pode continuar com outra instru\xE7\xE3o.",
|
|
309
311
|
"run.confirm": "Permitir {summary}?",
|
|
310
312
|
"run.reprompt": "\u21BB nenhuma chamada de tool \u2014 refor\xE7ando instru\xE7\xF5es (tentativa {attempt})",
|
|
313
|
+
"run.autocorrect": "\u21BB tool falhou \u2014 autocorrigindo com contexto extra",
|
|
311
314
|
"run.cancelled": "\u25A0 cancelado",
|
|
312
315
|
"repl.welcome": "Sess\xE3o interativa do Polypus.",
|
|
313
316
|
"repl.welcomeHint": " Digite /help para comandos, /exit para sair.",
|
|
@@ -1442,6 +1445,182 @@ function getTool(name) {
|
|
|
1442
1445
|
return TOOLS[name];
|
|
1443
1446
|
}
|
|
1444
1447
|
|
|
1448
|
+
// src/core/agent/correction.ts
|
|
1449
|
+
import { readFile as readFile4, readdir as readdir2 } from "fs/promises";
|
|
1450
|
+
import { dirname as dirname2, resolve as resolve6 } from "path";
|
|
1451
|
+
async function buildCorrection(call, output, deps) {
|
|
1452
|
+
const deterministic = await deterministicCorrection(call, output, deps);
|
|
1453
|
+
if (deterministic) return deterministic;
|
|
1454
|
+
if (deps.escalate) return deps.escalate({ call, output, toolSpec: deps.toolSpec });
|
|
1455
|
+
return null;
|
|
1456
|
+
}
|
|
1457
|
+
async function deterministicCorrection(call, output, deps) {
|
|
1458
|
+
const path = typeof call.arguments.path === "string" ? call.arguments.path : void 0;
|
|
1459
|
+
if (call.name === "edit_file" && /was not found/i.test(output) && path) {
|
|
1460
|
+
const search = typeof call.arguments.search === "string" ? call.arguments.search : "";
|
|
1461
|
+
const snippet = await snippetNearSearch(deps.workspace, path, search);
|
|
1462
|
+
if (snippet) {
|
|
1463
|
+
return [
|
|
1464
|
+
"AUTO-CORRECTION \u2014 the 'search' text does not exist verbatim in the file.",
|
|
1465
|
+
"Here is the file's actual content (line-numbered). Copy the 'search' value EXACTLY",
|
|
1466
|
+
"from these lines (including indentation and whitespace), then resend edit_file:",
|
|
1467
|
+
"",
|
|
1468
|
+
snippet
|
|
1469
|
+
].join("\n");
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
if (call.name === "edit_file" && /matched \d+ times/i.test(output) && path) {
|
|
1473
|
+
const search = typeof call.arguments.search === "string" ? call.arguments.search : "";
|
|
1474
|
+
const lines = await occurrenceLines(deps.workspace, path, search);
|
|
1475
|
+
if (lines.length > 0) {
|
|
1476
|
+
return [
|
|
1477
|
+
`AUTO-CORRECTION \u2014 the 'search' text is not unique; it appears at lines ${lines.join(", ")}.`,
|
|
1478
|
+
"Add more surrounding lines to 'search' so it matches exactly one location, then resend."
|
|
1479
|
+
].join("\n");
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
if (/ENOENT|no such file/i.test(output) && path) {
|
|
1483
|
+
const listing = await listNearest(deps.workspace, path);
|
|
1484
|
+
if (listing) {
|
|
1485
|
+
return [
|
|
1486
|
+
"AUTO-CORRECTION \u2014 that path does not exist. Nearby existing entries:",
|
|
1487
|
+
"",
|
|
1488
|
+
listing,
|
|
1489
|
+
"",
|
|
1490
|
+
"Use list_dir to confirm the correct path, or create the parent directory first."
|
|
1491
|
+
].join("\n");
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
if (/denied|not allowed/i.test(output)) {
|
|
1495
|
+
return [
|
|
1496
|
+
"AUTO-CORRECTION \u2014 this action was blocked by the permission policy.",
|
|
1497
|
+
`Editable paths (glob allow-list): ${deps.allow.join(", ") || "(none)"}.`,
|
|
1498
|
+
"Target a path inside the allow-list, or stop and report that the change is out of scope."
|
|
1499
|
+
].join("\n");
|
|
1500
|
+
}
|
|
1501
|
+
if (deps.toolSpec && /needs .* arguments|Invalid|required|Resend the tool call/i.test(output)) {
|
|
1502
|
+
return [
|
|
1503
|
+
"AUTO-CORRECTION \u2014 the call had missing or invalid arguments. Expected schema:",
|
|
1504
|
+
"",
|
|
1505
|
+
formatSchema(deps.toolSpec),
|
|
1506
|
+
"",
|
|
1507
|
+
"Resend the tool call with every required argument filled in."
|
|
1508
|
+
].join("\n");
|
|
1509
|
+
}
|
|
1510
|
+
return null;
|
|
1511
|
+
}
|
|
1512
|
+
function makeLLMEscalator(provider) {
|
|
1513
|
+
return async ({ call, output, toolSpec }) => {
|
|
1514
|
+
const prompt = [
|
|
1515
|
+
"A tool call just failed. Diagnose WHY in one or two sentences, then give a concrete,",
|
|
1516
|
+
"actionable instruction for how to correct the call. Do not apologize and do not call any tool.",
|
|
1517
|
+
"",
|
|
1518
|
+
`Tool: ${call.name}`,
|
|
1519
|
+
`Arguments: ${JSON.stringify(call.arguments)}`,
|
|
1520
|
+
`Error: ${output}`,
|
|
1521
|
+
toolSpec ? `Schema: ${JSON.stringify(toolSpec.parameters)}` : ""
|
|
1522
|
+
].join("\n");
|
|
1523
|
+
try {
|
|
1524
|
+
const res = await provider.chat({
|
|
1525
|
+
messages: [
|
|
1526
|
+
{ role: "system", content: "You are a debugging assistant for an autonomous coding agent." },
|
|
1527
|
+
{ role: "user", content: prompt }
|
|
1528
|
+
],
|
|
1529
|
+
params: { maxTokens: 400 }
|
|
1530
|
+
});
|
|
1531
|
+
const text2 = res.content.trim();
|
|
1532
|
+
return text2 ? `AUTO-CORRECTION (diagnosis):
|
|
1533
|
+
${text2}` : null;
|
|
1534
|
+
} catch {
|
|
1535
|
+
return null;
|
|
1536
|
+
}
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
async function readWorkspaceFile(workspace, path) {
|
|
1540
|
+
try {
|
|
1541
|
+
return await readFile4(resolve6(workspace, path), "utf8");
|
|
1542
|
+
} catch {
|
|
1543
|
+
return null;
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
function numberedWindow(content, center2, radius) {
|
|
1547
|
+
const lines = content.split("\n");
|
|
1548
|
+
const start = Math.max(0, center2 - radius);
|
|
1549
|
+
const end = Math.min(lines.length, center2 + radius + 1);
|
|
1550
|
+
const width = String(end).length;
|
|
1551
|
+
return lines.slice(start, end).map((line, i) => `${String(start + i + 1).padStart(width, " ")} | ${line}`).join("\n");
|
|
1552
|
+
}
|
|
1553
|
+
async function snippetNearSearch(workspace, path, search) {
|
|
1554
|
+
const content = await readWorkspaceFile(workspace, path);
|
|
1555
|
+
if (content === null) return null;
|
|
1556
|
+
const lines = content.split("\n");
|
|
1557
|
+
const anchor = bestAnchorLine(lines, search);
|
|
1558
|
+
return numberedWindow(content, anchor >= 0 ? anchor : 0, 6);
|
|
1559
|
+
}
|
|
1560
|
+
function bestAnchorLine(lines, search) {
|
|
1561
|
+
const target = search.split("\n").map((l) => l.trim()).find((l) => l.length > 0);
|
|
1562
|
+
if (!target) return -1;
|
|
1563
|
+
const targetWords = new Set(tokens(target));
|
|
1564
|
+
let best = -1;
|
|
1565
|
+
let bestScore = 0;
|
|
1566
|
+
lines.forEach((line, i) => {
|
|
1567
|
+
if (line.includes(target)) {
|
|
1568
|
+
if (bestScore < Number.MAX_SAFE_INTEGER) {
|
|
1569
|
+
best = i;
|
|
1570
|
+
bestScore = Number.MAX_SAFE_INTEGER;
|
|
1571
|
+
}
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
const score = tokens(line).filter((w) => targetWords.has(w)).length;
|
|
1575
|
+
if (score > bestScore) {
|
|
1576
|
+
best = i;
|
|
1577
|
+
bestScore = score;
|
|
1578
|
+
}
|
|
1579
|
+
});
|
|
1580
|
+
return best;
|
|
1581
|
+
}
|
|
1582
|
+
function tokens(s) {
|
|
1583
|
+
return s.split(/\W+/).filter((w) => w.length > 0);
|
|
1584
|
+
}
|
|
1585
|
+
async function occurrenceLines(workspace, path, search) {
|
|
1586
|
+
const content = await readWorkspaceFile(workspace, path);
|
|
1587
|
+
if (content === null || search.length === 0) return [];
|
|
1588
|
+
const out = [];
|
|
1589
|
+
let from = 0;
|
|
1590
|
+
for (; ; ) {
|
|
1591
|
+
const idx = content.indexOf(search, from);
|
|
1592
|
+
if (idx === -1) break;
|
|
1593
|
+
out.push(content.slice(0, idx).split("\n").length);
|
|
1594
|
+
from = idx + search.length;
|
|
1595
|
+
}
|
|
1596
|
+
return out;
|
|
1597
|
+
}
|
|
1598
|
+
async function listNearest(workspace, path) {
|
|
1599
|
+
let dir = dirname2(resolve6(workspace, path));
|
|
1600
|
+
for (let i = 0; i < 8; i++) {
|
|
1601
|
+
try {
|
|
1602
|
+
const entries = await readdir2(dir, { withFileTypes: true });
|
|
1603
|
+
const rel = dir === resolve6(workspace) ? "." : dir;
|
|
1604
|
+
const names = entries.slice(0, 40).map((e) => e.isDirectory() ? `${e.name}/` : e.name);
|
|
1605
|
+
return `${rel}:
|
|
1606
|
+
${names.join(" ") || "(empty)"}`;
|
|
1607
|
+
} catch {
|
|
1608
|
+
const parent = dirname2(dir);
|
|
1609
|
+
if (parent === dir) return null;
|
|
1610
|
+
dir = parent;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
return null;
|
|
1614
|
+
}
|
|
1615
|
+
function formatSchema(spec) {
|
|
1616
|
+
const props = spec.parameters.properties ?? {};
|
|
1617
|
+
const required = new Set(spec.parameters.required ?? []);
|
|
1618
|
+
const lines = Object.entries(props).map(
|
|
1619
|
+
([name, v]) => ` - ${name} (${v.type ?? "any"}, ${required.has(name) ? "required" : "optional"})` + (v.description ? `: ${v.description}` : "")
|
|
1620
|
+
);
|
|
1621
|
+
return lines.join("\n") || " (no parameters)";
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1445
1624
|
// src/core/agent/loop.ts
|
|
1446
1625
|
function looksLikeStall(text2) {
|
|
1447
1626
|
const lc = text2.toLowerCase();
|
|
@@ -1492,6 +1671,7 @@ async function runAgent(opts) {
|
|
|
1492
1671
|
let lastFailSig = "";
|
|
1493
1672
|
let failStreak = 0;
|
|
1494
1673
|
const maxToolRetries = opts.maxToolRetries ?? 3;
|
|
1674
|
+
const autoCorrect = opts.autoCorrect ?? true;
|
|
1495
1675
|
const usage = { promptTokens: 0, completionTokens: 0 };
|
|
1496
1676
|
for (let step = 1; step <= maxSteps; step++) {
|
|
1497
1677
|
if (opts.signal?.aborted) return { finished: false, reason: "cancelled", steps: step - 1, messages, usage };
|
|
@@ -1538,12 +1718,29 @@ async function runAgent(opts) {
|
|
|
1538
1718
|
const tool = getTool(call.name);
|
|
1539
1719
|
const result = tool ? await tool.run(call.arguments, ctx) : { ok: false, output: `Unknown tool "${call.name}". Available: ${toolSpecs().map((t2) => t2.name).join(", ")}` };
|
|
1540
1720
|
events?.onToolResult?.(call, result);
|
|
1541
|
-
|
|
1721
|
+
const sig = `${call.name}:${JSON.stringify(call.arguments)}`;
|
|
1722
|
+
let resultText = result.output;
|
|
1723
|
+
if (!result.ok && autoCorrect) {
|
|
1724
|
+
const guidance = await buildCorrection(call, result.output, {
|
|
1725
|
+
workspace: opts.workspace,
|
|
1726
|
+
allow: opts.promptContext.allow,
|
|
1727
|
+
toolSpec: tool?.spec,
|
|
1728
|
+
// Only spend a fixer-LLM call once the model has already repeated a
|
|
1729
|
+
// failing call โ the first failure gets deterministic help for free.
|
|
1730
|
+
escalate: sig === lastFailSig ? makeLLMEscalator(agent.provider) : void 0
|
|
1731
|
+
});
|
|
1732
|
+
if (guidance) {
|
|
1733
|
+
resultText = `${result.output}
|
|
1734
|
+
|
|
1735
|
+
${guidance}`;
|
|
1736
|
+
events?.onCorrection?.(call, guidance);
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
messages.push(driver.toolResultMessage(call, resultText));
|
|
1542
1740
|
if (result.ok) {
|
|
1543
1741
|
failStreak = 0;
|
|
1544
1742
|
lastFailSig = "";
|
|
1545
1743
|
} else {
|
|
1546
|
-
const sig = `${call.name}:${JSON.stringify(call.arguments)}`;
|
|
1547
1744
|
failStreak = sig === lastFailSig ? failStreak + 1 : 1;
|
|
1548
1745
|
lastFailSig = sig;
|
|
1549
1746
|
if (failStreak >= maxToolRetries) {
|
|
@@ -2403,6 +2600,10 @@ function renderEvents(spinner3) {
|
|
|
2403
2600
|
onReprompt(attempt) {
|
|
2404
2601
|
spinner3.stop();
|
|
2405
2602
|
console.log(pc7.yellow(" " + t("run.reprompt", { attempt })));
|
|
2603
|
+
},
|
|
2604
|
+
onCorrection() {
|
|
2605
|
+
spinner3.stop();
|
|
2606
|
+
console.log(pc7.yellow(" \u21BB " + t("run.autocorrect")));
|
|
2406
2607
|
}
|
|
2407
2608
|
};
|
|
2408
2609
|
}
|
|
@@ -2737,6 +2938,7 @@ function loadDotenv(paths) {
|
|
|
2737
2938
|
}
|
|
2738
2939
|
|
|
2739
2940
|
// src/cli/index.ts
|
|
2941
|
+
var { version: pkgVersion } = createRequire(import.meta.url)("../package.json");
|
|
2740
2942
|
async function launchInteractive() {
|
|
2741
2943
|
const config = await loadConfig();
|
|
2742
2944
|
if (config.agents.length === 0) {
|
|
@@ -2762,7 +2964,7 @@ async function resolveLocale() {
|
|
|
2762
2964
|
}
|
|
2763
2965
|
function buildProgram() {
|
|
2764
2966
|
const program = new Command();
|
|
2765
|
-
program.name("polypus").description(t("cli.description")).version(
|
|
2967
|
+
program.name("polypus").description(t("cli.description")).version(pkgVersion).option("--lang <locale>", t("cli.opt.lang")).action(() => launchInteractive());
|
|
2766
2968
|
program.command("setup").description(t("cli.cmd.setup")).action(() => setup());
|
|
2767
2969
|
program.command("add-agent").argument("<name>", t("cli.arg.addAgentName")).requiredOption("--provider <provider>", t("cli.opt.provider")).requiredOption("--model <model>", t("cli.opt.model")).option("--api-key <key>", t("cli.opt.apiKey")).option("--base-url <url>", t("cli.opt.baseUrl")).option("--tool-mode <mode>", t("cli.opt.toolMode"), "auto").option("--set-default", t("cli.opt.setDefault")).description(t("cli.cmd.addAgent")).action((name, opts) => addAgent(name, opts));
|
|
2768
2970
|
program.command("remove-agent").argument("<name>", t("cli.arg.removeAgentName")).description(t("cli.cmd.removeAgent")).action((name) => removeAgent(name));
|