neoagent 1.4.1 → 1.4.3
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/package.json +3 -1
- package/server/db/database.js +76 -0
- 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 +1041 -423
- package/server/routes/memory.js +3 -1
- 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/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();
|
|
@@ -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');
|
|
@@ -2,30 +2,80 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Embedding helpers for the semantic memory system.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
*
|
|
6
|
+
* Provider selection (in priority order):
|
|
7
|
+
* 1. Google (text-embedding-004, 768 dims) — when provider hint is 'google' and GOOGLE_AI_KEY is set
|
|
8
|
+
* 2. OpenAI (text-embedding-3-small, 1536 dims) — when OPENAI_API_KEY is set
|
|
9
|
+
* 3. Keyword fallback — when no API key is available
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
12
|
const https = require('https');
|
|
10
13
|
|
|
11
|
-
const
|
|
12
|
-
const
|
|
14
|
+
const OPENAI_MODEL = 'text-embedding-3-small';
|
|
15
|
+
const OPENAI_DIM = 1536;
|
|
16
|
+
const GOOGLE_MODEL = 'text-embedding-004';
|
|
17
|
+
const GOOGLE_DIM = 768;
|
|
13
18
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
async function
|
|
19
|
+
// Exported so callers can sanity-check stored vector dimensions if needed
|
|
20
|
+
const EMBED_DIM = OPENAI_DIM;
|
|
21
|
+
const EMBED_DIM_GOOGLE = GOOGLE_DIM;
|
|
22
|
+
|
|
23
|
+
async function getGeminiEmbedding(text) {
|
|
24
|
+
const apiKey = process.env.GOOGLE_AI_KEY;
|
|
25
|
+
if (!apiKey) return null;
|
|
26
|
+
if (!text || !text.trim()) return null;
|
|
27
|
+
|
|
28
|
+
const truncated = text.slice(0, 25000);
|
|
29
|
+
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
const body = JSON.stringify({
|
|
32
|
+
model: `models/${GOOGLE_MODEL}`,
|
|
33
|
+
content: { parts: [{ text: truncated }] }
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const path = `/v1beta/models/${GOOGLE_MODEL}:embedContent?key=${apiKey}`;
|
|
37
|
+
const options = {
|
|
38
|
+
hostname: 'generativelanguage.googleapis.com',
|
|
39
|
+
path,
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: {
|
|
42
|
+
'Content-Type': 'application/json',
|
|
43
|
+
'Content-Length': Buffer.byteLength(body)
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const req = https.request(options, (res) => {
|
|
48
|
+
let data = '';
|
|
49
|
+
res.on('data', chunk => { data += chunk; });
|
|
50
|
+
res.on('end', () => {
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(data);
|
|
53
|
+
const vec = parsed.embedding?.values;
|
|
54
|
+
if (!vec) return resolve(null);
|
|
55
|
+
resolve(new Float32Array(vec));
|
|
56
|
+
} catch {
|
|
57
|
+
resolve(null);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
req.on('error', () => resolve(null));
|
|
63
|
+
req.setTimeout(15000, () => { req.destroy(); resolve(null); });
|
|
64
|
+
req.write(body);
|
|
65
|
+
req.end();
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function getOpenAIEmbedding(text) {
|
|
19
70
|
const apiKey = process.env.OPENAI_API_KEY;
|
|
20
71
|
if (!apiKey) return null;
|
|
21
72
|
if (!text || !text.trim()) return null;
|
|
22
73
|
|
|
23
|
-
// Truncate very long text to stay within token limits (~8k tokens)
|
|
24
74
|
const truncated = text.slice(0, 25000);
|
|
25
75
|
|
|
26
|
-
return new Promise((resolve
|
|
76
|
+
return new Promise((resolve) => {
|
|
27
77
|
const body = JSON.stringify({
|
|
28
|
-
model:
|
|
78
|
+
model: OPENAI_MODEL,
|
|
29
79
|
input: truncated,
|
|
30
80
|
encoding_format: 'float'
|
|
31
81
|
});
|
|
@@ -64,6 +114,21 @@ async function getEmbedding(text) {
|
|
|
64
114
|
});
|
|
65
115
|
}
|
|
66
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Get an embedding vector for a piece of text.
|
|
119
|
+
* @param {string} text
|
|
120
|
+
* @param {string} [provider] - 'google' to prefer Gemini embeddings
|
|
121
|
+
* @returns {Float32Array|null}
|
|
122
|
+
*/
|
|
123
|
+
async function getEmbedding(text, provider) {
|
|
124
|
+
if (!text || !text.trim()) return null;
|
|
125
|
+
if (provider === 'google' && process.env.GOOGLE_AI_KEY) {
|
|
126
|
+
const vec = await getGeminiEmbedding(text);
|
|
127
|
+
if (vec) return vec;
|
|
128
|
+
}
|
|
129
|
+
return getOpenAIEmbedding(text);
|
|
130
|
+
}
|
|
131
|
+
|
|
67
132
|
/**
|
|
68
133
|
* Cosine similarity between two Float32Arrays.
|
|
69
134
|
* Returns a value in [-1, 1]; higher = more similar.
|
|
@@ -72,7 +137,7 @@ function cosineSimilarity(a, b) {
|
|
|
72
137
|
if (!a || !b || a.length !== b.length) return 0;
|
|
73
138
|
let dot = 0, magA = 0, magB = 0;
|
|
74
139
|
for (let i = 0; i < a.length; i++) {
|
|
75
|
-
dot
|
|
140
|
+
dot += a[i] * b[i];
|
|
76
141
|
magA += a[i] * a[i];
|
|
77
142
|
magB += b[i] * b[i];
|
|
78
143
|
}
|
|
@@ -122,5 +187,6 @@ module.exports = {
|
|
|
122
187
|
serializeEmbedding,
|
|
123
188
|
deserializeEmbedding,
|
|
124
189
|
keywordSimilarity,
|
|
125
|
-
EMBED_DIM
|
|
190
|
+
EMBED_DIM,
|
|
191
|
+
EMBED_DIM_GOOGLE
|
|
126
192
|
};
|