micro-models-agent 0.24.3 → 0.24.5

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/dist/main.js +169 -21
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -2125,9 +2125,9 @@ var init_defaults = __esm(() => {
2125
2125
  offloadKvCacheToGpu: true
2126
2126
  },
2127
2127
  retry: {
2128
- maxRetries: 3,
2129
- baseDelay: 1000,
2130
- maxDelay: 30000
2128
+ maxRetries: 5,
2129
+ baseDelay: 5000,
2130
+ maxDelay: 120000
2131
2131
  },
2132
2132
  maxToolIterations: 1000,
2133
2133
  stuckThreshold: 8,
@@ -4435,12 +4435,12 @@ class OpenAICompatProvider {
4435
4435
  };
4436
4436
  this.rateLimiter = createRateLimiter(config.rateLimits);
4437
4437
  }
4438
- async* chat(messages, tools) {
4438
+ async* chat(messages, tools, signal) {
4439
4439
  if (!this.rateLimiter.canMakeRequest()) {
4440
4440
  throw new Error(`Rate limit exceeded: ${this.rateLimiter.getConfig().maxRequestsPerMinute} requests per minute`);
4441
4441
  }
4442
4442
  this.rateLimiter.recordRequest();
4443
- const streamResult = this.doStream(messages, tools);
4443
+ const streamResult = this.doStream(messages, tools, signal);
4444
4444
  let hasToolCall = false;
4445
4445
  let hasText = false;
4446
4446
  let reasoningAcc = "";
@@ -4455,13 +4455,13 @@ class OpenAICompatProvider {
4455
4455
  yield chunk;
4456
4456
  }
4457
4457
  if (!hasToolCall && !hasText) {
4458
- const fallback = await this.doNonStreaming(messages, tools);
4458
+ const fallback = await this.doNonStreaming(messages, tools, signal);
4459
4459
  for (const chunk of fallback) {
4460
4460
  yield chunk;
4461
4461
  }
4462
4462
  }
4463
4463
  }
4464
- async* doStream(messages, tools) {
4464
+ async* doStream(messages, tools, externalSignal) {
4465
4465
  const body = {
4466
4466
  model: this.model,
4467
4467
  messages,
@@ -4488,6 +4488,15 @@ class OpenAICompatProvider {
4488
4488
  const controller = new AbortController;
4489
4489
  const totalTimeoutMs = 120000;
4490
4490
  const timeoutId = setTimeout(() => controller.abort(), totalTimeoutMs);
4491
+ if (externalSignal) {
4492
+ if (externalSignal.aborted) {
4493
+ controller.abort();
4494
+ } else {
4495
+ externalSignal.addEventListener("abort", () => controller.abort(), {
4496
+ once: true
4497
+ });
4498
+ }
4499
+ }
4491
4500
  const response = await this.fetchWithRetry(`${this.config.baseUrl}/chat/completions`, {
4492
4501
  method: "POST",
4493
4502
  headers,
@@ -4591,7 +4600,7 @@ class OpenAICompatProvider {
4591
4600
  reader.releaseLock();
4592
4601
  }
4593
4602
  }
4594
- async doNonStreaming(messages, tools) {
4603
+ async doNonStreaming(messages, tools, externalSignal) {
4595
4604
  const body = {
4596
4605
  model: this.model,
4597
4606
  messages,
@@ -4616,11 +4625,15 @@ class OpenAICompatProvider {
4616
4625
  headers["Authorization"] = `Bearer ${this.config.apiKey}`;
4617
4626
  }
4618
4627
  try {
4619
- const response = await this.fetchWithRetry(`${this.config.baseUrl}/chat/completions`, {
4628
+ const fetchInit = {
4620
4629
  method: "POST",
4621
4630
  headers,
4622
4631
  body: JSON.stringify(body)
4623
- });
4632
+ };
4633
+ if (externalSignal) {
4634
+ fetchInit.signal = externalSignal;
4635
+ }
4636
+ const response = await this.fetchWithRetry(`${this.config.baseUrl}/chat/completions`, fetchInit);
4624
4637
  if (!response.ok) {
4625
4638
  const errorText = await response.text();
4626
4639
  throw new Error(t("error.llm_api", {
@@ -4884,7 +4897,8 @@ function runCommand(command, options = {}) {
4884
4897
  cwd,
4885
4898
  callId,
4886
4899
  timeoutMs = 120000,
4887
- maxBuffer = 10 * 1024 * 1024
4900
+ maxBuffer = 10 * 1024 * 1024,
4901
+ onOutput
4888
4902
  } = options;
4889
4903
  return new Promise((resolve) => {
4890
4904
  let timedOut = false;
@@ -4903,6 +4917,7 @@ function runCommand(command, options = {}) {
4903
4917
  const decodeChunk = (chunk) => decoder ? decoder.decode(chunk, { stream: true }) : chunk.toString("utf-8");
4904
4918
  child.stdout?.on("data", (chunk) => {
4905
4919
  const text = decodeChunk(chunk);
4920
+ onOutput?.(text);
4906
4921
  const next = stdoutRef.value.length + text.length;
4907
4922
  if (next > maxBuffer) {
4908
4923
  stdoutRef.value = stdoutRef.value.slice(stdoutRef.value.length + text.length - maxBuffer) + text;
@@ -4912,6 +4927,7 @@ function runCommand(command, options = {}) {
4912
4927
  });
4913
4928
  child.stderr?.on("data", (chunk) => {
4914
4929
  const text = decodeChunk(chunk);
4930
+ onOutput?.(text);
4915
4931
  const next = stderrRef.value.length + text.length;
4916
4932
  if (next > maxBuffer) {
4917
4933
  stderrRef.value = stderrRef.value.slice(stderrRef.value.length + text.length - maxBuffer) + text;
@@ -4945,7 +4961,13 @@ function runCommand(command, options = {}) {
4945
4961
  clearTimeout(timer);
4946
4962
  if (callId)
4947
4963
  activeChildren.delete(callId);
4948
- resolve({ stdout: stdoutRef.value, stderr: stderrRef.value, code, signal, timedOut });
4964
+ resolve({
4965
+ stdout: stdoutRef.value,
4966
+ stderr: stderrRef.value,
4967
+ code,
4968
+ signal,
4969
+ timedOut
4970
+ });
4949
4971
  });
4950
4972
  });
4951
4973
  }
@@ -4954,11 +4976,61 @@ var init_runner = __esm(() => {
4954
4976
  activeChildren = new Map;
4955
4977
  });
4956
4978
 
4979
+ // src/tools/cache.ts
4980
+ class ToolResultCache {
4981
+ cache = new Map;
4982
+ maxEntries;
4983
+ defaultTtlMs;
4984
+ constructor(maxEntries = 50, defaultTtlMs = 60000) {
4985
+ this.maxEntries = maxEntries;
4986
+ this.defaultTtlMs = defaultTtlMs;
4987
+ }
4988
+ get(key) {
4989
+ const entry = this.cache.get(key);
4990
+ if (!entry)
4991
+ return;
4992
+ if (Date.now() > entry.expiresAt) {
4993
+ this.cache.delete(key);
4994
+ return;
4995
+ }
4996
+ this.cache.delete(key);
4997
+ this.cache.set(key, entry);
4998
+ return entry.value;
4999
+ }
5000
+ set(key, value, ttlMs) {
5001
+ if (this.cache.size >= this.maxEntries) {
5002
+ const firstKey = this.cache.keys().next().value;
5003
+ if (firstKey !== undefined) {
5004
+ this.cache.delete(firstKey);
5005
+ }
5006
+ }
5007
+ this.cache.set(key, {
5008
+ value,
5009
+ expiresAt: Date.now() + (ttlMs ?? this.defaultTtlMs)
5010
+ });
5011
+ }
5012
+ has(key) {
5013
+ return this.get(key) !== undefined;
5014
+ }
5015
+ clear() {
5016
+ this.cache.clear();
5017
+ }
5018
+ get size() {
5019
+ return this.cache.size;
5020
+ }
5021
+ }
5022
+ function hashKey(toolName, args) {
5023
+ const payload = toolName + JSON.stringify(args);
5024
+ const hash = Bun.hash(payload).toString(16).slice(0, 16);
5025
+ return hash;
5026
+ }
5027
+
4957
5028
  // src/tools/executor.ts
4958
5029
  class ToolExecutor {
4959
5030
  registry;
4960
5031
  ctx;
4961
5032
  pluginManager;
5033
+ cache = new ToolResultCache(50, CACHE_TTL_MS);
4962
5034
  constructor(registry, ctx, pluginManager) {
4963
5035
  this.registry = registry;
4964
5036
  this.ctx = ctx;
@@ -4973,6 +5045,18 @@ class ToolExecutor {
4973
5045
  toolCallId: call.id
4974
5046
  };
4975
5047
  }
5048
+ if (CACHEABLE_TOOLS.has(call.name)) {
5049
+ const key = hashKey(call.name, call.arguments);
5050
+ const cached = this.cache.get(key);
5051
+ if (cached !== undefined) {
5052
+ this.ctx.logger.debug(`Tool ${call.name}: CACHE HIT`);
5053
+ return {
5054
+ success: true,
5055
+ output: cached,
5056
+ toolCallId: call.id
5057
+ };
5058
+ }
5059
+ }
4976
5060
  for (const plugin of this.pluginManager.getAllPlugins()) {
4977
5061
  if (plugin.onBeforeTool) {
4978
5062
  try {
@@ -5043,6 +5127,11 @@ class ToolExecutor {
5043
5127
  }
5044
5128
  }
5045
5129
  }
5130
+ if (result.success && CACHEABLE_TOOLS.has(call.name)) {
5131
+ const key = hashKey(call.name, call.arguments);
5132
+ this.cache.set(key, result.output);
5133
+ this.ctx.logger.debug(`Tool ${call.name}: CACHED`);
5134
+ }
5046
5135
  this.ctx.logger.debug(`Tool ${call.name}: ${result.success ? "OK" : "FAIL"}`);
5047
5136
  return result;
5048
5137
  }
@@ -5052,6 +5141,9 @@ class ToolExecutor {
5052
5141
  getRegistry() {
5053
5142
  return this.registry;
5054
5143
  }
5144
+ getContext() {
5145
+ return this.ctx;
5146
+ }
5055
5147
  setScope(scope) {
5056
5148
  this.ctx.scope = scope;
5057
5149
  }
@@ -5059,10 +5151,11 @@ class ToolExecutor {
5059
5151
  this.ctx.llmProvider = provider;
5060
5152
  }
5061
5153
  }
5062
- var TOOL_EXECUTION_TIMEOUT_MS = 60000;
5154
+ var TOOL_EXECUTION_TIMEOUT_MS = 60000, CACHEABLE_TOOLS, CACHE_TTL_MS = 60000;
5063
5155
  var init_executor = __esm(() => {
5064
5156
  init_runner();
5065
5157
  init_i18n();
5158
+ CACHEABLE_TOOLS = new Set(["web_fetch"]);
5066
5159
  });
5067
5160
 
5068
5161
  // src/modules/security/path-validator.ts
@@ -5593,7 +5686,7 @@ function safeResolvePath(baseDir, userPath) {
5593
5686
  var init_path_utils = () => {};
5594
5687
 
5595
5688
  // src/tools/preview.ts
5596
- var MAX_PREVIEW_LINES = 15;
5689
+ var MAX_PREVIEW_LINES = 100;
5597
5690
 
5598
5691
  // src/tools/read-file.ts
5599
5692
  import { readFileSync as readFileSync5, existsSync as existsSync9 } from "fs";
@@ -6957,8 +7050,14 @@ var init_bash = __esm(() => {
6957
7050
  type: "object",
6958
7051
  properties: {
6959
7052
  command: { type: "string", description: "Shell command to execute" },
6960
- workdir: { type: "string", description: "Working directory (default: baseDir)" },
6961
- background: { type: "boolean", description: "Start the command in the background and return immediately with a process id (default: auto-detect long-running commands)" }
7053
+ workdir: {
7054
+ type: "string",
7055
+ description: "Working directory (default: baseDir)"
7056
+ },
7057
+ background: {
7058
+ type: "boolean",
7059
+ description: "Start the command in the background and return immediately with a process id (default: auto-detect long-running commands)"
7060
+ }
6962
7061
  },
6963
7062
  required: ["command"]
6964
7063
  },
@@ -7005,7 +7104,8 @@ ${t("proc.manage_hint", {
7005
7104
  const res = await runCommand(command, {
7006
7105
  cwd: workdir,
7007
7106
  callId: ctx.activeCallId,
7008
- timeoutMs: BASH_TIMEOUT_MS
7107
+ timeoutMs: BASH_TIMEOUT_MS,
7108
+ onOutput: ctx.onMeta
7009
7109
  });
7010
7110
  const parts = [res.stdout.trimEnd(), res.stderr.trimEnd()].filter(Boolean);
7011
7111
  let output = parts.join(`
@@ -9065,6 +9165,16 @@ var init_store = __esm(() => {
9065
9165
 
9066
9166
  // src/core/agent.ts
9067
9167
  import { join as join10 } from "path";
9168
+ function canRunInParallel(calls) {
9169
+ if (calls.length <= 1)
9170
+ return false;
9171
+ for (const call of calls) {
9172
+ if (SIDE_EFFECT_TOOLS.has(call.name) || WRITE_TOOLS.has(call.name)) {
9173
+ return false;
9174
+ }
9175
+ }
9176
+ return true;
9177
+ }
9068
9178
 
9069
9179
  class Agent {
9070
9180
  deps;
@@ -9232,7 +9342,7 @@ ${t("tool.truncated", { tokens: Math.ceil(removedChars / 2) })}`;
9232
9342
  const textChunks = [];
9233
9343
  this.emitPhase(iteration, "thinking", onPhase);
9234
9344
  try {
9235
- for await (const chunk of llmProvider.chat(history, allTools)) {
9345
+ for await (const chunk of llmProvider.chat(history, allTools, this.abortController?.signal)) {
9236
9346
  if (this.shutdownRequested)
9237
9347
  break;
9238
9348
  if (chunk.type === "text" && chunk.content) {
@@ -9327,7 +9437,7 @@ ${t("tool.truncated", { tokens: Math.ceil(removedChars / 2) })}`;
9327
9437
  });
9328
9438
  const summaries = [];
9329
9439
  let anyToolFailed = false;
9330
- for (const call of toolCalls) {
9440
+ const executeToolCall = async (call) => {
9331
9441
  this.setScope();
9332
9442
  const startTime = Date.now();
9333
9443
  pluginManager.runOnToolCall({
@@ -9338,11 +9448,28 @@ ${t("tool.truncated", { tokens: Math.ceil(removedChars / 2) })}`;
9338
9448
  onTool?.({ type: "start", tool: call.name, args: call.arguments });
9339
9449
  slog.logToolCall(call, iteration);
9340
9450
  const tokensBeforeTool = contextManager.getEstimatedTokens();
9451
+ const toolCtx = this.deps.toolExecutor?.getContext?.();
9452
+ const prevOnMeta = toolCtx?.onMeta;
9453
+ if (toolCtx) {
9454
+ toolCtx.onMeta = onMeta;
9455
+ }
9341
9456
  const result = await toolExecutor.execute(call, this.abortController?.signal);
9457
+ if (toolCtx) {
9458
+ toolCtx.onMeta = prevOnMeta;
9459
+ }
9342
9460
  const duration = Date.now() - startTime;
9461
+ pluginManager.runOnToolEnd({ iteration, logger }, { id: call.id, name: call.name, arguments: call.arguments }, result, duration);
9462
+ return { call, result, duration, tokensBefore: tokensBeforeTool };
9463
+ };
9464
+ const processToolResult = (entry) => {
9465
+ const {
9466
+ call,
9467
+ result,
9468
+ duration,
9469
+ tokensBefore: tokensBeforeTool
9470
+ } = entry;
9343
9471
  if (!result.success)
9344
9472
  anyToolFailed = true;
9345
- pluginManager.runOnToolEnd({ iteration, logger }, { id: call.id, name: call.name, arguments: call.arguments }, result, duration);
9346
9473
  if (result.display) {
9347
9474
  onMeta?.(`
9348
9475
  ` + result.display + `
@@ -9386,6 +9513,17 @@ ${t("tool.truncated", { tokens: Math.ceil(removedChars / 2) })}`;
9386
9513
  contextManager.compact();
9387
9514
  logger.debug("Context compacted after tool result");
9388
9515
  }
9516
+ };
9517
+ if (canRunInParallel(toolCalls)) {
9518
+ const entries = await Promise.all(toolCalls.map(executeToolCall));
9519
+ for (const entry of entries) {
9520
+ processToolResult(entry);
9521
+ }
9522
+ } else {
9523
+ for (const call of toolCalls) {
9524
+ const entry = await executeToolCall(call);
9525
+ processToolResult(entry);
9526
+ }
9389
9527
  }
9390
9528
  if (anyToolFailed) {
9391
9529
  consecutiveToolFailures++;
@@ -9596,7 +9734,7 @@ ${taskReminder}</system-summary>`
9596
9734
  });
9597
9735
  }
9598
9736
  }
9599
- var TOOL_RESULT_MAX_TOKENS_RATIO = 0.3, TOOL_RESULT_ABSOLUTE_MAX_CHARS = 15000;
9737
+ var TOOL_RESULT_MAX_TOKENS_RATIO = 0.3, TOOL_RESULT_ABSOLUTE_MAX_CHARS = 15000, WRITE_TOOLS, SIDE_EFFECT_TOOLS;
9600
9738
  var init_agent = __esm(() => {
9601
9739
  init_i18n();
9602
9740
  init_colors();
@@ -9604,6 +9742,14 @@ var init_agent = __esm(() => {
9604
9742
  init_processes();
9605
9743
  init_agent_moe();
9606
9744
  init_store();
9745
+ WRITE_TOOLS = new Set([
9746
+ "write_file",
9747
+ "edit_file",
9748
+ "delete_file",
9749
+ "move_file",
9750
+ "create_dir"
9751
+ ]);
9752
+ SIDE_EFFECT_TOOLS = new Set(["bash", "subagent", "pipeline_run"]);
9607
9753
  });
9608
9754
 
9609
9755
  // src/modules/context/manager.ts
@@ -15309,6 +15455,7 @@ function buildSystemInfo(config, baseDir, profileCompressed) {
15309
15455
  lines.push(``, `Windows environment — use Windows-compatible commands:`, `- Use "dir" instead of "ls". Use "dir /b" for bare listing.`, `- Use "type" or "Get-Content" instead of "cat".`, `- Use "cd" instead of "pwd". Use "echo %cd%" to print working directory.`, `- Use "copy" instead of "cp", "move" instead of "mv", "del" instead of "rm".`, `- Do not use "mkdir -p" — Windows mkdir creates intermediate dirs by default. Use the create_dir tool instead.`, `- Use forward slashes (/) or escaped backslashes (\\\\) in file paths.`, `- Your working directory is: ${baseDir}`);
15310
15456
  }
15311
15457
  lines.push(``, `Bash tool rules:`, `- Use the "workdir" parameter to run commands in a specific directory. Prefer workdir over "cd dir && cmd" chaining.`, `- Run one command per tool call. Split multi-step shell operations into separate bash calls.`, `- Background processes (dev servers, watchers, long-running npm install): use the "background: true" parameter or the tool will auto-detect and background them. Check output with process_log.`);
15458
+ lines.push(``, `Parallel tool calls:`, `- When you need to read multiple files, call read_file for ALL of them in a SINGLE response (one tool_calls array with multiple read_file calls). They will execute in parallel — much faster than reading one at a time.`, `- Example: to read 3 files, return one assistant message with 3 read_file tool_calls. Do NOT read them one by one in separate iterations.`, `- This works for: read_file, glob, grep, file_info, list_dir — any read-only operations.`, `- Do NOT parallelize write operations (write_file, edit_file, delete_file, bash) — these run sequentially.`);
15312
15459
  lines.push(``, `=== DEVELOPMENT RULES — follow these strictly ===`, ``, `1. DEPENDENCIES FIRST: Before writing any source code, ALWAYS install project dependencies (e.g., "npm install", "pip install -r requirements.txt", "cargo build", "go mod tidy"). Verify the package manager's lock file or dependency directory exists. Never write code that imports/uses packages that aren't installed yet.`, `2. TOOLKIT/FWK FIRST: If the task specifies a framework or UI library, initialize and configure it BEFORE writing application code. Run its project init command first, then add components/modules. Never write your own version of what the framework already provides.`, `3. ONE STEP AT A TIME: Follow the plan sequentially. Complete step N before starting step N+1. When a step is done: verify the deliverables exist and have real content (not empty), then call "plan update step=N status=done". Do not redo completed work.`, `4. VERIFY YOUR WORK: After creating/modifying files, verify they exist on disk. After installing dependencies, verify the package manager completed successfully. After any command, check its output for errors. Don't assume operations succeeded.`, `5. NO PREMATURE WORK: Do not create files for future steps. Do not add imports/references to packages or modules that haven't been installed yet. Do not reference files or components that don't exist yet. Build incrementally — one layer at a time.`, `6. WHEN STUCK: If a command fails 2+ times, STOP and try a different approach. Write files directly instead of using commands. Ask the user for help. Never repeat the same failing command more than twice.`, ``, `=== PLAN QUALITY RULES — your plan MUST follow these ===`, ``, `- Each step must describe CONCRETE deliverables: exact filenames with paths, exact packages to install, exact CLI commands to run. Avoid vague steps — be specific.`, `- A step like "Настройка проекта" or "Setup the project" is too vague — describe what exactly needs to be configured or set up.`, `- A step like "Создать src/components/Header.tsx с навигацией и логотипом, добавить в src/App.tsx импорт <Header />" is GOOD.`, `- Include file extensions (.tsx, .css, .json) and directory paths. Every step must mention at least one file or command.`, `- The plan must cover EVERYTHING needed: init → deps → framework setup → code → verification.`, `- Number of steps: 5-8 for a typical task. Too few means you're being vague. Too many means you're over-splitting.`);
15313
15460
  if (config.autoPlan) {
15314
15461
  lines.push(``, `Plan rule (MANDATORY): For ANY task that requires creating files, installing packages, or multiple actions — you MUST create a plan using the "plan" tool BEFORE starting work. Each step must describe a concrete deliverable (specific files to create, packages to install, commands to run). Do not combine unrelated work into one step.`);
@@ -26122,6 +26269,7 @@ class Repl {
26122
26269
  ${t("repl.interrupt")}
26123
26270
  `));
26124
26271
  this.agent.shutdown();
26272
+ setTimeout(() => process.exit(1), 3000);
26125
26273
  }
26126
26274
  this.lastEscTime = 0;
26127
26275
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "micro-models-agent",
3
- "version": "0.24.3",
3
+ "version": "0.24.5",
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": {