@cosmicstack/mercury-agent 0.5.0 → 0.5.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/dist/index.js +54 -13
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
}
|
|
@@ -1396,7 +1396,8 @@ var CLIChannel = class extends BaseChannel {
|
|
|
1396
1396
|
// src/core/agent.ts
|
|
1397
1397
|
var ToolCallLoopDetector = class {
|
|
1398
1398
|
recentCalls = [];
|
|
1399
|
-
maxEntries =
|
|
1399
|
+
maxEntries = 16;
|
|
1400
|
+
aborted = false;
|
|
1400
1401
|
record(toolName, params) {
|
|
1401
1402
|
const paramsKey = JSON.stringify(params).slice(0, 100);
|
|
1402
1403
|
this.recentCalls.push({ tool: toolName, params: paramsKey });
|
|
@@ -1407,33 +1408,47 @@ var ToolCallLoopDetector = class {
|
|
|
1407
1408
|
detect() {
|
|
1408
1409
|
if (this.recentCalls.length < 3) return null;
|
|
1409
1410
|
const last = this.recentCalls[this.recentCalls.length - 1];
|
|
1410
|
-
let
|
|
1411
|
+
let identicalCount = 0;
|
|
1411
1412
|
for (let i = this.recentCalls.length - 1; i >= 0; i--) {
|
|
1412
1413
|
if (this.recentCalls[i].tool === last.tool && this.recentCalls[i].params === last.params) {
|
|
1413
|
-
|
|
1414
|
+
identicalCount++;
|
|
1414
1415
|
} else {
|
|
1415
1416
|
break;
|
|
1416
1417
|
}
|
|
1417
1418
|
}
|
|
1418
|
-
if (
|
|
1419
|
-
|
|
1419
|
+
if (identicalCount >= 3) {
|
|
1420
|
+
this.aborted = true;
|
|
1421
|
+
return {
|
|
1422
|
+
tool: last.tool,
|
|
1423
|
+
count: identicalCount,
|
|
1424
|
+
message: `You called "${last.tool}" ${identicalCount} times with identical parameters and got the same result. Stop repeating this call entirely.`
|
|
1425
|
+
};
|
|
1420
1426
|
}
|
|
1421
1427
|
const lastTool = last.tool;
|
|
1422
|
-
let
|
|
1428
|
+
let sameToolCount = 0;
|
|
1423
1429
|
for (let i = this.recentCalls.length - 1; i >= 0; i--) {
|
|
1424
1430
|
if (this.recentCalls[i].tool === lastTool) {
|
|
1425
|
-
|
|
1431
|
+
sameToolCount++;
|
|
1426
1432
|
} else {
|
|
1427
1433
|
break;
|
|
1428
1434
|
}
|
|
1429
1435
|
}
|
|
1430
|
-
if (
|
|
1431
|
-
|
|
1436
|
+
if (sameToolCount >= 3) {
|
|
1437
|
+
this.aborted = true;
|
|
1438
|
+
return {
|
|
1439
|
+
tool: lastTool,
|
|
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.`
|
|
1442
|
+
};
|
|
1432
1443
|
}
|
|
1433
1444
|
return null;
|
|
1434
1445
|
}
|
|
1446
|
+
isAborted() {
|
|
1447
|
+
return this.aborted;
|
|
1448
|
+
}
|
|
1435
1449
|
reset() {
|
|
1436
1450
|
this.recentCalls = [];
|
|
1451
|
+
this.aborted = false;
|
|
1437
1452
|
}
|
|
1438
1453
|
};
|
|
1439
1454
|
var MAX_STEPS = 10;
|
|
@@ -1613,7 +1628,7 @@ You can override this:
|
|
|
1613
1628
|
if (toolCalls.length >= 3) {
|
|
1614
1629
|
const last3 = toolCalls.slice(-3);
|
|
1615
1630
|
if (last3[0] === last3[1] && last3[1] === last3[2]) {
|
|
1616
|
-
loopWarning = `[SYSTEM WARNING]
|
|
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.`;
|
|
1617
1632
|
}
|
|
1618
1633
|
}
|
|
1619
1634
|
}
|
|
@@ -1651,6 +1666,8 @@ You can override this:
|
|
|
1651
1666
|
let lastError = null;
|
|
1652
1667
|
let streamedText = "";
|
|
1653
1668
|
const loopDetector = new ToolCallLoopDetector();
|
|
1669
|
+
const loopAbortController = new AbortController();
|
|
1670
|
+
let loopWarningSent = false;
|
|
1654
1671
|
const canStream = msg.channelType === "cli" || msg.channelType === "telegram" && this.telegramStreaming;
|
|
1655
1672
|
for (const provider of fallbackIterator) {
|
|
1656
1673
|
try {
|
|
@@ -1662,6 +1679,7 @@ You can override this:
|
|
|
1662
1679
|
messages,
|
|
1663
1680
|
tools: this.capabilities.getTools(),
|
|
1664
1681
|
maxSteps: MAX_STEPS,
|
|
1682
|
+
abortSignal: loopAbortController.signal,
|
|
1665
1683
|
onStepFinish: async ({ toolCalls }) => {
|
|
1666
1684
|
if (toolCalls && toolCalls.length > 0) {
|
|
1667
1685
|
const names = toolCalls.map((tc) => tc.toolName).join(", ");
|
|
@@ -1671,7 +1689,13 @@ You can override this:
|
|
|
1671
1689
|
}
|
|
1672
1690
|
const loop = loopDetector.detect();
|
|
1673
1691
|
if (loop) {
|
|
1674
|
-
logger.warn({ tool: loop.tool, count: loop.count }, "Tool call loop detected");
|
|
1692
|
+
logger.warn({ tool: loop.tool, count: loop.count }, "Tool call loop detected \u2014 aborting generation");
|
|
1693
|
+
if (!loopWarningSent && channel && msg.channelType !== "internal") {
|
|
1694
|
+
loopWarningSent = true;
|
|
1695
|
+
await channel.send(`\u26A0 Loop detected \u2014 ${loop.tool} called ${loop.count}x in a row. Stopping to save tokens.`, msg.channelId).catch(() => {
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
loopAbortController.abort();
|
|
1675
1699
|
}
|
|
1676
1700
|
await channel.send(` [Using: ${names}]`, msg.channelId).catch(() => {
|
|
1677
1701
|
});
|
|
@@ -1706,6 +1730,7 @@ You can override this:
|
|
|
1706
1730
|
messages,
|
|
1707
1731
|
tools: this.capabilities.getTools(),
|
|
1708
1732
|
maxSteps: MAX_STEPS,
|
|
1733
|
+
abortSignal: loopAbortController.signal,
|
|
1709
1734
|
onStepFinish: async ({ toolCalls, text }) => {
|
|
1710
1735
|
if (toolCalls && toolCalls.length > 0) {
|
|
1711
1736
|
const names = toolCalls.map((tc) => tc.toolName).join(", ");
|
|
@@ -1715,7 +1740,13 @@ You can override this:
|
|
|
1715
1740
|
}
|
|
1716
1741
|
const loop = loopDetector.detect();
|
|
1717
1742
|
if (loop) {
|
|
1718
|
-
logger.warn({ tool: loop.tool, count: loop.count }, "Tool call loop detected");
|
|
1743
|
+
logger.warn({ tool: loop.tool, count: loop.count }, "Tool call loop detected \u2014 aborting generation");
|
|
1744
|
+
if (!loopWarningSent && channel && msg.channelType !== "internal") {
|
|
1745
|
+
loopWarningSent = true;
|
|
1746
|
+
await channel.send(`\u26A0 Loop detected \u2014 ${loop.tool} called ${loop.count}x in a row. Stopping to save tokens.`, msg.channelId).catch(() => {
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
loopAbortController.abort();
|
|
1719
1750
|
}
|
|
1720
1751
|
if (channel && msg.channelType !== "internal") {
|
|
1721
1752
|
await channel.send(` [Using: ${names}]`, msg.channelId).catch(() => {
|
|
@@ -1729,6 +1760,16 @@ You can override this:
|
|
|
1729
1760
|
this.providers.markSuccess(provider.name);
|
|
1730
1761
|
break;
|
|
1731
1762
|
} catch (err) {
|
|
1763
|
+
if (loopDetector.isAborted()) {
|
|
1764
|
+
logger.info("Generation aborted due to loop detection \u2014 using partial response");
|
|
1765
|
+
if (!result && streamedText) {
|
|
1766
|
+
result = { text: streamedText, usage: void 0 };
|
|
1767
|
+
}
|
|
1768
|
+
if (usedProvider) {
|
|
1769
|
+
this.providers.markSuccess(usedProvider.name);
|
|
1770
|
+
}
|
|
1771
|
+
break;
|
|
1772
|
+
}
|
|
1732
1773
|
lastError = err;
|
|
1733
1774
|
logger.warn({ provider: provider.name, err: err.message }, "Provider failed, trying fallback");
|
|
1734
1775
|
if (channel && msg.channelType !== "internal") {
|