neoagent 2.4.1-beta.9 → 2.4.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.
Files changed (234) hide show
  1. package/.env.example +33 -3
  2. package/LICENSE +111 -56
  3. package/README.md +8 -3
  4. package/docs/configuration.md +8 -0
  5. package/docs/getting-started.md +9 -3
  6. package/docs/index.md +4 -0
  7. package/extensions/chrome-browser/background.mjs +36 -4
  8. package/extensions/chrome-browser/icons/icon128.png +0 -0
  9. package/extensions/chrome-browser/icons/icon16.png +0 -0
  10. package/extensions/chrome-browser/icons/icon48.png +0 -0
  11. package/extensions/chrome-browser/icons/logo.svg +39 -8
  12. package/extensions/chrome-browser/manifest.json +3 -3
  13. package/extensions/chrome-browser/popup.html +5 -1
  14. package/extensions/chrome-browser/popup.js +13 -0
  15. package/flutter_app/android/app/src/main/AndroidManifest.xml +2 -1
  16. package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  17. package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  18. package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  19. package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  20. package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  21. package/flutter_app/assets/branding/app_icon_1024.png +0 -0
  22. package/flutter_app/assets/branding/app_icon_128.png +0 -0
  23. package/flutter_app/assets/branding/app_icon_192.png +0 -0
  24. package/flutter_app/assets/branding/app_icon_256.png +0 -0
  25. package/flutter_app/assets/branding/app_icon_32.png +0 -0
  26. package/flutter_app/assets/branding/app_icon_512.png +0 -0
  27. package/flutter_app/assets/branding/app_icon_64.png +0 -0
  28. package/flutter_app/assets/branding/app_icon_light_1024.png +0 -0
  29. package/flutter_app/assets/branding/app_icon_light_128.png +0 -0
  30. package/flutter_app/assets/branding/app_icon_light_192.png +0 -0
  31. package/flutter_app/assets/branding/app_icon_light_256.png +0 -0
  32. package/flutter_app/assets/branding/app_icon_light_32.png +0 -0
  33. package/flutter_app/assets/branding/app_icon_light_512.png +0 -0
  34. package/flutter_app/assets/branding/app_icon_light_64.png +0 -0
  35. package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
  36. package/flutter_app/assets/branding/tray_icon_light_template.png +0 -0
  37. package/flutter_app/assets/branding/tray_icon_template.png +0 -0
  38. package/flutter_app/lib/features/location/location_service.dart +3 -0
  39. package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +391 -382
  40. package/flutter_app/lib/features/onboarding/onboarding_companion_step.dart +743 -0
  41. package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +18 -16
  42. package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +19 -18
  43. package/flutter_app/lib/features/onboarding/onboarding_shell.dart +8 -1
  44. package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +16 -13
  45. package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +17 -13
  46. package/flutter_app/lib/main.dart +2 -0
  47. package/flutter_app/lib/main_account_settings.dart +10 -34
  48. package/flutter_app/lib/main_admin.dart +14 -1
  49. package/flutter_app/lib/main_app_shell.dart +377 -340
  50. package/flutter_app/lib/main_chat.dart +707 -227
  51. package/flutter_app/lib/main_controller.dart +197 -42
  52. package/flutter_app/lib/main_devices.dart +436 -4
  53. package/flutter_app/lib/main_integrations.dart +255 -6
  54. package/flutter_app/lib/main_launcher.dart +1 -1
  55. package/flutter_app/lib/main_model_picker.dart +685 -0
  56. package/flutter_app/lib/main_models.dart +212 -0
  57. package/flutter_app/lib/main_navigation.dart +1 -9
  58. package/flutter_app/lib/main_operations.dart +1417 -614
  59. package/flutter_app/lib/main_settings.dart +673 -871
  60. package/flutter_app/lib/main_shared.dart +971 -443
  61. package/flutter_app/lib/main_spacing.dart +4 -4
  62. package/flutter_app/lib/main_theme.dart +22 -14
  63. package/flutter_app/lib/main_unified.dart +5 -72
  64. package/flutter_app/lib/src/android_apk_drop_zone.dart +24 -0
  65. package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +13 -0
  66. package/flutter_app/lib/src/android_apk_drop_zone_web.dart +219 -0
  67. package/flutter_app/lib/src/backend_client.dart +46 -0
  68. package/flutter_app/lib/src/desktop_companion_actions.dart +30 -7
  69. package/flutter_app/lib/src/desktop_companion_io.dart +71 -1
  70. package/flutter_app/lib/src/security/password_strength.dart +84 -0
  71. package/flutter_app/lib/src/stream_renderer.dart +42 -3
  72. package/flutter_app/lib/src/theme/palette.dart +76 -34
  73. package/flutter_app/linux/runner/resources/app_icon.png +0 -0
  74. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
  75. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
  76. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
  77. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
  78. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
  79. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
  80. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
  81. package/flutter_app/pubspec.lock +2 -2
  82. package/flutter_app/pubspec.yaml +7 -1
  83. package/flutter_app/tool/branding_source/neoagent-icon-1024.png +0 -0
  84. package/flutter_app/tool/branding_source/neoagent-icon-128.png +0 -0
  85. package/flutter_app/tool/branding_source/neoagent-icon-16.png +0 -0
  86. package/flutter_app/tool/branding_source/neoagent-icon-180.png +0 -0
  87. package/flutter_app/tool/branding_source/neoagent-icon-192.png +0 -0
  88. package/flutter_app/tool/branding_source/neoagent-icon-256.png +0 -0
  89. package/flutter_app/tool/branding_source/neoagent-icon-32.png +0 -0
  90. package/flutter_app/tool/branding_source/neoagent-icon-48.png +0 -0
  91. package/flutter_app/tool/branding_source/neoagent-icon-512.png +0 -0
  92. package/flutter_app/tool/branding_source/neoagent-icon-64.png +0 -0
  93. package/flutter_app/tool/branding_source/neoagent-icon.svg +43 -0
  94. package/flutter_app/tool/generate_desktop_branding.py +154 -152
  95. package/flutter_app/web/favicon.png +0 -0
  96. package/flutter_app/web/favicon.svg +40 -9
  97. package/flutter_app/web/favicon_light.svg +43 -0
  98. package/flutter_app/web/icons/Icon-192-light.png +0 -0
  99. package/flutter_app/web/icons/Icon-192.png +0 -0
  100. package/flutter_app/web/icons/Icon-512-light.png +0 -0
  101. package/flutter_app/web/icons/Icon-512.png +0 -0
  102. package/flutter_app/web/icons/Icon-maskable-192-light.png +0 -0
  103. package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
  104. package/flutter_app/web/icons/Icon-maskable-512-light.png +0 -0
  105. package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
  106. package/flutter_app/windows/runner/main.cpp +7 -1
  107. package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
  108. package/lib/manager.js +445 -83
  109. package/package.json +17 -3
  110. package/runtime/paths.js +3 -1
  111. package/server/admin/access.js +198 -0
  112. package/server/admin/admin.css +268 -0
  113. package/server/admin/admin.js +348 -0
  114. package/server/admin/analytics.js +128 -0
  115. package/server/admin/index.html +1015 -0
  116. package/server/admin/login.html +290 -0
  117. package/server/admin/logo.svg +43 -0
  118. package/server/admin/sql.js +134 -0
  119. package/server/admin/users.js +147 -0
  120. package/server/config/origins.js +3 -1
  121. package/server/db/database.js +92 -0
  122. package/server/db/ftsQuery.js +27 -0
  123. package/server/http/routes.js +1 -0
  124. package/server/http/static.js +23 -6
  125. package/server/index.js +1 -1
  126. package/server/middleware/adminAuth.js +48 -0
  127. package/server/middleware/auth.js +1 -40
  128. package/server/public/.last_build_id +1 -1
  129. package/server/public/app_icon.png +0 -0
  130. package/server/public/assets/AssetManifest.bin +1 -1
  131. package/server/public/assets/AssetManifest.bin.json +1 -1
  132. package/server/public/assets/assets/branding/app_icon_1024.png +0 -0
  133. package/server/public/assets/assets/branding/app_icon_256.png +0 -0
  134. package/server/public/assets/assets/branding/app_icon_512.png +0 -0
  135. package/server/public/assets/assets/branding/app_icon_light_1024.png +0 -0
  136. package/server/public/assets/assets/branding/app_icon_light_256.png +0 -0
  137. package/server/public/assets/assets/branding/app_icon_light_512.png +0 -0
  138. package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
  139. package/server/public/assets/assets/branding/tray_icon_light_template.png +0 -0
  140. package/server/public/assets/assets/branding/tray_icon_template.png +0 -0
  141. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  142. package/server/public/assets/web/icons/Icon-192.png +0 -0
  143. package/server/public/favicon.png +0 -0
  144. package/server/public/favicon.svg +40 -9
  145. package/server/public/favicon_light.svg +43 -0
  146. package/server/public/flutter_bootstrap.js +2 -2
  147. package/server/public/icons/Icon-192-light.png +0 -0
  148. package/server/public/icons/Icon-192.png +0 -0
  149. package/server/public/icons/Icon-512-light.png +0 -0
  150. package/server/public/icons/Icon-512.png +0 -0
  151. package/server/public/icons/Icon-maskable-192-light.png +0 -0
  152. package/server/public/icons/Icon-maskable-192.png +0 -0
  153. package/server/public/icons/Icon-maskable-512-light.png +0 -0
  154. package/server/public/icons/Icon-maskable-512.png +0 -0
  155. package/server/public/main.dart.js +88537 -85936
  156. package/server/routes/admin.js +632 -0
  157. package/server/routes/agent_profiles.js +3 -0
  158. package/server/routes/agents.js +1 -1
  159. package/server/routes/auth.js +3 -1
  160. package/server/routes/mcp.js +29 -13
  161. package/server/routes/memory.js +23 -1
  162. package/server/routes/messaging.js +2 -0
  163. package/server/routes/screenHistory.js +14 -8
  164. package/server/routes/settings.js +10 -2
  165. package/server/routes/stream.js +1 -1
  166. package/server/routes/triggers.js +4 -4
  167. package/server/routes/voice_assistant.js +36 -1
  168. package/server/routes/workspace.js +86 -0
  169. package/server/services/account/admin_two_factor.js +132 -0
  170. package/server/services/account/password_policy.js +6 -1
  171. package/server/services/ai/completion.js +44 -0
  172. package/server/services/ai/engine.js +321 -500
  173. package/server/services/ai/imageAnalysis.js +9 -5
  174. package/server/services/ai/logFormat.js +46 -0
  175. package/server/services/ai/loopPolicy.js +11 -0
  176. package/server/services/ai/messagingFallback.js +228 -0
  177. package/server/services/ai/models.js +194 -55
  178. package/server/services/ai/providerRetry.js +169 -0
  179. package/server/services/ai/providers/anthropic.js +15 -4
  180. package/server/services/ai/providers/google.js +38 -11
  181. package/server/services/ai/providers/grok.js +19 -68
  182. package/server/services/ai/providers/grokOauth.js +141 -0
  183. package/server/services/ai/providers/nvidia.js +154 -0
  184. package/server/services/ai/providers/ollama.js +76 -39
  185. package/server/services/ai/providers/openai.js +20 -31
  186. package/server/services/ai/providers/openaiCodex.js +6 -2
  187. package/server/services/ai/providers/openaiCompatible.js +70 -0
  188. package/server/services/ai/providers/openrouter.js +162 -0
  189. package/server/services/ai/settings.js +30 -0
  190. package/server/services/ai/systemPrompt.js +51 -29
  191. package/server/services/ai/taskAnalysis.js +60 -1
  192. package/server/services/ai/toolEvidence.js +207 -0
  193. package/server/services/ai/toolSelector.js +8 -1
  194. package/server/services/ai/tools.js +78 -13
  195. package/server/services/browser/extension/registry.js +94 -9
  196. package/server/services/desktop/registry.js +104 -1
  197. package/server/services/desktop/screenRecorder.js +208 -98
  198. package/server/services/desktop/screen_recorder_support.js +46 -0
  199. package/server/services/integrations/google/provider.js +13 -0
  200. package/server/services/integrations/home_assistant/constants.js +88 -0
  201. package/server/services/integrations/home_assistant/network.js +207 -0
  202. package/server/services/integrations/home_assistant/provider.js +249 -0
  203. package/server/services/integrations/home_assistant/snapshot.js +98 -0
  204. package/server/services/integrations/home_assistant/tools.js +101 -0
  205. package/server/services/integrations/manager.js +5 -0
  206. package/server/services/integrations/registry.js +2 -0
  207. package/server/services/integrations/trello/provider.js +8 -2
  208. package/server/services/manager.js +51 -53
  209. package/server/services/mcp/client.js +208 -268
  210. package/server/services/mcp/client_support.js +172 -0
  211. package/server/services/mcp/recovery.js +116 -0
  212. package/server/services/mcp/tool_operations.js +123 -0
  213. package/server/services/memory/ingestion.js +185 -370
  214. package/server/services/memory/ingestion_coverage.js +129 -0
  215. package/server/services/memory/ingestion_documents.js +123 -0
  216. package/server/services/memory/ingestion_support.js +171 -0
  217. package/server/services/memory/intelligence.js +181 -0
  218. package/server/services/memory/manager.js +476 -32
  219. package/server/services/messaging/automation.js +71 -134
  220. package/server/services/messaging/formatting_guides.js +26 -1
  221. package/server/services/messaging/inbound_queue.js +111 -0
  222. package/server/services/messaging/manager.js +25 -8
  223. package/server/services/messaging/typing_keepalive.js +110 -0
  224. package/server/services/streaming/android-stream.js +293 -40
  225. package/server/services/tasks/integration_runtime.js +4 -1
  226. package/server/services/tasks/runtime.js +415 -56
  227. package/server/services/tasks/task_repository.js +48 -6
  228. package/server/services/websocket.js +8 -2
  229. package/server/services/widgets/service.js +17 -1
  230. package/server/services/workspace/manager.js +116 -0
  231. package/server/utils/logger.js +44 -6
  232. package/server/utils/security.js +3 -0
  233. package/server/utils/version.js +29 -19
  234. package/server/services/memory/openhuman_uplift.test.js +0 -98
@@ -5,6 +5,7 @@ const path = require('path');
5
5
 
6
6
  const { getProviderForUser } = require('./engine');
7
7
  const { createProviderInstance, getProviderCatalog } = require('./models');
8
+ const { withProviderRetry } = require('./providerRetry');
8
9
 
9
10
  function resolveImageMimeType(imagePath, overrideMimeType = null) {
10
11
  const normalized = String(overrideMimeType || '').trim().toLowerCase();
@@ -76,11 +77,14 @@ async function analyzeImageForUser({
76
77
  }
77
78
 
78
79
  try {
79
- const response = await candidate.provider.analyzeImage({
80
- imagePath,
81
- mimeType: resolveImageMimeType(imagePath, mimeType),
82
- question,
83
- });
80
+ const response = await withProviderRetry(
81
+ () => candidate.provider.analyzeImage({
82
+ imagePath,
83
+ mimeType: resolveImageMimeType(imagePath, mimeType),
84
+ question,
85
+ }),
86
+ { label: `ImageAnalysis ${candidate.providerName}` },
87
+ );
84
88
  return {
85
89
  description: String(response.content || '').trim(),
86
90
  model: response.model || null,
@@ -0,0 +1,46 @@
1
+ 'use strict';
2
+
3
+ // Small, dependency-free helpers for shaping values into safe log lines and
4
+ // for tolerant JSON parsing. Shared across the AI engine and its sibling
5
+ // modules so log formatting stays consistent and testable.
6
+
7
+ function shortenRunId(runId) {
8
+ const value = String(runId || '').trim();
9
+ if (!value) return 'unknown';
10
+ return value.length <= 8 ? value : value.slice(0, 8);
11
+ }
12
+
13
+ function summarizeForLog(value, maxChars = 220) {
14
+ if (value == null) return '';
15
+
16
+ let text = '';
17
+ if (typeof value === 'string') {
18
+ text = value;
19
+ } else {
20
+ try {
21
+ text = JSON.stringify(value);
22
+ } catch {
23
+ text = String(value);
24
+ }
25
+ }
26
+
27
+ const normalized = text.replace(/\s+/g, ' ').trim();
28
+ if (normalized.length <= maxChars) return normalized;
29
+ return `${normalized.slice(0, maxChars)}...`;
30
+ }
31
+
32
+ function parseMaybeJson(value, fallback = null) {
33
+ if (!value) return fallback;
34
+ if (typeof value === 'object') return value;
35
+ try {
36
+ return JSON.parse(value);
37
+ } catch {
38
+ return fallback;
39
+ }
40
+ }
41
+
42
+ module.exports = {
43
+ shortenRunId,
44
+ summarizeForLog,
45
+ parseMaybeJson,
46
+ };
@@ -46,6 +46,13 @@ function clampFinite(n, lo, hi, fallback) {
46
46
  * @returns {LoopPolicy}
47
47
  */
48
48
  function buildLoopPolicy(aiSettings = {}, triggerType = 'chat', analysisMode = 'execute', options = {}) {
49
+ const autonomyPolicy = options.autonomyPolicy && typeof options.autonomyPolicy === 'object'
50
+ ? options.autonomyPolicy
51
+ : {};
52
+ const complexity = String(autonomyPolicy.complexity || '').trim().toLowerCase();
53
+ const autonomyLevel = String(autonomyPolicy.autonomy_level || autonomyPolicy.autonomyLevel || '').trim().toLowerCase();
54
+ const parallelWork = autonomyPolicy.parallel_work === true || autonomyPolicy.parallelWork === true;
55
+
49
56
  // ── maxIterations ────────────────────────────────────────────────────────
50
57
  // Resolve raw value from options → aiSettings → mode/context defaults,
51
58
  // then clamp to [1, MAX_ALLOWED_ITERATIONS] and floor to integer.
@@ -58,6 +65,10 @@ function buildLoopPolicy(aiSettings = {}, triggerType = 'chat', analysisMode = '
58
65
  rawIterations = DEFAULT_WIDGET_MAX_ITERATIONS;
59
66
  } else if (analysisMode === 'plan_execute') {
60
67
  rawIterations = DEFAULT_PLAN_EXECUTE_MAX_ITERATIONS;
68
+ } else if (complexity === 'complex' || autonomyLevel === 'high') {
69
+ rawIterations = DEFAULT_PLAN_EXECUTE_MAX_ITERATIONS;
70
+ } else if (parallelWork || complexity === 'standard') {
71
+ rawIterations = Math.max(DEFAULT_MAX_ITERATIONS, 28);
61
72
  } else {
62
73
  rawIterations = DEFAULT_MAX_ITERATIONS;
63
74
  }
@@ -0,0 +1,228 @@
1
+ 'use strict';
2
+
3
+ // Deterministic, model-free helpers for shaping outgoing messages and for
4
+ // constructing honest fallback replies when a run fails or the model returns a
5
+ // blank message. Kept free of engine state so the behavior is pure and unit
6
+ // testable: every function derives its output from its arguments alone.
7
+
8
+ const {
9
+ buildPlatformFormattingGuide,
10
+ normalizeOutgoingMessageForPlatform,
11
+ } = require('../messaging/formatting_guides');
12
+ const { summarizeForLog } = require('./logFormat');
13
+
14
+ function normalizeOutgoingMessage(content, platform = null, options = {}) {
15
+ const normalized = normalizeOutgoingMessageForPlatform(platform, content);
16
+ if (options.collapseWhitespace === false) {
17
+ return normalized;
18
+ }
19
+ return normalized.replace(/\s+/g, ' ').trim();
20
+ }
21
+
22
+ function clampRunContext(text, maxChars) {
23
+ const value = normalizeOutgoingMessage(text);
24
+ if (!value) return '';
25
+ if (value.length <= maxChars) return value;
26
+ return `${value.slice(0, maxChars)}...`;
27
+ }
28
+
29
+ function joinSentMessages(messages = []) {
30
+ if (!Array.isArray(messages)) return '';
31
+ return messages
32
+ .map((message) => String(message || '').trim())
33
+ .filter(Boolean)
34
+ .join('\n\n');
35
+ }
36
+
37
+ function normalizeInterimText(content, platform = null) {
38
+ return normalizeOutgoingMessageForPlatform(platform, content, {
39
+ stripNoResponseMarker: false,
40
+ }).trim();
41
+ }
42
+
43
+ function buildBlankMessagingReplyPrompt(attempt, platform = null) {
44
+ const formattingGuide = buildPlatformFormattingGuide(platform);
45
+ if (attempt <= 1) {
46
+ return `You must send one non-empty reply for the external messaging user right now. Do not call tools. Give either: (a) the concrete outcome, or (b) a clear blocker. If tool work already happened, summarize what you actually tried and where it got blocked. Do not ask the user to repeat the original request. Do not promise future work unless that work already happened in this run or will happen automatically before this reply is sent.\n\n${formattingGuide}`;
47
+ }
48
+
49
+ return `Your previous reply was empty. Return one non-empty message now. Do not call tools. If needed, apologize briefly and explain the blocker in one sentence. Use the run evidence already in the conversation instead of asking the user to restate the task. Do not promise future work unless that work already happened in this run or will happen automatically before this reply is sent.\n\n${formattingGuide}`;
50
+ }
51
+
52
+ function parseToolExecutionSummary(item) {
53
+ if (!item?.summary) return null;
54
+ try {
55
+ const parsed = JSON.parse(item.summary);
56
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+
62
+ function toolWorkDescription(toolName) {
63
+ const name = String(toolName || '');
64
+ if (name === 'execute_command') return 'ran shell commands';
65
+ if (name === 'read_file' || name === 'search_files' || name === 'list_directory') return 'checked files';
66
+ if (name === 'web_search' || name === 'http_request') return 'looked up supporting information';
67
+ if (name.startsWith('browser_')) return 'checked the browser state';
68
+ if (name.startsWith('android_')) return 'checked the Android state';
69
+ if (name === 'read_health_data' || name.startsWith('recordings_')) return 'checked stored data';
70
+ return '';
71
+ }
72
+
73
+ function summarizeRecentWork(toolExecutions = []) {
74
+ const descriptions = [];
75
+ for (const item of toolExecutions.slice(-6)) {
76
+ const description = toolWorkDescription(item?.toolName);
77
+ if (!description || descriptions.includes(description)) continue;
78
+ descriptions.push(description);
79
+ if (descriptions.length >= 2) break;
80
+ }
81
+
82
+ if (descriptions.length === 0) return '';
83
+ if (descriptions.length === 1) return `I ${descriptions[0]}`;
84
+ return `I ${descriptions[0]} and ${descriptions[1]}`;
85
+ }
86
+
87
+ function hasFailureSignal(text) {
88
+ const normalized = normalizeOutgoingMessage(text);
89
+ if (!normalized) return false;
90
+ return /\b(error|failed|failure|traceback|exception|timed out|timeout|not found|no such file|permission denied|unable to|cannot|could not|module not found)\b/i.test(normalized);
91
+ }
92
+
93
+ function extractToolFailureMessage(item) {
94
+ const directError = normalizeOutgoingMessage(item?.error || '');
95
+ if (directError) return directError;
96
+
97
+ const summary = parseToolExecutionSummary(item);
98
+ if (!summary) return '';
99
+
100
+ const candidates = [
101
+ summary.message,
102
+ summary.note,
103
+ summary.stderr,
104
+ summary.stdout,
105
+ summary.content,
106
+ summary.excerpt,
107
+ summary.result,
108
+ summary.summary,
109
+ ];
110
+
111
+ if (summary.status === 'error') {
112
+ for (const candidate of candidates) {
113
+ const normalized = normalizeOutgoingMessage(candidate || '');
114
+ if (normalized) return normalized;
115
+ }
116
+ if (summary.exitCode != null) {
117
+ return `The last shell command exited with code ${summary.exitCode}`;
118
+ }
119
+ }
120
+
121
+ for (const candidate of candidates) {
122
+ const normalized = normalizeOutgoingMessage(candidate || '');
123
+ if (hasFailureSignal(normalized)) return normalized;
124
+ }
125
+
126
+ return '';
127
+ }
128
+
129
+ function buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions = [] }) {
130
+ const workSummary = summarizeRecentWork(toolExecutions);
131
+ const blocker = [...toolExecutions].reverse()
132
+ .map((item) => extractToolFailureMessage(item))
133
+ .find(Boolean);
134
+
135
+ if (workSummary && blocker) {
136
+ return `${workSummary}, but I got blocked: ${blocker}. I do not have a confirmed finished result yet.`;
137
+ }
138
+ if (blocker) {
139
+ return `I got blocked while working on this: ${blocker}. I do not have a confirmed finished result yet.`;
140
+ }
141
+ if (workSummary && stepIndex > 0) {
142
+ return `${workSummary}, but I do not have a confirmed finished result yet.`;
143
+ }
144
+ if (failedStepCount > 0) {
145
+ return 'I ran into a tool problem while working on your request, so I do not have a confirmed finished result yet.';
146
+ }
147
+ if (stepIndex > 0) {
148
+ return 'I completed part of the work, but I do not have a confirmed finished result yet.';
149
+ }
150
+ return 'I could not produce a reliable final reply just now.';
151
+ }
152
+
153
+ function buildMessagingFailureScenario({ err, failedStepCount, stepIndex, toolExecutions = [] }) {
154
+ const parts = [];
155
+ const runtimeError = normalizeOutgoingMessage(err?.message || '');
156
+ const workSummary = summarizeRecentWork(toolExecutions);
157
+ const blocker = [...toolExecutions].reverse()
158
+ .map((item) => extractToolFailureMessage(item))
159
+ .find(Boolean);
160
+
161
+ if (runtimeError) {
162
+ parts.push(`Runtime error: ${summarizeForLog(runtimeError, 260)}.`);
163
+ }
164
+ if (workSummary) {
165
+ parts.push(`Observed work before failure: ${workSummary}.`);
166
+ }
167
+ if (blocker) {
168
+ parts.push(`Most specific blocker from run evidence: ${summarizeForLog(blocker, 260)}.`);
169
+ }
170
+ if (stepIndex > 0) {
171
+ parts.push(`Completed steps before failure: ${stepIndex}.`);
172
+ }
173
+ if (failedStepCount > 0) {
174
+ parts.push(`Failed tool steps: ${failedStepCount}.`);
175
+ }
176
+
177
+ return parts.join(' ');
178
+ }
179
+
180
+ function buildDeterministicMessagingErrorReply({ err, failedStepCount, stepIndex, toolExecutions = [] }) {
181
+ const message = normalizeOutgoingMessage(err?.message || '');
182
+ if (/no ai providers? are currently available/i.test(message)) {
183
+ return 'I cannot continue right now because no AI provider is available for this account. Please check the provider settings.';
184
+ }
185
+
186
+ if (/(timeout|timed out)/i.test(message)) {
187
+ return 'I hit a timeout while processing your request and could not finish it reliably.';
188
+ }
189
+
190
+ const blocker = [...toolExecutions].reverse()
191
+ .map((item) => extractToolFailureMessage(item))
192
+ .find(Boolean);
193
+ if (blocker) {
194
+ return `I got blocked while checking this: ${blocker}.`;
195
+ }
196
+
197
+ if (message) {
198
+ return `I got blocked while working on this: ${message}.`;
199
+ }
200
+
201
+ return buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions });
202
+ }
203
+
204
+ function buildModelFailureLoopPrompt({ failedModel, nextModel, errorMessage }) {
205
+ return [
206
+ `The previous model call on "${failedModel}" failed with: ${summarizeForLog(errorMessage, 220)}.`,
207
+ `Continue on "${nextModel}" and recover autonomously.`,
208
+ 'If a previous plan depended on that failed call, adjust your approach and proceed end-to-end.',
209
+ 'Only ask the user for help if no safe path remains.'
210
+ ].join(' ');
211
+ }
212
+
213
+ module.exports = {
214
+ normalizeOutgoingMessage,
215
+ clampRunContext,
216
+ joinSentMessages,
217
+ normalizeInterimText,
218
+ buildBlankMessagingReplyPrompt,
219
+ parseToolExecutionSummary,
220
+ toolWorkDescription,
221
+ summarizeRecentWork,
222
+ hasFailureSignal,
223
+ extractToolFailureMessage,
224
+ buildDeterministicMessagingFallback,
225
+ buildMessagingFailureScenario,
226
+ buildDeterministicMessagingErrorReply,
227
+ buildModelFailureLoopPrompt,
228
+ };
@@ -6,6 +6,9 @@ const { OpenAIProvider } = require('./providers/openai');
6
6
  const { GithubCopilotProvider } = require('./providers/githubCopilot');
7
7
  const { OpenAICodexProvider } = require('./providers/openaiCodex');
8
8
  const { ClaudeCodeProvider } = require('./providers/claudeCode');
9
+ const { GrokOAuthProvider } = require('./providers/grokOauth');
10
+ const { NvidiaProvider } = require('./providers/nvidia');
11
+ const { OpenRouterProvider } = require('./providers/openrouter');
9
12
  const {
10
13
  AI_PROVIDER_DEFINITIONS,
11
14
  getProviderConfigs,
@@ -13,12 +16,21 @@ const {
13
16
  } = require('./settings');
14
17
 
15
18
  const STATIC_MODELS = [
19
+ // — xAI OAuth — fallback entries shown when grok-oauth token is invalid/exhausted.
20
+ // When the token is valid, DYNAMIC_PROVIDERS will replace these with the live list.
16
21
  {
17
- id: 'grok-4-1-fast-reasoning',
18
- label: 'Grok 4.1 (Personality / Default)',
19
- provider: 'grok',
20
- purpose: 'general'
22
+ id: 'grok-4',
23
+ label: 'Grok 4 (xAI OAuth)',
24
+ provider: 'grok-oauth',
25
+ purpose: 'general',
26
+ },
27
+ {
28
+ id: 'grok-4-mini',
29
+ label: 'Grok 4 Mini (xAI OAuth)',
30
+ provider: 'grok-oauth',
31
+ purpose: 'fast',
21
32
  },
33
+ // — GitHub Copilot (subscription; no public /models endpoint) ————————
22
34
  {
23
35
  id: 'gpt-5.3',
24
36
  label: 'GPT-5.3 (Copilot Default)',
@@ -31,6 +43,7 @@ const STATIC_MODELS = [
31
43
  provider: 'github-copilot',
32
44
  purpose: 'coding'
33
45
  },
46
+ // — OpenAI Codex ——————————————————————————————————————————————————————
34
47
  {
35
48
  id: 'gpt-5.5',
36
49
  label: 'GPT-5.5 (Codex Default)',
@@ -49,29 +62,12 @@ const STATIC_MODELS = [
49
62
  provider: 'openai-codex',
50
63
  purpose: 'general'
51
64
  },
65
+ // — Claude Code ————————————————————————————————————————————————————————
52
66
  {
53
- id: 'gpt-5-nano',
54
- label: 'GPT-5 Nano (Fast / Subagents)',
55
- provider: 'openai',
56
- purpose: 'fast'
57
- },
58
- {
59
- id: 'gpt-5-mini',
60
- label: 'GPT-5 Mini (Planning / Complex)',
61
- provider: 'openai',
62
- purpose: 'planning'
63
- },
64
- {
65
- id: 'claude-sonnet-4-20250514',
66
- label: 'Claude Sonnet 4 (Analysis / Writing)',
67
- provider: 'anthropic',
68
- purpose: 'planning'
69
- },
70
- {
71
- id: 'claude-3-5-haiku-20241022',
72
- label: 'Claude 3.5 Haiku (Fast)',
73
- provider: 'anthropic',
74
- purpose: 'fast'
67
+ id: 'claude-opus-4-8',
68
+ label: 'Claude Opus 4.8 (Claude Code / Flagship)',
69
+ provider: 'claude-code',
70
+ purpose: 'general'
75
71
  },
76
72
  {
77
73
  id: 'claude-opus-4-7',
@@ -91,29 +87,155 @@ const STATIC_MODELS = [
91
87
  provider: 'claude-code',
92
88
  purpose: 'fast'
93
89
  },
94
- {
95
- id: 'gemini-3.1-flash-lite-preview',
96
- label: 'Gemini 3.1 Flash Lite (Preview)',
97
- provider: 'google',
98
- purpose: 'general'
99
- },
90
+ // — MiniMax ————————————————————————————————————————————————————————————
100
91
  {
101
92
  id: 'MiniMax-M2.7',
102
93
  label: 'MiniMax M2.7 (Coding Plan)',
103
94
  provider: 'minimax',
104
95
  purpose: 'coding'
105
96
  },
97
+ // — Ollama — default suggestions (full list loaded dynamically) ————————
106
98
  {
107
99
  id: 'qwen3.5:4b',
108
100
  label: 'Qwen 3.5 4B (Local / Ollama)',
109
101
  provider: 'ollama',
110
- purpose: 'general'
102
+ purpose: 'general',
103
+ },
104
+ {
105
+ id: 'gemma4:12b',
106
+ label: 'Gemma 4 12B (Local / Ollama)',
107
+ provider: 'ollama',
108
+ purpose: 'general',
111
109
  }
112
110
  ];
113
111
 
112
+ // Maps a provider id to its class and which runtime fields its constructor takes.
113
+ // Adding a provider is a one-line entry here instead of another dispatch branch.
114
+ // `apiKey`/`baseUrl` mirror exactly what each constructor was historically given;
115
+ // they intentionally do not derive from AI_PROVIDER_DEFINITIONS.supportsBaseUrl,
116
+ // which disagrees for github-copilot/openai-codex (those read their base URL from
117
+ // env, not from per-user config).
118
+ const PROVIDER_FACTORIES = Object.freeze({
119
+ grok: { Provider: GrokProvider, apiKey: true, baseUrl: true },
120
+ openai: { Provider: OpenAIProvider, apiKey: true, baseUrl: true },
121
+ anthropic: { Provider: AnthropicProvider, apiKey: true, baseUrl: true },
122
+ google: { Provider: GoogleProvider, apiKey: true, baseUrl: false },
123
+ minimax: { Provider: AnthropicProvider, apiKey: true, baseUrl: true },
124
+ ollama: { Provider: OllamaProvider, apiKey: false, baseUrl: true },
125
+ 'github-copilot': { Provider: GithubCopilotProvider, apiKey: true, baseUrl: false },
126
+ 'openai-codex': { Provider: OpenAICodexProvider, apiKey: true, baseUrl: false },
127
+ 'claude-code': { Provider: ClaudeCodeProvider, apiKey: true, baseUrl: false },
128
+ 'grok-oauth': { Provider: GrokOAuthProvider, apiKey: true, baseUrl: false },
129
+ nvidia: { Provider: NvidiaProvider, apiKey: true, baseUrl: true },
130
+ openrouter: { Provider: OpenRouterProvider, apiKey: true, baseUrl: true },
131
+ });
132
+
114
133
  const dynamicModelsByBaseUrl = new Map();
115
134
  const REFRESH_INTERVAL = 30000; // 30 seconds
116
135
 
136
+ // Unified dynamic model cache for all API-backed providers.
137
+ // Keyed by `${providerId}:${apiKey.slice(0,8)}` to handle per-user keys.
138
+ const providerModelCache = new Map();
139
+ const DYNAMIC_REFRESH_INTERVAL = 5 * 60 * 1000; // 5 minutes
140
+
141
+ // Populated from OpenRouter's /models response; used to price-classify models
142
+ // from all providers. Keyed by both the full OpenRouter ID ("openai/gpt-5-mini")
143
+ // and the bare model ID ("gpt-5-mini") for cross-provider lookup.
144
+ const openrouterPricingCache = new Map();
145
+
146
+ // Providers whose full model list is fetched from their API at runtime.
147
+ // grok-oauth inherits listModels() from GrokProvider and uses the same xAI endpoint.
148
+ const DYNAMIC_PROVIDERS = ['openai', 'anthropic', 'google', 'nvidia', 'grok', 'grok-oauth', 'openrouter'];
149
+
150
+ function inferModelPurpose(id) {
151
+ const s = id.toLowerCase();
152
+ if (/flash|nano|lite|tiny|haiku|scout|mini(?!max)|small/.test(s)) return 'fast';
153
+ if (/r1|qwq|o[0-9]|reasoning|thinking/.test(s)) return 'planning';
154
+ if (/code|coder|starcoder|devstral|codex|codegemma/.test(s)) return 'coding';
155
+ return 'general';
156
+ }
157
+
158
+ // Pricing tiers: free=$0 cheap<$0.50/1M medium=$0.50–$5/1M expensive>$5/1M
159
+ // Uses live prices from openrouterPricingCache; returns null when unknown.
160
+ function classifyPriceTier(modelId) {
161
+ const costPerM = openrouterPricingCache.get(modelId);
162
+ if (costPerM === undefined) return null;
163
+ if (costPerM === 0) return 'free';
164
+ if (costPerM < 0.5) return 'cheap';
165
+ if (costPerM < 5) return 'medium';
166
+ return 'expensive';
167
+ }
168
+
169
+ // Per-provider functions that turn a raw model object from listModels() into a display label.
170
+ const PROVIDER_LABEL_FN = {
171
+ openai: (m) => `${m.id} (OpenAI)`,
172
+ anthropic: (m) => `${m.name || m.id} (Anthropic)`,
173
+ google: (m) => `${m.name || m.id} (Google)`,
174
+ nvidia: (m) => `${m.id} (NVIDIA NIM)`,
175
+ grok: (m) => `${m.id} (xAI)`,
176
+ openrouter:(m) => `${m.name || m.id} (OpenRouter)`,
177
+ };
178
+
179
+ async function refreshProviderModelList(providerId, apiKey, baseUrl) {
180
+ const cacheKey = `${providerId}:${(apiKey || '').slice(0, 8)}`;
181
+ const existing = providerModelCache.get(cacheKey);
182
+ const now = Date.now();
183
+
184
+ if (existing && now - existing.lastRefresh <= DYNAMIC_REFRESH_INTERVAL) {
185
+ return existing.models;
186
+ }
187
+
188
+ try {
189
+ const factory = PROVIDER_FACTORIES[providerId];
190
+ const config = {};
191
+ if (factory.apiKey) config.apiKey = apiKey;
192
+ if (factory.baseUrl) config.baseUrl = baseUrl;
193
+ const provider = new factory.Provider(config);
194
+
195
+ const raw = await provider.listModels();
196
+
197
+ // OpenRouter returns live pricing — populate the shared cache so all
198
+ // other providers can resolve their price tier without a lookup table.
199
+ if (providerId === 'openrouter') {
200
+ for (const m of raw) {
201
+ if (m.pricing?.prompt == null) continue;
202
+ const inputPerM = parseFloat(m.pricing.prompt) * 1_000_000;
203
+ openrouterPricingCache.set(m.id, inputPerM);
204
+ // Also index by the bare model ID (everything after the first "/")
205
+ // so that e.g. "gpt-5-mini" resolves from "openai/gpt-5-mini".
206
+ if (m.id.includes('/')) {
207
+ const bareId = m.id.slice(m.id.indexOf('/') + 1);
208
+ if (!openrouterPricingCache.has(bareId)) {
209
+ openrouterPricingCache.set(bareId, inputPerM);
210
+ }
211
+ }
212
+ }
213
+ }
214
+
215
+ const labelFn = PROVIDER_LABEL_FN[providerId] || ((m) => m.id);
216
+ const models = raw.map((m) => ({
217
+ id: m.id,
218
+ label: labelFn(m),
219
+ provider: providerId,
220
+ purpose: inferModelPurpose(m.id),
221
+ }));
222
+
223
+ providerModelCache.set(cacheKey, { models, lastRefresh: now });
224
+ return models;
225
+ } catch (err) {
226
+ console.warn(`[Models] Failed to refresh ${providerId} models:`, err.message);
227
+ // Always record a lastRefresh so we don't hammer the API on every request.
228
+ // Permanent errors (auth/billing/credits) get a longer backoff.
229
+ const isPermanent = /401|403|unauthorized|forbidden|credits|spending/i.test(err.message);
230
+ const backoff = isPermanent ? 30 * 60 * 1000 : DYNAMIC_REFRESH_INTERVAL;
231
+ providerModelCache.set(cacheKey, {
232
+ models: existing?.models || [],
233
+ lastRefresh: now - DYNAMIC_REFRESH_INTERVAL + backoff,
234
+ });
235
+ return existing?.models || [];
236
+ }
237
+ }
238
+
117
239
  async function probeOllama(baseUrl, timeoutMs = 1500) {
118
240
  const controller = new AbortController();
119
241
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -277,8 +399,9 @@ async function getSupportedModels(userId, agentId = null) {
277
399
 
278
400
  const all = [...STATIC_MODELS];
279
401
  const staticIds = new Set(STATIC_MODELS.map((model) => model.id));
280
- const ollama = providerById.get('ollama');
281
402
 
403
+ // Ollama: dynamic list from local server
404
+ const ollama = providerById.get('ollama');
282
405
  if (ollama?.enabled) {
283
406
  const dynamicModels = await refreshDynamicModels(ollama.baseUrl);
284
407
  for (const model of dynamicModels) {
@@ -288,10 +411,35 @@ async function getSupportedModels(userId, agentId = null) {
288
411
  }
289
412
  }
290
413
 
414
+ // API-backed providers: fetch model lists in parallel
415
+ const dynamicFetches = DYNAMIC_PROVIDERS
416
+ .filter((id) => providerById.get(id)?.available)
417
+ .map(async (id) => {
418
+ const runtime = getProviderRuntimeConfig(userId, id, agentId);
419
+ return refreshProviderModelList(id, runtime.apiKey, runtime.baseUrl);
420
+ });
421
+
422
+ const dynamicResults = await Promise.allSettled(dynamicFetches);
423
+ for (const result of dynamicResults) {
424
+ if (result.status === 'fulfilled') {
425
+ for (const model of result.value) {
426
+ if (!staticIds.has(model.id)) {
427
+ all.push(model);
428
+ }
429
+ }
430
+ }
431
+ }
432
+
291
433
  return all.map((model) => {
292
434
  const provider = providerById.get(model.provider);
435
+ // Ollama models are always local/free; all others look up the OpenRouter
436
+ // pricing cache (populated above by Promise.allSettled).
437
+ const priceTier = model.provider === 'ollama'
438
+ ? 'free'
439
+ : (model.priceTier ?? classifyPriceTier(model.id));
293
440
  return {
294
441
  ...model,
442
+ priceTier,
295
443
  available: provider?.available !== false,
296
444
  providerStatus: provider?.status || 'unknown',
297
445
  providerStatusLabel: provider?.statusLabel || 'Unknown'
@@ -315,7 +463,7 @@ async function refreshDynamicModels(baseUrl) {
315
463
  id: name,
316
464
  label: `${name} (Ollama / Local)`,
317
465
  provider: 'ollama',
318
- purpose: 'general'
466
+ purpose: 'general',
319
467
  }));
320
468
 
321
469
  dynamicModelsByBaseUrl.set(cacheKey, {
@@ -331,6 +479,11 @@ async function refreshDynamicModels(baseUrl) {
331
479
  }
332
480
 
333
481
  function createProviderInstance(providerStr, userId = null, configOverrides = {}) {
482
+ const factory = PROVIDER_FACTORIES[providerStr];
483
+ if (!factory) {
484
+ throw new Error(`Unknown provider: ${providerStr}`);
485
+ }
486
+
334
487
  const { agentId = null, ...providerOverrides } = configOverrides || {};
335
488
  const runtime = getProviderRuntimeConfig(userId, providerStr, agentId);
336
489
 
@@ -341,30 +494,16 @@ function createProviderInstance(providerStr, userId = null, configOverrides = {}
341
494
  throw new Error(`Provider '${providerStr}' is not configured on this deployment.`);
342
495
  }
343
496
 
344
- if (providerStr === 'grok') {
345
- return new GrokProvider({ apiKey: runtime.apiKey, baseUrl: runtime.baseUrl, ...providerOverrides });
346
- } else if (providerStr === 'openai') {
347
- return new OpenAIProvider({ apiKey: runtime.apiKey, baseUrl: runtime.baseUrl, ...providerOverrides });
348
- } else if (providerStr === 'anthropic') {
349
- return new AnthropicProvider({ apiKey: runtime.apiKey, baseUrl: runtime.baseUrl, ...providerOverrides });
350
- } else if (providerStr === 'google') {
351
- return new GoogleProvider({ apiKey: runtime.apiKey, ...providerOverrides });
352
- } else if (providerStr === 'minimax') {
353
- return new AnthropicProvider({ apiKey: runtime.apiKey, baseUrl: runtime.baseUrl, ...providerOverrides });
354
- } else if (providerStr === 'ollama') {
355
- return new OllamaProvider({ baseUrl: runtime.baseUrl, ...providerOverrides });
356
- } else if (providerStr === 'github-copilot') {
357
- return new GithubCopilotProvider({ apiKey: runtime.apiKey, ...providerOverrides });
358
- } else if (providerStr === 'openai-codex') {
359
- return new OpenAICodexProvider({ apiKey: runtime.apiKey, ...providerOverrides });
360
- } else if (providerStr === 'claude-code') {
361
- return new ClaudeCodeProvider({ apiKey: runtime.apiKey, ...providerOverrides });
362
- }
363
- throw new Error(`Unknown provider: ${providerStr}`);
497
+ const config = {};
498
+ if (factory.apiKey) config.apiKey = runtime.apiKey;
499
+ if (factory.baseUrl) config.baseUrl = runtime.baseUrl;
500
+
501
+ return new factory.Provider({ ...config, ...providerOverrides });
364
502
  }
365
503
 
366
504
  module.exports = {
367
505
  AI_PROVIDER_DEFINITIONS,
506
+ PROVIDER_FACTORIES,
368
507
  SUPPORTED_MODELS: STATIC_MODELS, // Backward compatibility
369
508
  createProviderInstance,
370
509
  getProviderCatalog,