micro-models-agent 1.6.0 → 1.7.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 (3) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/main.js +186 -144
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@ All notable changes to Micro Models Agent (MMA) will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [1.7.0] - 2026-09-22
8
+
9
+ ### Added
10
+
11
+ - **Model capability resolution** (`src/modules/providers/capabilities.ts`, `src/config/types.ts`): a single source of truth for a model's `vision`/`tools` abilities, merged from user override (`config.modelCapabilities[model]`) → live native probe (LM Studio `/api/v1/models` `capabilities`, read by `probeModel` in `src/llm/model-loader.ts`) → provider preset. Exposed to tools via the live `ToolContext.capabilities` getter.
12
+ - **First-class tool image channel** (`src/tools/types.ts` `ToolResult.images`, `src/core/agent/tool-images.ts`): tools can return images; the agent loop queues them onto the next user message (`queueToolImages`), keeping `role:"tool"` content text-only and portable.
13
+
14
+ ### Fixed
15
+
16
+ - **Vision misdetection for non-`-vl` models** (`src/tools/browser.ts`): the `qwen3.5-9b`-style models report `vision: true` in LM Studio but the name-based `modelSupportsVision` heuristic returned `false`, so screenshots were silently downgraded to a text notice. Name matching is removed; the browser tool now reads `ctx.capabilities.vision`.
17
+ - **Browser screenshots never reached the model** (`src/tools/browser.ts`): screenshots were embedded as base64 text inside the `role:"tool"` output, which backends never decode and the budget truncates. They are now delivered as `image_url` parts; text-only models get the snapshot instead.
18
+
19
+ ### Changed
20
+
21
+ - **Browser module prompt** (`src/modules/browser/module.ts`): no longer asks the model to self-assess vision support — screenshot delivery is automatic.
22
+
7
23
  ## [1.6.0] - 2026-09-22
8
24
 
9
25
  ### Added
package/dist/main.js CHANGED
@@ -2312,6 +2312,7 @@ var init_defaults = __esm(() => {
2312
2312
  description: "browser + file reading for visual tasks"
2313
2313
  }
2314
2314
  },
2315
+ modelCapabilities: {},
2315
2316
  contextWindow: 32768,
2316
2317
  contextBudget: {
2317
2318
  systemPrompt: 0.1,
@@ -5118,7 +5119,7 @@ var init_domains = __esm(() => {
5118
5119
  "reasoning",
5119
5120
  "hallucination"
5120
5121
  ],
5121
- provider: ["provider", "orchestrator", "retry", "pricing"],
5122
+ provider: ["provider", "orchestrator", "retry", "pricing", "modelCapabilities"],
5122
5123
  moe: ["moe", "experts"],
5123
5124
  security: ["security"],
5124
5125
  browser: ["browser"],
@@ -7547,7 +7548,7 @@ var init_factory = __esm(() => {
7547
7548
  });
7548
7549
 
7549
7550
  // src/llm/model-loader.ts
7550
- async function getLoadedContextLength(baseUrl, model, timeoutMs = 3000) {
7551
+ async function probeModel(baseUrl, model, timeoutMs = 3000) {
7551
7552
  const base = baseUrl.replace(/\/$/, "").replace(/\/v1\/?$/, "");
7552
7553
  try {
7553
7554
  const response = await fetch(`${base}/api/v1/models`, {
@@ -7561,10 +7562,18 @@ async function getLoadedContextLength(baseUrl, model, timeoutMs = 3000) {
7561
7562
  if (!found)
7562
7563
  return null;
7563
7564
  const loadedInstance = (found.loaded_instances ?? [])[0];
7564
- const actual = loadedInstance?.config?.context_length ?? found.max_context_length;
7565
- if (typeof actual !== "number" || actual <= 0)
7565
+ const contextLength = loadedInstance?.config?.context_length ?? found.max_context_length;
7566
+ if (typeof contextLength !== "number" || contextLength <= 0)
7566
7567
  return null;
7567
- return { model: found.key ?? found.name ?? model, actual };
7568
+ const raw = found.capabilities;
7569
+ let capabilities;
7570
+ if (raw && typeof raw.vision === "boolean") {
7571
+ capabilities = { vision: raw.vision };
7572
+ if (typeof raw.trained_for_tool_use === "boolean") {
7573
+ capabilities.tools = raw.trained_for_tool_use;
7574
+ }
7575
+ }
7576
+ return { model: found.key ?? found.name ?? model, contextLength, capabilities };
7568
7577
  } catch {
7569
7578
  return null;
7570
7579
  }
@@ -15436,6 +15445,15 @@ var init_tool_output = __esm(() => {
15436
15445
  init_i18n();
15437
15446
  });
15438
15447
 
15448
+ // src/core/agent/tool-images.ts
15449
+ function queueToolImages(sink, images) {
15450
+ if (!sink || !images || images.length === 0)
15451
+ return;
15452
+ for (const image of images) {
15453
+ sink.addPendingImage(image);
15454
+ }
15455
+ }
15456
+
15439
15457
  // src/core/agent/tool-batch.ts
15440
15458
  class ToolBatchExecutor {
15441
15459
  deps;
@@ -15565,6 +15583,7 @@ class ToolBatchExecutor {
15565
15583
  arguments: call.arguments
15566
15584
  });
15567
15585
  answeredToolCallIds.add(call.id);
15586
+ queueToolImages(contextManager, result.images);
15568
15587
  const tokensAfterTool = contextManager.getEstimatedTokens();
15569
15588
  const costAfterTool = this.deps.costTracker.total;
15570
15589
  onTool?.({
@@ -21290,6 +21309,119 @@ var init_session = __esm(() => {
21290
21309
  init_i18n();
21291
21310
  });
21292
21311
 
21312
+ // src/llm/image-utils.ts
21313
+ var exports_image_utils = {};
21314
+ __export(exports_image_utils, {
21315
+ readClipboardImage: () => readClipboardImage,
21316
+ loadUrlAsDataUrl: () => loadUrlAsDataUrl,
21317
+ loadFileAsDataUrl: () => loadFileAsDataUrl,
21318
+ detectMime: () => detectMime,
21319
+ bufferToDataUrl: () => bufferToDataUrl
21320
+ });
21321
+ import { readFileSync as readFileSync20 } from "fs";
21322
+ import { extname as extname4 } from "path";
21323
+ function detectMime(filePath) {
21324
+ const ext = extname4(filePath).toLowerCase();
21325
+ return MIME_MAP[ext] ?? "image/png";
21326
+ }
21327
+ async function readClipboardImage() {
21328
+ if (typeof Bun !== "undefined" && typeof Bun.Image !== "undefined") {
21329
+ try {
21330
+ const img = Bun.Image.fromClipboard();
21331
+ if (img) {
21332
+ const buf = await img.resize(800, 800, { fit: "inside" }).jpeg({ quality: 60 }).buffer();
21333
+ return Buffer.from(buf);
21334
+ }
21335
+ } catch {}
21336
+ }
21337
+ return readClipboardFallback();
21338
+ }
21339
+ async function readClipboardFallback() {
21340
+ const { platform: platform7 } = await import("os");
21341
+ const { execSync } = await import("child_process");
21342
+ const { readFileSync: readFileSync21, unlinkSync: unlinkSync5 } = await import("fs");
21343
+ const { join: join29 } = await import("path");
21344
+ if (platform7() !== "linux")
21345
+ return null;
21346
+ const tmpPath = join29(process.env.TEMP || process.env.TMP || "/tmp", `mma-clip-${Date.now()}.png`);
21347
+ const commands = [
21348
+ `wl-paste --type image/png > "${tmpPath}" 2>/dev/null`,
21349
+ `xclip -selection clipboard -t image/png -o > "${tmpPath}" 2>/dev/null`
21350
+ ];
21351
+ for (const cmd of commands) {
21352
+ try {
21353
+ execSync(cmd, { timeout: 5000 });
21354
+ const buf = readFileSync21(tmpPath);
21355
+ unlinkSync5(tmpPath);
21356
+ if (buf.length > 0)
21357
+ return buf;
21358
+ } catch {}
21359
+ }
21360
+ try {
21361
+ unlinkSync5(tmpPath);
21362
+ } catch {}
21363
+ return null;
21364
+ }
21365
+ async function loadFileAsDataUrl(filePath) {
21366
+ const buf = readFileSync20(filePath);
21367
+ if (typeof Bun !== "undefined" && typeof Bun.Image !== "undefined") {
21368
+ try {
21369
+ const img = new Bun.Image(buf);
21370
+ const { width } = await img.metadata();
21371
+ const targetWidth = Math.min(800, width || 800);
21372
+ const dataUrl = await img.resize(targetWidth, 800, { fit: "inside" }).jpeg({ quality: 60 }).dataurl();
21373
+ return { dataUrl };
21374
+ } catch {}
21375
+ }
21376
+ const b64 = buf.toString("base64");
21377
+ const ext = filePath.split(".").pop()?.toLowerCase();
21378
+ const mime = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : ext === "webp" ? "image/webp" : "image/png";
21379
+ return { dataUrl: `data:${mime};base64,${b64}` };
21380
+ }
21381
+ async function loadUrlAsDataUrl(url) {
21382
+ const resp = await fetch(url);
21383
+ if (!resp.ok) {
21384
+ throw new Error(`Failed to fetch image: ${resp.status} ${resp.statusText}`);
21385
+ }
21386
+ const buf = Buffer.from(await resp.arrayBuffer());
21387
+ if (typeof Bun !== "undefined" && typeof Bun.Image !== "undefined") {
21388
+ try {
21389
+ const img = new Bun.Image(buf);
21390
+ const { width } = await img.metadata();
21391
+ const targetWidth = Math.min(800, width || 800);
21392
+ const dataUrl = await img.resize(targetWidth, 800, { fit: "inside" }).jpeg({ quality: 60 }).dataurl();
21393
+ return { dataUrl };
21394
+ } catch {}
21395
+ }
21396
+ const b64 = buf.toString("base64");
21397
+ return { dataUrl: `data:image/png;base64,${b64}` };
21398
+ }
21399
+ async function bufferToDataUrl(buf) {
21400
+ if (typeof Bun !== "undefined" && typeof Bun.Image !== "undefined") {
21401
+ try {
21402
+ const img = new Bun.Image(buf);
21403
+ const { width } = await img.metadata();
21404
+ const targetWidth = Math.min(800, width || 800);
21405
+ const dataUrl = await img.resize(targetWidth, 800, { fit: "inside" }).jpeg({ quality: 60 }).dataurl();
21406
+ return { dataUrl };
21407
+ } catch {}
21408
+ }
21409
+ const b64 = buf.toString("base64");
21410
+ return { dataUrl: `data:image/png;base64,${b64}` };
21411
+ }
21412
+ var MIME_MAP;
21413
+ var init_image_utils = __esm(() => {
21414
+ MIME_MAP = {
21415
+ ".png": "image/png",
21416
+ ".jpg": "image/jpeg",
21417
+ ".jpeg": "image/jpeg",
21418
+ ".gif": "image/gif",
21419
+ ".webp": "image/webp",
21420
+ ".bmp": "image/bmp",
21421
+ ".svg": "image/svg+xml"
21422
+ };
21423
+ });
21424
+
21293
21425
  // src/tools/browser.ts
21294
21426
  import { join as join29 } from "path";
21295
21427
  import { mkdirSync as mkdirSync16 } from "fs";
@@ -21326,18 +21458,9 @@ function formatScreenshotForTextModel(snapshot) {
21326
21458
 
21327
21459
  ${snapshot}`;
21328
21460
  }
21329
- function modelSupportsVision(model) {
21330
- const m = model.toLowerCase();
21331
- return /vision|\bvl\b|qwen.*vl|gpt-4|claude|gemini|llava|pixtral/.test(m);
21332
- }
21333
- function formatScreenshotResult(snapshot, screenshot, supportsVision) {
21334
- if (!supportsVision) {
21335
- return formatScreenshotForTextModel(snapshot);
21336
- }
21337
- const base642 = screenshot.toString("base64");
21338
- return `${snapshot}
21339
-
21340
- [Screenshot: data:image/png;base64,${base642}]`;
21461
+ async function screenshotToImagePart(screenshot) {
21462
+ const { dataUrl } = await bufferToDataUrl(screenshot);
21463
+ return { type: "image_url", image_url: { url: dataUrl } };
21341
21464
  }
21342
21465
  function createBrowserTool() {
21343
21466
  return {
@@ -21402,12 +21525,15 @@ function createBrowserTool() {
21402
21525
  }
21403
21526
  const s = getSession(ctx);
21404
21527
  const result = await serialize(() => s.execute(action, args));
21405
- const supportsVision = modelSupportsVision(ctx.config.model);
21406
21528
  if (result.screenshot) {
21407
- return {
21408
- success: result.success,
21409
- output: formatScreenshotResult(result.output, result.screenshot, supportsVision)
21410
- };
21529
+ if (ctx.capabilities?.vision) {
21530
+ return {
21531
+ success: result.success,
21532
+ output: result.output,
21533
+ images: [await screenshotToImagePart(result.screenshot)]
21534
+ };
21535
+ }
21536
+ return { success: result.success, output: formatScreenshotForTextModel(result.output) };
21411
21537
  }
21412
21538
  return { success: result.success, output: result.output };
21413
21539
  }
@@ -21426,125 +21552,13 @@ var session = null, sessionFactory, actionChain;
21426
21552
  var init_browser = __esm(() => {
21427
21553
  init_session();
21428
21554
  init_types();
21555
+ init_image_utils();
21429
21556
  init_i18n();
21430
21557
  init_output();
21431
21558
  init_utils();
21432
21559
  actionChain = Promise.resolve();
21433
21560
  });
21434
21561
 
21435
- // src/llm/image-utils.ts
21436
- var exports_image_utils = {};
21437
- __export(exports_image_utils, {
21438
- readClipboardImage: () => readClipboardImage,
21439
- loadUrlAsDataUrl: () => loadUrlAsDataUrl,
21440
- loadFileAsDataUrl: () => loadFileAsDataUrl,
21441
- detectMime: () => detectMime,
21442
- bufferToDataUrl: () => bufferToDataUrl
21443
- });
21444
- import { readFileSync as readFileSync20 } from "fs";
21445
- import { extname as extname4 } from "path";
21446
- function detectMime(filePath) {
21447
- const ext = extname4(filePath).toLowerCase();
21448
- return MIME_MAP[ext] ?? "image/png";
21449
- }
21450
- async function readClipboardImage() {
21451
- if (typeof Bun !== "undefined" && typeof Bun.Image !== "undefined") {
21452
- try {
21453
- const img = Bun.Image.fromClipboard();
21454
- if (img) {
21455
- const buf = await img.resize(800, 800, { fit: "inside" }).jpeg({ quality: 60 }).buffer();
21456
- return Buffer.from(buf);
21457
- }
21458
- } catch {}
21459
- }
21460
- return readClipboardFallback();
21461
- }
21462
- async function readClipboardFallback() {
21463
- const { platform: platform7 } = await import("os");
21464
- const { execSync } = await import("child_process");
21465
- const { readFileSync: readFileSync21, unlinkSync: unlinkSync5 } = await import("fs");
21466
- const { join: join30 } = await import("path");
21467
- if (platform7() !== "linux")
21468
- return null;
21469
- const tmpPath = join30(process.env.TEMP || process.env.TMP || "/tmp", `mma-clip-${Date.now()}.png`);
21470
- const commands = [
21471
- `wl-paste --type image/png > "${tmpPath}" 2>/dev/null`,
21472
- `xclip -selection clipboard -t image/png -o > "${tmpPath}" 2>/dev/null`
21473
- ];
21474
- for (const cmd of commands) {
21475
- try {
21476
- execSync(cmd, { timeout: 5000 });
21477
- const buf = readFileSync21(tmpPath);
21478
- unlinkSync5(tmpPath);
21479
- if (buf.length > 0)
21480
- return buf;
21481
- } catch {}
21482
- }
21483
- try {
21484
- unlinkSync5(tmpPath);
21485
- } catch {}
21486
- return null;
21487
- }
21488
- async function loadFileAsDataUrl(filePath) {
21489
- const buf = readFileSync20(filePath);
21490
- if (typeof Bun !== "undefined" && typeof Bun.Image !== "undefined") {
21491
- try {
21492
- const img = new Bun.Image(buf);
21493
- const { width } = await img.metadata();
21494
- const targetWidth = Math.min(800, width || 800);
21495
- const dataUrl = await img.resize(targetWidth, 800, { fit: "inside" }).jpeg({ quality: 60 }).dataurl();
21496
- return { dataUrl };
21497
- } catch {}
21498
- }
21499
- const b64 = buf.toString("base64");
21500
- const ext = filePath.split(".").pop()?.toLowerCase();
21501
- const mime = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : ext === "webp" ? "image/webp" : "image/png";
21502
- return { dataUrl: `data:${mime};base64,${b64}` };
21503
- }
21504
- async function loadUrlAsDataUrl(url) {
21505
- const resp = await fetch(url);
21506
- if (!resp.ok) {
21507
- throw new Error(`Failed to fetch image: ${resp.status} ${resp.statusText}`);
21508
- }
21509
- const buf = Buffer.from(await resp.arrayBuffer());
21510
- if (typeof Bun !== "undefined" && typeof Bun.Image !== "undefined") {
21511
- try {
21512
- const img = new Bun.Image(buf);
21513
- const { width } = await img.metadata();
21514
- const targetWidth = Math.min(800, width || 800);
21515
- const dataUrl = await img.resize(targetWidth, 800, { fit: "inside" }).jpeg({ quality: 60 }).dataurl();
21516
- return { dataUrl };
21517
- } catch {}
21518
- }
21519
- const b64 = buf.toString("base64");
21520
- return { dataUrl: `data:image/png;base64,${b64}` };
21521
- }
21522
- async function bufferToDataUrl(buf) {
21523
- if (typeof Bun !== "undefined" && typeof Bun.Image !== "undefined") {
21524
- try {
21525
- const img = new Bun.Image(buf);
21526
- const { width } = await img.metadata();
21527
- const targetWidth = Math.min(800, width || 800);
21528
- const dataUrl = await img.resize(targetWidth, 800, { fit: "inside" }).jpeg({ quality: 60 }).dataurl();
21529
- return { dataUrl };
21530
- } catch {}
21531
- }
21532
- const b64 = buf.toString("base64");
21533
- return { dataUrl: `data:image/png;base64,${b64}` };
21534
- }
21535
- var MIME_MAP;
21536
- var init_image_utils = __esm(() => {
21537
- MIME_MAP = {
21538
- ".png": "image/png",
21539
- ".jpg": "image/jpeg",
21540
- ".jpeg": "image/jpeg",
21541
- ".gif": "image/gif",
21542
- ".webp": "image/webp",
21543
- ".bmp": "image/bmp",
21544
- ".svg": "image/svg+xml"
21545
- };
21546
- });
21547
-
21548
21562
  // src/tools/attach-image.ts
21549
21563
  import { existsSync as existsSync35 } from "fs";
21550
21564
  import { resolve as resolve17 } from "path";
@@ -25291,7 +25305,7 @@ class BrowserModule {
25291
25305
  "Workflow: open(url) → read snapshot → click/type on element numbers → read updated snapshot.",
25292
25306
  'Each snapshot includes a "Content" section with the visible page text, a "Console" section with browser messages, and network errors. Use these to understand the page, detect JS errors, and verify the result of your actions.',
25293
25307
  'Always start with "open" action. Use element numbers from snapshot for click/type targets.',
25294
- 'Use "screenshot" action only if the model supports vision. Otherwise rely on the text snapshot.'
25308
+ 'The "screenshot" action is delivered automatically: a vision model receives the image, a text-only model receives the text snapshot instead. You do not need to decide — just call it and read what you get.'
25295
25309
  ].join(`
25296
25310
  `),
25297
25311
  priority: "normal",
@@ -27704,6 +27718,19 @@ var init_context_log = __esm(() => {
27704
27718
  SEP2 = "─".repeat(78);
27705
27719
  });
27706
27720
 
27721
+ // src/modules/providers/capabilities.ts
27722
+ function resolveModelCapabilities(preset, probe, override) {
27723
+ const base = preset ? { vision: preset.vision, tools: preset.tools } : { ...DEFAULT_MODEL_CAPABILITIES };
27724
+ return { ...base, ...probe ?? {}, ...override ?? {} };
27725
+ }
27726
+ var DEFAULT_MODEL_CAPABILITIES;
27727
+ var init_capabilities = __esm(() => {
27728
+ DEFAULT_MODEL_CAPABILITIES = {
27729
+ vision: false,
27730
+ tools: false
27731
+ };
27732
+ });
27733
+
27707
27734
  // src/core/environment.ts
27708
27735
  import { existsSync as existsSync50, readFileSync as readFileSync32, readdirSync as readdirSync16 } from "fs";
27709
27736
  import { spawnSync as spawnSync3 } from "child_process";
@@ -28335,7 +28362,9 @@ function buildSystemInfo(config, baseDir, profileCompressed) {
28335
28362
  lines.push(`Runtime verification: when a bug is described in terms of runtime behavior (what happens when the app runs), reproduce it — run the app / build / browser tool — and confirm the fix against actual runtime output before calling "plan update step=N status=done". Check existing servers first with process_list/process_log: a stale dev server started BEFORE your change still runs the old pipeline and keeps failing even after the files on disk are fixed. Restart or kill it (process_kill) and verify the exact endpoint/port the user reported — a fresh server on a new port does NOT prove the user's error is gone.`);
28336
28363
  const sec = config.security;
28337
28364
  if (sec?.enabled) {
28338
- const secLines = [`SECURITY RESTRICTIONS (enforced — blocked calls return "[SECURITY BLOCKED]", do not retry them):`];
28365
+ const secLines = [
28366
+ `SECURITY RESTRICTIONS (enforced — blocked calls return "[SECURITY BLOCKED]", do not retry them):`
28367
+ ];
28339
28368
  if (sec.bash?.enabled && sec.bash.blacklist.length > 0) {
28340
28369
  const cmds = sec.bash.blacklist.slice(0, 15).join(", ");
28341
28370
  secLines.push(`Forbidden bash commands: ${cmds}${sec.bash.blacklist.length > 15 ? ", …" : ""}.`);
@@ -28417,19 +28446,23 @@ async function bootstrap(configDir, projectDir, noAgentsMd, exitOnComplete, reas
28417
28446
  logger4.info(t("env.model_loaded", { model: config.model, seconds: String(loadResult.loadTime) }));
28418
28447
  }
28419
28448
  }
28449
+ const capabilitiesRef = {};
28420
28450
  const contextProbePromise = (async () => {
28421
28451
  try {
28422
- const probe = await getLoadedContextLength(config.provider.baseUrl, config.model);
28452
+ const probe = await probeModel(config.provider.baseUrl, config.model);
28423
28453
  if (!probe)
28424
28454
  return null;
28425
- if (probe.actual < config.contextWindow) {
28426
- logger4.logSilent("warn", `Context probe: model "${probe.model}" is loaded with ${probe.actual} tokens, but contextWindow is configured as ${config.contextWindow} — overflow/429 risk. Lower contextWindow or reload the model with a larger context.`);
28427
- } else if (probe.actual > config.contextWindow) {
28428
- logger4.logSilent("info", `Context probe: model "${probe.model}" supports ${probe.actual} tokens — consider "mma context ${probe.actual}" to use the full window.`);
28455
+ if (probe.capabilities)
28456
+ capabilitiesRef.probe = probe.capabilities;
28457
+ const contextProbe = { model: probe.model, actual: probe.contextLength };
28458
+ if (probe.contextLength < config.contextWindow) {
28459
+ logger4.logSilent("warn", `Context probe: model "${probe.model}" is loaded with ${probe.contextLength} tokens, but contextWindow is configured as ${config.contextWindow} — overflow/429 risk. Lower contextWindow or reload the model with a larger context.`);
28460
+ } else if (probe.contextLength > config.contextWindow) {
28461
+ logger4.logSilent("info", `Context probe: model "${probe.model}" supports ${probe.contextLength} tokens — consider "mma context ${probe.contextLength}" to use the full window.`);
28429
28462
  } else {
28430
- logger4.logSilent("debug", `Context probe: configured contextWindow matches loaded context (${probe.actual})`);
28463
+ logger4.logSilent("debug", `Context probe: configured contextWindow matches loaded context (${probe.contextLength})`);
28431
28464
  }
28432
- return probe;
28465
+ return contextProbe;
28433
28466
  } catch {
28434
28467
  return null;
28435
28468
  }
@@ -28608,6 +28641,14 @@ async function bootstrap(configDir, projectDir, noAgentsMd, exitOnComplete, reas
28608
28641
  get: () => sessionManager,
28609
28642
  configurable: true
28610
28643
  });
28644
+ Object.defineProperty(toolCtx, "capabilities", {
28645
+ get: () => {
28646
+ const cfg = configRef.current;
28647
+ const spec = BUILTIN_PROVIDERS.find((p) => p.type === cfg.provider.type);
28648
+ return resolveModelCapabilities(spec?.capabilities, capabilitiesRef.probe, cfg.modelCapabilities?.[cfg.model]);
28649
+ },
28650
+ configurable: true
28651
+ });
28611
28652
  const toolExecutor = new ToolExecutor(toolRegistry, toolCtx, pluginManager);
28612
28653
  toolCtx.llmProvider = llmProvider;
28613
28654
  toolCtx.toolExecutor = toolExecutor;
@@ -28831,6 +28872,7 @@ var init_bootstrap = __esm(() => {
28831
28872
  init_i18n();
28832
28873
  init_agent();
28833
28874
  init_context_log();
28875
+ init_capabilities();
28834
28876
  init_version();
28835
28877
  init_environment();
28836
28878
  init_utils();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "micro-models-agent",
3
- "version": "1.6.0",
3
+ "version": "1.7.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": {