neoagent 1.4.1 → 1.4.4
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/docs/skills.md +4 -0
- package/lib/manager.js +6 -0
- package/package.json +3 -1
- package/server/db/database.js +117 -0
- package/server/index.js +9 -1
- package/server/public/app.html +124 -49
- package/server/public/assets/world-office-dark.png +0 -0
- package/server/public/assets/world-office-light.png +0 -0
- package/server/public/css/app.css +575 -242
- package/server/public/css/styles.css +445 -121
- package/server/public/js/app.js +1042 -424
- package/server/routes/memory.js +3 -1
- package/server/routes/mobile-health.js +28 -0
- package/server/routes/settings.js +40 -2
- package/server/routes/skills.js +124 -85
- package/server/routes/store.js +100 -0
- package/server/services/ai/compaction.js +14 -30
- package/server/services/ai/engine.js +222 -200
- package/server/services/ai/history.js +188 -0
- package/server/services/ai/learning.js +143 -0
- package/server/services/ai/settings.js +80 -0
- package/server/services/ai/systemPrompt.js +57 -119
- package/server/services/ai/toolResult.js +151 -0
- package/server/services/ai/toolRunner.js +24 -6
- package/server/services/ai/toolSelector.js +140 -0
- package/server/services/ai/tools.js +71 -2
- package/server/services/health/ingestion.js +175 -0
- package/server/services/manager.js +25 -2
- package/server/services/memory/embeddings.js +80 -14
- package/server/services/memory/manager.js +209 -16
- package/server/services/websocket.js +19 -6
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
function clampText(text, maxChars) {
|
|
2
|
+
const str = String(text || '');
|
|
3
|
+
if (str.length <= maxChars) return str;
|
|
4
|
+
return `${str.slice(0, maxChars)}\n...[truncated, ${str.length} chars total]`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function lineExcerpt(text, maxLines = 12, maxChars = 700) {
|
|
8
|
+
const str = String(text || '').trim();
|
|
9
|
+
if (!str) return '';
|
|
10
|
+
return clampText(str.split('\n').slice(0, maxLines).join('\n'), maxChars);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function toJsonText(value, maxChars) {
|
|
14
|
+
const raw = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
|
|
15
|
+
return clampText(raw, maxChars);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function trimObject(obj) {
|
|
19
|
+
return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined && value !== null && value !== ''));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function clampEnvelope(envelope, hardLimit) {
|
|
23
|
+
const raw = JSON.stringify(envelope);
|
|
24
|
+
if (raw.length <= hardLimit) return raw;
|
|
25
|
+
|
|
26
|
+
const trimmed = { ...envelope };
|
|
27
|
+
if (trimmed.summary) trimmed.summary = clampText(trimmed.summary, Math.max(200, hardLimit - 300));
|
|
28
|
+
if (trimmed.stdout) trimmed.stdout = clampText(trimmed.stdout, Math.max(160, hardLimit - 400));
|
|
29
|
+
if (trimmed.stderr) trimmed.stderr = clampText(trimmed.stderr, Math.max(120, hardLimit - 400));
|
|
30
|
+
if (trimmed.content) trimmed.content = clampText(trimmed.content, Math.max(160, hardLimit - 400));
|
|
31
|
+
if (trimmed.excerpt) trimmed.excerpt = clampText(trimmed.excerpt, Math.max(160, hardLimit - 400));
|
|
32
|
+
if (trimmed.result) trimmed.result = clampText(trimmed.result, Math.max(160, hardLimit - 400));
|
|
33
|
+
|
|
34
|
+
const fallback = JSON.stringify(trimmed);
|
|
35
|
+
if (fallback.length <= hardLimit) return fallback;
|
|
36
|
+
return clampText(fallback, hardLimit);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function compactToolResult(toolName, toolArgs = {}, toolResult, options = {}) {
|
|
40
|
+
const softLimit = Math.max(400, Math.min(Number(options.softLimit) || 1200, 2000));
|
|
41
|
+
const hardLimit = Math.max(softLimit, Math.min(Number(options.hardLimit) || 2000, 3000));
|
|
42
|
+
|
|
43
|
+
let envelope;
|
|
44
|
+
|
|
45
|
+
switch (toolName) {
|
|
46
|
+
case 'execute_command':
|
|
47
|
+
envelope = trimObject({
|
|
48
|
+
tool: toolName,
|
|
49
|
+
exitCode: toolResult?.exitCode,
|
|
50
|
+
cwd: toolResult?.cwd || toolArgs.cwd,
|
|
51
|
+
killed: toolResult?.killed || false,
|
|
52
|
+
stdout: lineExcerpt(toolResult?.stdout, 12, Math.floor(softLimit * 0.45)),
|
|
53
|
+
stderr: lineExcerpt(toolResult?.stderr, 8, Math.floor(softLimit * 0.25))
|
|
54
|
+
});
|
|
55
|
+
break;
|
|
56
|
+
|
|
57
|
+
case 'read_file':
|
|
58
|
+
envelope = trimObject({
|
|
59
|
+
tool: toolName,
|
|
60
|
+
path: toolArgs.path,
|
|
61
|
+
startLine: toolArgs.start_line,
|
|
62
|
+
endLine: toolArgs.end_line,
|
|
63
|
+
content: lineExcerpt(toolResult?.content || toolResult, 20, Math.floor(softLimit * 0.7))
|
|
64
|
+
});
|
|
65
|
+
break;
|
|
66
|
+
|
|
67
|
+
case 'search_files':
|
|
68
|
+
envelope = trimObject({
|
|
69
|
+
tool: toolName,
|
|
70
|
+
count: toolResult?.count || toolResult?.matches?.length || 0,
|
|
71
|
+
matches: (toolResult?.matches || []).slice(0, 6).map((match) => trimObject({
|
|
72
|
+
file: match.file,
|
|
73
|
+
line: match.line,
|
|
74
|
+
content: clampText(match.content, 160)
|
|
75
|
+
}))
|
|
76
|
+
});
|
|
77
|
+
break;
|
|
78
|
+
|
|
79
|
+
case 'browser_extract':
|
|
80
|
+
envelope = trimObject({
|
|
81
|
+
tool: toolName,
|
|
82
|
+
selector: toolArgs.selector || 'body',
|
|
83
|
+
attribute: toolArgs.attribute || 'innerText',
|
|
84
|
+
excerpt: lineExcerpt(toolResult?.result || toolResult?.content || toolResult, 18, Math.floor(softLimit * 0.7))
|
|
85
|
+
});
|
|
86
|
+
break;
|
|
87
|
+
|
|
88
|
+
case 'http_request':
|
|
89
|
+
envelope = trimObject({
|
|
90
|
+
tool: toolName,
|
|
91
|
+
status: toolResult?.status,
|
|
92
|
+
headers: trimObject({
|
|
93
|
+
contentType: toolResult?.headers?.['content-type'] || toolResult?.headers?.['Content-Type'],
|
|
94
|
+
contentLength: toolResult?.headers?.['content-length'] || toolResult?.headers?.['Content-Length']
|
|
95
|
+
}),
|
|
96
|
+
excerpt: lineExcerpt(toolResult?.body || toolResult, 18, Math.floor(softLimit * 0.65))
|
|
97
|
+
});
|
|
98
|
+
break;
|
|
99
|
+
|
|
100
|
+
case 'send_message':
|
|
101
|
+
case 'make_call':
|
|
102
|
+
case 'memory_save':
|
|
103
|
+
case 'memory_recall':
|
|
104
|
+
case 'memory_update_core':
|
|
105
|
+
case 'memory_read':
|
|
106
|
+
case 'memory_write':
|
|
107
|
+
case 'create_scheduled_task':
|
|
108
|
+
case 'schedule_run':
|
|
109
|
+
case 'list_scheduled_tasks':
|
|
110
|
+
case 'delete_scheduled_task':
|
|
111
|
+
case 'update_scheduled_task':
|
|
112
|
+
envelope = trimObject({
|
|
113
|
+
tool: toolName,
|
|
114
|
+
status: toolResult?.success === false || toolResult?.error ? 'error' : 'ok',
|
|
115
|
+
message: clampText(toolResult?.message || toolResult?.error || '', Math.floor(softLimit * 0.45)),
|
|
116
|
+
result: clampText(JSON.stringify(trimObject({
|
|
117
|
+
id: toolResult?.id,
|
|
118
|
+
key: toolResult?.key,
|
|
119
|
+
deleted: toolResult?.deleted,
|
|
120
|
+
sent: toolResult?.sent,
|
|
121
|
+
count: Array.isArray(toolResult?.results) ? toolResult.results.length : undefined
|
|
122
|
+
})), Math.floor(softLimit * 0.35))
|
|
123
|
+
});
|
|
124
|
+
break;
|
|
125
|
+
|
|
126
|
+
case 'spawn_subagent':
|
|
127
|
+
envelope = trimObject({
|
|
128
|
+
tool: toolName,
|
|
129
|
+
iterations: toolResult?.iterations,
|
|
130
|
+
tokens: toolResult?.tokens,
|
|
131
|
+
runId: toolResult?.runId,
|
|
132
|
+
summary: clampText(toolResult?.subagent_result || toolResult?.error || '', Math.floor(softLimit * 0.55))
|
|
133
|
+
});
|
|
134
|
+
break;
|
|
135
|
+
|
|
136
|
+
default:
|
|
137
|
+
envelope = trimObject({
|
|
138
|
+
tool: toolName,
|
|
139
|
+
summary: toJsonText(toolResult, Math.floor(softLimit * 0.75))
|
|
140
|
+
});
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return clampEnvelope(envelope, hardLimit);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = {
|
|
148
|
+
compactToolResult,
|
|
149
|
+
clampText,
|
|
150
|
+
lineExcerpt
|
|
151
|
+
};
|
|
@@ -87,7 +87,7 @@ class SkillRunner {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
getSkillsForPrompt() {
|
|
90
|
-
const skills = Array.from(this.skills.values());
|
|
90
|
+
const skills = Array.from(this.skills.values()).filter((skill) => skill.metadata.enabled !== false);
|
|
91
91
|
if (skills.length === 0) return '';
|
|
92
92
|
|
|
93
93
|
let prompt = '\n## Available Skills\n';
|
|
@@ -103,7 +103,7 @@ class SkillRunner {
|
|
|
103
103
|
getToolDefinitions() {
|
|
104
104
|
const tools = [];
|
|
105
105
|
for (const skill of this.skills.values()) {
|
|
106
|
-
if (skill.metadata.tool) {
|
|
106
|
+
if (skill.metadata.enabled !== false && skill.metadata.tool) {
|
|
107
107
|
tools.push({
|
|
108
108
|
name: skill.name,
|
|
109
109
|
description: skill.description,
|
|
@@ -117,6 +117,9 @@ class SkillRunner {
|
|
|
117
117
|
async executeTool(toolName, args) {
|
|
118
118
|
const skill = this.skills.get(toolName);
|
|
119
119
|
if (!skill) return null;
|
|
120
|
+
if (skill.metadata.enabled === false) {
|
|
121
|
+
return { error: `Skill '${toolName}' is disabled` };
|
|
122
|
+
}
|
|
120
123
|
|
|
121
124
|
if (skill.metadata.command) {
|
|
122
125
|
const { CLIExecutor } = require('../cli/executor');
|
|
@@ -140,8 +143,10 @@ class SkillRunner {
|
|
|
140
143
|
const filePath = path.join(skillDir, 'SKILL.md');
|
|
141
144
|
fs.writeFileSync(filePath, frontmatter + `\n\n${instructions}`);
|
|
142
145
|
|
|
143
|
-
db.prepare(
|
|
144
|
-
|
|
146
|
+
db.prepare(`
|
|
147
|
+
INSERT OR REPLACE INTO skills (name, description, file_path, metadata, enabled, auto_created, updated_at)
|
|
148
|
+
VALUES (?, ?, ?, ?, ?, 1, datetime('now'))
|
|
149
|
+
`).run(safeName, description, filePath, JSON.stringify(metadata), metadata.enabled === false ? 0 : 1);
|
|
145
150
|
|
|
146
151
|
this.loadSkillFile(filePath);
|
|
147
152
|
|
|
@@ -167,12 +172,24 @@ class SkillRunner {
|
|
|
167
172
|
|
|
168
173
|
const frontmatter = this._buildFrontmatter(name, newDesc, metaToWrite);
|
|
169
174
|
fs.writeFileSync(skill.filePath, frontmatter + `\n\n${newInstructions}`);
|
|
170
|
-
db.prepare('UPDATE skills SET description = ?, updated_at = datetime(\'now\') WHERE name = ?')
|
|
175
|
+
db.prepare('UPDATE skills SET description = ?, metadata = ?, enabled = ?, updated_at = datetime(\'now\') WHERE name = ?')
|
|
176
|
+
.run(newDesc, JSON.stringify(metaToWrite || {}), metaToWrite?.enabled === false ? 0 : 1, name);
|
|
171
177
|
this.loadSkillFile(skill.filePath);
|
|
172
178
|
|
|
173
179
|
return { success: true, name, path: skill.filePath };
|
|
174
180
|
}
|
|
175
181
|
|
|
182
|
+
getSkill(name) {
|
|
183
|
+
return this.skills.get(name) || null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
setSkillEnabled(name, enabled) {
|
|
187
|
+
const skill = this.skills.get(name);
|
|
188
|
+
if (!skill) return { error: `Skill '${name}' not found` };
|
|
189
|
+
const metadata = { ...skill.metadata, enabled: !!enabled };
|
|
190
|
+
return this.updateSkill(name, { metadata });
|
|
191
|
+
}
|
|
192
|
+
|
|
176
193
|
deleteSkill(name) {
|
|
177
194
|
const skill = this.skills.get(name);
|
|
178
195
|
if (!skill) return { error: `Skill '${name}' not found` };
|
|
@@ -211,7 +228,8 @@ class SkillRunner {
|
|
|
211
228
|
name: s.name,
|
|
212
229
|
description: s.description,
|
|
213
230
|
metadata: s.metadata,
|
|
214
|
-
filePath: s.filePath
|
|
231
|
+
filePath: s.filePath,
|
|
232
|
+
enabled: s.metadata.enabled !== false
|
|
215
233
|
}));
|
|
216
234
|
}
|
|
217
235
|
}
|
|
@@ -0,0 +1,140 @@
|
|
|
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
|
+
};
|
|
16
|
+
|
|
17
|
+
function containsAny(text, patterns) {
|
|
18
|
+
return patterns.some((pattern) => pattern.test(text));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function detectRequestedPacks(task = '', options = {}) {
|
|
22
|
+
const text = String(task || '').toLowerCase();
|
|
23
|
+
const packs = new Set();
|
|
24
|
+
|
|
25
|
+
if (containsAny(text, [
|
|
26
|
+
/\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/,
|
|
27
|
+
/\b(read|open|inspect)\s+(the\s+)?(file|repo|code)\b/
|
|
28
|
+
])) {
|
|
29
|
+
packs.add('code');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (containsAny(text, [
|
|
33
|
+
/\b(web|website|url|page|browser|click|navigate|scrape|search|google|lookup|http|fetch|api request|screenshot)\b/,
|
|
34
|
+
/\bopen\b.*\bsite\b/
|
|
35
|
+
])) {
|
|
36
|
+
packs.add('web');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (containsAny(text, [
|
|
40
|
+
/\b(message|reply|respond|text|whatsapp|telegram|discord|dm|email|call|phone|notify|send to)\b/,
|
|
41
|
+
/\[no response\]/,
|
|
42
|
+
/\bsend_message\b/,
|
|
43
|
+
/\bmake_call\b/
|
|
44
|
+
])) {
|
|
45
|
+
packs.add('messaging');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (containsAny(text, [
|
|
49
|
+
/\bmemory\b/,
|
|
50
|
+
/\bremember\b/,
|
|
51
|
+
/\brecall\b/,
|
|
52
|
+
/\bprevious chat\b/,
|
|
53
|
+
/\blast time\b/,
|
|
54
|
+
/\bpast conversation\b/,
|
|
55
|
+
/\bpreference\b/,
|
|
56
|
+
/\bprofile\b/,
|
|
57
|
+
/\bsoul\b/
|
|
58
|
+
])) {
|
|
59
|
+
packs.add('memory');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (containsAny(text, [
|
|
63
|
+
/\bschedule\b/,
|
|
64
|
+
/\bcron\b/,
|
|
65
|
+
/\bremind\b/,
|
|
66
|
+
/\brecurring\b/,
|
|
67
|
+
/\bweekly\b/,
|
|
68
|
+
/\bdaily\b/,
|
|
69
|
+
/\bone-time\b/,
|
|
70
|
+
/\btask\b.*\blater\b/
|
|
71
|
+
])) {
|
|
72
|
+
packs.add('scheduling');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (containsAny(text, [/\bprotocol\b/, /\bplaybook\b/])) {
|
|
76
|
+
packs.add('protocols');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (containsAny(text, [/\bskill\b/, /\binstall skill\b/, /\bcreate skill\b/])) {
|
|
80
|
+
packs.add('skills');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (containsAny(text, [/\bimage\b/, /\bpicture\b/, /\bphoto\b/, /\bgraph\b/, /\bchart\b/, /\btable\b/, /\bqr\b/, /\bocr\b/])) {
|
|
84
|
+
packs.add('images');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (containsAny(text, [/\btable\b/, /\bspreadsheet\b/, /\bgraph\b/, /\bchart\b/])) {
|
|
88
|
+
packs.add('tables');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (containsAny(text, [/\bsub-?agent\b/, /\bdelegate\b/, /\bparallel\b/, /\bbackground worker\b/])) {
|
|
92
|
+
packs.add('subagents');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (containsAny(text, [/\bmcp\b/, /\bmodel context protocol\b/, /\bserver tool\b/])) {
|
|
96
|
+
packs.add('mcpAdmin');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (options.mediaAttachments?.length) {
|
|
100
|
+
packs.add('images');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return packs;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function maybeSelectMcpTools(text, mcpTools = []) {
|
|
107
|
+
const normalized = String(text || '').toLowerCase();
|
|
108
|
+
if (!normalized || !mcpTools.length) return [];
|
|
109
|
+
|
|
110
|
+
const explicitMcp = /\bmcp\b|\bmodel context protocol\b/.test(normalized);
|
|
111
|
+
return mcpTools.filter((tool) => {
|
|
112
|
+
const name = String(tool.name || '').toLowerCase();
|
|
113
|
+
const original = String(tool.originalName || '').toLowerCase();
|
|
114
|
+
const server = String(tool.serverId || '').toLowerCase();
|
|
115
|
+
return explicitMcp || normalized.includes(name) || normalized.includes(original) || (server && normalized.includes(server));
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function selectToolsForTask(task, builtInTools = [], mcpTools = [], options = {}) {
|
|
120
|
+
const packs = detectRequestedPacks(task, options);
|
|
121
|
+
const allowNames = new Set(ALWAYS_ON_TOOLS);
|
|
122
|
+
|
|
123
|
+
for (const pack of packs) {
|
|
124
|
+
for (const toolName of PACKS[pack] || []) {
|
|
125
|
+
allowNames.add(toolName);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const selectedBuiltIns = builtInTools.filter((tool) => allowNames.has(tool.name));
|
|
130
|
+
const selectedMcp = maybeSelectMcpTools(task, mcpTools);
|
|
131
|
+
|
|
132
|
+
return [...selectedBuiltIns, ...selectedMcp];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = {
|
|
136
|
+
ALWAYS_ON_TOOLS,
|
|
137
|
+
PACKS,
|
|
138
|
+
detectRequestedPacks,
|
|
139
|
+
selectToolsForTask
|
|
140
|
+
};
|
|
@@ -3,12 +3,54 @@ const path = require('path');
|
|
|
3
3
|
const db = require('../../db/database');
|
|
4
4
|
const { DATA_DIR } = require('../../../runtime/paths');
|
|
5
5
|
|
|
6
|
+
function compactText(text, maxChars = 120) {
|
|
7
|
+
const str = String(text || '').replace(/\s+/g, ' ').trim();
|
|
8
|
+
if (str.length <= maxChars) return str;
|
|
9
|
+
const trimmed = str.slice(0, maxChars);
|
|
10
|
+
const sentenceBreak = Math.max(trimmed.lastIndexOf('. '), trimmed.lastIndexOf('; '), trimmed.lastIndexOf(', '));
|
|
11
|
+
if (sentenceBreak > 40) return trimmed.slice(0, sentenceBreak + 1).trim();
|
|
12
|
+
return `${trimmed.trim()}...`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function compactToolDefinition(tool, options = {}) {
|
|
16
|
+
const compact = {
|
|
17
|
+
name: tool.name,
|
|
18
|
+
parameters: {
|
|
19
|
+
...(tool.parameters || { type: 'object', properties: {} }),
|
|
20
|
+
properties: {}
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
if (options.includeDescriptions) {
|
|
25
|
+
compact.description = compactText(tool.description, 120);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (tool.parameters?.properties) {
|
|
29
|
+
const properties = {};
|
|
30
|
+
for (const [key, value] of Object.entries(tool.parameters.properties)) {
|
|
31
|
+
properties[key] = { ...value };
|
|
32
|
+
if (options.includeDescriptions && value.description) {
|
|
33
|
+
properties[key].description = compactText(value.description, 70);
|
|
34
|
+
} else {
|
|
35
|
+
delete properties[key].description;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
compact.parameters = {
|
|
39
|
+
...compact.parameters,
|
|
40
|
+
properties
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return compact;
|
|
45
|
+
}
|
|
46
|
+
|
|
6
47
|
/**
|
|
7
48
|
* Returns the list of available tools for the agent.
|
|
8
49
|
* @param {object} app - Express app instance.
|
|
50
|
+
* @param {object} options - Tool filtering options.
|
|
9
51
|
* @returns {Array} List of tool definitions.
|
|
10
52
|
*/
|
|
11
|
-
function getAvailableTools(app) {
|
|
53
|
+
function getAvailableTools(app, options = {}) {
|
|
12
54
|
const tools = [
|
|
13
55
|
{
|
|
14
56
|
name: 'execute_command',
|
|
@@ -154,6 +196,18 @@ function getAvailableTools(app) {
|
|
|
154
196
|
required: ['query']
|
|
155
197
|
}
|
|
156
198
|
},
|
|
199
|
+
{
|
|
200
|
+
name: 'session_search',
|
|
201
|
+
description: 'Search past runs and message threads for commands, decisions, file paths, or context from earlier conversations.',
|
|
202
|
+
parameters: {
|
|
203
|
+
type: 'object',
|
|
204
|
+
properties: {
|
|
205
|
+
query: { type: 'string', description: 'What to search for in prior sessions.' },
|
|
206
|
+
limit: { type: 'number', description: 'How many matching sessions to return (default 6).' }
|
|
207
|
+
},
|
|
208
|
+
required: ['query']
|
|
209
|
+
}
|
|
210
|
+
},
|
|
157
211
|
{
|
|
158
212
|
name: 'memory_update_core',
|
|
159
213
|
description: 'Update core memory — always-injected facts that appear in every prompt. Use for critical always-relevant info: user\'s name, their main job, key standing preferences, how they want you to behave. Keep each entry concise.',
|
|
@@ -530,7 +584,12 @@ function getAvailableTools(app) {
|
|
|
530
584
|
}
|
|
531
585
|
];
|
|
532
586
|
|
|
533
|
-
|
|
587
|
+
const compacted = tools.map((tool) => compactToolDefinition(tool, options));
|
|
588
|
+
if (options.names && Array.isArray(options.names)) {
|
|
589
|
+
const allow = new Set(options.names);
|
|
590
|
+
return compacted.filter((tool) => allow.has(tool.name));
|
|
591
|
+
}
|
|
592
|
+
return compacted;
|
|
534
593
|
}
|
|
535
594
|
|
|
536
595
|
/**
|
|
@@ -725,6 +784,16 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
725
784
|
return { results };
|
|
726
785
|
}
|
|
727
786
|
|
|
787
|
+
case 'session_search': {
|
|
788
|
+
const { MemoryManager } = require('../memory/manager');
|
|
789
|
+
const mm = new MemoryManager();
|
|
790
|
+
const results = mm.searchConversations(userId, args.query, {
|
|
791
|
+
sessions: args.limit || 6
|
|
792
|
+
});
|
|
793
|
+
if (!results.length) return { results: [], message: 'No matching sessions found' };
|
|
794
|
+
return { results };
|
|
795
|
+
}
|
|
796
|
+
|
|
728
797
|
case 'memory_update_core': {
|
|
729
798
|
const { MemoryManager } = require('../memory/manager');
|
|
730
799
|
const mm = new MemoryManager();
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const db = require('../../db/database');
|
|
3
|
+
const { v4: uuidv4 } = require('uuid');
|
|
4
|
+
|
|
5
|
+
function parseIsoOrNull(value) {
|
|
6
|
+
if (!value) return null;
|
|
7
|
+
const date = new Date(value);
|
|
8
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function asJson(value) {
|
|
12
|
+
return JSON.stringify(value == null ? null : value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function asText(value) {
|
|
16
|
+
if (value == null) return null;
|
|
17
|
+
const text = String(value).trim();
|
|
18
|
+
return text.length ? text : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function buildFallbackRecordId(metricType, record) {
|
|
22
|
+
const hash = crypto.createHash('sha1');
|
|
23
|
+
hash.update(metricType || '');
|
|
24
|
+
hash.update('\n');
|
|
25
|
+
hash.update(asJson(record));
|
|
26
|
+
return hash.digest('hex');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeRecord(record = {}) {
|
|
30
|
+
const metricType = asText(record.metricType || record.type);
|
|
31
|
+
if (!metricType) return null;
|
|
32
|
+
|
|
33
|
+
const payload = record.payload && typeof record.payload === 'object'
|
|
34
|
+
? record.payload
|
|
35
|
+
: {};
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
metricType,
|
|
39
|
+
recordId: asText(record.recordId || payload.recordId) || buildFallbackRecordId(metricType, record),
|
|
40
|
+
startTime: parseIsoOrNull(record.startTime || payload.startTime),
|
|
41
|
+
endTime: parseIsoOrNull(record.endTime || payload.endTime),
|
|
42
|
+
recordedAt: parseIsoOrNull(record.recordedAt || payload.recordedAt || record.endTime || record.startTime),
|
|
43
|
+
numericValue: Number.isFinite(Number(record.numericValue)) ? Number(record.numericValue) : null,
|
|
44
|
+
textValue: asText(record.textValue),
|
|
45
|
+
unit: asText(record.unit),
|
|
46
|
+
sourceAppId: asText(record.sourceAppId || payload.sourceAppId),
|
|
47
|
+
sourceDevice: asText(record.sourceDevice || payload.sourceDevice),
|
|
48
|
+
lastModifiedTime: parseIsoOrNull(record.lastModifiedTime || payload.lastModifiedTime),
|
|
49
|
+
payloadJson: asJson(payload),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const insertRun = db.prepare(`
|
|
54
|
+
INSERT INTO health_sync_runs (
|
|
55
|
+
id, user_id, source, provider, sync_window_start, sync_window_end,
|
|
56
|
+
record_count, summary_json, payload_json
|
|
57
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
58
|
+
`);
|
|
59
|
+
|
|
60
|
+
const upsertSample = db.prepare(`
|
|
61
|
+
INSERT INTO health_metric_samples (
|
|
62
|
+
user_id, sync_run_id, metric_type, record_id, start_time, end_time, recorded_at,
|
|
63
|
+
numeric_value, text_value, unit, source_app_id, source_device, last_modified_time, payload_json
|
|
64
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
65
|
+
ON CONFLICT(user_id, metric_type, record_id) DO UPDATE SET
|
|
66
|
+
sync_run_id = excluded.sync_run_id,
|
|
67
|
+
start_time = excluded.start_time,
|
|
68
|
+
end_time = excluded.end_time,
|
|
69
|
+
recorded_at = excluded.recorded_at,
|
|
70
|
+
numeric_value = excluded.numeric_value,
|
|
71
|
+
text_value = excluded.text_value,
|
|
72
|
+
unit = excluded.unit,
|
|
73
|
+
source_app_id = excluded.source_app_id,
|
|
74
|
+
source_device = excluded.source_device,
|
|
75
|
+
last_modified_time = excluded.last_modified_time,
|
|
76
|
+
payload_json = excluded.payload_json,
|
|
77
|
+
updated_at = datetime('now')
|
|
78
|
+
`);
|
|
79
|
+
|
|
80
|
+
const ingestHealthSyncTx = db.transaction((userId, body) => {
|
|
81
|
+
const runId = uuidv4();
|
|
82
|
+
const source = asText(body.source) || 'android-health-connect';
|
|
83
|
+
const provider = asText(body.provider);
|
|
84
|
+
const windowStart = parseIsoOrNull(body.windowStart);
|
|
85
|
+
const windowEnd = parseIsoOrNull(body.windowEnd);
|
|
86
|
+
const records = Array.isArray(body.records)
|
|
87
|
+
? body.records.map(normalizeRecord).filter(Boolean)
|
|
88
|
+
: [];
|
|
89
|
+
|
|
90
|
+
insertRun.run(
|
|
91
|
+
runId,
|
|
92
|
+
userId,
|
|
93
|
+
source,
|
|
94
|
+
provider,
|
|
95
|
+
windowStart,
|
|
96
|
+
windowEnd,
|
|
97
|
+
records.length,
|
|
98
|
+
asJson(body.summary || {}),
|
|
99
|
+
asJson(body),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
for (const record of records) {
|
|
103
|
+
upsertSample.run(
|
|
104
|
+
userId,
|
|
105
|
+
runId,
|
|
106
|
+
record.metricType,
|
|
107
|
+
record.recordId,
|
|
108
|
+
record.startTime,
|
|
109
|
+
record.endTime,
|
|
110
|
+
record.recordedAt,
|
|
111
|
+
record.numericValue,
|
|
112
|
+
record.textValue,
|
|
113
|
+
record.unit,
|
|
114
|
+
record.sourceAppId,
|
|
115
|
+
record.sourceDevice,
|
|
116
|
+
record.lastModifiedTime,
|
|
117
|
+
record.payloadJson,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
runId,
|
|
123
|
+
source,
|
|
124
|
+
provider,
|
|
125
|
+
windowStart,
|
|
126
|
+
windowEnd,
|
|
127
|
+
recordCount: records.length,
|
|
128
|
+
acceptedMetrics: [...new Set(records.map((record) => record.metricType))],
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
function ingestHealthSync(userId, body = {}) {
|
|
133
|
+
if (!userId) throw new Error('Missing user');
|
|
134
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) {
|
|
135
|
+
throw new Error('Health sync payload must be a JSON object');
|
|
136
|
+
}
|
|
137
|
+
return ingestHealthSyncTx(userId, body);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function getHealthSyncStatus(userId) {
|
|
141
|
+
const lastRun = db.prepare(`
|
|
142
|
+
SELECT id, source, provider, sync_window_start, sync_window_end, record_count, summary_json, created_at
|
|
143
|
+
FROM health_sync_runs
|
|
144
|
+
WHERE user_id = ?
|
|
145
|
+
ORDER BY created_at DESC
|
|
146
|
+
LIMIT 1
|
|
147
|
+
`).get(userId);
|
|
148
|
+
|
|
149
|
+
const metrics = db.prepare(`
|
|
150
|
+
SELECT metric_type, COUNT(*) AS sample_count, MAX(COALESCE(end_time, recorded_at, start_time)) AS last_seen_at
|
|
151
|
+
FROM health_metric_samples
|
|
152
|
+
WHERE user_id = ?
|
|
153
|
+
GROUP BY metric_type
|
|
154
|
+
ORDER BY metric_type ASC
|
|
155
|
+
`).all(userId);
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
lastRun: lastRun ? {
|
|
159
|
+
...lastRun,
|
|
160
|
+
summary: (() => {
|
|
161
|
+
try { return JSON.parse(lastRun.summary_json || '{}'); } catch { return {}; }
|
|
162
|
+
})(),
|
|
163
|
+
} : null,
|
|
164
|
+
metrics: metrics.map((metric) => ({
|
|
165
|
+
metricType: metric.metric_type,
|
|
166
|
+
sampleCount: Number(metric.sample_count || 0),
|
|
167
|
+
lastSeenAt: metric.last_seen_at || null,
|
|
168
|
+
})),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = {
|
|
173
|
+
getHealthSyncStatus,
|
|
174
|
+
ingestHealthSync,
|
|
175
|
+
};
|
|
@@ -5,7 +5,9 @@ const { MemoryManager } = require('./memory/manager');
|
|
|
5
5
|
const { MCPClient } = require('./mcp/client');
|
|
6
6
|
const { BrowserController } = require('./browser/controller');
|
|
7
7
|
const { AgentEngine } = require('./ai/engine');
|
|
8
|
+
const { LearningManager } = require('./ai/learning');
|
|
8
9
|
const { MultiStepOrchestrator } = require('./ai/multiStep');
|
|
10
|
+
const { SkillRunner } = require('./ai/toolRunner');
|
|
9
11
|
const { MessagingManager } = require('./messaging/manager');
|
|
10
12
|
const { Scheduler } = require('./scheduler/cron');
|
|
11
13
|
const { setupWebSocket } = require('./websocket');
|
|
@@ -29,7 +31,21 @@ async function startServices(app, io) {
|
|
|
29
31
|
}
|
|
30
32
|
app.locals.browserController = browserController;
|
|
31
33
|
|
|
32
|
-
const
|
|
34
|
+
const skillRunner = new SkillRunner();
|
|
35
|
+
await skillRunner.loadSkills();
|
|
36
|
+
app.locals.skillRunner = skillRunner;
|
|
37
|
+
|
|
38
|
+
const learningManager = new LearningManager(skillRunner, io);
|
|
39
|
+
app.locals.learningManager = learningManager;
|
|
40
|
+
|
|
41
|
+
const agentEngine = new AgentEngine(io, {
|
|
42
|
+
memoryManager,
|
|
43
|
+
mcpClient,
|
|
44
|
+
browserController,
|
|
45
|
+
messagingManager: null,
|
|
46
|
+
skillRunner,
|
|
47
|
+
learningManager
|
|
48
|
+
});
|
|
33
49
|
app.locals.agentEngine = agentEngine;
|
|
34
50
|
|
|
35
51
|
const multiStep = new MultiStepOrchestrator(agentEngine, io);
|
|
@@ -167,7 +183,14 @@ async function startServices(app, io) {
|
|
|
167
183
|
agentEngine.scheduler = scheduler;
|
|
168
184
|
scheduler.start();
|
|
169
185
|
|
|
170
|
-
setupWebSocket(io, {
|
|
186
|
+
setupWebSocket(io, {
|
|
187
|
+
agentEngine,
|
|
188
|
+
messagingManager,
|
|
189
|
+
mcpClient,
|
|
190
|
+
scheduler,
|
|
191
|
+
memoryManager,
|
|
192
|
+
app
|
|
193
|
+
});
|
|
171
194
|
app.locals.io = io;
|
|
172
195
|
|
|
173
196
|
console.log('All services initialized');
|