aether-code 0.43.0 → 0.43.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/agent.js +39 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aether-code",
3
- "version": "0.43.0",
3
+ "version": "0.43.2",
4
4
  "description": "Uncensored AI coding agent for your terminal — Claude Code alternative with MCP support. Reads code, writes files, runs commands. Drives IDA Pro, Roblox Studio, Wireshark, Blender, and any MCP server. No refusal layer.",
5
5
  "homepage": "https://trynoguard.com",
6
6
  "repository": {
package/src/agent.js CHANGED
@@ -13,6 +13,9 @@ import { scanProject, detectVerifyCommand, detectBuildCommand } from "./project-
13
13
  import { c, divider, turn, toolLabel, toolSummary, makeTokenStripper, errorLine, startSpinner } from "./render.js";
14
14
 
15
15
  const DEFAULT_MAX_TURNS = 25;
16
+ const DEFAULT_MAX_CREDITS = 500;
17
+ const MAX_TOTAL_TOOL_CALLS = 40;
18
+ const MAX_CONSECUTIVE_TOOL_ONLY = 6;
16
19
 
17
20
  // Rolling history window (messages) sent upstream per turn. The session keeps
18
21
  // the full transcript locally, but we only forward the anchor (first message,
@@ -86,6 +89,7 @@ export async function runAgent({
86
89
  autoYes = false,
87
90
  unsafePaths = false,
88
91
  maxTurns = DEFAULT_MAX_TURNS,
92
+ maxCredits = DEFAULT_MAX_CREDITS,
89
93
  model = null, // null = server default (gemma). Premium models gated server-side.
90
94
  onTokens = () => {},
91
95
  // Optional MCPManager. When provided, its tools are merged into the agent's
@@ -141,6 +145,8 @@ export async function runAgent({
141
145
  let appliedNothingNudges = 0;
142
146
  let verifyRounds = 0;
143
147
  let buildRounds = 0;
148
+ let consecutiveToolOnlyTurns = 0;
149
+ let totalToolCalls = 0;
144
150
 
145
151
  for (let i = 0; i < maxTurns; i++) {
146
152
  // No turn header and no leading blank here — each step (assistant text and
@@ -215,13 +221,17 @@ export async function runAgent({
215
221
  process.stdout.write(tail);
216
222
  }
217
223
  if (lastWasText) process.stdout.write("\n");
218
- totalCredits += res.creditsCharged ?? 0;
224
+ const turnCredits = res.creditsCharged ?? 0;
225
+ totalCredits += turnCredits;
219
226
  totalIn += res.usage?.prompt_tokens ?? 0;
220
227
  totalOut += res.usage?.completion_tokens ?? 0;
221
228
  if (typeof res.balanceAfter === "number") lastBalance = res.balanceAfter;
222
229
  onTokens({ totalCredits, totalIn, totalOut, balance: lastBalance });
223
- // Per-turn cost line removed for a cleaner look — the session summary at the
224
- // end carries the totals.
230
+
231
+ // Per-turn cost — visible so users catch runaway loops early
232
+ const inK = ((res.usage?.prompt_tokens ?? 0) / 1000).toFixed(1);
233
+ const outTok = res.usage?.completion_tokens ?? 0;
234
+ console.log(c.gray(` ${inK}k→${outTok} tokens · ${turnCredits} credits (total: ${totalCredits})`));
225
235
 
226
236
  // Push assistant message into history
227
237
  messages.push({
@@ -231,6 +241,31 @@ export async function runAgent({
231
241
  });
232
242
 
233
243
  const toolCalls = res.message.tool_calls ?? [];
244
+ const hasTextContent = !!(res.message.content && res.message.content.trim());
245
+
246
+ if (toolCalls.length > 0 && !hasTextContent) {
247
+ consecutiveToolOnlyTurns++;
248
+ } else if (hasTextContent) {
249
+ consecutiveToolOnlyTurns = 0;
250
+ }
251
+
252
+ // Credit cap — hard stop to prevent runaway cost
253
+ if (maxCredits > 0 && totalCredits >= maxCredits) {
254
+ console.log(c.red(`\n[Credit budget exhausted: ${totalCredits} credits used, limit was ${maxCredits}. Stopping.]`));
255
+ return { ok: true, totalCredits, totalIn, totalOut, balance: lastBalance, messages };
256
+ }
257
+
258
+ // Tool call count cap
259
+ if (toolCalls.length > 0 && totalToolCalls + toolCalls.length > MAX_TOTAL_TOOL_CALLS) {
260
+ console.log(c.red(`\n[Tool call limit reached (${MAX_TOTAL_TOOL_CALLS}). Stopping.]`));
261
+ return { ok: true, totalCredits, totalIn, totalOut, balance: lastBalance, messages };
262
+ }
263
+
264
+ // Consecutive tool-only turns cap (model stuck in a tool loop with no text)
265
+ if (consecutiveToolOnlyTurns >= MAX_CONSECUTIVE_TOOL_ONLY) {
266
+ console.log(c.red(`\n[Too many consecutive tool-only turns (${MAX_CONSECUTIVE_TOOL_ONLY}). Stopping.]`));
267
+ return { ok: true, totalCredits, totalIn, totalOut, balance: lastBalance, messages };
268
+ }
234
269
  if (toolCalls.length === 0) {
235
270
  // Completeness guard: on a modification task, if the model read files it
236
271
  // never edited, it likely either described the fix instead of applying it
@@ -343,6 +378,7 @@ export async function runAgent({
343
378
 
344
379
  // Execute each tool call. Show the actual args (now that we have them
345
380
  // fully assembled) and run.
381
+ totalToolCalls += toolCalls.length;
346
382
  for (const call of toolCalls) {
347
383
  let args = {};
348
384
  try { args = JSON.parse(call.function.arguments || "{}"); } catch { /* leave empty */ }