@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 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", 5e4)
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 = 10;
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 consecutiveCount = 0;
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
- consecutiveCount++;
1414
+ identicalCount++;
1414
1415
  } else {
1415
1416
  break;
1416
1417
  }
1417
1418
  }
1418
- if (consecutiveCount >= 3) {
1419
- return { tool: last.tool, count: consecutiveCount };
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 toolCount = 0;
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
- toolCount++;
1431
+ sameToolCount++;
1426
1432
  } else {
1427
1433
  break;
1428
1434
  }
1429
1435
  }
1430
- if (toolCount >= 4) {
1431
- return { tool: lastTool, count: toolCount };
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] 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.`;
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") {