micro-models-agent 0.16.1 → 0.16.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.
@@ -113,6 +113,8 @@ export class Agent {
113
113
  let apiPromptTokens = 0;
114
114
  let apiCompletionTokens = 0;
115
115
  const MAX_HALLUCINATION_RETRIES = 3;
116
+ let consecutiveToolFailures = 0;
117
+ const MAX_CONSECUTIVE_TOOL_FAILURES = 5;
116
118
  while (iteration < config.maxToolIterations) {
117
119
  iteration++;
118
120
  pluginManager.runOnBeforeThink({
@@ -246,6 +248,7 @@ export class Agent {
246
248
  })),
247
249
  });
248
250
  const summaries = [];
251
+ let anyToolFailed = false;
249
252
  for (const call of toolCalls) {
250
253
  this.setScope();
251
254
  const startTime = Date.now();
@@ -258,6 +261,8 @@ export class Agent {
258
261
  slog.logToolCall(call, iteration);
259
262
  const result = await toolExecutor.execute(call);
260
263
  const duration = Date.now() - startTime;
264
+ if (!result.success)
265
+ anyToolFailed = true;
261
266
  pluginManager.runOnToolEnd({ iteration, logger }, { id: call.id, name: call.name, arguments: call.arguments }, result, duration);
262
267
  if (result.display) {
263
268
  onMeta?.("\n" + result.display + "\n");
@@ -292,6 +297,22 @@ export class Agent {
292
297
  logger.debug("Context compacted after tool result");
293
298
  }
294
299
  }
300
+ if (anyToolFailed) {
301
+ consecutiveToolFailures++;
302
+ }
303
+ else {
304
+ consecutiveToolFailures = 0;
305
+ }
306
+ if (consecutiveToolFailures >= MAX_CONSECUTIVE_TOOL_FAILURES) {
307
+ const recoveryMsg = t("exec.consecutive_failures_recovery", { count: consecutiveToolFailures });
308
+ logger.warn(`Consecutive tool failures: ${consecutiveToolFailures}`);
309
+ const taskSnippet = input.length > 200 ? input.slice(0, 200) + "..." : input;
310
+ const taskReminder = t("exec.task_reminder", { task: taskSnippet });
311
+ contextManager.addMessage({
312
+ role: "user",
313
+ content: `<system-summary>${recoveryMsg}\n${taskReminder}</system-summary>`,
314
+ });
315
+ }
295
316
  contextManager.addMessage({
296
317
  role: "user",
297
318
  content: `<system-summary>${summaries.join("\n")}</system-summary>`,
package/dist/i18n/en.json CHANGED
@@ -375,6 +375,8 @@
375
375
  "exec.tool_errors": "Tool {tool} failed {count} times. Suggest alternative approach.",
376
376
  "exec.tool_errors_recovery": "[Tool {tool} failed {count} times. Stop using this tool and try a different approach or explain the issue to the user.]",
377
377
  "exec.repetitive_tool": "[You called {tool} {count} times in a row with the same arguments and result. Stop repeating — try a different approach: create files, change arguments, or explain the problem to the user.]",
378
+ "exec.consecutive_failures_recovery": "[{count} consecutive tool calls failed. You are going in circles — stop trying commands that keep failing. Explain the problem to the user, or try a completely different approach: write files directly, use a different tool, or break the task into smaller parts.]",
379
+ "exec.task_reminder": "[Remember the original task: {task}. Do not switch to unrelated tasks. If you cannot complete the original task, explain why to the user and ask for guidance.]",
378
380
  "exec.plan_warning": "[\u26a0 Warning: you are working on step {step} - \"{description}\". Your {tool} call touches files not mentioned in the current step. Focus on the current step or update the plan via plan update.]",
379
381
  "exec.off_track": "[\u26a0 Outside plan: step {stepId} is \"{description}\", but you're using {tool} on a different path. Explain or return.]",
380
382
  "exec.audit_pass": "[\u2713] Task complete: {done}/{total} steps done, {files} files verified",
package/dist/i18n/ru.json CHANGED
@@ -375,6 +375,8 @@
375
375
  "exec.tool_errors": "Инструмент {tool} сбоил {count} раз. Предложите другой подход.",
376
376
  "exec.tool_errors_recovery": "[Инструмент {tool} сбоил {count} раз. Прекратите использовать этот инструмент и попробуйте другой подход или объясните проблему пользователю.]",
377
377
  "exec.repetitive_tool": "[Вы вызвали {tool} {count} раз подряд с одинаковыми аргументами и результатом. Прекратите повторять — попробуйте другой подход: создайте файлы, измените аргументы или объясните пользователю проблему.]",
378
+ "exec.consecutive_failures_recovery": "[{count} последовательных вызовов тулов завершились ошибкой. Вы ходите по кругу — прекратите пытаться выполнить команды, которые постоянно падают. Объясните проблему пользователю или попробуйте совершенно другой подход: напишите файлы напрямую, используйте другой инструмент или разбейте задачу на части.]",
379
+ "exec.task_reminder": "[Напоминание о задаче: {task}. Не переключайтесь на посторонние задачи. Если не можете выполнить задачу — объясните почему и спросите совета.]",
378
380
  "exec.plan_warning": "[⚠ Внимание: вы работаете над шагом {step} — \"{description}\". Ваш вызов {tool} касается файлов, не упомянутых в текущем шаге. Сосредоточьтесь на текущем шаге или обновите план через plan update.]",
379
381
  "exec.off_track": "[⚠ Вне плана: шаг {stepId} — \"{description}\", но вы используете {tool} для другого пути. Объясните или вернитесь.]",
380
382
  "exec.audit_pass": "[✓] Задача выполнена: {done}/{total} шагов, {files} файлов проверено",
@@ -271,7 +271,10 @@ export class ExecutionModule {
271
271
  },
272
272
  onAfterTool: (ctx, call, result) => {
273
273
  if (!result.success) {
274
- this.stuckDetector.recordToolError("tool");
274
+ this.stuckDetector.recordToolError(call.name);
275
+ }
276
+ else {
277
+ this.stuckDetector.recordToolSuccess();
275
278
  }
276
279
  if (this.tracker &&
277
280
  result.success &&
@@ -9,6 +9,8 @@ export class StuckDetector {
9
9
  recentToolCalls = [];
10
10
  maxRecentCalls = 10;
11
11
  repetitionThreshold = 3;
12
+ consecutiveFailures = 0;
13
+ lastFailedTool = '';
12
14
  constructor(threshold = 8, errorThreshold = 3) {
13
15
  this.threshold = threshold;
14
16
  this.errorThreshold = errorThreshold;
@@ -31,6 +33,12 @@ export class StuckDetector {
31
33
  }
32
34
  recordToolError(toolName) {
33
35
  this.toolErrors.set(toolName, (this.toolErrors.get(toolName) || 0) + 1);
36
+ this.consecutiveFailures++;
37
+ this.lastFailedTool = toolName;
38
+ }
39
+ recordToolSuccess() {
40
+ this.consecutiveFailures = 0;
41
+ this.lastFailedTool = '';
34
42
  }
35
43
  setCurrentStep(stepId, description) {
36
44
  if (stepId !== this.currentStepId) {
@@ -63,6 +71,12 @@ export class StuckDetector {
63
71
  }
64
72
  return count >= this.repetitionThreshold;
65
73
  }
74
+ hasConsecutiveFailures() {
75
+ return this.consecutiveFailures >= this.errorThreshold;
76
+ }
77
+ getConsecutiveFailuresCount() {
78
+ return this.consecutiveFailures;
79
+ }
66
80
  getRepetitiveToolMessage() {
67
81
  if (!this.hasRepetitiveToolCalls())
68
82
  return '';
@@ -83,6 +97,9 @@ export class StuckDetector {
83
97
  if (this.isStuck()) {
84
98
  return t('exec.stuck_recovery', { iterations: this.iterationsOnCurrentStep });
85
99
  }
100
+ if (this.hasConsecutiveFailures()) {
101
+ return t('exec.consecutive_failures_recovery', { count: this.consecutiveFailures });
102
+ }
86
103
  if (this.hasRepetitiveToolCalls()) {
87
104
  return this.getRepetitiveToolMessage();
88
105
  }
@@ -109,5 +126,7 @@ export class StuckDetector {
109
126
  reset() {
110
127
  this.iterationsOnCurrentStep = 0;
111
128
  this.toolErrors.clear();
129
+ this.consecutiveFailures = 0;
130
+ this.lastFailedTool = '';
112
131
  }
113
132
  }
@@ -19,6 +19,8 @@ export const DEFAULT_BASH_CONFIG = DEFAULT_SECURITY_CONFIG?.bash || FALLBACK_BAS
19
19
  * "NODE_ENV=prod node app.js" → "node"
20
20
  * "env PATH=/x rm -rf /" → "rm"
21
21
  * "cmd=rm; $cmd -rf /" → "cmd" (variable indirection — not expanded)
22
+ * "powershell Remove-Item x" → "Remove-Item"
23
+ * "cmd /c del /s /q" → "del"
22
24
  */
23
25
  function extractBaseCommand(trimmed) {
24
26
  const tokens = trimmed.split(/\s+/);
@@ -30,7 +32,8 @@ function extractBaseCommand(trimmed) {
30
32
  i++;
31
33
  continue;
32
34
  }
33
- if (tok === "sudo" || tok === "env" || tok === "command" || tok === "exec" || tok === "nohup") {
35
+ if (tok === "sudo" || tok === "env" || tok === "command" || tok === "exec" || tok === "nohup" ||
36
+ tok === "powershell" || tok === "pwsh" || tok === "cmd") {
34
37
  i++;
35
38
  continue;
36
39
  }
@@ -6,6 +6,7 @@ import { runCommand, processRegistry, isLongRunningCommand } from '../modules/pr
6
6
  import { t } from '../i18n/index';
7
7
  import { platform } from 'os';
8
8
  const BASH_TIMEOUT_MS = 120_000;
9
+ const MAX_PREVIEW_LINES = 25;
9
10
  function adaptCommandForWindows(command) {
10
11
  if (platform() !== 'win32')
11
12
  return command;
@@ -121,6 +122,10 @@ export const bashTool = {
121
122
  if (securityConfig?.logCommands) {
122
123
  logBashCommand(ctx.sessionId, sanitizeCommandForLog(command), res.code === 0, `Working directory: ${workdir}, Output length: ${res.stdout.length}`);
123
124
  }
125
+ const lines = output.split('\n');
126
+ if (lines.length > MAX_PREVIEW_LINES) {
127
+ output = lines.slice(0, MAX_PREVIEW_LINES).join('\n') + `\n... (${lines.length - MAX_PREVIEW_LINES} more lines)`;
128
+ }
124
129
  return { success: res.code === 0, output };
125
130
  }
126
131
  catch (e) {
@@ -1,8 +1,9 @@
1
1
  import { globSync } from 'fs';
2
2
  import { t } from '../i18n/index';
3
+ const MAX_PREVIEW_LINES = 25;
3
4
  export const globTool = {
4
5
  name: 'glob',
5
- description: 'Search for files matching a glob pattern. Uses standard glob syntax (e.g., **/*.ts, src/**/*.test.ts).',
6
+ description: `Search for files matching a glob pattern. Shows up to ${MAX_PREVIEW_LINES} results by default. Uses standard glob syntax (e.g., **/*.ts, src/**/*.test.ts).`,
6
7
  tags: ['file', 'code', 'research'],
7
8
  parameters: {
8
9
  type: 'object',
@@ -14,6 +15,12 @@ export const globTool = {
14
15
  handler: async (ctx, args) => {
15
16
  const pattern = String(args.pattern);
16
17
  const results = globSync(pattern, { cwd: ctx.baseDir });
17
- return { success: true, output: results.length > 0 ? results.join('\n') : t('file.no_matches') };
18
+ if (results.length === 0) {
19
+ return { success: true, output: t('file.no_matches') };
20
+ }
21
+ const truncated = results.length > MAX_PREVIEW_LINES
22
+ ? `\n... (${results.length - MAX_PREVIEW_LINES} more files)`
23
+ : '';
24
+ return { success: true, output: results.slice(0, MAX_PREVIEW_LINES).join('\n') + truncated };
18
25
  },
19
26
  };
@@ -2,9 +2,16 @@ import { execFileSync, execSync } from 'child_process';
2
2
  import { resolve } from 'path';
3
3
  import { t } from '../i18n/index';
4
4
  import { logBashCommand } from '../modules/security/audit-log';
5
+ const MAX_PREVIEW_LINES = 25;
6
+ function truncateLines(output) {
7
+ const lines = output.split('\n');
8
+ if (lines.length <= MAX_PREVIEW_LINES)
9
+ return output;
10
+ return lines.slice(0, MAX_PREVIEW_LINES).join('\n') + `\n... (${lines.length - MAX_PREVIEW_LINES} more lines)`;
11
+ }
5
12
  export const grepTool = {
6
13
  name: 'grep',
7
- description: 'Search file contents using a regular expression. Uses ripgrep (rg) if available, otherwise falls back to grep -r.',
14
+ description: `Search file contents using a regular expression. Shows up to ${MAX_PREVIEW_LINES} matching lines by default. Uses ripgrep (rg) if available, otherwise falls back to grep -r.`,
8
15
  tags: ['file', 'code', 'research'],
9
16
  parameters: {
10
17
  type: 'object',
@@ -32,7 +39,7 @@ export const grepTool = {
32
39
  maxBuffer: 1024 * 1024,
33
40
  cwd: ctx.baseDir,
34
41
  });
35
- return { success: true, output: output || t('file.no_matches') };
42
+ return { success: true, output: truncateLines(output || t('file.no_matches')) };
36
43
  }
37
44
  catch (e) {
38
45
  if (e.status === 1)
@@ -45,7 +52,7 @@ export const grepTool = {
45
52
  maxBuffer: 1024 * 1024,
46
53
  cwd: ctx.baseDir,
47
54
  });
48
- return { success: true, output: output || t('file.no_matches') };
55
+ return { success: true, output: truncateLines(output || t('file.no_matches')) };
49
56
  }
50
57
  catch (e2) {
51
58
  if (e2.status === 1)
@@ -3,9 +3,10 @@ import { resolve } from 'path';
3
3
  import { t } from '../i18n/index';
4
4
  import { isPathInScope } from '../modules/security/path-validator';
5
5
  import { safeResolvePath } from './path-utils';
6
+ const MAX_PREVIEW_LINES = 25;
6
7
  export const listDirTool = {
7
8
  name: 'list_dir',
8
- description: 'List files and directories in a given path.',
9
+ description: `List files and directories in a given path. Shows up to ${MAX_PREVIEW_LINES} entries by default.`,
9
10
  tags: ['file'],
10
11
  parameters: {
11
12
  type: 'object',
@@ -35,6 +36,12 @@ export const listDirTool = {
35
36
  const full = resolve(resolved, e);
36
37
  return statSync(full).isDirectory() ? `${e}/` : e;
37
38
  });
38
- return { success: true, output: lines.join('\n') || t('file.empty') };
39
+ if (lines.length === 0) {
40
+ return { success: true, output: t('file.empty') };
41
+ }
42
+ const truncated = lines.length > MAX_PREVIEW_LINES
43
+ ? `\n... (${lines.length - MAX_PREVIEW_LINES} more entries)`
44
+ : '';
45
+ return { success: true, output: lines.slice(0, MAX_PREVIEW_LINES).join('\n') + truncated };
39
46
  },
40
47
  };
@@ -1,14 +1,15 @@
1
1
  import { processRegistry } from '../modules/processes';
2
2
  import { t } from '../i18n/index';
3
+ const DEFAULT_TAIL = 25;
3
4
  export const processLogTool = {
4
5
  name: 'process_log',
5
- description: 'Show the buffered output of a background process started via bash. Use after starting a dev server to verify it came up without errors, and while it runs to check its state.',
6
+ description: `Show the buffered output of a background process started via bash. Shows the last ${DEFAULT_TAIL} lines by default. Use after starting a dev server to verify it came up without errors, and while it runs to check its state.`,
6
7
  tags: ['shell'],
7
8
  parameters: {
8
9
  type: 'object',
9
10
  properties: {
10
11
  id: { type: 'string', description: 'Process id from bash output or process_list' },
11
- tail: { type: 'number', description: 'Number of trailing lines to show (default: all buffered lines, max 300)' },
12
+ tail: { type: 'number', description: `Number of trailing lines to show (default: ${DEFAULT_TAIL}, max 300)` },
12
13
  },
13
14
  required: ['id'],
14
15
  },
@@ -23,7 +24,7 @@ export const processLogTool = {
23
24
  : entry.status === 'exited'
24
25
  ? t('proc.status_exited')
25
26
  : t('proc.status_killed');
26
- const tail = typeof args.tail === 'number' ? args.tail : undefined;
27
+ const tail = typeof args.tail === 'number' ? args.tail : DEFAULT_TAIL;
27
28
  const log = processRegistry.getLog(id, tail);
28
29
  if (!log) {
29
30
  return {
@@ -6,7 +6,7 @@ import { DEFAULT_SECURITY_CONFIG } from "../config/security";
6
6
  import { logSecurityBlock } from "../modules/security/audit-log";
7
7
  import { safeResolvePath } from "./path-utils";
8
8
  /** Default number of lines returned when the caller omits `limit`. */
9
- const DEFAULT_LIMIT = 50;
9
+ const DEFAULT_LIMIT = 25;
10
10
  export const readFileTool = {
11
11
  name: "read_file",
12
12
  description: `Read a file from the filesystem. Reads up to ${DEFAULT_LIMIT} lines at a time by default; use offset to page through large files.`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "micro-models-agent",
3
- "version": "0.16.1",
3
+ "version": "0.16.2",
4
4
  "description": "Micro Models Agent (MMA) — LLM agent harness for small models (Qwen3.5-9B, 32K-64K context)",
5
5
  "type": "module",
6
6
  "bin": {