neoagent 1.5.0 → 1.5.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "Proactive personal AI agent with no limits",
5
5
  "license": "MIT",
6
6
  "main": "server/index.js",
@@ -593,6 +593,13 @@
593
593
  <span>Create disabled draft skills from successful multi-step runs</span>
594
594
  </label>
595
595
  </div>
596
+ <div class="form-group">
597
+ <label class="form-label">Smart Selection</label>
598
+ <label class="flex items-center gap-2" style="cursor:pointer;">
599
+ <input type="checkbox" id="settingSmarterModelSelector" autocomplete="off" data-bwignore="true">
600
+ <span>Automatically select best model based on task type (coding, planning, etc.)</span>
601
+ </label>
602
+ </div>
596
603
  <div class="form-group">
597
604
  <label class="form-label">Default Chat Model</label>
598
605
  <select id="settingDefaultChatModel" class="input" autocomplete="off" data-bwignore="true">
@@ -605,6 +612,13 @@
605
612
  <option value="auto">Smart Selector (Auto)</option>
606
613
  </select>
607
614
  </div>
615
+ <div class="form-group">
616
+ <label class="form-label">Fallback Model</label>
617
+ <select id="settingFallbackModelId" class="input" autocomplete="off" data-bwignore="true">
618
+ <option value="gpt-5-nano">GPT-5 Nano (Default Cloud Fallback)</option>
619
+ </select>
620
+ <small class="text-muted" style="font-size: 11px; display: block; margin-top: 4px;">Used if the primary model fails or is offline.</small>
621
+ </div>
608
622
  <div class="form-group">
609
623
  <label class="form-label">Smart Selector Allowed Models</label>
610
624
  <div id="modelCheckboxesContainer" style="display:flex; flex-direction:column; gap:8px;">
@@ -1849,6 +1849,9 @@ $("#settingsBtn").addEventListener("click", async () => {
1849
1849
  $("#settingAutoSkillLearning").checked =
1850
1850
  settings.auto_skill_learning !== false &&
1851
1851
  settings.auto_skill_learning !== "false";
1852
+ $("#settingSmarterModelSelector").checked =
1853
+ settings.smarter_model_selector !== false &&
1854
+ settings.smarter_model_selector !== "false";
1852
1855
 
1853
1856
  const enabledModels = Array.isArray(settings.enabled_models) ? settings.enabled_models : (meta.models || []).map(m => m.id);
1854
1857
 
@@ -1858,6 +1861,8 @@ $("#settingsBtn").addEventListener("click", async () => {
1858
1861
  if (chatModelSelect && subagentModelSelect && meta.models) {
1859
1862
  chatModelSelect.innerHTML = '<option value="auto">Smart Selector (Auto)</option>';
1860
1863
  subagentModelSelect.innerHTML = '<option value="auto">Smart Selector (Auto)</option>';
1864
+ const fallbackModelSelect = $("#settingFallbackModelId");
1865
+ if (fallbackModelSelect) fallbackModelSelect.innerHTML = "";
1861
1866
 
1862
1867
  for (const modelDef of meta.models) {
1863
1868
  const chatOption = document.createElement("option");
@@ -1869,10 +1874,20 @@ $("#settingsBtn").addEventListener("click", async () => {
1869
1874
  subagentOption.value = modelDef.id;
1870
1875
  subagentOption.textContent = modelDef.label;
1871
1876
  subagentModelSelect.appendChild(subagentOption);
1877
+
1878
+ if (fallbackModelSelect) {
1879
+ const fallbackOption = document.createElement("option");
1880
+ fallbackOption.value = modelDef.id;
1881
+ fallbackOption.textContent = modelDef.label;
1882
+ fallbackModelSelect.appendChild(fallbackOption);
1883
+ }
1872
1884
  }
1873
1885
 
1874
1886
  chatModelSelect.value = settings.default_chat_model || "auto";
1875
1887
  subagentModelSelect.value = settings.default_subagent_model || "auto";
1888
+ if ($("#settingFallbackModelId")) {
1889
+ $("#settingFallbackModelId").value = settings.fallback_model_id || "gpt-5-nano";
1890
+ }
1876
1891
 
1877
1892
  const indicator = $("#modelIndicator");
1878
1893
  if (indicator) {
@@ -1947,9 +1962,11 @@ $("#saveSettings").addEventListener("click", async () => {
1947
1962
  heartbeat_enabled: $("#settingHeartbeat").checked,
1948
1963
  headless_browser: $("#settingHeadlessBrowser").checked,
1949
1964
  auto_skill_learning: $("#settingAutoSkillLearning").checked,
1965
+ smarter_model_selector: $("#settingSmarterModelSelector").checked,
1950
1966
  enabled_models: enabledModels,
1951
1967
  default_chat_model: defaultChatModel,
1952
- default_subagent_model: defaultSubagentModel
1968
+ default_subagent_model: defaultSubagentModel,
1969
+ fallback_model_id: $("#settingFallbackModelId") ? $("#settingFallbackModelId").value : 'gpt-5-nano'
1953
1970
  },
1954
1971
  });
1955
1972
 
@@ -25,9 +25,11 @@ function getProviderForUser(userId, task = '', isSubagent = false, modelOverride
25
25
  let defaultChatModel = 'auto';
26
26
  let defaultSubagentModel = 'auto';
27
27
 
28
+ let smarterSelection = true;
29
+
28
30
  try {
29
- const rows = db.prepare('SELECT key, value FROM user_settings WHERE user_id = ? AND key IN (?, ?, ?)')
30
- .all(userId, 'enabled_models', 'default_chat_model', 'default_subagent_model');
31
+ const rows = db.prepare('SELECT key, value FROM user_settings WHERE user_id = ? AND key IN (?, ?, ?, ?)')
32
+ .all(userId, 'enabled_models', 'default_chat_model', 'default_subagent_model', 'smarter_model_selector');
31
33
 
32
34
  for (const row of rows) {
33
35
  if (!row.value) continue;
@@ -40,6 +42,7 @@ function getProviderForUser(userId, task = '', isSubagent = false, modelOverride
40
42
  if (row.key === 'enabled_models') enabledIds = parsedVal;
41
43
  if (row.key === 'default_chat_model') defaultChatModel = parsedVal;
42
44
  if (row.key === 'default_subagent_model') defaultSubagentModel = parsedVal;
45
+ if (row.key === 'smarter_model_selector') smarterSelection = parsedVal !== false && parsedVal !== 'false';
43
46
  }
44
47
  } catch (e) {
45
48
  console.error('Failed to fetch model settings:', e.message);
@@ -70,9 +73,21 @@ function getProviderForUser(userId, task = '', isSubagent = false, modelOverride
70
73
  selectedModelDef = SUPPORTED_MODELS.find((m) => m.id === userSelectedDefault) || fallbackModel;
71
74
  } else {
72
75
  const taskStr = String(task || '').toLowerCase();
73
- const isPlanning = /\b(plan|think|analy[sz]e|complex|step by step)\b/.test(taskStr);
76
+
77
+ // Basic detection
78
+ let isPlanning = /\b(plan|think|analy[sz]e|complex|step by step)\b/.test(taskStr);
79
+ let isCoding = false;
80
+
81
+ // Enhanced detection if enabled
82
+ if (smarterSelection) {
83
+ isPlanning = isPlanning || /\b(reason|strategy|logical|math|complex)\b/.test(taskStr);
84
+ isCoding = /\b(code|program|script|debug|refactor|function|implementation|logic)\b/.test(taskStr);
85
+ }
86
+
74
87
  if (isPlanning) {
75
88
  selectedModelDef = availableModels.find((m) => m.purpose === 'planning') || fallbackModel;
89
+ } else if (isCoding) {
90
+ selectedModelDef = availableModels.find((m) => m.purpose === 'coding') || availableModels.find((m) => m.purpose === 'planning') || fallbackModel;
76
91
  } else if (isSubagent) {
77
92
  selectedModelDef = availableModels.find((m) => m.purpose === 'fast') || fallbackModel;
78
93
  } else {
@@ -302,28 +317,70 @@ class AgentEngine {
302
317
  let streamContent = '';
303
318
  const callOptions = { model, reasoningEffort: this.getReasoningEffort(providerName, options) };
304
319
 
305
- if (options.stream !== false) {
306
- const gen = provider.stream(messages, tools, callOptions);
307
- for await (const chunk of gen) {
308
- if (chunk.type === 'content') {
309
- streamContent += chunk.content;
310
- this.emit(userId, 'run:stream', { runId, content: streamContent, iteration });
311
- }
312
- if (chunk.type === 'done') {
313
- response = chunk;
320
+ const tryModelCall = async (retryForFallback = true) => {
321
+ try {
322
+ if (options.stream !== false) {
323
+ const gen = provider.stream(messages, tools, callOptions);
324
+ for await (const chunk of gen) {
325
+ if (chunk.type === 'content') {
326
+ streamContent += chunk.content;
327
+ this.emit(userId, 'run:stream', { runId, content: streamContent, iteration });
328
+ }
329
+ if (chunk.type === 'done') {
330
+ response = chunk;
331
+ }
332
+ if (chunk.type === 'tool_calls') {
333
+ response = {
334
+ content: chunk.content || streamContent,
335
+ toolCalls: chunk.toolCalls,
336
+ finishReason: 'tool_calls',
337
+ usage: chunk.usage || null
338
+ };
339
+ }
340
+ }
341
+ } else {
342
+ response = await provider.chat(messages, tools, callOptions);
314
343
  }
315
- if (chunk.type === 'tool_calls') {
316
- response = {
317
- content: chunk.content || streamContent,
318
- toolCalls: chunk.toolCalls,
319
- finishReason: 'tool_calls',
320
- usage: chunk.usage || null
321
- };
344
+ } catch (err) {
345
+ console.error(`[Engine] Model call failed (${model}):`, err.message);
346
+ if (retryForFallback && aiSettings.fallback_model_id && aiSettings.fallback_model_id !== model) {
347
+ console.log(`[Engine] Attempting fallback to: ${aiSettings.fallback_model_id}`);
348
+ const fallback = getProviderForUser(userId, userMessage, triggerType === 'subagent', aiSettings.fallback_model_id);
349
+ // Update local state for the retry
350
+ const nextProvider = fallback.provider;
351
+ const nextModel = fallback.model;
352
+ const nextProviderName = fallback.providerName;
353
+
354
+ // Recursive call once
355
+ const retryOptions = { ...callOptions, model: nextModel, reasoningEffort: this.getReasoningEffort(nextProviderName, options) };
356
+
357
+ if (options.stream !== false) {
358
+ const gen = nextProvider.stream(messages, tools, retryOptions);
359
+ for await (const chunk of gen) {
360
+ if (chunk.type === 'content') {
361
+ streamContent += chunk.content;
362
+ this.emit(userId, 'run:stream', { runId, content: streamContent, iteration });
363
+ }
364
+ if (chunk.type === 'done') response = chunk;
365
+ if (chunk.type === 'tool_calls') {
366
+ response = {
367
+ content: chunk.content || streamContent,
368
+ toolCalls: chunk.toolCalls,
369
+ finishReason: 'tool_calls',
370
+ usage: chunk.usage || null
371
+ };
372
+ }
373
+ }
374
+ } else {
375
+ response = await nextProvider.chat(messages, tools, retryOptions);
376
+ }
377
+ } else {
378
+ throw err;
322
379
  }
323
380
  }
324
- } else {
325
- response = await provider.chat(messages, tools, callOptions);
326
- }
381
+ };
382
+
383
+ await tryModelCall();
327
384
 
328
385
  if (!response) {
329
386
  response = { content: streamContent, toolCalls: [], finishReason: 'stop', usage: null };
@@ -1,6 +1,7 @@
1
1
  const { GrokProvider } = require('./providers/grok');
2
2
  const { OpenAIProvider } = require('./providers/openai');
3
3
  const { GoogleProvider } = require('./providers/google');
4
+ const { OllamaProvider } = require('./providers/ollama');
4
5
 
5
6
  const SUPPORTED_MODELS = [
6
7
  {
@@ -26,6 +27,30 @@ const SUPPORTED_MODELS = [
26
27
  label: 'Gemini 3.1 Flash Lite (Preview)',
27
28
  provider: 'google',
28
29
  purpose: 'general'
30
+ },
31
+ {
32
+ id: 'llama3.1:8b',
33
+ label: 'Llama 3.1 8B (Local / General)',
34
+ provider: 'ollama',
35
+ purpose: 'general'
36
+ },
37
+ {
38
+ id: 'phi4-mini',
39
+ label: 'Phi-4 Mini (Local / Fast)',
40
+ provider: 'ollama',
41
+ purpose: 'fast'
42
+ },
43
+ {
44
+ id: 'phi4',
45
+ label: 'Phi-4 (Local / Planning)',
46
+ provider: 'ollama',
47
+ purpose: 'planning'
48
+ },
49
+ {
50
+ id: 'qwen2.5-coder:7b',
51
+ label: 'Qwen 2.5 Coder 7B (Local / Coding)',
52
+ provider: 'ollama',
53
+ purpose: 'coding'
29
54
  }
30
55
  ];
31
56
 
@@ -36,6 +61,8 @@ function createProviderInstance(providerStr) {
36
61
  return new OpenAIProvider({ apiKey: process.env.OPENAI_API_KEY });
37
62
  } else if (providerStr === 'google') {
38
63
  return new GoogleProvider({ apiKey: process.env.GOOGLE_AI_KEY });
64
+ } else if (providerStr === 'ollama') {
65
+ return new OllamaProvider({ baseUrl: process.env.OLLAMA_URL });
39
66
  }
40
67
  throw new Error(`Unknown provider: ${providerStr}`);
41
68
  }
@@ -19,6 +19,32 @@ class OllamaProvider extends BaseProvider {
19
19
  }
20
20
  }
21
21
 
22
+ async ensureModel(model) {
23
+ const models = await this.listModels();
24
+ // Normalization: Ollama often adds :latest if no tag is specified
25
+ const normalizedModel = model.includes(':') ? model : `${model}:latest`;
26
+ const found = models.some(m => m === model || m === normalizedModel);
27
+
28
+ if (found) return true;
29
+
30
+ console.log(`[Ollama] Model '${model}' not found, pulling from registry...`);
31
+ try {
32
+ const res = await fetch(`${this.baseUrl}/api/pull`, {
33
+ method: 'POST',
34
+ headers: { 'Content-Type': 'application/json' },
35
+ body: JSON.stringify({ name: model, stream: false })
36
+ });
37
+ if (!res.ok) throw new Error(`Pull failed: ${res.statusText}`);
38
+ console.log(`[Ollama] Model '${model}' pulled successfully.`);
39
+ // Refresh local model list
40
+ await this.listModels();
41
+ return true;
42
+ } catch (e) {
43
+ console.error(`[Ollama] Failed to pull model '${model}':`, e.message);
44
+ throw e;
45
+ }
46
+ }
47
+
22
48
  getContextWindow(model) {
23
49
  return 128000;
24
50
  }
@@ -36,6 +62,7 @@ class OllamaProvider extends BaseProvider {
36
62
 
37
63
  async chat(messages, tools = [], options = {}) {
38
64
  const model = options.model || this.config.model || 'llama3.1';
65
+ await this.ensureModel(model);
39
66
  const body = {
40
67
  model,
41
68
  messages: messages.map(m => ({
@@ -86,6 +113,7 @@ class OllamaProvider extends BaseProvider {
86
113
 
87
114
  async *stream(messages, tools = [], options = {}) {
88
115
  const model = options.model || this.config.model || 'llama3.1';
116
+ await this.ensureModel(model);
89
117
  const body = {
90
118
  model,
91
119
  messages: messages.map(m => ({
@@ -5,7 +5,9 @@ const DEFAULT_AI_SETTINGS = Object.freeze({
5
5
  chat_history_window: 8,
6
6
  tool_replay_budget_chars: 1200,
7
7
  subagent_max_iterations: 6,
8
- auto_skill_learning: true
8
+ auto_skill_learning: true,
9
+ fallback_model_id: 'gpt-5-nano',
10
+ smarter_model_selector: true
9
11
  });
10
12
 
11
13
  function parseSettingValue(value) {
@@ -21,14 +23,16 @@ function ensureDefaultAiSettings(userId) {
21
23
  if (!userId) return { ...DEFAULT_AI_SETTINGS };
22
24
 
23
25
  const existing = db.prepare(
24
- 'SELECT key, value FROM user_settings WHERE user_id = ? AND key IN (?, ?, ?, ?, ?)'
26
+ 'SELECT key, value FROM user_settings WHERE user_id = ? AND key IN (?, ?, ?, ?, ?, ?, ?)'
25
27
  ).all(
26
28
  userId,
27
29
  'cost_mode',
28
30
  'chat_history_window',
29
31
  'tool_replay_budget_chars',
30
32
  'subagent_max_iterations',
31
- 'auto_skill_learning'
33
+ 'auto_skill_learning',
34
+ 'fallback_model_id',
35
+ 'smarter_model_selector'
32
36
  );
33
37
 
34
38
  const seen = new Set(existing.map((row) => row.key));
@@ -49,14 +53,16 @@ function getAiSettings(userId) {
49
53
  if (!userId) return { ...DEFAULT_AI_SETTINGS };
50
54
 
51
55
  const rows = db.prepare(
52
- 'SELECT key, value FROM user_settings WHERE user_id = ? AND key IN (?, ?, ?, ?, ?)'
56
+ 'SELECT key, value FROM user_settings WHERE user_id = ? AND key IN (?, ?, ?, ?, ?, ?, ?)'
53
57
  ).all(
54
58
  userId,
55
59
  'cost_mode',
56
60
  'chat_history_window',
57
61
  'tool_replay_budget_chars',
58
62
  'subagent_max_iterations',
59
- 'auto_skill_learning'
63
+ 'auto_skill_learning',
64
+ 'fallback_model_id',
65
+ 'smarter_model_selector'
60
66
  );
61
67
 
62
68
  const settings = { ...DEFAULT_AI_SETTINGS };
@@ -69,6 +75,8 @@ function getAiSettings(userId) {
69
75
  settings.subagent_max_iterations = Math.max(2, Math.min(Number(settings.subagent_max_iterations) || DEFAULT_AI_SETTINGS.subagent_max_iterations, 12));
70
76
  settings.cost_mode = typeof settings.cost_mode === 'string' ? settings.cost_mode : DEFAULT_AI_SETTINGS.cost_mode;
71
77
  settings.auto_skill_learning = settings.auto_skill_learning !== false && settings.auto_skill_learning !== 'false';
78
+ settings.smarter_model_selector = settings.smarter_model_selector !== false && settings.smarter_model_selector !== 'false';
79
+ settings.fallback_model_id = typeof settings.fallback_model_id === 'string' ? settings.fallback_model_id : DEFAULT_AI_SETTINGS.fallback_model_id;
72
80
 
73
81
  return settings;
74
82
  }