micro-models-agent 0.27.0 → 0.28.0

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 +136 -30
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -2229,7 +2229,9 @@ var init_defaults = __esm(() => {
2229
2229
  navigationTimeout: 15000
2230
2230
  },
2231
2231
  ui: {
2232
- spinner: true
2232
+ spinner: true,
2233
+ toolStyle: "inline",
2234
+ toolComments: true
2233
2235
  },
2234
2236
  mcpServers: {
2235
2237
  context7: {
@@ -4514,12 +4516,12 @@ class OpenAICompatProvider {
4514
4516
  };
4515
4517
  this.rateLimiter = createRateLimiter(config.rateLimits);
4516
4518
  }
4517
- async* chat(messages, tools) {
4519
+ async* chat(messages, tools, signal) {
4518
4520
  if (!this.rateLimiter.canMakeRequest()) {
4519
4521
  throw new Error(`Rate limit exceeded: ${this.rateLimiter.getConfig().maxRequestsPerMinute} requests per minute`);
4520
4522
  }
4521
4523
  this.rateLimiter.recordRequest();
4522
- const streamResult = this.doStream(messages, tools);
4524
+ const streamResult = this.doStream(messages, tools, signal);
4523
4525
  let hasToolCall = false;
4524
4526
  let hasText = false;
4525
4527
  let reasoningAcc = "";
@@ -4534,13 +4536,13 @@ class OpenAICompatProvider {
4534
4536
  yield chunk;
4535
4537
  }
4536
4538
  if (!hasToolCall && !hasText) {
4537
- const fallback = await this.doNonStreaming(messages, tools);
4539
+ const fallback = await this.doNonStreaming(messages, tools, signal);
4538
4540
  for (const chunk of fallback) {
4539
4541
  yield chunk;
4540
4542
  }
4541
4543
  }
4542
4544
  }
4543
- async* doStream(messages, tools) {
4545
+ async* doStream(messages, tools, signal) {
4544
4546
  const body = {
4545
4547
  model: this.model,
4546
4548
  messages,
@@ -4567,11 +4569,12 @@ class OpenAICompatProvider {
4567
4569
  const controller = new AbortController;
4568
4570
  const totalTimeoutMs = 120000;
4569
4571
  const timeoutId = setTimeout(() => controller.abort(), totalTimeoutMs);
4572
+ const abortSignal = signal ? AbortSignal.any([controller.signal, signal]) : controller.signal;
4570
4573
  const response = await this.fetchWithRetry(`${this.config.baseUrl}/chat/completions`, {
4571
4574
  method: "POST",
4572
4575
  headers,
4573
4576
  body: JSON.stringify(body),
4574
- signal: controller.signal
4577
+ signal: abortSignal
4575
4578
  });
4576
4579
  if (!response.ok) {
4577
4580
  clearTimeout(timeoutId);
@@ -4670,7 +4673,7 @@ class OpenAICompatProvider {
4670
4673
  reader.releaseLock();
4671
4674
  }
4672
4675
  }
4673
- async doNonStreaming(messages, tools) {
4676
+ async doNonStreaming(messages, tools, signal) {
4674
4677
  const body = {
4675
4678
  model: this.model,
4676
4679
  messages,
@@ -4698,7 +4701,8 @@ class OpenAICompatProvider {
4698
4701
  const response = await this.fetchWithRetry(`${this.config.baseUrl}/chat/completions`, {
4699
4702
  method: "POST",
4700
4703
  headers,
4701
- body: JSON.stringify(body)
4704
+ body: JSON.stringify(body),
4705
+ signal
4702
4706
  });
4703
4707
  if (!response.ok) {
4704
4708
  const errorText = await response.text();
@@ -4792,7 +4796,7 @@ class OpenAICompatProvider {
4792
4796
  if (attempt < maxRetries) {
4793
4797
  const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
4794
4798
  const jitter = Math.random() * baseDelay * 0.1;
4795
- await this.sleep(delay + jitter);
4799
+ await this.sleep(delay + jitter, init.signal ?? undefined);
4796
4800
  }
4797
4801
  }
4798
4802
  throw lastError ?? new Error(t("error.llm_retries"));
@@ -4800,8 +4804,23 @@ class OpenAICompatProvider {
4800
4804
  isRetryable(status) {
4801
4805
  return status === 429 || status >= 500;
4802
4806
  }
4803
- sleep(ms) {
4804
- return new Promise((resolve) => setTimeout(resolve, ms));
4807
+ sleep(ms, signal) {
4808
+ return new Promise((resolve, reject) => {
4809
+ if (signal?.aborted) {
4810
+ reject(new DOMException("Aborted", "AbortError"));
4811
+ return;
4812
+ }
4813
+ let timer;
4814
+ const onAbort = () => {
4815
+ clearTimeout(timer);
4816
+ reject(new DOMException("Aborted", "AbortError"));
4817
+ };
4818
+ timer = setTimeout(() => {
4819
+ signal?.removeEventListener("abort", onAbort);
4820
+ resolve();
4821
+ }, ms);
4822
+ signal?.addEventListener("abort", onAbort, { once: true });
4823
+ });
4805
4824
  }
4806
4825
  }
4807
4826
  var init_openai_compat = __esm(() => {
@@ -5959,9 +5978,9 @@ function formatLine(line, maxNumWidth) {
5959
5978
  const num = line.type === "remove" ? line.oldNum : line.newNum;
5960
5979
  const numStr = num !== null ? String(num).padStart(maxNumWidth) : " ".repeat(maxNumWidth);
5961
5980
  if (line.type === "remove") {
5962
- return pc.red(`${pc.bgRed(`${numStr} - ${line.content}`)}`);
5981
+ return `${numStr} ${pc.red("-")} ${line.content}`;
5963
5982
  } else if (line.type === "add") {
5964
- return pc.green(`${pc.bgGreen(`${numStr} + ${line.content}`)}`);
5983
+ return `${numStr} ${pc.green("+")} ${line.content}`;
5965
5984
  } else if (line.content === "...") {
5966
5985
  return pc.dim(` ${" ".repeat(maxNumWidth)}...`);
5967
5986
  } else {
@@ -6000,7 +6019,7 @@ function generateNewFileDiff(content) {
6000
6019
  const diffLines = [];
6001
6020
  for (let i = 0;i < lines.length; i++) {
6002
6021
  const numStr = String(i + 1).padStart(maxNumWidth);
6003
- diffLines.push(pc.green(`${pc.bgGreen(`${numStr} + ${lines[i]}`)}`));
6022
+ diffLines.push(`${numStr} ${pc.green("+")} ${lines[i]}`);
6004
6023
  }
6005
6024
  if (diffLines.length > MAX_DIFF_LINES) {
6006
6025
  const truncated = diffLines.slice(0, MAX_DIFF_LINES);
@@ -6018,7 +6037,7 @@ function generateDeleteDiff(content) {
6018
6037
  const diffLines = [];
6019
6038
  for (let i = 0;i < lines.length; i++) {
6020
6039
  const numStr = String(i + 1).padStart(maxNumWidth);
6021
- diffLines.push(pc.red(`${pc.bgRed(`${numStr} - ${lines[i]}`)}`));
6040
+ diffLines.push(`${numStr} ${pc.red("-")} ${lines[i]}`);
6022
6041
  }
6023
6042
  if (diffLines.length > MAX_DIFF_LINES) {
6024
6043
  const truncated = diffLines.slice(0, MAX_DIFF_LINES);
@@ -9547,6 +9566,18 @@ var init_store = __esm(() => {
9547
9566
 
9548
9567
  // src/core/agent.ts
9549
9568
  import { join as join10 } from "path";
9569
+ function isToolCallJson(text) {
9570
+ const trimmed = text.trim();
9571
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
9572
+ try {
9573
+ JSON.parse(trimmed);
9574
+ return true;
9575
+ } catch {
9576
+ return false;
9577
+ }
9578
+ }
9579
+ return false;
9580
+ }
9550
9581
 
9551
9582
  class Agent {
9552
9583
  deps;
@@ -9734,7 +9765,7 @@ ${t("tool.truncated", { tokens: Math.ceil(removedChars / 2) })}`;
9734
9765
  const textChunks = [];
9735
9766
  this.emitPhase(iteration, "thinking", onPhase);
9736
9767
  try {
9737
- for await (const chunk of llmProvider.chat(history, allTools)) {
9768
+ for await (const chunk of llmProvider.chat(history, allTools, this.abortController?.signal)) {
9738
9769
  if (this.shutdownRequested)
9739
9770
  break;
9740
9771
  if (chunk.type === "text" && chunk.content) {
@@ -9776,6 +9807,10 @@ ${t("tool.truncated", { tokens: Math.ceil(removedChars / 2) })}`;
9776
9807
  }
9777
9808
  }
9778
9809
  } catch (err) {
9810
+ if (this.shutdownRequested || err?.name === "AbortError") {
9811
+ logger.info("LLM call aborted (interrupt)");
9812
+ break;
9813
+ }
9779
9814
  logger.error(`LLM call failed: ${err.message}`);
9780
9815
  slog.logError(err.message);
9781
9816
  pluginManager.runOnError({ iteration, logger }, err);
@@ -9788,7 +9823,12 @@ ${t("tool.truncated", { tokens: Math.ceil(removedChars / 2) })}`;
9788
9823
  } finally {
9789
9824
  this.emitPhase(iteration, "done", onPhase);
9790
9825
  }
9791
- if (!sawToolCall && textChunks.length > 0) {
9826
+ if (this.shutdownRequested) {
9827
+ break;
9828
+ }
9829
+ const toolComments = this.deps.config.ui?.toolComments ?? true;
9830
+ const showText = textChunks.length > 0 && (!sawToolCall || toolComments && !isToolCallJson(textContent));
9831
+ if (showText) {
9792
9832
  for (const chunk of textChunks) {
9793
9833
  const textOut = pluginManager.runOnText({ iteration, logger }, chunk);
9794
9834
  onChunk?.(textOut);
@@ -16821,6 +16861,10 @@ function box(lines, opts = {}) {
16821
16861
  out.push(pc.dim(`└${"─".repeat(width - 2)}┘`));
16822
16862
  return out;
16823
16863
  }
16864
+ function divider(width) {
16865
+ const w = Math.min(width ?? getTerminalWidth(), 60);
16866
+ return pc.dim("─".repeat(w));
16867
+ }
16824
16868
  var init_box = __esm(() => {
16825
16869
  init_string_width();
16826
16870
  init_colors();
@@ -26563,6 +26607,28 @@ init_spinner();
26563
26607
  init_box();
26564
26608
  init_table();
26565
26609
  init_i18n();
26610
+ var GUTTER = " ";
26611
+ function toolMarker(tool) {
26612
+ switch (tool) {
26613
+ case "write_file":
26614
+ case "edit_file":
26615
+ case "create_dir":
26616
+ case "move_file":
26617
+ case "delete_file":
26618
+ return "←";
26619
+ case "read_file":
26620
+ case "list_dir":
26621
+ case "file_info":
26622
+ return "→";
26623
+ case "glob":
26624
+ case "grep":
26625
+ return "✱";
26626
+ case "bash":
26627
+ return "$";
26628
+ default:
26629
+ return "⚙";
26630
+ }
26631
+ }
26566
26632
  function isRichTerminal() {
26567
26633
  return Boolean(process.stdout.isTTY) && !process.env.CI;
26568
26634
  }
@@ -26592,12 +26658,14 @@ class Renderer {
26592
26658
  out;
26593
26659
  err;
26594
26660
  width;
26661
+ toolStyle;
26595
26662
  card = null;
26596
26663
  constructor(opts = {}) {
26597
26664
  this.rich = opts.rich ?? isRichTerminal();
26598
26665
  this.out = opts.out ?? process.stdout;
26599
26666
  this.err = opts.err ?? process.stderr;
26600
26667
  this.width = opts.width ?? getTerminalWidth();
26668
+ this.toolStyle = opts.toolStyle ?? "inline";
26601
26669
  this.spinner = new Spinner({
26602
26670
  enabled: this.rich && (opts.spinner ?? true),
26603
26671
  stream: this.err,
@@ -26614,11 +26682,24 @@ class Renderer {
26614
26682
  meta(chunk) {
26615
26683
  this.spinner.stop();
26616
26684
  if (this.card) {
26617
- this.card.body.push(chunk);
26685
+ if (this.toolStyle === "inline") {
26686
+ this.writeInlineBody(chunk);
26687
+ } else {
26688
+ this.card.body.push(chunk);
26689
+ }
26618
26690
  } else {
26619
26691
  this.out.write(chunk);
26620
26692
  }
26621
26693
  }
26694
+ writeInlineBody(chunk) {
26695
+ for (const line of chunk.split(`
26696
+ `)) {
26697
+ if (line.trim() === "")
26698
+ continue;
26699
+ this.out.write(`${GUTTER}${line}
26700
+ `);
26701
+ }
26702
+ }
26622
26703
  reasoning(chunk) {
26623
26704
  this.spinner.stop();
26624
26705
  this.out.write(pc.dim(chunk));
@@ -26640,6 +26721,13 @@ ${pc.dim("⚙")} ${friendlyTool(tool)}${summary ? ` ${pc.dim(summary)}` : ""}
26640
26721
  return;
26641
26722
  }
26642
26723
  this.card = { tool, args, body: [], start: Date.now() };
26724
+ if (this.toolStyle === "inline") {
26725
+ const marker = toolMarker(tool);
26726
+ this.out.write(`
26727
+ ${pc.dim(marker)} ${friendlyTool(tool)}${summary ? ` ${pc.dim(summary)}` : ""}
26728
+ `);
26729
+ return;
26730
+ }
26643
26731
  this.spinner.start(`${pc.dim("⚙")} ${friendlyTool(tool)}${summary ? ` ${pc.dim(summary)}` : ""}`);
26644
26732
  }
26645
26733
  toolEnd(_tool, duration, error, ctxDelta) {
@@ -26655,6 +26743,20 @@ ${pc.dim("⚙")} ${friendlyTool(tool)}${summary ? ` ${pc.dim(summary)}` : ""}
26655
26743
  if (!this.card)
26656
26744
  return;
26657
26745
  const { tool, args, body } = this.card;
26746
+ const marker = error ? pc.red("✗") : pc.green("✓");
26747
+ let footer = `${marker} ${pc.dim(`${duration}ms`)}`;
26748
+ if (ctxDelta !== undefined && ctxDelta !== 0) {
26749
+ const deltaStr = ctxDelta > 0 ? pc.green(`+${ctxDelta}`) : pc.yellow(`${ctxDelta} ↓`);
26750
+ footer += ` ${pc.dim("ctx")} ${deltaStr}`;
26751
+ }
26752
+ if (this.toolStyle === "inline") {
26753
+ this.out.write(`${GUTTER}${footer}
26754
+ `);
26755
+ this.out.write(`${divider(this.width)}
26756
+ `);
26757
+ this.card = null;
26758
+ return;
26759
+ }
26658
26760
  const lines = [];
26659
26761
  const summary = summarizeArgs2(args);
26660
26762
  if (summary)
@@ -26666,12 +26768,6 @@ ${pc.dim("⚙")} ${friendlyTool(tool)}${summary ? ` ${pc.dim(summary)}` : ""}
26666
26768
  lines.push(line);
26667
26769
  }
26668
26770
  }
26669
- const marker = error ? pc.red("✗") : pc.green("✓");
26670
- let footer = `${marker} ${pc.dim(`${duration}ms`)}`;
26671
- if (ctxDelta !== undefined && ctxDelta !== 0) {
26672
- const deltaStr = ctxDelta > 0 ? pc.green(`+${ctxDelta}`) : pc.yellow(`${ctxDelta} ↓`);
26673
- footer += ` ${pc.dim("ctx")} ${deltaStr}`;
26674
- }
26675
26771
  lines.push(footer);
26676
26772
  const title = `${marker} ${friendlyTool(tool)}`;
26677
26773
  for (const line of box(lines, { title, width: this.width })) {
@@ -26894,18 +26990,20 @@ class Repl {
26894
26990
  readline2.emitKeypressEvents(process.stdin);
26895
26991
  process.stdin.on("keypress", async (str, key) => {
26896
26992
  if (key.name === "escape") {
26993
+ const escBytes = key.sequence ? (key.sequence.match(/\x1b/g) || []).length : 1;
26897
26994
  const now = Date.now();
26898
- if (now - this.lastEscTime < this.doubleEscDelay) {
26995
+ const withinWindow = now - this.lastEscTime < this.doubleEscDelay;
26996
+ this.lastEscTime = now;
26997
+ if (escBytes >= 2 || withinWindow) {
26998
+ this.lastEscTime = 0;
26899
26999
  if (this.agentRunning) {
26900
27000
  process.stdout.write(pc.yellow(`
26901
27001
  ${t("repl.interrupt")}
26902
27002
  `));
26903
27003
  this.agent.shutdown();
26904
27004
  }
26905
- this.lastEscTime = 0;
26906
- return;
26907
27005
  }
26908
- this.lastEscTime = now;
27006
+ return;
26909
27007
  }
26910
27008
  if (key.ctrl && key.name === "v" && !this.agentRunning) {
26911
27009
  try {
@@ -26918,6 +27016,10 @@ ${t("repl.interrupt")}
26918
27016
  console.log(pc.green(`
26919
27017
  ${t("image.attached", { source: "clipboard", size: `${sizeKb} KB` })}`));
26920
27018
  this.rl.prompt();
27019
+ } else {
27020
+ console.log(pc.yellow(`
27021
+ ${t("image.clipboard_empty")}`));
27022
+ this.rl.prompt();
26921
27023
  }
26922
27024
  } catch {}
26923
27025
  }
@@ -26962,7 +27064,8 @@ ${t("image.attached", { source: "clipboard", size: `${sizeKb} KB` })}`));
26962
27064
  process.stdout.write(`
26963
27065
  ` + pc.green(t("repl.agent")));
26964
27066
  const renderer = new Renderer({
26965
- spinner: this.config.ui?.spinner ?? true
27067
+ spinner: this.config.ui?.spinner ?? true,
27068
+ toolStyle: this.config.ui?.toolStyle ?? "inline"
26966
27069
  });
26967
27070
  const result = await this.agent.run(input, (c) => renderer.text(c), (m) => renderer.meta(m), (ev) => {
26968
27071
  if (ev.type === "start") {
@@ -27179,7 +27282,10 @@ async function main() {
27179
27282
  `);
27180
27283
  process.exit(result2.success ? 0 : 1);
27181
27284
  }
27182
- const renderer = new Renderer({ spinner: config.ui?.spinner ?? true });
27285
+ const renderer = new Renderer({
27286
+ spinner: config.ui?.spinner ?? true,
27287
+ toolStyle: config.ui?.toolStyle ?? "inline"
27288
+ });
27183
27289
  const result = await agent.run(prompt, (chunk) => renderer.text(chunk), (meta) => renderer.meta(meta), (ev) => {
27184
27290
  if (ev.type === "start") {
27185
27291
  renderer.toolStart(ev.tool, ev.args);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "micro-models-agent",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
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": {