neoagent 1.4.8 → 1.4.10

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.4.8",
3
+ "version": "1.4.10",
4
4
  "description": "Proactive personal AI agent with no limits",
5
5
  "license": "MIT",
6
6
  "main": "server/index.js",
@@ -243,7 +243,9 @@ class AgentEngine {
243
243
  this.emit(userId, 'run:start', { runId, title: runTitle, model, triggerType, triggerSource });
244
244
 
245
245
  const systemPrompt = await this.buildSystemPrompt(userId, { ...(options.context || {}), userMessage });
246
- const builtInTools = this.getAvailableTools(app);
246
+ // Pass short descriptions so the model always knows every available tool.
247
+ // compactToolDefinition caps tool desc at 120 chars, param desc at 70 chars.
248
+ const builtInTools = this.getAvailableTools(app, { includeDescriptions: true });
247
249
  const mcpManager = app?.locals?.mcpManager || app?.locals?.mcpClient || this.mcpManager;
248
250
  const mcpTools = mcpManager ? mcpManager.getAllTools(userId) : [];
249
251
  const tools = selectToolsForTask(userMessage, builtInTools, mcpTools, options);
@@ -468,17 +470,21 @@ class AgentEngine {
468
470
 
469
471
  const runMeta = this.activeRuns.get(runId);
470
472
  const messagingSent = runMeta?.messagingSent || false;
471
- const lastToolName = runMeta?.lastToolName;
472
- const lastToolTarget = runMeta?.lastToolTarget;
473
473
  this.activeRuns.delete(runId);
474
474
  this.emit(userId, 'run:complete', { runId, content: lastContent, totalTokens, iterations: iteration, triggerSource });
475
475
 
476
- const lastActionWasSendToChat = lastToolName === 'send_message' && lastToolTarget === options.chatId;
477
- if (triggerSource === 'messaging' && options.source && options.chatId && (!lastActionWasSendToChat || forcedFinalResponse)) {
478
- if (lastContent && lastContent.trim() && lastContent.trim() !== '[NO RESPONSE]') {
476
+ // Fallback: if this was a messaging-triggered run and the AI never called
477
+ // send_message itself, auto-send its final text as a reply.
478
+ // We check messagingSent (not just the last tool) so a send_message followed
479
+ // by any other tool (memory_save, think, etc.) does NOT fire a duplicate.
480
+ if (triggerSource === 'messaging' && options.source && options.chatId && !messagingSent) {
481
+ // Strip [NO RESPONSE] markers the AI may have embedded anywhere in the text,
482
+ // then only send if real content remains.
483
+ const cleanedContent = (lastContent || '').replace(/\[NO RESPONSE\]/gi, '').trim();
484
+ if (cleanedContent && cleanedContent !== '[NO RESPONSE]') {
479
485
  const manager = this.messagingManager;
480
486
  if (manager) {
481
- const chunks = lastContent.split(/\n\s*\n/).filter((c) => c.trim().length > 0);
487
+ const chunks = cleanedContent.split(/\n\s*\n/).filter((c) => c.trim().length > 0);
482
488
  (async () => {
483
489
  for (let i = 0; i < chunks.length; i++) {
484
490
  if (i > 0) {
@@ -1,145 +1,39 @@
1
- const ALWAYS_ON_TOOLS = ['notify_user'];
2
-
3
- const PACKS = {
4
- code: ['execute_command', 'read_file', 'list_directory', 'search_files'],
5
- web: ['web_search', 'http_request', 'browser_navigate', 'browser_extract', 'browser_click', 'browser_type', 'browser_screenshot'],
6
- messaging: ['send_message', 'make_call'],
7
- memory: ['memory_recall', 'session_search', 'memory_save', 'memory_update_core', 'memory_read', 'memory_write'],
8
- scheduling: ['create_scheduled_task', 'schedule_run', 'list_scheduled_tasks', 'update_scheduled_task', 'delete_scheduled_task'],
9
- protocols: ['manage_protocols'],
10
- skills: ['create_skill', 'list_skills', 'update_skill', 'delete_skill'],
11
- images: ['generate_image', 'analyze_image'],
12
- tables: ['generate_table', 'generate_graph'],
13
- subagents: ['spawn_subagent'],
14
- mcpAdmin: ['mcp_add_server', 'mcp_list_servers', 'mcp_remove_server'],
15
- health: ['read_health_data']
16
- };
17
-
18
- function containsAny(text, patterns) {
19
- return patterns.some((pattern) => pattern.test(text));
20
- }
21
-
22
- function detectRequestedPacks(task = '', options = {}) {
23
- const text = String(task || '').toLowerCase();
24
- const packs = new Set();
25
-
26
- if (containsAny(text, [
27
- /\b(run|execute|command|shell|terminal|bash|zsh|npm|node|python|script|repo|code|bug|fix|patch|test|build|file|folder|directory|grep|search files?)\b/,
28
- /\b(read|open|inspect)\s+(the\s+)?(file|repo|code)\b/
29
- ])) {
30
- packs.add('code');
31
- }
32
-
33
- if (containsAny(text, [
34
- /\b(web|website|url|page|browser|click|navigate|scrape|search|google|lookup|http|fetch|api request|screenshot)\b/,
35
- /\bopen\b.*\bsite\b/
36
- ])) {
37
- packs.add('web');
38
- }
39
-
40
- if (containsAny(text, [
41
- /\b(message|reply|respond|text|whatsapp|telegram|discord|dm|email|call|phone|notify|send to)\b/,
42
- /\[no response\]/,
43
- /\bsend_message\b/,
44
- /\bmake_call\b/
45
- ])) {
46
- packs.add('messaging');
47
- }
48
-
49
- if (containsAny(text, [
50
- /\bmemory\b/,
51
- /\bremember\b/,
52
- /\brecall\b/,
53
- /\bprevious chat\b/,
54
- /\blast time\b/,
55
- /\bpast conversation\b/,
56
- /\bpreference\b/,
57
- /\bprofile\b/,
58
- /\bsoul\b/
59
- ])) {
60
- packs.add('memory');
61
- }
62
-
63
- if (containsAny(text, [
64
- /\bschedule\b/,
65
- /\bcron\b/,
66
- /\bremind\b/,
67
- /\brecurring\b/,
68
- /\bweekly\b/,
69
- /\bdaily\b/,
70
- /\bone-time\b/,
71
- /\btask\b.*\blater\b/
72
- ])) {
73
- packs.add('scheduling');
74
- }
75
-
76
- if (containsAny(text, [/\bprotocol\b/, /\bplaybook\b/])) {
77
- packs.add('protocols');
78
- }
79
-
80
- if (containsAny(text, [/\bskill\b/, /\binstall skill\b/, /\bcreate skill\b/])) {
81
- packs.add('skills');
82
- }
83
-
84
- if (containsAny(text, [/\bimage\b/, /\bpicture\b/, /\bphoto\b/, /\bgraph\b/, /\bchart\b/, /\btable\b/, /\bqr\b/, /\bocr\b/])) {
85
- packs.add('images');
86
- }
87
-
88
- if (containsAny(text, [/\btable\b/, /\bspreadsheet\b/, /\bgraph\b/, /\bchart\b/])) {
89
- packs.add('tables');
90
- }
91
-
92
- if (containsAny(text, [/\bsub-?agent\b/, /\bdelegate\b/, /\bparallel\b/, /\bbackground worker\b/])) {
93
- packs.add('subagents');
94
- }
95
-
96
- if (containsAny(text, [/\bmcp\b/, /\bmodel context protocol\b/, /\bserver tool\b/])) {
97
- packs.add('mcpAdmin');
98
- }
99
-
100
- if (options.mediaAttachments?.length) {
101
- packs.add('images');
102
- }
103
-
104
- if (containsAny(text, [/\bhealth\b/, /\bfitness\b/, /\bsteps\b/, /\bsleep\b/, /\bheart rate\b/, /\bworkout\b/, /\bblood\b/, /\bsamsung health\b/, /\bhealth connect\b/])) {
105
- packs.add('health');
106
- }
107
-
108
- return packs;
109
- }
110
-
111
- function maybeSelectMcpTools(text, mcpTools = []) {
112
- const normalized = String(text || '').toLowerCase();
113
- if (!normalized || !mcpTools.length) return [];
114
-
1
+ 'use strict';
2
+
3
+ /**
4
+ * Tool selection strategy:
5
+ *
6
+ * Built-ins: always passed in full — descriptions are capped short by
7
+ * compactToolDefinition({ includeDescriptions: true }) in tools.js, so the
8
+ * overhead is a fixed ~100 tokens/tool and the model always knows every tool
9
+ * that exists.
10
+ *
11
+ * MCP tools: user-defined and potentially numerous. Include all when the set
12
+ * is small; keyword-filter when the registry grows large.
13
+ */
14
+
15
+ const MCP_ALWAYS_INCLUDE_THRESHOLD = 20;
16
+
17
+ function selectMcpTools(task, mcpTools = []) {
18
+ if (!mcpTools.length) return [];
19
+ if (mcpTools.length <= MCP_ALWAYS_INCLUDE_THRESHOLD) return mcpTools;
20
+
21
+ // Large MCP registry: match by tool name, original name, or server id so we
22
+ // still surface the right tools without dumping hundreds of schemas.
23
+ const normalized = String(task || '').toLowerCase();
115
24
  const explicitMcp = /\bmcp\b|\bmodel context protocol\b/.test(normalized);
25
+
116
26
  return mcpTools.filter((tool) => {
27
+ if (explicitMcp) return true;
117
28
  const name = String(tool.name || '').toLowerCase();
118
29
  const original = String(tool.originalName || '').toLowerCase();
119
30
  const server = String(tool.serverId || '').toLowerCase();
120
- return explicitMcp || normalized.includes(name) || normalized.includes(original) || (server && normalized.includes(server));
31
+ return normalized.includes(name) || normalized.includes(original) || (server && normalized.includes(server));
121
32
  });
122
33
  }
123
34
 
124
- function selectToolsForTask(task, builtInTools = [], mcpTools = [], options = {}) {
125
- const packs = detectRequestedPacks(task, options);
126
- const allowNames = new Set(ALWAYS_ON_TOOLS);
127
-
128
- for (const pack of packs) {
129
- for (const toolName of PACKS[pack] || []) {
130
- allowNames.add(toolName);
131
- }
132
- }
133
-
134
- const selectedBuiltIns = builtInTools.filter((tool) => allowNames.has(tool.name));
135
- const selectedMcp = maybeSelectMcpTools(task, mcpTools);
136
-
137
- return [...selectedBuiltIns, ...selectedMcp];
35
+ function selectToolsForTask(task, builtInTools = [], mcpTools = [], _options = {}) {
36
+ return [...builtInTools, ...selectMcpTools(task, mcpTools)];
138
37
  }
139
38
 
140
- module.exports = {
141
- ALWAYS_ON_TOOLS,
142
- PACKS,
143
- detectRequestedPacks,
144
- selectToolsForTask
145
- };
39
+ module.exports = { selectToolsForTask, selectMcpTools };
@@ -588,7 +588,7 @@ function getAvailableTools(app, options = {}) {
588
588
  parameters: {
589
589
  type: 'object',
590
590
  properties: {
591
- metric_type: { type: 'string', description: 'The specific metric to query, e.g. "Steps", "HeartRate", "SleepSession". Optional.' },
591
+ metric_type: { type: 'string', description: 'The specific metric to query, e.g. "steps", "heart_rate", "sleep_session", "exercise_session", "weight". Use the summary (no metric_type) first to see what\'s available. Optional.' },
592
592
  limit: { type: 'number', description: 'Maximum number of recent records to return if metric_type is specified (default 50, max 200).' }
593
593
  }
594
594
  }
@@ -169,6 +169,15 @@ function getHealthSyncStatus(userId) {
169
169
  };
170
170
  }
171
171
 
172
+ function normalizeMetricType(raw) {
173
+ // Accept any casing/spacing: "HeartRate" → "heart_rate", "Steps" → "steps", etc.
174
+ return String(raw || '')
175
+ .trim()
176
+ .replace(/([a-z])([A-Z])/g, '$1_$2') // camelCase/PascalCase → snake_case
177
+ .replace(/[\s-]+/g, '_') // spaces/dashes → underscore
178
+ .toLowerCase();
179
+ }
180
+
172
181
  function readHealthData(userId, metricType, limit = 50) {
173
182
  if (!metricType) {
174
183
  const metrics = db.prepare(`
@@ -181,6 +190,8 @@ function readHealthData(userId, metricType, limit = 50) {
181
190
  return { metrics };
182
191
  }
183
192
 
193
+ const normalizedType = normalizeMetricType(metricType);
194
+
184
195
  const samples = db.prepare(`
185
196
  SELECT
186
197
  start_time, end_time, recorded_at,
@@ -191,10 +202,10 @@ function readHealthData(userId, metricType, limit = 50) {
191
202
  WHERE user_id = ? AND metric_type = ?
192
203
  ORDER BY COALESCE(end_time, recorded_at, start_time) DESC
193
204
  LIMIT ?
194
- `).all(userId, metricType, limit);
205
+ `).all(userId, normalizedType, limit);
195
206
 
196
207
  return {
197
- metricType,
208
+ metricType: normalizedType,
198
209
  samples: samples.map(s => ({
199
210
  ...s,
200
211
  payload: s.payload_json ? JSON.parse(s.payload_json) : null,
@@ -107,8 +107,8 @@ async function startServices(app, io) {
107
107
  : '';
108
108
 
109
109
  const prompt = isVoiceCall
110
- ? `You are on a live phone call. The caller (${msg.senderName || msg.sender}) said:\n<caller_speech>\n${msg.content}\n</caller_speech>\n\nRespond via send_message with platform="telnyx" and to="${msg.chatId}". Note: once you successfully send your voice response, end the tool loop and output exactly [NO RESPONSE] as your final text.`
111
- : `You received a ${msg.platform} message from ${msg.senderName || msg.sender} (chat: ${msg.chatId}):\n<external_message>\n${msg.content}\n</external_message>${mediaNote}${discordContext}${sttNote}\n\nReply via send_message with platform="${msg.platform}" and to="${msg.chatId}". Note: once you successfully send your reply, end the tool loop and output exactly [NO RESPONSE] as your final text to prevent double-notifications.`;
110
+ ? `You are on a live phone call. The caller (${msg.senderName || msg.sender}) said:\n<caller_speech>\n${msg.content}\n</caller_speech>\n\nRespond via send_message with platform="telnyx" and to="${msg.chatId}".`
111
+ : `You received a ${msg.platform} message from ${msg.senderName || msg.sender} (chat: ${msg.chatId}):\n<external_message>\n${msg.content}\n</external_message>${mediaNote}${discordContext}${sttNote}\n\nReply via send_message with platform="${msg.platform}" and to="${msg.chatId}".`;
112
112
 
113
113
  let convRow = db.prepare(
114
114
  'SELECT id FROM conversations WHERE user_id = ? AND platform = ? AND platform_chat_id = ?'