@cosmicstack/mercury-agent 0.5.1 → 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 +296 -68
- 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
|
@@ -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,13 +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 =
|
|
1400
|
-
|
|
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
|
+
}
|
|
1401
1570
|
record(toolName, params) {
|
|
1402
1571
|
const paramsKey = JSON.stringify(params).slice(0, 100);
|
|
1403
1572
|
this.recentCalls.push({ tool: toolName, params: paramsKey });
|
|
@@ -1405,7 +1574,7 @@ var ToolCallLoopDetector = class {
|
|
|
1405
1574
|
this.recentCalls.shift();
|
|
1406
1575
|
}
|
|
1407
1576
|
}
|
|
1408
|
-
|
|
1577
|
+
detectIdentical() {
|
|
1409
1578
|
if (this.recentCalls.length < 3) return null;
|
|
1410
1579
|
const last = this.recentCalls[this.recentCalls.length - 1];
|
|
1411
1580
|
let identicalCount = 0;
|
|
@@ -1416,39 +1585,42 @@ var ToolCallLoopDetector = class {
|
|
|
1416
1585
|
break;
|
|
1417
1586
|
}
|
|
1418
1587
|
}
|
|
1419
|
-
if (identicalCount >=
|
|
1420
|
-
this.
|
|
1588
|
+
if (identicalCount >= _ToolCallLoopDetector.getIdenticalThreshold()) {
|
|
1589
|
+
this.hardAborted = true;
|
|
1421
1590
|
return {
|
|
1422
1591
|
tool: last.tool,
|
|
1423
1592
|
count: identicalCount,
|
|
1424
|
-
message: `You called "${last.tool}" ${identicalCount} times with identical parameters and got the same result.
|
|
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.`
|
|
1425
1594
|
};
|
|
1426
1595
|
}
|
|
1427
|
-
|
|
1596
|
+
return null;
|
|
1597
|
+
}
|
|
1598
|
+
detectSameTool() {
|
|
1599
|
+
if (this.recentCalls.length < 4) return null;
|
|
1600
|
+
const last = this.recentCalls[this.recentCalls.length - 1];
|
|
1428
1601
|
let sameToolCount = 0;
|
|
1429
1602
|
for (let i = this.recentCalls.length - 1; i >= 0; i--) {
|
|
1430
|
-
if (this.recentCalls[i].tool ===
|
|
1603
|
+
if (this.recentCalls[i].tool === last.tool) {
|
|
1431
1604
|
sameToolCount++;
|
|
1432
1605
|
} else {
|
|
1433
1606
|
break;
|
|
1434
1607
|
}
|
|
1435
1608
|
}
|
|
1436
|
-
|
|
1437
|
-
|
|
1609
|
+
const threshold = _ToolCallLoopDetector.getSameToolThreshold(last.tool);
|
|
1610
|
+
if (sameToolCount >= threshold) {
|
|
1438
1611
|
return {
|
|
1439
|
-
tool:
|
|
1440
|
-
count: sameToolCount
|
|
1441
|
-
message: `You called "${lastTool}" ${sameToolCount} times in a row with slightly different parameters and it isn't working. Stop \u2014 the approach is wrong. Step back, tell the user what you tried and what failed, and suggest alternatives instead of retrying.`
|
|
1612
|
+
tool: last.tool,
|
|
1613
|
+
count: sameToolCount
|
|
1442
1614
|
};
|
|
1443
1615
|
}
|
|
1444
1616
|
return null;
|
|
1445
1617
|
}
|
|
1446
|
-
|
|
1447
|
-
return this.
|
|
1618
|
+
isHardAborted() {
|
|
1619
|
+
return this.hardAborted;
|
|
1448
1620
|
}
|
|
1449
1621
|
reset() {
|
|
1450
1622
|
this.recentCalls = [];
|
|
1451
|
-
this.
|
|
1623
|
+
this.hardAborted = false;
|
|
1452
1624
|
}
|
|
1453
1625
|
};
|
|
1454
1626
|
var MAX_STEPS = 10;
|
|
@@ -1612,30 +1784,6 @@ You can override this:
|
|
|
1612
1784
|
const recentMemory = this.shortTerm.getRecent(msg.channelId, 10);
|
|
1613
1785
|
const relevantFacts = this.longTerm.search(msg.content, 3);
|
|
1614
1786
|
const messages = [];
|
|
1615
|
-
const recentSteps = this.shortTerm.getRecent(msg.channelId, 4);
|
|
1616
|
-
let loopWarning = null;
|
|
1617
|
-
if (recentSteps.length >= 3) {
|
|
1618
|
-
const toolCallPattern = /\[Using: (.+?)\]/g;
|
|
1619
|
-
const toolCalls = [];
|
|
1620
|
-
for (const m of recentSteps) {
|
|
1621
|
-
if (m.role === "assistant") {
|
|
1622
|
-
let match;
|
|
1623
|
-
while ((match = toolCallPattern.exec(m.content)) !== null) {
|
|
1624
|
-
toolCalls.push(match[1]);
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
}
|
|
1628
|
-
if (toolCalls.length >= 3) {
|
|
1629
|
-
const last3 = toolCalls.slice(-3);
|
|
1630
|
-
if (last3[0] === last3[1] && last3[1] === last3[2]) {
|
|
1631
|
-
loopWarning = `[SYSTEM WARNING] In previous turns you called ${last3[0]} repeatedly. Do NOT call it again. If something failed, explain the failure to the user and suggest alternatives.`;
|
|
1632
|
-
}
|
|
1633
|
-
}
|
|
1634
|
-
}
|
|
1635
|
-
if (loopWarning) {
|
|
1636
|
-
messages.push({ role: "user", content: loopWarning });
|
|
1637
|
-
messages.push({ role: "assistant", content: "Understood. I will try a different approach." });
|
|
1638
|
-
}
|
|
1639
1787
|
if (relevantFacts.length > 0) {
|
|
1640
1788
|
messages.push({
|
|
1641
1789
|
role: "user",
|
|
@@ -1687,18 +1835,44 @@ You can override this:
|
|
|
1687
1835
|
for (const tc of toolCalls) {
|
|
1688
1836
|
loopDetector.record(tc.toolName, tc.args);
|
|
1689
1837
|
}
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
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");
|
|
1693
1844
|
if (!loopWarningSent && channel && msg.channelType !== "internal") {
|
|
1694
1845
|
loopWarningSent = true;
|
|
1695
|
-
await channel.send(`\u26A0
|
|
1846
|
+
await channel.send(`\u26A0 Repeated call detected \u2014 ${hardLoop.tool} called ${hardLoop.count}x with same params. Stopping.`, msg.channelId).catch(() => {
|
|
1696
1847
|
});
|
|
1697
1848
|
}
|
|
1698
1849
|
loopAbortController.abort();
|
|
1699
1850
|
}
|
|
1700
|
-
|
|
1701
|
-
|
|
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
|
+
}
|
|
1875
|
+
}
|
|
1702
1876
|
}
|
|
1703
1877
|
}
|
|
1704
1878
|
});
|
|
@@ -1738,19 +1912,43 @@ You can override this:
|
|
|
1738
1912
|
for (const tc of toolCalls) {
|
|
1739
1913
|
loopDetector.record(tc.toolName, tc.args);
|
|
1740
1914
|
}
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
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");
|
|
1744
1921
|
if (!loopWarningSent && channel && msg.channelType !== "internal") {
|
|
1745
1922
|
loopWarningSent = true;
|
|
1746
|
-
await channel.send(`\u26A0
|
|
1923
|
+
await channel.send(`\u26A0 Repeated call detected \u2014 ${hardLoop.tool} called ${hardLoop.count}x with same params. Stopping.`, msg.channelId).catch(() => {
|
|
1747
1924
|
});
|
|
1748
1925
|
}
|
|
1749
1926
|
loopAbortController.abort();
|
|
1750
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
|
+
}
|
|
1941
|
+
}
|
|
1751
1942
|
if (channel && msg.channelType !== "internal") {
|
|
1752
|
-
|
|
1753
|
-
|
|
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
|
+
}
|
|
1754
1952
|
}
|
|
1755
1953
|
}
|
|
1756
1954
|
}
|
|
@@ -1760,11 +1958,14 @@ You can override this:
|
|
|
1760
1958
|
this.providers.markSuccess(provider.name);
|
|
1761
1959
|
break;
|
|
1762
1960
|
} catch (err) {
|
|
1763
|
-
if (loopDetector.
|
|
1961
|
+
if (loopDetector.isHardAborted() || loopAbortController.signal.aborted) {
|
|
1764
1962
|
logger.info("Generation aborted due to loop detection \u2014 using partial response");
|
|
1765
1963
|
if (!result && streamedText) {
|
|
1766
1964
|
result = { text: streamedText, usage: void 0 };
|
|
1767
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
|
+
}
|
|
1768
1969
|
if (usedProvider) {
|
|
1769
1970
|
this.providers.markSuccess(usedProvider.name);
|
|
1770
1971
|
}
|
|
@@ -2989,6 +3190,32 @@ var TelegramChannel = class extends BaseChannel {
|
|
|
2989
3190
|
}, 12e4);
|
|
2990
3191
|
});
|
|
2991
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
|
+
}
|
|
2992
3219
|
async handleAccessRequest(userId, chatId, username, firstName) {
|
|
2993
3220
|
const approvedUser = findTelegramApprovedUser(this.config, userId);
|
|
2994
3221
|
if (approvedUser) {
|
|
@@ -4003,7 +4230,7 @@ function createEditFileTool(permissions, getCwd) {
|
|
|
4003
4230
|
import { tool as tool7 } from "ai";
|
|
4004
4231
|
import { z as z7 } from "zod";
|
|
4005
4232
|
import { existsSync as existsSync12, statSync as statSync2 } from "fs";
|
|
4006
|
-
import { resolve as resolve9, basename, isAbsolute as isAbsolute7 } from "path";
|
|
4233
|
+
import { resolve as resolve9, basename as basename2, isAbsolute as isAbsolute7 } from "path";
|
|
4007
4234
|
function createSendFileTool(permissions, getCwd, sendFile) {
|
|
4008
4235
|
return tool7({
|
|
4009
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.",
|
|
@@ -4029,7 +4256,7 @@ function createSendFileTool(permissions, getCwd, sendFile) {
|
|
|
4029
4256
|
}
|
|
4030
4257
|
try {
|
|
4031
4258
|
await sendFile(resolved);
|
|
4032
|
-
const filename =
|
|
4259
|
+
const filename = basename2(resolved);
|
|
4033
4260
|
const sizeStr = stat.size > 1024 * 1024 ? `${(stat.size / (1024 * 1024)).toFixed(1)}MB` : `${Math.round(stat.size / 1024)}KB`;
|
|
4034
4261
|
return `File sent: ${filename} (${sizeStr})`;
|
|
4035
4262
|
} catch (err) {
|
|
@@ -6851,6 +7078,7 @@ async function runAgent(isDaemon = false) {
|
|
|
6851
7078
|
console.log(chalk7.green(` ${name} is live. Type a message and press Enter.`));
|
|
6852
7079
|
console.log(chalk7.dim(" Ctrl+C to exit \xB7 /help for commands"));
|
|
6853
7080
|
console.log("");
|
|
7081
|
+
cliChannel?.showPrompt();
|
|
6854
7082
|
} else {
|
|
6855
7083
|
logger.info({ channels: activeCh, tools: toolNames }, "Mercury is live (daemon mode)");
|
|
6856
7084
|
}
|