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
|
@@ -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
|
-
|
|
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
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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 =
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
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
|
|
31
|
+
return normalized.includes(name) || normalized.includes(original) || (server && normalized.includes(server));
|
|
121
32
|
});
|
|
122
33
|
}
|
|
123
34
|
|
|
124
|
-
function selectToolsForTask(task, builtInTools = [], mcpTools = [],
|
|
125
|
-
|
|
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. "
|
|
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,
|
|
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}"
|
|
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}"
|
|
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 = ?'
|