open-agents-ai 0.26.0 → 0.27.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/index.js +194 -9
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6635,9 +6635,18 @@ var init_vision = __esm({
6635
6635
  },
6636
6636
  required: ["image"]
6637
6637
  };
6638
+ /** Active Ollama model name — used as vision fallback if it has vision capability */
6639
+ _activeModel = "";
6640
+ /** Whether the active model has vision capability */
6641
+ _activeModelHasVision = false;
6638
6642
  constructor(workingDir) {
6639
6643
  this.workingDir = workingDir;
6640
6644
  }
6645
+ /** Set the active Ollama model and whether it supports vision */
6646
+ setActiveModel(model, hasVision) {
6647
+ this._activeModel = model;
6648
+ this._activeModelHasVision = hasVision;
6649
+ }
6641
6650
  async execute(args) {
6642
6651
  const start = performance.now();
6643
6652
  const rawPath = args["image"];
@@ -6738,7 +6747,8 @@ Coordinates are normalized (0-1). Multiply by image width/height for pixel value
6738
6747
  }
6739
6748
  async tryOllamaVision(buffer, filename, action, prompt, length, start) {
6740
6749
  const ollamaHost = process.env["OLLAMA_HOST"] || "http://localhost:11434";
6741
- const model = process.env["OLLAMA_VISION_MODEL"] || "moondream";
6750
+ const envModel = process.env["OLLAMA_VISION_MODEL"];
6751
+ const model = envModel || (this._activeModelHasVision && this._activeModel ? this._activeModel : "moondream");
6742
6752
  const imageBase64 = buffer.toString("base64");
6743
6753
  let ollamaPrompt;
6744
6754
  switch (action) {
@@ -6965,7 +6975,16 @@ var init_desktop_click = __esm({
6965
6975
  DesktopClickTool = class {
6966
6976
  workingDir;
6967
6977
  name = "desktop_click";
6968
- description = "Click on a UI element identified by natural language description. Takes a screenshot, uses Moondream vision to find the described element, then clicks at that location using xdotool (Linux) or cliclick (macOS). Example: desktop_click({ target: 'the Save button' })";
6978
+ description = "Click on a UI element identified by natural language description. Takes a screenshot, uses vision to find the described element, then clicks at that location using xdotool (Linux) or cliclick (macOS). Example: desktop_click({ target: 'the Save button' })";
6979
+ /** Active Ollama model name — used as vision fallback if it has vision capability */
6980
+ _activeModel = "";
6981
+ /** Whether the active model has vision capability */
6982
+ _activeModelHasVision = false;
6983
+ /** Set the active Ollama model and whether it supports vision */
6984
+ setActiveModel(model, hasVision) {
6985
+ this._activeModel = model;
6986
+ this._activeModelHasVision = hasVision;
6987
+ }
6969
6988
  parameters = {
6970
6989
  type: "object",
6971
6990
  properties: {
@@ -7050,7 +7069,8 @@ var init_desktop_click = __esm({
7050
7069
  if (!visionWorked) {
7051
7070
  try {
7052
7071
  const ollamaHost = process.env["OLLAMA_HOST"] || "http://localhost:11434";
7053
- const ollamaModel = process.env["OLLAMA_VISION_MODEL"] || "moondream";
7072
+ const envModel = process.env["OLLAMA_VISION_MODEL"];
7073
+ const ollamaModel = envModel || (this._activeModelHasVision && this._activeModel ? this._activeModel : "moondream");
7054
7074
  const imageBase64 = readFileSync11(screenshotPath).toString("base64");
7055
7075
  const res = await fetch(`${ollamaHost}/api/generate`, {
7056
7076
  method: "POST",
@@ -7148,7 +7168,16 @@ Screenshot: ${screenshotPath}`,
7148
7168
  DesktopDescribeTool = class {
7149
7169
  workingDir;
7150
7170
  name = "desktop_describe";
7151
- description = "Take a screenshot and describe what's on the desktop using Moondream vision. Optionally ask a specific question about the screen contents. Use this to become aware of the desktop environment.";
7171
+ description = "Take a screenshot and describe what's on the desktop using vision. Optionally ask a specific question about the screen contents. Use this to become aware of the desktop environment.";
7172
+ /** Active Ollama model name — used as vision fallback if it has vision capability */
7173
+ _activeModel = "";
7174
+ /** Whether the active model has vision capability */
7175
+ _activeModelHasVision = false;
7176
+ /** Set the active Ollama model and whether it supports vision */
7177
+ setActiveModel(model, hasVision) {
7178
+ this._activeModel = model;
7179
+ this._activeModelHasVision = hasVision;
7180
+ }
7152
7181
  parameters = {
7153
7182
  type: "object",
7154
7183
  properties: {
@@ -7214,7 +7243,8 @@ ${caption}`);
7214
7243
  if (!visionWorked) {
7215
7244
  try {
7216
7245
  const ollamaHost = process.env["OLLAMA_HOST"] || "http://localhost:11434";
7217
- const ollamaModel = process.env["OLLAMA_VISION_MODEL"] || "moondream";
7246
+ const envModel = process.env["OLLAMA_VISION_MODEL"];
7247
+ const ollamaModel = envModel || (this._activeModelHasVision && this._activeModel ? this._activeModel : "moondream");
7218
7248
  const imageBase64 = imageBuffer.toString("base64");
7219
7249
  const ollamaPrompt = question || "Describe what you see on this desktop screenshot in detail. Include visible applications, windows, text, and UI elements.";
7220
7250
  const res = await fetch(`${ollamaHost}/api/generate`, {
@@ -12783,6 +12813,52 @@ async function queryContextSize(baseUrl, modelName, apiKey) {
12783
12813
  return ollamaSize;
12784
12814
  return queryOpenAIContextSize(baseUrl, modelName, apiKey);
12785
12815
  }
12816
+ async function queryModelCapabilities(baseUrl, modelName) {
12817
+ const caps = { vision: false, toolUse: false, thinking: false };
12818
+ try {
12819
+ const normalized = normalizeBaseUrl(baseUrl);
12820
+ const res = await fetch(`${normalized}/api/show`, {
12821
+ method: "POST",
12822
+ headers: { "Content-Type": "application/json" },
12823
+ body: JSON.stringify({ name: modelName }),
12824
+ signal: AbortSignal.timeout(1e4)
12825
+ });
12826
+ if (!res.ok)
12827
+ return caps;
12828
+ const data = await res.json();
12829
+ if (Array.isArray(data.capabilities)) {
12830
+ if (data.capabilities.includes("vision"))
12831
+ caps.vision = true;
12832
+ if (data.capabilities.includes("tools"))
12833
+ caps.toolUse = true;
12834
+ if (data.capabilities.includes("thinking"))
12835
+ caps.thinking = true;
12836
+ }
12837
+ if (data.model_info) {
12838
+ for (const key of Object.keys(data.model_info)) {
12839
+ const k = key.toLowerCase();
12840
+ if (k.includes("vision.block_count") || k.includes("clip.") || k.includes("image_token_id") || k.includes("projector") || k.includes("vision.embedding_length")) {
12841
+ const val = data.model_info[key];
12842
+ if (val !== null && val !== void 0 && val !== 0 && val !== "") {
12843
+ caps.vision = true;
12844
+ }
12845
+ }
12846
+ }
12847
+ }
12848
+ const nameLower = modelName.toLowerCase();
12849
+ if (/qwen3|qwen2\.5|llama3\.[13]|mistral|mixtral|command-r|gemma3|devstral|deepseek/.test(nameLower)) {
12850
+ caps.toolUse = true;
12851
+ }
12852
+ if (data.template) {
12853
+ if (data.template.includes("<think>") || data.template.includes("thinking")) {
12854
+ caps.thinking = true;
12855
+ }
12856
+ }
12857
+ return caps;
12858
+ } catch {
12859
+ return caps;
12860
+ }
12861
+ }
12786
12862
  function formatBytes(bytes) {
12787
12863
  if (bytes < 1024)
12788
12864
  return `${bytes} B`;
@@ -14685,6 +14761,63 @@ function isFirstRun() {
14685
14761
  return true;
14686
14762
  }
14687
14763
  }
14764
+ async function ensureVisionDeps(onInfo) {
14765
+ const log = onInfo ?? (() => {
14766
+ });
14767
+ try {
14768
+ execSync13("which tesseract", { stdio: "pipe", timeout: 3e3 });
14769
+ } catch {
14770
+ log("Installing tesseract-ocr...");
14771
+ try {
14772
+ const cmds = [
14773
+ "sudo -n apt-get install -y tesseract-ocr 2>/dev/null",
14774
+ "sudo -n dnf install -y tesseract 2>/dev/null",
14775
+ "sudo -n pacman -S --noconfirm tesseract 2>/dev/null",
14776
+ "brew install tesseract 2>/dev/null"
14777
+ ];
14778
+ let installed = false;
14779
+ for (const cmd of cmds) {
14780
+ try {
14781
+ execSync13(cmd, { stdio: "pipe", timeout: 6e4 });
14782
+ installed = true;
14783
+ break;
14784
+ } catch {
14785
+ }
14786
+ }
14787
+ if (installed)
14788
+ log("Tesseract installed.");
14789
+ else
14790
+ log("Could not auto-install tesseract (install manually: sudo apt install tesseract-ocr)");
14791
+ } catch {
14792
+ }
14793
+ }
14794
+ try {
14795
+ execSync13("which moondream-station", { stdio: "pipe", timeout: 3e3 });
14796
+ } catch {
14797
+ log("Installing moondream-station...");
14798
+ try {
14799
+ const pipCmds = [
14800
+ "pip3 install moondream-station 2>/dev/null",
14801
+ "pip install moondream-station 2>/dev/null",
14802
+ "python3 -m pip install moondream-station 2>/dev/null"
14803
+ ];
14804
+ let installed = false;
14805
+ for (const cmd of pipCmds) {
14806
+ try {
14807
+ execSync13(cmd, { stdio: "pipe", timeout: 12e4 });
14808
+ installed = true;
14809
+ break;
14810
+ } catch {
14811
+ }
14812
+ }
14813
+ if (installed)
14814
+ log("moondream-station installed.");
14815
+ else
14816
+ log("Could not auto-install moondream-station (install manually: pip install moondream-station)");
14817
+ } catch {
14818
+ }
14819
+ }
14820
+ }
14688
14821
  function expandedModelName(baseModel) {
14689
14822
  return `open-agents-${baseModel.replace(":", "-").replace(/\./g, "")}`;
14690
14823
  }
@@ -15484,6 +15617,10 @@ async function switchModel(query, ctx, local = false) {
15484
15617
  ctx.setContextWindowSize(ctxSize);
15485
15618
  }
15486
15619
  }
15620
+ if (ctx.setCapabilities) {
15621
+ const caps = await queryModelCapabilities(ctx.config.backendUrl, finalModel);
15622
+ ctx.setCapabilities(caps);
15623
+ }
15487
15624
  } catch (err) {
15488
15625
  renderError(`Failed to switch model: ${err instanceof Error ? err.message : String(err)}`);
15489
15626
  }
@@ -18666,6 +18803,18 @@ var init_status_bar = __esm({
18666
18803
  setContextWindowSize(size) {
18667
18804
  this.metrics.contextWindowSize = size;
18668
18805
  }
18806
+ /** Model capabilities — shown as emoji indicators on the status bar */
18807
+ _caps = {
18808
+ vision: false,
18809
+ toolUse: false,
18810
+ thinking: false
18811
+ };
18812
+ /** Update model capability indicators */
18813
+ setCapabilities(caps) {
18814
+ this._caps = caps;
18815
+ if (this.active)
18816
+ this.renderFooterPreserveCursor();
18817
+ }
18669
18818
  /** Update token metrics from a token_usage event */
18670
18819
  updateMetrics(update) {
18671
18820
  if (update.promptTokens !== void 0)
@@ -18822,7 +18971,15 @@ var init_status_bar = __esm({
18822
18971
  const countdown = this._countdown > 0 ? c2.dim(` ${this._countdown}s`) : "";
18823
18972
  recordingLabel = pipe + dot + pastel2(210, " REC") + countdown;
18824
18973
  }
18825
- return ` ${tokInLabel}${pipe}${tokOutLabel}${pipe}${ctxLabel}${costLabel}${recordingLabel}`;
18974
+ const capParts = [];
18975
+ if (this._caps.vision)
18976
+ capParts.push("\u{1F441}");
18977
+ if (this._caps.toolUse)
18978
+ capParts.push("\u{1F527}");
18979
+ if (this._caps.thinking)
18980
+ capParts.push("\u{1F9E0}");
18981
+ const capsLabel = capParts.length > 0 ? pipe + pastel2(183, capParts.join(" ")) : "";
18982
+ return ` ${tokInLabel}${pipe}${tokOutLabel}${pipe}${ctxLabel}${costLabel}${capsLabel}${recordingLabel}`;
18826
18983
  }
18827
18984
  // -------------------------------------------------------------------------
18828
18985
  // Private
@@ -19157,7 +19314,7 @@ Use task_status("${taskId}") or task_output("${taskId}") to check progress.`
19157
19314
  }
19158
19315
  };
19159
19316
  }
19160
- function startTask(task, config, repoRoot, voice, stream, taskStores, bruteForce, statusBar, sudoCallback, costTracker, onComplete, taskType, contextWindowSize) {
19317
+ function startTask(task, config, repoRoot, voice, stream, taskStores, bruteForce, statusBar, sudoCallback, costTracker, onComplete, taskType, contextWindowSize, modelCaps) {
19161
19318
  const modelTier = getModelTier(config.model);
19162
19319
  const projectCtx = buildProjectContext(repoRoot, taskStores?.contextStores);
19163
19320
  let dynamicContext = formatContextForPrompt(projectCtx, modelTier);
@@ -19190,6 +19347,12 @@ function startTask(task, config, repoRoot, voice, stream, taskStores, bruteForce
19190
19347
  }
19191
19348
  }
19192
19349
  }
19350
+ const hasVision = modelCaps?.vision ?? false;
19351
+ for (const tool of tools) {
19352
+ if ("setActiveModel" in tool && typeof tool.setActiveModel === "function") {
19353
+ tool.setActiveModel(config.model, hasVision);
19354
+ }
19355
+ }
19193
19356
  runner.registerTools(tools);
19194
19357
  const filesTouched = /* @__PURE__ */ new Set();
19195
19358
  const toolSequence = [];
@@ -19489,11 +19652,29 @@ async function startInteractive(config, repoPath) {
19489
19652
  }
19490
19653
  }).catch(() => {
19491
19654
  });
19655
+ let resolvedCaps = {
19656
+ vision: false,
19657
+ toolUse: false,
19658
+ thinking: false
19659
+ };
19660
+ queryModelCapabilities(config.backendUrl, config.model).then((caps) => {
19661
+ resolvedCaps = caps;
19662
+ statusBar.setCapabilities(caps);
19663
+ }).catch(() => {
19664
+ });
19492
19665
  const provider = detectProvider(config.backendUrl);
19493
19666
  const costTracker = new CostTracker(provider.id);
19494
19667
  const sessionMetrics = new SessionMetrics();
19495
19668
  const workEvaluator = new WorkEvaluator();
19496
19669
  ensureTranscribeCliBackground();
19670
+ ensureVisionDeps((msg) => {
19671
+ if (statusBar?.isActive) {
19672
+ statusBar.beginContentWrite();
19673
+ renderInfo(msg);
19674
+ statusBar.endContentWrite();
19675
+ }
19676
+ }).catch(() => {
19677
+ });
19497
19678
  const voiceEngine = new VoiceEngine();
19498
19679
  const streamRenderer = new StreamRenderer();
19499
19680
  if (savedSettings.voice) {
@@ -19791,6 +19972,10 @@ async function startInteractive(config, repoPath) {
19791
19972
  resolvedContextWindowSize = size;
19792
19973
  statusBar.setContextWindowSize(size);
19793
19974
  },
19975
+ setCapabilities: (caps) => {
19976
+ resolvedCaps = caps;
19977
+ statusBar.setCapabilities(caps);
19978
+ },
19794
19979
  hasActiveTask: () => activeTask !== null,
19795
19980
  abortTask() {
19796
19981
  if (!activeTask)
@@ -19946,7 +20131,7 @@ Execute this skill now. Follow the behavioral guidance above.`;
19946
20131
  toolPatternStore: toolPatternStore ?? void 0
19947
20132
  }, bruteForceEnabled, statusBar, handleSudoRequest, costTracker, (summary) => {
19948
20133
  lastCompletedSummary = summary;
19949
- }, currentTaskType, resolvedContextWindowSize);
20134
+ }, currentTaskType, resolvedContextWindowSize, resolvedCaps);
19950
20135
  activeTask = task;
19951
20136
  showPrompt();
19952
20137
  await task.promise;
@@ -20048,7 +20233,7 @@ Summarize or analyze this transcription as appropriate.`;
20048
20233
  toolPatternStore: toolPatternStore ?? void 0
20049
20234
  }, bruteForceEnabled, statusBar, handleSudoRequest, costTracker, (summary) => {
20050
20235
  lastCompletedSummary = summary;
20051
- }, currentTaskType, resolvedContextWindowSize);
20236
+ }, currentTaskType, resolvedContextWindowSize, resolvedCaps);
20052
20237
  activeTask = task;
20053
20238
  showPrompt();
20054
20239
  await task.promise;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "description": "AI coding agent powered by open-source models (Ollama/vLLM) — interactive TUI with agentic tool-calling loop",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",