@cosmicstack/mercury-agent 0.5.0 → 0.5.2
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 +1 -1
- package/dist/index.js +331 -62
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -235,4 +235,4 @@ MIT © [Cosmic Stack](https://github.com/cosmicstack-labs)
|
|
|
235
235
|
|
|
236
236
|
## Suggestions and Contributions
|
|
237
237
|
|
|
238
|
-
For suggestions, contributions, or any inquiries, please reach out to us at [
|
|
238
|
+
For suggestions, contributions, or any inquiries, please reach out to us at [mercury@cosmicstack.org](mailto:mercury@cosmicstack.org).
|
package/dist/index.js
CHANGED
|
@@ -115,7 +115,7 @@ function getDefaultConfig() {
|
|
|
115
115
|
intervalMinutes: getEnvNum("HEARTBEAT_INTERVAL_MINUTES", 60)
|
|
116
116
|
},
|
|
117
117
|
tokens: {
|
|
118
|
-
dailyBudget: getEnvNum("DAILY_TOKEN_BUDGET",
|
|
118
|
+
dailyBudget: getEnvNum("DAILY_TOKEN_BUDGET", 1e6)
|
|
119
119
|
}
|
|
120
120
|
};
|
|
121
121
|
}
|
|
@@ -916,9 +916,34 @@ var BaseChannel = class {
|
|
|
916
916
|
import { Marked } from "marked";
|
|
917
917
|
import chalk from "chalk";
|
|
918
918
|
var lexer = new Marked();
|
|
919
|
+
var HTML_ENTITIES = {
|
|
920
|
+
"&": "&",
|
|
921
|
+
"<": "<",
|
|
922
|
+
">": ">",
|
|
923
|
+
""": '"',
|
|
924
|
+
"'": "'",
|
|
925
|
+
"'": "'",
|
|
926
|
+
"'": "'",
|
|
927
|
+
" ": " "
|
|
928
|
+
};
|
|
929
|
+
function decodeHtmlEntities(text) {
|
|
930
|
+
return text.replace(/&(?:#[xX]?[0-9a-fA-F]+|[a-zA-Z]+);/g, (match) => {
|
|
931
|
+
if (HTML_ENTITIES[match]) return HTML_ENTITIES[match];
|
|
932
|
+
if (match.startsWith("&#x") || match.startsWith("&#X")) {
|
|
933
|
+
const code = parseInt(match.slice(3, -1), 16);
|
|
934
|
+
return isNaN(code) ? match : String.fromCodePoint(code);
|
|
935
|
+
}
|
|
936
|
+
if (match.startsWith("&#")) {
|
|
937
|
+
const code = parseInt(match.slice(2, -1), 10);
|
|
938
|
+
return isNaN(code) ? match : String.fromCodePoint(code);
|
|
939
|
+
}
|
|
940
|
+
return match;
|
|
941
|
+
});
|
|
942
|
+
}
|
|
919
943
|
function renderMarkdown(text) {
|
|
920
944
|
try {
|
|
921
|
-
const
|
|
945
|
+
const decoded = decodeHtmlEntities(text);
|
|
946
|
+
const tokens = lexer.lexer(decoded);
|
|
922
947
|
const result = renderTokens(tokens);
|
|
923
948
|
return result.replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
924
949
|
} catch {
|
|
@@ -1106,6 +1131,67 @@ function mdToTelegram(text) {
|
|
|
1106
1131
|
return out;
|
|
1107
1132
|
}
|
|
1108
1133
|
|
|
1134
|
+
// src/utils/tool-label.ts
|
|
1135
|
+
var TOOL_LABELS = {
|
|
1136
|
+
fetch_url: { icon: "\u2197", label: "Fetching", argKey: "url", argTransform: extractDomain },
|
|
1137
|
+
read_file: { icon: "\u{1F4C4}", label: "Reading", argKey: "path", argTransform: basename },
|
|
1138
|
+
write_file: { icon: "\u270F\uFE0F", label: "Writing", argKey: "path", argTransform: basename },
|
|
1139
|
+
create_file: { icon: "\u2728", label: "Creating", argKey: "path", argTransform: basename },
|
|
1140
|
+
edit_file: { icon: "\u2702\uFE0F", label: "Editing", argKey: "path", argTransform: basename },
|
|
1141
|
+
delete_file: { icon: "\u{1F5D1}", label: "Deleting", argKey: "path", argTransform: basename },
|
|
1142
|
+
list_dir: { icon: "\u{1F4C2}", label: "Listing", argKey: "path", argTransform: basename },
|
|
1143
|
+
approve_scope: { icon: "\u{1F513}", label: "Approving scope" },
|
|
1144
|
+
run_command: { icon: "\u2328", label: "Running command", argKey: "command", argTransform: truncate(40) },
|
|
1145
|
+
cd: { icon: "\u{1F4C2}", label: "Changing dir to", argKey: "path" },
|
|
1146
|
+
approve_command: { icon: "\u2705", label: "Approving command" },
|
|
1147
|
+
send_message: { icon: "\u{1F4AC}", label: "Sending message" },
|
|
1148
|
+
send_file: { icon: "\u{1F4CE}", label: "Sending file", argKey: "path", argTransform: basename },
|
|
1149
|
+
git_status: { icon: "\u{1F4CA}", label: "Git status" },
|
|
1150
|
+
git_diff: { icon: "\u{1F4DD}", label: "Git diff" },
|
|
1151
|
+
git_log: { icon: "\u{1F4CB}", label: "Git log" },
|
|
1152
|
+
git_add: { icon: "\u2795", label: "Staging files" },
|
|
1153
|
+
git_commit: { icon: "\u{1F4BE}", label: "Committing" },
|
|
1154
|
+
git_push: { icon: "\u2B06", label: "Pushing" },
|
|
1155
|
+
create_pr: { icon: "\u{1F500}", label: "Creating PR" },
|
|
1156
|
+
review_pr: { icon: "\u{1F440}", label: "Reviewing PR" },
|
|
1157
|
+
list_issues: { icon: "\u{1F4CB}", label: "Listing issues" },
|
|
1158
|
+
create_issue: { icon: "\u{1F41B}", label: "Creating issue" },
|
|
1159
|
+
github_api: { icon: "\u{1F500}", label: "GitHub API", argKey: "path", argTransform: truncate(30) },
|
|
1160
|
+
schedule_task: { icon: "\u23F0", label: "Scheduling task" },
|
|
1161
|
+
cancel_task: { icon: "\u274C", label: "Cancelling task" },
|
|
1162
|
+
list_tasks: { icon: "\u{1F4CB}", label: "Listing tasks" },
|
|
1163
|
+
use_skill: { icon: "\u{1F9E0}", label: "Using skill", argKey: "name" },
|
|
1164
|
+
list_skills: { icon: "\u{1F4CB}", label: "Listing skills" },
|
|
1165
|
+
install_skill: { icon: "\u{1F4E5}", label: "Installing skill" },
|
|
1166
|
+
budget_status: { icon: "\u{1F4B0}", label: "Budget status" }
|
|
1167
|
+
};
|
|
1168
|
+
function extractDomain(url) {
|
|
1169
|
+
try {
|
|
1170
|
+
return new URL(url).hostname;
|
|
1171
|
+
} catch {
|
|
1172
|
+
return url;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
function basename(p) {
|
|
1176
|
+
const parts = p.replace(/\\/g, "/").split("/");
|
|
1177
|
+
return parts[parts.length - 1] || p;
|
|
1178
|
+
}
|
|
1179
|
+
function truncate(maxLen) {
|
|
1180
|
+
return (v) => v.length > maxLen ? v.slice(0, maxLen) + "\u2026" : v;
|
|
1181
|
+
}
|
|
1182
|
+
function formatToolStep(toolName, args) {
|
|
1183
|
+
const config = TOOL_LABELS[toolName];
|
|
1184
|
+
if (!config) {
|
|
1185
|
+
return toolName;
|
|
1186
|
+
}
|
|
1187
|
+
let detail = "";
|
|
1188
|
+
if (config.argKey && args[config.argKey] !== void 0) {
|
|
1189
|
+
const raw = String(args[config.argKey]);
|
|
1190
|
+
detail = config.argTransform ? config.argTransform(raw) : raw;
|
|
1191
|
+
}
|
|
1192
|
+
return detail ? `${config.icon} ${config.label} ${detail}` : `${config.icon} ${config.label}`;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1109
1195
|
// src/utils/arrow-select.ts
|
|
1110
1196
|
import readline from "readline";
|
|
1111
1197
|
import chalk2 from "chalk";
|
|
@@ -1226,6 +1312,12 @@ async function selectWithArrowKeys(title, options, config = {}) {
|
|
|
1226
1312
|
}
|
|
1227
1313
|
|
|
1228
1314
|
// src/channels/cli.ts
|
|
1315
|
+
var USER_PROMPT = " You: ";
|
|
1316
|
+
var USER_PROMPT_VISIBLE_LEN = USER_PROMPT.length;
|
|
1317
|
+
function agentPrefix(name, suffix) {
|
|
1318
|
+
const time = suffix ?? "";
|
|
1319
|
+
return chalk3.cyan(` ${name}:`) + time;
|
|
1320
|
+
}
|
|
1229
1321
|
var CLIChannel = class extends BaseChannel {
|
|
1230
1322
|
type = "cli";
|
|
1231
1323
|
rl = null;
|
|
@@ -1233,6 +1325,9 @@ var CLIChannel = class extends BaseChannel {
|
|
|
1233
1325
|
menuDepth = 0;
|
|
1234
1326
|
menuAbortController = null;
|
|
1235
1327
|
outputInProgress = 0;
|
|
1328
|
+
streamActive = false;
|
|
1329
|
+
streamToolLines = 0;
|
|
1330
|
+
lastUserInput = "";
|
|
1236
1331
|
constructor(agentName = "Mercury") {
|
|
1237
1332
|
super();
|
|
1238
1333
|
this.agentName = agentName;
|
|
@@ -1243,16 +1338,17 @@ var CLIChannel = class extends BaseChannel {
|
|
|
1243
1338
|
async start() {
|
|
1244
1339
|
this.createInterface();
|
|
1245
1340
|
this.ready = true;
|
|
1246
|
-
this.showPrompt();
|
|
1247
1341
|
logger.info("CLI channel started");
|
|
1248
1342
|
}
|
|
1249
1343
|
createInterface() {
|
|
1250
1344
|
this.rl = readline2.createInterface({
|
|
1251
1345
|
input: process.stdin,
|
|
1252
|
-
output: process.stdout
|
|
1253
|
-
prompt: " You: "
|
|
1346
|
+
output: process.stdout
|
|
1254
1347
|
});
|
|
1348
|
+
this.rl.setPrompt(chalk3.yellow(USER_PROMPT));
|
|
1349
|
+
this.rl._promptLength = USER_PROMPT_VISIBLE_LEN;
|
|
1255
1350
|
this.rl.on("line", (line) => {
|
|
1351
|
+
this.lastUserInput = line.trim();
|
|
1256
1352
|
const trimmed = line.trim();
|
|
1257
1353
|
if (!trimmed) {
|
|
1258
1354
|
this.showPrompt();
|
|
@@ -1279,9 +1375,9 @@ var CLIChannel = class extends BaseChannel {
|
|
|
1279
1375
|
this.beginOutput();
|
|
1280
1376
|
const timeStr = elapsedMs != null ? chalk3.dim(` (${(elapsedMs / 1e3).toFixed(1)}s)`) : "";
|
|
1281
1377
|
const rendered = renderMarkdown(content);
|
|
1378
|
+
const indented = this.indent(rendered);
|
|
1282
1379
|
console.log("");
|
|
1283
|
-
console.log(
|
|
1284
|
-
const indented = rendered.split("\n").map((line) => ` ${line}`).join("\n");
|
|
1380
|
+
console.log(agentPrefix(this.agentName, timeStr));
|
|
1285
1381
|
console.log(indented);
|
|
1286
1382
|
console.log("");
|
|
1287
1383
|
this.endOutput();
|
|
@@ -1298,23 +1394,69 @@ var CLIChannel = class extends BaseChannel {
|
|
|
1298
1394
|
const stat = fs.statSync(resolved);
|
|
1299
1395
|
const sizeStr = stat.size > 1024 * 1024 ? `${(stat.size / (1024 * 1024)).toFixed(1)}MB` : stat.size > 1024 ? `${(stat.size / 1024).toFixed(1)}KB` : `${stat.size}B`;
|
|
1300
1396
|
console.log("");
|
|
1301
|
-
console.log(
|
|
1397
|
+
console.log(agentPrefix(this.agentName, chalk3.dim(" (file)")));
|
|
1302
1398
|
console.log(chalk3.dim(` path: ${resolved}`));
|
|
1303
1399
|
console.log(chalk3.dim(` size: ${sizeStr}`));
|
|
1304
1400
|
console.log("");
|
|
1305
1401
|
this.endOutput();
|
|
1306
1402
|
}
|
|
1403
|
+
async sendToolFeedback(toolName, args) {
|
|
1404
|
+
const label = formatToolStep(toolName, args);
|
|
1405
|
+
if (this.streamActive) {
|
|
1406
|
+
this.streamToolLines++;
|
|
1407
|
+
process.stdout.write(chalk3.dim(`
|
|
1408
|
+
${label}
|
|
1409
|
+
`));
|
|
1410
|
+
} else {
|
|
1411
|
+
console.log(chalk3.dim(` ${label}`));
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1307
1414
|
async stream(content, _targetId) {
|
|
1308
1415
|
this.closeActiveMenu();
|
|
1309
1416
|
this.beginOutput();
|
|
1310
|
-
|
|
1311
|
-
|
|
1417
|
+
this.streamActive = true;
|
|
1418
|
+
this.streamToolLines = 0;
|
|
1419
|
+
if (!process.stdout.isTTY) {
|
|
1420
|
+
process.stdout.write(chalk3.cyan(` ${this.agentName}: `));
|
|
1421
|
+
let full2 = "";
|
|
1422
|
+
for await (const chunk of content) {
|
|
1423
|
+
process.stdout.write(chunk);
|
|
1424
|
+
full2 += chunk;
|
|
1425
|
+
}
|
|
1426
|
+
this.streamActive = false;
|
|
1427
|
+
console.log("\n");
|
|
1428
|
+
this.endOutput();
|
|
1429
|
+
return full2;
|
|
1430
|
+
}
|
|
1431
|
+
console.log(chalk3.cyan(` ${this.agentName}:`));
|
|
1312
1432
|
let full = "";
|
|
1313
1433
|
for await (const chunk of content) {
|
|
1314
1434
|
process.stdout.write(chunk);
|
|
1315
1435
|
full += chunk;
|
|
1316
1436
|
}
|
|
1317
|
-
|
|
1437
|
+
this.streamActive = false;
|
|
1438
|
+
if (!full.trim()) {
|
|
1439
|
+
console.log("");
|
|
1440
|
+
this.endOutput();
|
|
1441
|
+
return full;
|
|
1442
|
+
}
|
|
1443
|
+
const termWidth = process.stdout.columns || 80;
|
|
1444
|
+
let textLineCount = 0;
|
|
1445
|
+
for (const line of full.split("\n")) {
|
|
1446
|
+
const visualLen = line.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
1447
|
+
textLineCount += Math.max(1, Math.ceil((visualLen + 2) / termWidth));
|
|
1448
|
+
}
|
|
1449
|
+
const totalLines = 1 + textLineCount + this.streamToolLines;
|
|
1450
|
+
process.stdout.write(`\x1B[${totalLines}A`);
|
|
1451
|
+
for (let i = 0; i < totalLines; i++) {
|
|
1452
|
+
process.stdout.write("\x1B[2K\x1B[1B");
|
|
1453
|
+
}
|
|
1454
|
+
process.stdout.write(`\x1B[${totalLines}A`);
|
|
1455
|
+
const rendered = renderMarkdown(full);
|
|
1456
|
+
const indented = this.indent(rendered);
|
|
1457
|
+
console.log(agentPrefix(this.agentName));
|
|
1458
|
+
console.log(indented);
|
|
1459
|
+
console.log("");
|
|
1318
1460
|
this.endOutput();
|
|
1319
1461
|
return full;
|
|
1320
1462
|
}
|
|
@@ -1323,8 +1465,8 @@ var CLIChannel = class extends BaseChannel {
|
|
|
1323
1465
|
}
|
|
1324
1466
|
showPrompt() {
|
|
1325
1467
|
if (this.rl) {
|
|
1326
|
-
|
|
1327
|
-
|
|
1468
|
+
process.stdout.write("\x1B[2K\r");
|
|
1469
|
+
process.stdout.write(chalk3.yellow(USER_PROMPT));
|
|
1328
1470
|
}
|
|
1329
1471
|
}
|
|
1330
1472
|
async withMenu(runner) {
|
|
@@ -1377,6 +1519,9 @@ var CLIChannel = class extends BaseChannel {
|
|
|
1377
1519
|
if (!this.ready || this.rl) return;
|
|
1378
1520
|
this.createInterface();
|
|
1379
1521
|
}
|
|
1522
|
+
indent(text) {
|
|
1523
|
+
return text.split("\n").map((line) => ` ${line}`).join("\n");
|
|
1524
|
+
}
|
|
1380
1525
|
async prompt(question) {
|
|
1381
1526
|
return new Promise((resolve13) => {
|
|
1382
1527
|
this.rl?.question(question, (answer) => resolve13(answer.trim()));
|
|
@@ -1391,12 +1536,37 @@ var CLIChannel = class extends BaseChannel {
|
|
|
1391
1536
|
});
|
|
1392
1537
|
});
|
|
1393
1538
|
}
|
|
1539
|
+
async askToContinue(question, _targetId) {
|
|
1540
|
+
return new Promise((resolve13) => {
|
|
1541
|
+
console.log("");
|
|
1542
|
+
console.log(chalk3.yellow(` \u26A0 ${question}`));
|
|
1543
|
+
this.rl?.question(chalk3.yellow(" Continue? [y/N] "), (answer) => {
|
|
1544
|
+
const val = answer.trim().toLowerCase();
|
|
1545
|
+
resolve13(val === "y" || val === "yes");
|
|
1546
|
+
});
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1394
1549
|
};
|
|
1395
1550
|
|
|
1396
1551
|
// src/core/agent.ts
|
|
1397
|
-
var ToolCallLoopDetector = class {
|
|
1552
|
+
var ToolCallLoopDetector = class _ToolCallLoopDetector {
|
|
1398
1553
|
recentCalls = [];
|
|
1399
|
-
maxEntries =
|
|
1554
|
+
maxEntries = 20;
|
|
1555
|
+
hardAborted = false;
|
|
1556
|
+
static HIGH_TOLERANCE_TOOLS = /* @__PURE__ */ new Set([
|
|
1557
|
+
"fetch_url",
|
|
1558
|
+
"read_file",
|
|
1559
|
+
"list_dir",
|
|
1560
|
+
"web_search",
|
|
1561
|
+
"github_api",
|
|
1562
|
+
"run_command"
|
|
1563
|
+
]);
|
|
1564
|
+
static getIdenticalThreshold() {
|
|
1565
|
+
return 3;
|
|
1566
|
+
}
|
|
1567
|
+
static getSameToolThreshold(toolName) {
|
|
1568
|
+
return _ToolCallLoopDetector.HIGH_TOLERANCE_TOOLS.has(toolName) ? 6 : 4;
|
|
1569
|
+
}
|
|
1400
1570
|
record(toolName, params) {
|
|
1401
1571
|
const paramsKey = JSON.stringify(params).slice(0, 100);
|
|
1402
1572
|
this.recentCalls.push({ tool: toolName, params: paramsKey });
|
|
@@ -1404,36 +1574,53 @@ var ToolCallLoopDetector = class {
|
|
|
1404
1574
|
this.recentCalls.shift();
|
|
1405
1575
|
}
|
|
1406
1576
|
}
|
|
1407
|
-
|
|
1577
|
+
detectIdentical() {
|
|
1408
1578
|
if (this.recentCalls.length < 3) return null;
|
|
1409
1579
|
const last = this.recentCalls[this.recentCalls.length - 1];
|
|
1410
|
-
let
|
|
1580
|
+
let identicalCount = 0;
|
|
1411
1581
|
for (let i = this.recentCalls.length - 1; i >= 0; i--) {
|
|
1412
1582
|
if (this.recentCalls[i].tool === last.tool && this.recentCalls[i].params === last.params) {
|
|
1413
|
-
|
|
1583
|
+
identicalCount++;
|
|
1414
1584
|
} else {
|
|
1415
1585
|
break;
|
|
1416
1586
|
}
|
|
1417
1587
|
}
|
|
1418
|
-
if (
|
|
1419
|
-
|
|
1588
|
+
if (identicalCount >= _ToolCallLoopDetector.getIdenticalThreshold()) {
|
|
1589
|
+
this.hardAborted = true;
|
|
1590
|
+
return {
|
|
1591
|
+
tool: last.tool,
|
|
1592
|
+
count: identicalCount,
|
|
1593
|
+
message: `[SYSTEM] You called "${last.tool}" ${identicalCount} times with identical parameters and got the same result. This is a hard loop \u2014 stop immediately.`
|
|
1594
|
+
};
|
|
1420
1595
|
}
|
|
1421
|
-
|
|
1422
|
-
|
|
1596
|
+
return null;
|
|
1597
|
+
}
|
|
1598
|
+
detectSameTool() {
|
|
1599
|
+
if (this.recentCalls.length < 4) return null;
|
|
1600
|
+
const last = this.recentCalls[this.recentCalls.length - 1];
|
|
1601
|
+
let sameToolCount = 0;
|
|
1423
1602
|
for (let i = this.recentCalls.length - 1; i >= 0; i--) {
|
|
1424
|
-
if (this.recentCalls[i].tool ===
|
|
1425
|
-
|
|
1603
|
+
if (this.recentCalls[i].tool === last.tool) {
|
|
1604
|
+
sameToolCount++;
|
|
1426
1605
|
} else {
|
|
1427
1606
|
break;
|
|
1428
1607
|
}
|
|
1429
1608
|
}
|
|
1430
|
-
|
|
1431
|
-
|
|
1609
|
+
const threshold = _ToolCallLoopDetector.getSameToolThreshold(last.tool);
|
|
1610
|
+
if (sameToolCount >= threshold) {
|
|
1611
|
+
return {
|
|
1612
|
+
tool: last.tool,
|
|
1613
|
+
count: sameToolCount
|
|
1614
|
+
};
|
|
1432
1615
|
}
|
|
1433
1616
|
return null;
|
|
1434
1617
|
}
|
|
1618
|
+
isHardAborted() {
|
|
1619
|
+
return this.hardAborted;
|
|
1620
|
+
}
|
|
1435
1621
|
reset() {
|
|
1436
1622
|
this.recentCalls = [];
|
|
1623
|
+
this.hardAborted = false;
|
|
1437
1624
|
}
|
|
1438
1625
|
};
|
|
1439
1626
|
var MAX_STEPS = 10;
|
|
@@ -1597,30 +1784,6 @@ You can override this:
|
|
|
1597
1784
|
const recentMemory = this.shortTerm.getRecent(msg.channelId, 10);
|
|
1598
1785
|
const relevantFacts = this.longTerm.search(msg.content, 3);
|
|
1599
1786
|
const messages = [];
|
|
1600
|
-
const recentSteps = this.shortTerm.getRecent(msg.channelId, 4);
|
|
1601
|
-
let loopWarning = null;
|
|
1602
|
-
if (recentSteps.length >= 3) {
|
|
1603
|
-
const toolCallPattern = /\[Using: (.+?)\]/g;
|
|
1604
|
-
const toolCalls = [];
|
|
1605
|
-
for (const m of recentSteps) {
|
|
1606
|
-
if (m.role === "assistant") {
|
|
1607
|
-
let match;
|
|
1608
|
-
while ((match = toolCallPattern.exec(m.content)) !== null) {
|
|
1609
|
-
toolCalls.push(match[1]);
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
|
-
}
|
|
1613
|
-
if (toolCalls.length >= 3) {
|
|
1614
|
-
const last3 = toolCalls.slice(-3);
|
|
1615
|
-
if (last3[0] === last3[1] && last3[1] === last3[2]) {
|
|
1616
|
-
loopWarning = `[SYSTEM WARNING] You have called ${last3[0]} 3+ times in a row with the same result. Stop repeating this call. Try a different approach \u2014 if you're failing on permissions, try a different path. If you're failing on git push auth, use github_api with PUT /repos/{owner}/{repo}/contents/{path} to push files directly through the API.`;
|
|
1617
|
-
}
|
|
1618
|
-
}
|
|
1619
|
-
}
|
|
1620
|
-
if (loopWarning) {
|
|
1621
|
-
messages.push({ role: "user", content: loopWarning });
|
|
1622
|
-
messages.push({ role: "assistant", content: "Understood. I will try a different approach." });
|
|
1623
|
-
}
|
|
1624
1787
|
if (relevantFacts.length > 0) {
|
|
1625
1788
|
messages.push({
|
|
1626
1789
|
role: "user",
|
|
@@ -1651,6 +1814,8 @@ You can override this:
|
|
|
1651
1814
|
let lastError = null;
|
|
1652
1815
|
let streamedText = "";
|
|
1653
1816
|
const loopDetector = new ToolCallLoopDetector();
|
|
1817
|
+
const loopAbortController = new AbortController();
|
|
1818
|
+
let loopWarningSent = false;
|
|
1654
1819
|
const canStream = msg.channelType === "cli" || msg.channelType === "telegram" && this.telegramStreaming;
|
|
1655
1820
|
for (const provider of fallbackIterator) {
|
|
1656
1821
|
try {
|
|
@@ -1662,6 +1827,7 @@ You can override this:
|
|
|
1662
1827
|
messages,
|
|
1663
1828
|
tools: this.capabilities.getTools(),
|
|
1664
1829
|
maxSteps: MAX_STEPS,
|
|
1830
|
+
abortSignal: loopAbortController.signal,
|
|
1665
1831
|
onStepFinish: async ({ toolCalls }) => {
|
|
1666
1832
|
if (toolCalls && toolCalls.length > 0) {
|
|
1667
1833
|
const names = toolCalls.map((tc) => tc.toolName).join(", ");
|
|
@@ -1669,12 +1835,44 @@ You can override this:
|
|
|
1669
1835
|
for (const tc of toolCalls) {
|
|
1670
1836
|
loopDetector.record(tc.toolName, tc.args);
|
|
1671
1837
|
}
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1838
|
+
if (toolCalls.some((tc) => tc.toolName === "use_skill")) {
|
|
1839
|
+
loopDetector.reset();
|
|
1840
|
+
}
|
|
1841
|
+
const hardLoop = loopDetector.detectIdentical();
|
|
1842
|
+
if (hardLoop) {
|
|
1843
|
+
logger.warn({ tool: hardLoop.tool, count: hardLoop.count }, "Hard loop detected \u2014 aborting");
|
|
1844
|
+
if (!loopWarningSent && channel && msg.channelType !== "internal") {
|
|
1845
|
+
loopWarningSent = true;
|
|
1846
|
+
await channel.send(`\u26A0 Repeated call detected \u2014 ${hardLoop.tool} called ${hardLoop.count}x with same params. Stopping.`, msg.channelId).catch(() => {
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
loopAbortController.abort();
|
|
1850
|
+
}
|
|
1851
|
+
const softLoop = loopDetector.detectSameTool();
|
|
1852
|
+
if (softLoop && !loopWarningSent && channel && msg.channelType !== "internal") {
|
|
1853
|
+
loopWarningSent = true;
|
|
1854
|
+
const shouldContinue = await channel.askToContinue(
|
|
1855
|
+
`${softLoop.tool} has been called ${softLoop.count}x in a row. This might be a loop.`,
|
|
1856
|
+
msg.channelId
|
|
1857
|
+
).catch(() => false);
|
|
1858
|
+
if (shouldContinue) {
|
|
1859
|
+
loopDetector.reset();
|
|
1860
|
+
loopWarningSent = false;
|
|
1861
|
+
} else {
|
|
1862
|
+
loopAbortController.abort();
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
if (channel && msg.channelType !== "internal") {
|
|
1866
|
+
if (channel instanceof CLIChannel) {
|
|
1867
|
+
for (const tc of toolCalls) {
|
|
1868
|
+
await channel.sendToolFeedback(tc.toolName, tc.args).catch(() => {
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
} else {
|
|
1872
|
+
await channel.send(` [Using: ${names}]`, msg.channelId).catch(() => {
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1675
1875
|
}
|
|
1676
|
-
await channel.send(` [Using: ${names}]`, msg.channelId).catch(() => {
|
|
1677
|
-
});
|
|
1678
1876
|
}
|
|
1679
1877
|
}
|
|
1680
1878
|
});
|
|
@@ -1706,6 +1904,7 @@ You can override this:
|
|
|
1706
1904
|
messages,
|
|
1707
1905
|
tools: this.capabilities.getTools(),
|
|
1708
1906
|
maxSteps: MAX_STEPS,
|
|
1907
|
+
abortSignal: loopAbortController.signal,
|
|
1709
1908
|
onStepFinish: async ({ toolCalls, text }) => {
|
|
1710
1909
|
if (toolCalls && toolCalls.length > 0) {
|
|
1711
1910
|
const names = toolCalls.map((tc) => tc.toolName).join(", ");
|
|
@@ -1713,13 +1912,43 @@ You can override this:
|
|
|
1713
1912
|
for (const tc of toolCalls) {
|
|
1714
1913
|
loopDetector.record(tc.toolName, tc.args);
|
|
1715
1914
|
}
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1915
|
+
if (toolCalls.some((tc) => tc.toolName === "use_skill")) {
|
|
1916
|
+
loopDetector.reset();
|
|
1917
|
+
}
|
|
1918
|
+
const hardLoop = loopDetector.detectIdentical();
|
|
1919
|
+
if (hardLoop) {
|
|
1920
|
+
logger.warn({ tool: hardLoop.tool, count: hardLoop.count }, "Hard loop detected \u2014 aborting");
|
|
1921
|
+
if (!loopWarningSent && channel && msg.channelType !== "internal") {
|
|
1922
|
+
loopWarningSent = true;
|
|
1923
|
+
await channel.send(`\u26A0 Repeated call detected \u2014 ${hardLoop.tool} called ${hardLoop.count}x with same params. Stopping.`, msg.channelId).catch(() => {
|
|
1924
|
+
});
|
|
1925
|
+
}
|
|
1926
|
+
loopAbortController.abort();
|
|
1927
|
+
}
|
|
1928
|
+
const softLoop = loopDetector.detectSameTool();
|
|
1929
|
+
if (softLoop && !loopWarningSent && channel && msg.channelType !== "internal") {
|
|
1930
|
+
loopWarningSent = true;
|
|
1931
|
+
const shouldContinue = await channel.askToContinue(
|
|
1932
|
+
`${softLoop.tool} has been called ${softLoop.count}x in a row. This might be a loop.`,
|
|
1933
|
+
msg.channelId
|
|
1934
|
+
).catch(() => false);
|
|
1935
|
+
if (shouldContinue) {
|
|
1936
|
+
loopDetector.reset();
|
|
1937
|
+
loopWarningSent = false;
|
|
1938
|
+
} else {
|
|
1939
|
+
loopAbortController.abort();
|
|
1940
|
+
}
|
|
1719
1941
|
}
|
|
1720
1942
|
if (channel && msg.channelType !== "internal") {
|
|
1721
|
-
|
|
1722
|
-
|
|
1943
|
+
if (channel instanceof CLIChannel) {
|
|
1944
|
+
for (const tc of toolCalls) {
|
|
1945
|
+
await channel.sendToolFeedback(tc.toolName, tc.args).catch(() => {
|
|
1946
|
+
});
|
|
1947
|
+
}
|
|
1948
|
+
} else {
|
|
1949
|
+
await channel.send(` [Using: ${names}]`, msg.channelId).catch(() => {
|
|
1950
|
+
});
|
|
1951
|
+
}
|
|
1723
1952
|
}
|
|
1724
1953
|
}
|
|
1725
1954
|
}
|
|
@@ -1729,6 +1958,19 @@ You can override this:
|
|
|
1729
1958
|
this.providers.markSuccess(provider.name);
|
|
1730
1959
|
break;
|
|
1731
1960
|
} catch (err) {
|
|
1961
|
+
if (loopDetector.isHardAborted() || loopAbortController.signal.aborted) {
|
|
1962
|
+
logger.info("Generation aborted due to loop detection \u2014 using partial response");
|
|
1963
|
+
if (!result && streamedText) {
|
|
1964
|
+
result = { text: streamedText, usage: void 0 };
|
|
1965
|
+
}
|
|
1966
|
+
if (!result) {
|
|
1967
|
+
result = { text: "I stopped because I was repeating the same tool calls. What would you like me to do differently?", usage: void 0 };
|
|
1968
|
+
}
|
|
1969
|
+
if (usedProvider) {
|
|
1970
|
+
this.providers.markSuccess(usedProvider.name);
|
|
1971
|
+
}
|
|
1972
|
+
break;
|
|
1973
|
+
}
|
|
1732
1974
|
lastError = err;
|
|
1733
1975
|
logger.warn({ provider: provider.name, err: err.message }, "Provider failed, trying fallback");
|
|
1734
1976
|
if (channel && msg.channelType !== "internal") {
|
|
@@ -2948,6 +3190,32 @@ var TelegramChannel = class extends BaseChannel {
|
|
|
2948
3190
|
}, 12e4);
|
|
2949
3191
|
});
|
|
2950
3192
|
}
|
|
3193
|
+
async askToContinue(question, targetId) {
|
|
3194
|
+
const chatIds = this.resolveTargetChatIds(targetId);
|
|
3195
|
+
const chatId = chatIds[0];
|
|
3196
|
+
if (!chatId || !this.bot) return false;
|
|
3197
|
+
const id = `loop_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
3198
|
+
const keyboard = new InlineKeyboard().text("Continue", `${id}:yes`).text("Stop", `${id}:no`);
|
|
3199
|
+
try {
|
|
3200
|
+
await this.bot.api.sendMessage(chatId, mdToTelegram(question), {
|
|
3201
|
+
parse_mode: "HTML",
|
|
3202
|
+
reply_markup: keyboard
|
|
3203
|
+
});
|
|
3204
|
+
} catch {
|
|
3205
|
+
await this.bot.api.sendMessage(chatId, question, {
|
|
3206
|
+
reply_markup: keyboard
|
|
3207
|
+
});
|
|
3208
|
+
}
|
|
3209
|
+
return new Promise((resolve13) => {
|
|
3210
|
+
this.pendingApprovals.set(`${id}:yes`, () => resolve13(true));
|
|
3211
|
+
this.pendingApprovals.set(`${id}:no`, () => resolve13(false));
|
|
3212
|
+
setTimeout(() => {
|
|
3213
|
+
this.pendingApprovals.delete(`${id}:yes`);
|
|
3214
|
+
this.pendingApprovals.delete(`${id}:no`);
|
|
3215
|
+
resolve13(false);
|
|
3216
|
+
}, 12e4);
|
|
3217
|
+
});
|
|
3218
|
+
}
|
|
2951
3219
|
async handleAccessRequest(userId, chatId, username, firstName) {
|
|
2952
3220
|
const approvedUser = findTelegramApprovedUser(this.config, userId);
|
|
2953
3221
|
if (approvedUser) {
|
|
@@ -3962,7 +4230,7 @@ function createEditFileTool(permissions, getCwd) {
|
|
|
3962
4230
|
import { tool as tool7 } from "ai";
|
|
3963
4231
|
import { z as z7 } from "zod";
|
|
3964
4232
|
import { existsSync as existsSync12, statSync as statSync2 } from "fs";
|
|
3965
|
-
import { resolve as resolve9, basename, isAbsolute as isAbsolute7 } from "path";
|
|
4233
|
+
import { resolve as resolve9, basename as basename2, isAbsolute as isAbsolute7 } from "path";
|
|
3966
4234
|
function createSendFileTool(permissions, getCwd, sendFile) {
|
|
3967
4235
|
return tool7({
|
|
3968
4236
|
description: "Send a file to the user. On Telegram the file is uploaded as an attachment to the relevant approved recipients. On CLI the file path and size are displayed. The path must be within an allowed read scope.",
|
|
@@ -3988,7 +4256,7 @@ function createSendFileTool(permissions, getCwd, sendFile) {
|
|
|
3988
4256
|
}
|
|
3989
4257
|
try {
|
|
3990
4258
|
await sendFile(resolved);
|
|
3991
|
-
const filename =
|
|
4259
|
+
const filename = basename2(resolved);
|
|
3992
4260
|
const sizeStr = stat.size > 1024 * 1024 ? `${(stat.size / (1024 * 1024)).toFixed(1)}MB` : `${Math.round(stat.size / 1024)}KB`;
|
|
3993
4261
|
return `File sent: ${filename} (${sizeStr})`;
|
|
3994
4262
|
} catch (err) {
|
|
@@ -6810,6 +7078,7 @@ async function runAgent(isDaemon = false) {
|
|
|
6810
7078
|
console.log(chalk7.green(` ${name} is live. Type a message and press Enter.`));
|
|
6811
7079
|
console.log(chalk7.dim(" Ctrl+C to exit \xB7 /help for commands"));
|
|
6812
7080
|
console.log("");
|
|
7081
|
+
cliChannel?.showPrompt();
|
|
6813
7082
|
} else {
|
|
6814
7083
|
logger.info({ channels: activeCh, tools: toolNames }, "Mercury is live (daemon mode)");
|
|
6815
7084
|
}
|