neoagent 1.4.0 → 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/.env.example +5 -0
- package/com.neoagent.plist +8 -6
- package/docs/configuration.md +9 -1
- package/docs/skills.md +6 -2
- package/lib/manager.js +37 -10
- package/package.json +4 -1
- package/runtime/paths.js +80 -0
- package/server/db/database.js +78 -4
- package/server/index.js +5 -5
- 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 +42 -6
- package/server/routes/skills.js +124 -84
- package/server/routes/store.js +102 -1
- package/server/services/ai/compaction.js +15 -31
- package/server/services/ai/engine.js +224 -202
- package/server/services/ai/history.js +188 -0
- package/server/services/ai/learning.js +143 -0
- package/server/services/ai/providers/google.js +8 -1
- package/server/services/ai/settings.js +80 -0
- package/server/services/ai/systemPrompt.js +57 -98
- package/server/services/ai/toolResult.js +151 -0
- package/server/services/ai/toolRunner.js +26 -7
- package/server/services/ai/toolSelector.js +140 -0
- package/server/services/ai/tools.js +158 -5
- package/server/services/browser/controller.js +124 -48
- package/server/services/manager.js +26 -3
- package/server/services/mcp/client.js +1 -1
- package/server/services/memory/embeddings.js +80 -14
- package/server/services/memory/manager.js +211 -17
- package/server/services/messaging/telnyx.js +3 -2
- package/server/services/messaging/whatsapp.js +3 -2
- package/server/services/scheduler/cron.js +6 -1
- 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
|
+
};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const db = require('../../db/database');
|
|
4
|
+
const { AGENT_DATA_DIR } = require('../../../runtime/paths');
|
|
4
5
|
|
|
5
|
-
const SKILLS_DIR = path.join(
|
|
6
|
+
const SKILLS_DIR = path.join(AGENT_DATA_DIR, 'skills');
|
|
6
7
|
|
|
7
8
|
class SkillRunner {
|
|
8
9
|
constructor() {
|
|
@@ -86,7 +87,7 @@ class SkillRunner {
|
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
getSkillsForPrompt() {
|
|
89
|
-
const skills = Array.from(this.skills.values());
|
|
90
|
+
const skills = Array.from(this.skills.values()).filter((skill) => skill.metadata.enabled !== false);
|
|
90
91
|
if (skills.length === 0) return '';
|
|
91
92
|
|
|
92
93
|
let prompt = '\n## Available Skills\n';
|
|
@@ -102,7 +103,7 @@ class SkillRunner {
|
|
|
102
103
|
getToolDefinitions() {
|
|
103
104
|
const tools = [];
|
|
104
105
|
for (const skill of this.skills.values()) {
|
|
105
|
-
if (skill.metadata.tool) {
|
|
106
|
+
if (skill.metadata.enabled !== false && skill.metadata.tool) {
|
|
106
107
|
tools.push({
|
|
107
108
|
name: skill.name,
|
|
108
109
|
description: skill.description,
|
|
@@ -116,6 +117,9 @@ class SkillRunner {
|
|
|
116
117
|
async executeTool(toolName, args) {
|
|
117
118
|
const skill = this.skills.get(toolName);
|
|
118
119
|
if (!skill) return null;
|
|
120
|
+
if (skill.metadata.enabled === false) {
|
|
121
|
+
return { error: `Skill '${toolName}' is disabled` };
|
|
122
|
+
}
|
|
119
123
|
|
|
120
124
|
if (skill.metadata.command) {
|
|
121
125
|
const { CLIExecutor } = require('../cli/executor');
|
|
@@ -139,8 +143,10 @@ class SkillRunner {
|
|
|
139
143
|
const filePath = path.join(skillDir, 'SKILL.md');
|
|
140
144
|
fs.writeFileSync(filePath, frontmatter + `\n\n${instructions}`);
|
|
141
145
|
|
|
142
|
-
db.prepare(
|
|
143
|
-
|
|
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);
|
|
144
150
|
|
|
145
151
|
this.loadSkillFile(filePath);
|
|
146
152
|
|
|
@@ -166,12 +172,24 @@ class SkillRunner {
|
|
|
166
172
|
|
|
167
173
|
const frontmatter = this._buildFrontmatter(name, newDesc, metaToWrite);
|
|
168
174
|
fs.writeFileSync(skill.filePath, frontmatter + `\n\n${newInstructions}`);
|
|
169
|
-
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);
|
|
170
177
|
this.loadSkillFile(skill.filePath);
|
|
171
178
|
|
|
172
179
|
return { success: true, name, path: skill.filePath };
|
|
173
180
|
}
|
|
174
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
|
+
|
|
175
193
|
deleteSkill(name) {
|
|
176
194
|
const skill = this.skills.get(name);
|
|
177
195
|
if (!skill) return { error: `Skill '${name}' not found` };
|
|
@@ -210,7 +228,8 @@ class SkillRunner {
|
|
|
210
228
|
name: s.name,
|
|
211
229
|
description: s.description,
|
|
212
230
|
metadata: s.metadata,
|
|
213
|
-
filePath: s.filePath
|
|
231
|
+
filePath: s.filePath,
|
|
232
|
+
enabled: s.metadata.enabled !== false
|
|
214
233
|
}));
|
|
215
234
|
}
|
|
216
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
|
+
};
|
|
@@ -1,13 +1,56 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const db = require('../../db/database');
|
|
4
|
+
const { DATA_DIR } = require('../../../runtime/paths');
|
|
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
|
+
}
|
|
4
46
|
|
|
5
47
|
/**
|
|
6
48
|
* Returns the list of available tools for the agent.
|
|
7
49
|
* @param {object} app - Express app instance.
|
|
50
|
+
* @param {object} options - Tool filtering options.
|
|
8
51
|
* @returns {Array} List of tool definitions.
|
|
9
52
|
*/
|
|
10
|
-
function getAvailableTools(app) {
|
|
53
|
+
function getAvailableTools(app, options = {}) {
|
|
11
54
|
const tools = [
|
|
12
55
|
{
|
|
13
56
|
name: 'execute_command',
|
|
@@ -99,6 +142,21 @@ function getAvailableTools(app) {
|
|
|
99
142
|
required: ['script']
|
|
100
143
|
}
|
|
101
144
|
},
|
|
145
|
+
{
|
|
146
|
+
name: 'web_search',
|
|
147
|
+
description: 'Search the public web without opening the browser. Uses Brave Search API for fast result retrieval.',
|
|
148
|
+
parameters: {
|
|
149
|
+
type: 'object',
|
|
150
|
+
properties: {
|
|
151
|
+
query: { type: 'string', description: 'Search query to run' },
|
|
152
|
+
count: { type: 'number', description: 'Maximum number of results to return (default 5, max 10)' },
|
|
153
|
+
country: { type: 'string', description: 'Optional country code bias, e.g. "US", "DE", "GB"' },
|
|
154
|
+
search_lang: { type: 'string', description: 'Optional search language code, e.g. "en", "de"' },
|
|
155
|
+
freshness: { type: 'string', enum: ['pd', 'pw', 'pm', 'py'], description: 'Optional recency filter: past day, week, month, or year' }
|
|
156
|
+
},
|
|
157
|
+
required: ['query']
|
|
158
|
+
}
|
|
159
|
+
},
|
|
102
160
|
{
|
|
103
161
|
name: 'manage_protocols',
|
|
104
162
|
description: 'Read, list, create, update, or delete text-based protocols (a pre-set list of instructions/actions). If user asks to execute a protocol, you should read it and follow its instructions.',
|
|
@@ -138,6 +196,18 @@ function getAvailableTools(app) {
|
|
|
138
196
|
required: ['query']
|
|
139
197
|
}
|
|
140
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
|
+
},
|
|
141
211
|
{
|
|
142
212
|
name: 'memory_update_core',
|
|
143
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.',
|
|
@@ -514,7 +584,12 @@ function getAvailableTools(app) {
|
|
|
514
584
|
}
|
|
515
585
|
];
|
|
516
586
|
|
|
517
|
-
|
|
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;
|
|
518
593
|
}
|
|
519
594
|
|
|
520
595
|
/**
|
|
@@ -593,6 +668,74 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
593
668
|
return await controller.evaluate(args.script);
|
|
594
669
|
}
|
|
595
670
|
|
|
671
|
+
case 'web_search': {
|
|
672
|
+
const apiKey = process.env.BRAVE_SEARCH_API_KEY;
|
|
673
|
+
if (!apiKey) return { error: 'BRAVE_SEARCH_API_KEY is not configured' };
|
|
674
|
+
|
|
675
|
+
const controller = new AbortController();
|
|
676
|
+
const timeoutMs = 20000;
|
|
677
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
678
|
+
|
|
679
|
+
try {
|
|
680
|
+
const limit = Math.max(1, Math.min(Number(args.count) || 5, 10));
|
|
681
|
+
const params = new URLSearchParams({
|
|
682
|
+
q: args.query,
|
|
683
|
+
count: String(limit),
|
|
684
|
+
text_decorations: 'false',
|
|
685
|
+
result_filter: 'web'
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
if (args.country) params.set('country', String(args.country).toUpperCase());
|
|
689
|
+
if (args.search_lang) params.set('search_lang', String(args.search_lang).toLowerCase());
|
|
690
|
+
if (args.freshness) params.set('freshness', args.freshness);
|
|
691
|
+
|
|
692
|
+
const res = await fetch(`https://api.search.brave.com/res/v1/web/search?${params.toString()}`, {
|
|
693
|
+
headers: {
|
|
694
|
+
Accept: 'application/json',
|
|
695
|
+
'X-Subscription-Token': apiKey
|
|
696
|
+
},
|
|
697
|
+
signal: controller.signal
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
const text = await res.text();
|
|
701
|
+
let data = null;
|
|
702
|
+
try {
|
|
703
|
+
data = JSON.parse(text);
|
|
704
|
+
} catch {
|
|
705
|
+
data = null;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (!res.ok) {
|
|
709
|
+
return {
|
|
710
|
+
error: `Brave Search API request failed with status ${res.status}`,
|
|
711
|
+
details: data || text.slice(0, 1000)
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const rawResults = Array.isArray(data?.web?.results) ? data.web.results : [];
|
|
716
|
+
const results = rawResults.slice(0, limit).map((item, index) => ({
|
|
717
|
+
rank: index + 1,
|
|
718
|
+
title: item.title || '',
|
|
719
|
+
url: item.url || '',
|
|
720
|
+
description: item.description || '',
|
|
721
|
+
age: item.age || null,
|
|
722
|
+
language: item.language || null,
|
|
723
|
+
profile: item.profile?.long_name || item.profile?.name || null
|
|
724
|
+
}));
|
|
725
|
+
|
|
726
|
+
return {
|
|
727
|
+
query: args.query,
|
|
728
|
+
count: results.length,
|
|
729
|
+
results
|
|
730
|
+
};
|
|
731
|
+
} catch (err) {
|
|
732
|
+
if (err.name === 'AbortError') return { error: `Brave Search API request timed out after ${timeoutMs} ms` };
|
|
733
|
+
return { error: err.message };
|
|
734
|
+
} finally {
|
|
735
|
+
clearTimeout(timer);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
596
739
|
case 'manage_protocols': {
|
|
597
740
|
try {
|
|
598
741
|
if (args.action === 'list') {
|
|
@@ -641,6 +784,16 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
641
784
|
return { results };
|
|
642
785
|
}
|
|
643
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
|
+
|
|
644
797
|
case 'memory_update_core': {
|
|
645
798
|
const { MemoryManager } = require('../memory/manager');
|
|
646
799
|
const mm = new MemoryManager();
|
|
@@ -686,13 +839,13 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
686
839
|
const end = args.end_line || lines.length;
|
|
687
840
|
const sliced = lines.slice(start, end).join('\n');
|
|
688
841
|
return {
|
|
689
|
-
content: sliced.length >
|
|
842
|
+
content: sliced.length > 20000 ? sliced.slice(0, 20000) + '\n...[truncated]' : sliced,
|
|
690
843
|
totalLines: lines.length,
|
|
691
844
|
rangeShown: [start + 1, Math.min(end, lines.length)]
|
|
692
845
|
};
|
|
693
846
|
}
|
|
694
847
|
const content = fs.readFileSync(args.path, encoding);
|
|
695
|
-
return { content: content.length >
|
|
848
|
+
return { content: content.length > 20000 ? content.slice(0, 20000) + '\n...[truncated]' : content };
|
|
696
849
|
} catch (err) {
|
|
697
850
|
return { error: err.message };
|
|
698
851
|
}
|
|
@@ -1000,7 +1153,7 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
1000
1153
|
n: count,
|
|
1001
1154
|
response_format: 'b64_json'
|
|
1002
1155
|
});
|
|
1003
|
-
const MEDIA_DIR = path.join(
|
|
1156
|
+
const MEDIA_DIR = path.join(DATA_DIR, 'media');
|
|
1004
1157
|
if (!fs.existsSync(MEDIA_DIR)) fs.mkdirSync(MEDIA_DIR, { recursive: true });
|
|
1005
1158
|
const savedPaths = [];
|
|
1006
1159
|
for (const img of result.data) {
|