squidclaw 2.4.0 ā 2.6.0
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/lib/builtin-plugins/analytics/index.js +23 -0
- package/lib/builtin-plugins/analytics/plugin.json +7 -0
- package/lib/builtin-plugins/auto-responder/index.js +16 -0
- package/lib/builtin-plugins/auto-responder/plugin.json +7 -0
- package/lib/builtin-plugins/translator/index.js +22 -0
- package/lib/builtin-plugins/translator/plugin.json +7 -0
- package/lib/core/agent-tools-mixin.js +2 -1
- package/lib/engine.js +17 -0
- package/lib/features/plugins.js +278 -0
- package/lib/features/sessions.js +269 -0
- package/lib/middleware/commands.js +91 -0
- package/lib/middleware/plugins.js +41 -0
- package/lib/middleware/response-sender.js +2 -1
- package/lib/tools/router.js +86 -2
- package/package.json +1 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
let messageCount = 0;
|
|
2
|
+
let startTime = Date.now();
|
|
3
|
+
|
|
4
|
+
export async function onMessage({ message, contactId, agentId, engine }) {
|
|
5
|
+
messageCount++;
|
|
6
|
+
return null; // Don't intercept, just count
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getTools() {
|
|
10
|
+
return [
|
|
11
|
+
'### Analytics Report',
|
|
12
|
+
'---TOOL:analytics:report---',
|
|
13
|
+
'Show messaging analytics and stats for this session.',
|
|
14
|
+
];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function onTool({ toolName, engine }) {
|
|
18
|
+
if (toolName !== 'analytics') return null;
|
|
19
|
+
const uptime = ((Date.now() - startTime) / 3600000).toFixed(1);
|
|
20
|
+
return {
|
|
21
|
+
result: `š Session Analytics:\n⢠Messages this session: ${messageCount}\n⢠Session uptime: ${uptime}h\n⢠Engine uptime: ${(process.uptime() / 3600).toFixed(1)}h\n⢠Memory: ${(process.memoryUsage().rss / 1024 / 1024).toFixed(0)} MB`,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
let responses = {};
|
|
2
|
+
|
|
3
|
+
export async function onLoad({ config }) {
|
|
4
|
+
responses = config.responses || {
|
|
5
|
+
'ping': 'pong! š',
|
|
6
|
+
'marco': 'polo! š',
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function onMessage({ message }) {
|
|
11
|
+
const lower = message.toLowerCase().trim();
|
|
12
|
+
if (responses[lower]) {
|
|
13
|
+
return { handled: true, response: responses[lower] };
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function getTools() {
|
|
2
|
+
return [
|
|
3
|
+
'### Translate Text',
|
|
4
|
+
'---TOOL:translate:target_language|text to translate---',
|
|
5
|
+
'Translate text to another language. Examples: translate:arabic|Hello world, translate:english|Ł
Ų±ŲŲØŲ§',
|
|
6
|
+
];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function onTool({ toolName, toolArg, engine }) {
|
|
10
|
+
if (toolName !== 'translate') return null;
|
|
11
|
+
const pipeIdx = toolArg.indexOf('|');
|
|
12
|
+
if (pipeIdx === -1) return { result: 'Format: language|text' };
|
|
13
|
+
const lang = toolArg.slice(0, pipeIdx).trim();
|
|
14
|
+
const text = toolArg.slice(pipeIdx + 1).trim();
|
|
15
|
+
|
|
16
|
+
const response = await engine.aiGateway.chat([
|
|
17
|
+
{ role: 'system', content: `Translate the following text to ${lang}. Return ONLY the translation, nothing else.` },
|
|
18
|
+
{ role: 'user', content: text },
|
|
19
|
+
], { model: engine.config.ai?.defaultModel });
|
|
20
|
+
|
|
21
|
+
return { result: response.content };
|
|
22
|
+
}
|
|
@@ -73,8 +73,9 @@ export function addToolSupport(agent, toolRouter, knowledgeBase) {
|
|
|
73
73
|
return result;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
// File attachment (pptx,
|
|
76
|
+
// File attachment (pptx, excel, pdf, html)
|
|
77
77
|
if (toolResult.toolUsed && toolResult.filePath) {
|
|
78
|
+
logger.info('agent', 'FILE TOOL: ' + toolResult.toolName + ' -> ' + toolResult.filePath);
|
|
78
79
|
result.filePath = toolResult.filePath;
|
|
79
80
|
result.fileName = toolResult.fileName;
|
|
80
81
|
result.messages = [toolResult.toolResult || 'Here\'s your file! š'];
|
package/lib/engine.js
CHANGED
|
@@ -63,6 +63,8 @@ export class SquidclawEngine {
|
|
|
63
63
|
pipeline.use('auto-links', autoLinksMiddleware);
|
|
64
64
|
pipeline.use('auto-memory', autoMemoryMiddleware);
|
|
65
65
|
const { configChatMiddleware } = await import('./middleware/config-chat.js');
|
|
66
|
+
const { pluginMiddleware } = await import('./middleware/plugins.js');
|
|
67
|
+
pipeline.use('plugins', pluginMiddleware);
|
|
66
68
|
pipeline.use('config-chat', configChatMiddleware);
|
|
67
69
|
pipeline.use('skill-check', skillCheckMiddleware);
|
|
68
70
|
pipeline.use('typing', typingMiddleware); // wraps AI call with typing indicator
|
|
@@ -226,6 +228,21 @@ export class SquidclawEngine {
|
|
|
226
228
|
if (pending.c > 0) console.log(` ā° Reminders: ${pending.c} pending`);
|
|
227
229
|
} catch {}
|
|
228
230
|
|
|
231
|
+
// Plugins
|
|
232
|
+
try {
|
|
233
|
+
const { PluginManager } = await import('./features/plugins.js');
|
|
234
|
+
this.plugins = new PluginManager(this);
|
|
235
|
+
const count = await this.plugins.loadAll();
|
|
236
|
+
if (count > 0) console.log(' š Plugins: ' + count + ' loaded');
|
|
237
|
+
await this.plugins.fireHook('onStart', { engine: this });
|
|
238
|
+
} catch (err) { logger.error('engine', 'Plugins init failed: ' + err.message); }
|
|
239
|
+
|
|
240
|
+
// Sessions
|
|
241
|
+
try {
|
|
242
|
+
const { SessionManager } = await import('./features/sessions.js');
|
|
243
|
+
this.sessions = new SessionManager(this.storage, this);
|
|
244
|
+
} catch (err) { logger.error('engine', 'Sessions init failed: ' + err.message); }
|
|
245
|
+
|
|
229
246
|
// Cron jobs
|
|
230
247
|
try {
|
|
231
248
|
const { CronManager } = await import('./features/cron.js');
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* š¦ Plugin System
|
|
3
|
+
* Load/unload plugins dynamically from ~/.squidclaw/plugins/
|
|
4
|
+
*
|
|
5
|
+
* Plugin structure:
|
|
6
|
+
* plugins/my-plugin/
|
|
7
|
+
* plugin.json ā manifest (name, version, description, hooks)
|
|
8
|
+
* index.js ā main entry (exports hooks)
|
|
9
|
+
* README.md ā optional docs
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { logger } from '../core/logger.js';
|
|
13
|
+
import { readdirSync, existsSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
|
|
14
|
+
import { join } from 'path';
|
|
15
|
+
import { getHome } from '../core/config.js';
|
|
16
|
+
import { pathToFileURL } from 'url';
|
|
17
|
+
|
|
18
|
+
export class PluginManager {
|
|
19
|
+
constructor(engine) {
|
|
20
|
+
this.engine = engine;
|
|
21
|
+
this.plugins = new Map(); // name -> { manifest, module, status }
|
|
22
|
+
this.pluginsDir = join(getHome(), 'plugins');
|
|
23
|
+
mkdirSync(this.pluginsDir, { recursive: true });
|
|
24
|
+
this._initDb();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_initDb() {
|
|
28
|
+
this.engine.storage.db.exec(`
|
|
29
|
+
CREATE TABLE IF NOT EXISTS plugins (
|
|
30
|
+
name TEXT PRIMARY KEY,
|
|
31
|
+
enabled INTEGER DEFAULT 1,
|
|
32
|
+
config TEXT DEFAULT '{}',
|
|
33
|
+
installed_at TEXT DEFAULT (datetime('now'))
|
|
34
|
+
)
|
|
35
|
+
`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// āā Load all plugins āā
|
|
39
|
+
|
|
40
|
+
async loadAll() {
|
|
41
|
+
if (!existsSync(this.pluginsDir)) return;
|
|
42
|
+
|
|
43
|
+
const dirs = readdirSync(this.pluginsDir, { withFileTypes: true })
|
|
44
|
+
.filter(d => d.isDirectory())
|
|
45
|
+
.map(d => d.name);
|
|
46
|
+
|
|
47
|
+
let loaded = 0;
|
|
48
|
+
for (const dir of dirs) {
|
|
49
|
+
try {
|
|
50
|
+
await this.load(dir);
|
|
51
|
+
loaded++;
|
|
52
|
+
} catch (err) {
|
|
53
|
+
logger.error('plugins', `Failed to load ${dir}: ${err.message}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (loaded > 0) {
|
|
58
|
+
logger.info('plugins', `Loaded ${loaded}/${dirs.length} plugins`);
|
|
59
|
+
}
|
|
60
|
+
return loaded;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// āā Load single plugin āā
|
|
64
|
+
|
|
65
|
+
async load(name) {
|
|
66
|
+
const pluginDir = join(this.pluginsDir, name);
|
|
67
|
+
const manifestPath = join(pluginDir, 'plugin.json');
|
|
68
|
+
const indexPath = join(pluginDir, 'index.js');
|
|
69
|
+
|
|
70
|
+
if (!existsSync(manifestPath)) {
|
|
71
|
+
throw new Error('No plugin.json found');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
75
|
+
manifest.name = manifest.name || name;
|
|
76
|
+
|
|
77
|
+
// Check if disabled in DB
|
|
78
|
+
const dbEntry = this.engine.storage.db.prepare('SELECT * FROM plugins WHERE name = ?').get(manifest.name);
|
|
79
|
+
if (dbEntry && !dbEntry.enabled) {
|
|
80
|
+
this.plugins.set(manifest.name, { manifest, module: null, status: 'disabled' });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Load module
|
|
85
|
+
if (!existsSync(indexPath)) {
|
|
86
|
+
throw new Error('No index.js found');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Dynamic import with cache busting for hot reload
|
|
90
|
+
const moduleUrl = pathToFileURL(indexPath).href + '?t=' + Date.now();
|
|
91
|
+
const module = await import(moduleUrl);
|
|
92
|
+
|
|
93
|
+
// Register in DB
|
|
94
|
+
this.engine.storage.db.prepare(
|
|
95
|
+
'INSERT OR REPLACE INTO plugins (name, enabled, config) VALUES (?, 1, ?)'
|
|
96
|
+
).run(manifest.name, JSON.stringify(dbEntry?.config ? JSON.parse(dbEntry.config) : {}));
|
|
97
|
+
|
|
98
|
+
// Call onLoad hook
|
|
99
|
+
if (module.onLoad) {
|
|
100
|
+
const pluginConfig = dbEntry ? JSON.parse(dbEntry.config || '{}') : {};
|
|
101
|
+
await module.onLoad({ engine: this.engine, config: pluginConfig, logger });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.plugins.set(manifest.name, { manifest, module, status: 'active', dir: pluginDir });
|
|
105
|
+
logger.info('plugins', `Loaded: ${manifest.name} v${manifest.version || '?'}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// āā Unload plugin āā
|
|
109
|
+
|
|
110
|
+
async unload(name) {
|
|
111
|
+
const plugin = this.plugins.get(name);
|
|
112
|
+
if (!plugin) throw new Error('Plugin not found: ' + name);
|
|
113
|
+
|
|
114
|
+
if (plugin.module?.onUnload) {
|
|
115
|
+
await plugin.module.onUnload({ engine: this.engine });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
this.plugins.delete(name);
|
|
119
|
+
logger.info('plugins', `Unloaded: ${name}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// āā Enable/Disable āā
|
|
123
|
+
|
|
124
|
+
enable(name) {
|
|
125
|
+
this.engine.storage.db.prepare('UPDATE plugins SET enabled = 1 WHERE name = ?').run(name);
|
|
126
|
+
logger.info('plugins', `Enabled: ${name}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
disable(name) {
|
|
130
|
+
this.engine.storage.db.prepare('UPDATE plugins SET enabled = 0 WHERE name = ?').run(name);
|
|
131
|
+
this.unload(name).catch(() => {});
|
|
132
|
+
logger.info('plugins', `Disabled: ${name}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// āā Hot reload āā
|
|
136
|
+
|
|
137
|
+
async reload(name) {
|
|
138
|
+
await this.unload(name).catch(() => {});
|
|
139
|
+
await this.load(name);
|
|
140
|
+
logger.info('plugins', `Reloaded: ${name}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// āā Fire hooks āā
|
|
144
|
+
|
|
145
|
+
async fireHook(hookName, context) {
|
|
146
|
+
const results = [];
|
|
147
|
+
for (const [name, plugin] of this.plugins) {
|
|
148
|
+
if (plugin.status !== 'active' || !plugin.module) continue;
|
|
149
|
+
|
|
150
|
+
const hook = plugin.module[hookName];
|
|
151
|
+
if (typeof hook === 'function') {
|
|
152
|
+
try {
|
|
153
|
+
const result = await hook(context);
|
|
154
|
+
if (result) results.push({ plugin: name, result });
|
|
155
|
+
} catch (err) {
|
|
156
|
+
logger.error('plugins', `${name}.${hookName} failed: ${err.message}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return results;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Hook types:
|
|
165
|
+
* - onLoad({ engine, config, logger }) ā plugin loaded
|
|
166
|
+
* - onUnload({ engine }) ā plugin unloading
|
|
167
|
+
* - onMessage({ message, contactId, agentId, metadata, engine }) ā incoming message, return { handled, response } to intercept
|
|
168
|
+
* - onResponse({ response, contactId, agentId, engine }) ā before sending response, can modify
|
|
169
|
+
* - onTool({ toolName, toolArg, agentId, engine }) ā custom tool handler, return { result } to handle
|
|
170
|
+
* - onStart({ engine }) ā engine started
|
|
171
|
+
* - onStop({ engine }) ā engine stopping
|
|
172
|
+
* - getTools() ā return array of tool descriptions for AI prompt
|
|
173
|
+
*/
|
|
174
|
+
|
|
175
|
+
// āā Get plugin tool descriptions āā
|
|
176
|
+
|
|
177
|
+
getToolDescriptions() {
|
|
178
|
+
const tools = [];
|
|
179
|
+
for (const [name, plugin] of this.plugins) {
|
|
180
|
+
if (plugin.status !== 'active' || !plugin.module?.getTools) continue;
|
|
181
|
+
try {
|
|
182
|
+
const pluginTools = plugin.module.getTools();
|
|
183
|
+
if (Array.isArray(pluginTools)) {
|
|
184
|
+
tools.push(...pluginTools);
|
|
185
|
+
}
|
|
186
|
+
} catch {}
|
|
187
|
+
}
|
|
188
|
+
return tools;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// āā Handle custom tool calls āā
|
|
192
|
+
|
|
193
|
+
async handleTool(toolName, toolArg, agentId) {
|
|
194
|
+
const results = await this.fireHook('onTool', { toolName, toolArg, agentId, engine: this.engine });
|
|
195
|
+
if (results.length > 0 && results[0].result) {
|
|
196
|
+
return results[0].result;
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// āā List plugins āā
|
|
202
|
+
|
|
203
|
+
list() {
|
|
204
|
+
return Array.from(this.plugins.entries()).map(([name, p]) => ({
|
|
205
|
+
name,
|
|
206
|
+
version: p.manifest.version || '?',
|
|
207
|
+
description: p.manifest.description || '',
|
|
208
|
+
status: p.status,
|
|
209
|
+
hooks: p.module ? Object.keys(p.module).filter(k => k.startsWith('on') || k === 'getTools') : [],
|
|
210
|
+
author: p.manifest.author || '',
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// āā Create plugin scaffold āā
|
|
215
|
+
|
|
216
|
+
scaffold(name, description) {
|
|
217
|
+
const dir = join(this.pluginsDir, name);
|
|
218
|
+
mkdirSync(dir, { recursive: true });
|
|
219
|
+
|
|
220
|
+
writeFileSync(join(dir, 'plugin.json'), JSON.stringify({
|
|
221
|
+
name,
|
|
222
|
+
version: '1.0.0',
|
|
223
|
+
description: description || 'A Squidclaw plugin',
|
|
224
|
+
author: '',
|
|
225
|
+
hooks: ['onMessage', 'onTool', 'getTools'],
|
|
226
|
+
}, null, 2));
|
|
227
|
+
|
|
228
|
+
writeFileSync(join(dir, 'index.js'), `/**
|
|
229
|
+
* ${name} ā Squidclaw Plugin
|
|
230
|
+
* ${description || ''}
|
|
231
|
+
*/
|
|
232
|
+
|
|
233
|
+
// Called when plugin loads
|
|
234
|
+
export async function onLoad({ engine, config, logger }) {
|
|
235
|
+
logger.info('${name}', 'Plugin loaded!');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Called when plugin unloads
|
|
239
|
+
export async function onUnload({ engine }) {}
|
|
240
|
+
|
|
241
|
+
// Called on every incoming message ā return { handled: true, response } to intercept
|
|
242
|
+
export async function onMessage({ message, contactId, agentId, engine }) {
|
|
243
|
+
// Example: respond to a specific keyword
|
|
244
|
+
// if (message.toLowerCase().includes('hello plugin')) {
|
|
245
|
+
// return { handled: true, response: 'Hello from ${name} plugin! š' };
|
|
246
|
+
// }
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Called before response is sent ā can modify
|
|
251
|
+
export async function onResponse({ response, contactId, agentId, engine }) {
|
|
252
|
+
return response; // return modified response or null
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Handle custom tool calls
|
|
256
|
+
export async function onTool({ toolName, toolArg, agentId, engine }) {
|
|
257
|
+
// if (toolName === 'my_custom_tool') {
|
|
258
|
+
// return { result: 'Custom tool result!' };
|
|
259
|
+
// }
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Return tool descriptions for the AI prompt
|
|
264
|
+
export function getTools() {
|
|
265
|
+
return [
|
|
266
|
+
// '### My Custom Tool',
|
|
267
|
+
// '---TOOL:my_custom_tool:argument---',
|
|
268
|
+
// 'Description of what this tool does.',
|
|
269
|
+
];
|
|
270
|
+
}
|
|
271
|
+
`);
|
|
272
|
+
|
|
273
|
+
writeFileSync(join(dir, 'README.md'), `# ${name}\n\n${description || 'A Squidclaw plugin.'}\n\n## Installation\n\nCopy to \`~/.squidclaw/plugins/${name}/\`\n`);
|
|
274
|
+
|
|
275
|
+
logger.info('plugins', `Scaffolded: ${dir}`);
|
|
276
|
+
return dir;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* š¦ Session Manager
|
|
3
|
+
* Multiple isolated conversations per agent
|
|
4
|
+
* Types: main (default), isolated (fresh context), thread (linked to parent)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { logger } from '../core/logger.js';
|
|
8
|
+
import crypto from 'crypto';
|
|
9
|
+
|
|
10
|
+
export class SessionManager {
|
|
11
|
+
constructor(storage, engine) {
|
|
12
|
+
this.storage = storage;
|
|
13
|
+
this.engine = engine;
|
|
14
|
+
this.active = new Map(); // sessionKey -> session
|
|
15
|
+
this._initDb();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
_initDb() {
|
|
19
|
+
this.storage.db.exec(`
|
|
20
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
21
|
+
id TEXT PRIMARY KEY,
|
|
22
|
+
agent_id TEXT NOT NULL,
|
|
23
|
+
contact_id TEXT NOT NULL,
|
|
24
|
+
type TEXT DEFAULT 'main',
|
|
25
|
+
label TEXT,
|
|
26
|
+
parent_id TEXT,
|
|
27
|
+
model TEXT,
|
|
28
|
+
system_prompt TEXT,
|
|
29
|
+
status TEXT DEFAULT 'active',
|
|
30
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
31
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
32
|
+
metadata TEXT DEFAULT '{}'
|
|
33
|
+
);
|
|
34
|
+
CREATE TABLE IF NOT EXISTS session_messages (
|
|
35
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
36
|
+
session_id TEXT NOT NULL,
|
|
37
|
+
role TEXT NOT NULL,
|
|
38
|
+
content TEXT NOT NULL,
|
|
39
|
+
tokens INTEGER DEFAULT 0,
|
|
40
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
41
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id)
|
|
42
|
+
);
|
|
43
|
+
CREATE INDEX IF NOT EXISTS idx_session_messages_sid ON session_messages(session_id);
|
|
44
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent_id, contact_id);
|
|
45
|
+
`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// āā Session CRUD āā
|
|
49
|
+
|
|
50
|
+
create(agentId, contactId, options = {}) {
|
|
51
|
+
const id = 'sess_' + crypto.randomBytes(6).toString('hex');
|
|
52
|
+
const type = options.type || 'main';
|
|
53
|
+
const label = options.label || null;
|
|
54
|
+
const parentId = options.parentId || null;
|
|
55
|
+
const model = options.model || null;
|
|
56
|
+
const systemPrompt = options.systemPrompt || null;
|
|
57
|
+
|
|
58
|
+
this.storage.db.prepare(
|
|
59
|
+
'INSERT INTO sessions (id, agent_id, contact_id, type, label, parent_id, model, system_prompt, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
|
60
|
+
).run(id, agentId, contactId, type, label, parentId, model, systemPrompt, JSON.stringify(options.metadata || {}));
|
|
61
|
+
|
|
62
|
+
const session = { id, agentId, contactId, type, label, parentId, model, systemPrompt, status: 'active' };
|
|
63
|
+
this.active.set(id, session);
|
|
64
|
+
|
|
65
|
+
logger.info('sessions', `Created ${type} session ${id}${label ? ' [' + label + ']' : ''}`);
|
|
66
|
+
return session;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
get(sessionId) {
|
|
70
|
+
const cached = this.active.get(sessionId);
|
|
71
|
+
if (cached) return cached;
|
|
72
|
+
|
|
73
|
+
const row = this.storage.db.prepare('SELECT * FROM sessions WHERE id = ?').get(sessionId);
|
|
74
|
+
if (row) {
|
|
75
|
+
this.active.set(row.id, row);
|
|
76
|
+
return row;
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get or create main session for a contact
|
|
83
|
+
*/
|
|
84
|
+
getMain(agentId, contactId) {
|
|
85
|
+
let session = this.storage.db.prepare(
|
|
86
|
+
"SELECT * FROM sessions WHERE agent_id = ? AND contact_id = ? AND type = 'main' AND status = 'active' LIMIT 1"
|
|
87
|
+
).get(agentId, contactId);
|
|
88
|
+
|
|
89
|
+
if (!session) {
|
|
90
|
+
session = this.create(agentId, contactId, { type: 'main', label: 'Main' });
|
|
91
|
+
}
|
|
92
|
+
return session;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Spawn an isolated session
|
|
97
|
+
*/
|
|
98
|
+
spawn(agentId, contactId, options = {}) {
|
|
99
|
+
return this.create(agentId, contactId, {
|
|
100
|
+
type: options.type || 'isolated',
|
|
101
|
+
label: options.label || options.task?.slice(0, 50),
|
|
102
|
+
parentId: options.parentId,
|
|
103
|
+
model: options.model,
|
|
104
|
+
systemPrompt: options.systemPrompt || (options.task ?
|
|
105
|
+
'You are a focused sub-agent. Complete this task concisely:\n\n' + options.task : null),
|
|
106
|
+
metadata: { task: options.task, timeout: options.timeout },
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* List sessions for a contact
|
|
112
|
+
*/
|
|
113
|
+
list(agentId, contactId, options = {}) {
|
|
114
|
+
let query = 'SELECT * FROM sessions WHERE agent_id = ?';
|
|
115
|
+
const params = [agentId];
|
|
116
|
+
|
|
117
|
+
if (contactId) {
|
|
118
|
+
query += ' AND contact_id = ?';
|
|
119
|
+
params.push(contactId);
|
|
120
|
+
}
|
|
121
|
+
if (options.type) {
|
|
122
|
+
query += ' AND type = ?';
|
|
123
|
+
params.push(options.type);
|
|
124
|
+
}
|
|
125
|
+
if (options.status || !options.includeEnded) {
|
|
126
|
+
query += ' AND status = ?';
|
|
127
|
+
params.push(options.status || 'active');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
query += ' ORDER BY created_at DESC';
|
|
131
|
+
if (options.limit) {
|
|
132
|
+
query += ' LIMIT ?';
|
|
133
|
+
params.push(options.limit);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return this.storage.db.prepare(query).all(...params);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* End a session
|
|
141
|
+
*/
|
|
142
|
+
end(sessionId) {
|
|
143
|
+
this.storage.db.prepare("UPDATE sessions SET status = 'ended', updated_at = datetime('now') WHERE id = ?").run(sessionId);
|
|
144
|
+
this.active.delete(sessionId);
|
|
145
|
+
logger.info('sessions', `Ended session ${sessionId}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// āā Session Messages āā
|
|
149
|
+
|
|
150
|
+
addMessage(sessionId, role, content, tokens = 0) {
|
|
151
|
+
this.storage.db.prepare(
|
|
152
|
+
'INSERT INTO session_messages (session_id, role, content, tokens) VALUES (?, ?, ?, ?)'
|
|
153
|
+
).run(sessionId, role, content, tokens);
|
|
154
|
+
|
|
155
|
+
this.storage.db.prepare("UPDATE sessions SET updated_at = datetime('now') WHERE id = ?").run(sessionId);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
getHistory(sessionId, limit = 50) {
|
|
159
|
+
return this.storage.db.prepare(
|
|
160
|
+
'SELECT role, content, created_at FROM session_messages WHERE session_id = ? ORDER BY created_at ASC LIMIT ?'
|
|
161
|
+
).all(sessionId, limit);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
clearHistory(sessionId) {
|
|
165
|
+
this.storage.db.prepare('DELETE FROM session_messages WHERE session_id = ?').run(sessionId);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// āā Session AI Processing āā
|
|
169
|
+
|
|
170
|
+
async process(sessionId, message) {
|
|
171
|
+
const session = this.get(sessionId);
|
|
172
|
+
if (!session) throw new Error('Session not found: ' + sessionId);
|
|
173
|
+
|
|
174
|
+
// Save user message
|
|
175
|
+
this.addMessage(sessionId, 'user', message);
|
|
176
|
+
|
|
177
|
+
// Build messages array
|
|
178
|
+
const history = this.getHistory(sessionId);
|
|
179
|
+
const model = session.model || this.engine.config.ai?.defaultModel;
|
|
180
|
+
|
|
181
|
+
// Get system prompt
|
|
182
|
+
let systemPrompt = session.system_prompt || session.systemPrompt;
|
|
183
|
+
if (!systemPrompt && session.type === 'main') {
|
|
184
|
+
// Use agent's normal prompt for main sessions
|
|
185
|
+
const agent = this.engine.agents?.get(session.agent_id);
|
|
186
|
+
if (agent?.promptBuilder) {
|
|
187
|
+
systemPrompt = await agent.promptBuilder.build(agent, session.contact_id, message);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const messages = [];
|
|
192
|
+
if (systemPrompt) messages.push({ role: 'system', content: systemPrompt });
|
|
193
|
+
messages.push(...history.map(h => ({ role: h.role === 'system' ? 'user' : h.role, content: h.content })));
|
|
194
|
+
|
|
195
|
+
// Call AI
|
|
196
|
+
const response = await this.engine.aiGateway.chat(messages, {
|
|
197
|
+
model,
|
|
198
|
+
fallbackChain: this.engine.config.ai?.fallbackChain,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Save assistant response
|
|
202
|
+
this.addMessage(sessionId, 'assistant', response.content, response.outputTokens);
|
|
203
|
+
|
|
204
|
+
// Track usage
|
|
205
|
+
await this.storage.trackUsage(session.agent_id, response.model, response.inputTokens, response.outputTokens, response.costUsd);
|
|
206
|
+
|
|
207
|
+
logger.info('sessions', `Session ${sessionId} processed (${response.model}, ${response.outputTokens} tokens)`);
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
content: response.content,
|
|
211
|
+
model: response.model,
|
|
212
|
+
tokens: response.inputTokens + response.outputTokens,
|
|
213
|
+
cost: response.costUsd,
|
|
214
|
+
sessionId,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// āā Sub-agent spawning with auto-completion āā
|
|
219
|
+
|
|
220
|
+
async runTask(agentId, contactId, task, options = {}) {
|
|
221
|
+
const session = this.spawn(agentId, contactId, {
|
|
222
|
+
type: 'isolated',
|
|
223
|
+
task,
|
|
224
|
+
model: options.model,
|
|
225
|
+
label: options.label || task.slice(0, 50),
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const timeout = options.timeout || 60000;
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
const result = await Promise.race([
|
|
232
|
+
this.process(session.id, task),
|
|
233
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Task timeout')), timeout)),
|
|
234
|
+
]);
|
|
235
|
+
|
|
236
|
+
this.end(session.id);
|
|
237
|
+
return { ...result, status: 'complete' };
|
|
238
|
+
} catch (err) {
|
|
239
|
+
this.end(session.id);
|
|
240
|
+
return { content: 'Task failed: ' + err.message, status: 'error', sessionId: session.id };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// āā Send message to another session āā
|
|
245
|
+
|
|
246
|
+
async send(targetSessionId, message) {
|
|
247
|
+
const session = this.get(targetSessionId);
|
|
248
|
+
if (!session) throw new Error('Session not found');
|
|
249
|
+
return this.process(targetSessionId, message);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// āā Stats āā
|
|
253
|
+
|
|
254
|
+
getStats(agentId) {
|
|
255
|
+
const active = this.storage.db.prepare(
|
|
256
|
+
"SELECT COUNT(*) as c FROM sessions WHERE agent_id = ? AND status = 'active'"
|
|
257
|
+
).get(agentId)?.c || 0;
|
|
258
|
+
|
|
259
|
+
const total = this.storage.db.prepare(
|
|
260
|
+
'SELECT COUNT(*) as c FROM sessions WHERE agent_id = ?'
|
|
261
|
+
).get(agentId)?.c || 0;
|
|
262
|
+
|
|
263
|
+
const messages = this.storage.db.prepare(
|
|
264
|
+
'SELECT COUNT(*) as c FROM session_messages sm JOIN sessions s ON sm.session_id = s.id WHERE s.agent_id = ?'
|
|
265
|
+
).get(agentId)?.c || 0;
|
|
266
|
+
|
|
267
|
+
return { active, total, messages };
|
|
268
|
+
}
|
|
269
|
+
}
|
|
@@ -145,6 +145,97 @@ export async function commandsMiddleware(ctx, next) {
|
|
|
145
145
|
return;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
if (cmd === '/plugins' || cmd === '/plugin') {
|
|
149
|
+
if (!ctx.engine.plugins) { await ctx.reply('ā Plugin system not available'); return; }
|
|
150
|
+
const args = msg.split(/\s+/).slice(1);
|
|
151
|
+
const sub = args[0];
|
|
152
|
+
|
|
153
|
+
if (!sub || sub === 'list') {
|
|
154
|
+
const plugins = ctx.engine.plugins.list();
|
|
155
|
+
if (plugins.length === 0) {
|
|
156
|
+
await ctx.reply('š No plugins installed\n\nCreate one: /plugin new <name>\nOr drop a plugin folder in ~/.squidclaw/plugins/');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const lines = plugins.map(p =>
|
|
160
|
+
(p.status === 'active' ? 'š¢' : 'āøļø') + ' *' + p.name + '* v' + p.version + '\n ' + p.description + '\n Hooks: ' + p.hooks.join(', ')
|
|
161
|
+
);
|
|
162
|
+
await ctx.reply('š *Plugins*\n\n' + lines.join('\n\n'));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (sub === 'new' || sub === 'create') {
|
|
167
|
+
const name = args[1] || 'my-plugin';
|
|
168
|
+
const desc = args.slice(2).join(' ') || '';
|
|
169
|
+
const dir = ctx.engine.plugins.scaffold(name, desc);
|
|
170
|
+
await ctx.reply('š Plugin scaffolded!\n\nš ' + dir + '\n\nEdit index.js to add your logic, then /plugin reload ' + name);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (sub === 'reload') {
|
|
175
|
+
const name = args[1];
|
|
176
|
+
if (!name) { await ctx.reply('Usage: /plugin reload <name>'); return; }
|
|
177
|
+
try {
|
|
178
|
+
await ctx.engine.plugins.reload(name);
|
|
179
|
+
await ctx.reply('š Reloaded: ' + name);
|
|
180
|
+
} catch (err) { await ctx.reply('ā ' + err.message); }
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (sub === 'enable') {
|
|
185
|
+
ctx.engine.plugins.enable(args[1]);
|
|
186
|
+
await ctx.reply('ā
Enabled: ' + args[1]);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (sub === 'disable') {
|
|
191
|
+
ctx.engine.plugins.disable(args[1]);
|
|
192
|
+
await ctx.reply('āøļø Disabled: ' + args[1]);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (sub === 'unload') {
|
|
197
|
+
await ctx.engine.plugins.unload(args[1]);
|
|
198
|
+
await ctx.reply('š Unloaded: ' + args[1]);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
await ctx.reply('š *Plugin Commands*\n\n/plugin list ā show plugins\n/plugin new <name> ā create plugin\n/plugin reload <name> ā hot reload\n/plugin enable <name>\n/plugin disable <name>');
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (cmd === '/sessions') {
|
|
207
|
+
if (!ctx.engine.sessions) { await ctx.reply('ā Sessions not available'); return; }
|
|
208
|
+
const args = msg.slice(10).trim();
|
|
209
|
+
|
|
210
|
+
if (args.startsWith('new ')) {
|
|
211
|
+
const task = args.slice(4).trim();
|
|
212
|
+
await ctx.reply('š Spawning session: ' + task.slice(0, 50) + '...');
|
|
213
|
+
const result = await ctx.engine.sessions.runTask(ctx.agentId, ctx.contactId, task);
|
|
214
|
+
await ctx.reply('ā
*Session Complete*\n\n' + result.content.slice(0, 2000));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (args === 'stats') {
|
|
219
|
+
const stats = ctx.engine.sessions.getStats(ctx.agentId);
|
|
220
|
+
await ctx.reply('š *Sessions*\n\nš¢ Active: ' + stats.active + '\nš Total: ' + stats.total + '\nš¬ Messages: ' + stats.messages);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (args.startsWith('end ')) {
|
|
225
|
+
ctx.engine.sessions.end(args.slice(4).trim());
|
|
226
|
+
await ctx.reply('ā
Session ended');
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const sessions = ctx.engine.sessions.list(ctx.agentId, ctx.contactId, { limit: 10 });
|
|
231
|
+
if (sessions.length === 0) { await ctx.reply('š No active sessions'); return; }
|
|
232
|
+
const lines = sessions.map(s =>
|
|
233
|
+
(s.status === 'active' ? 'š¢' : 'ā«') + ' *' + (s.label || 'Untitled') + '*\n Type: ' + s.type + '\n ID: `' + s.id + '`'
|
|
234
|
+
);
|
|
235
|
+
await ctx.reply('š *Sessions*\n\n' + lines.join('\n\n') + '\n\n/sessions new <task> ā spawn\n/sessions end <id> ā close\n/sessions stats');
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
148
239
|
if (cmd === '/cron') {
|
|
149
240
|
const args = msg.slice(6).trim();
|
|
150
241
|
if (!ctx.engine.cron) { await ctx.reply('ā Cron not available'); return; }
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin middleware ā fires onMessage hook before AI processing
|
|
3
|
+
*/
|
|
4
|
+
export async function pluginMiddleware(ctx, next) {
|
|
5
|
+
if (!ctx.engine.plugins) { await next(); return; }
|
|
6
|
+
|
|
7
|
+
// Fire onMessage hooks
|
|
8
|
+
const results = await ctx.engine.plugins.fireHook('onMessage', {
|
|
9
|
+
message: ctx.message,
|
|
10
|
+
contactId: ctx.contactId,
|
|
11
|
+
agentId: ctx.agentId,
|
|
12
|
+
metadata: ctx.metadata,
|
|
13
|
+
engine: ctx.engine,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// Check if any plugin handled it
|
|
17
|
+
for (const r of results) {
|
|
18
|
+
if (r.result?.handled) {
|
|
19
|
+
ctx.response = {
|
|
20
|
+
messages: [r.result.response],
|
|
21
|
+
reaction: r.result.reaction || null,
|
|
22
|
+
};
|
|
23
|
+
ctx.handled = true;
|
|
24
|
+
// Skip to response sender
|
|
25
|
+
await next();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
await next();
|
|
31
|
+
|
|
32
|
+
// Fire onResponse hooks (post-AI)
|
|
33
|
+
if (ctx.response) {
|
|
34
|
+
await ctx.engine.plugins.fireHook('onResponse', {
|
|
35
|
+
response: ctx.response,
|
|
36
|
+
contactId: ctx.contactId,
|
|
37
|
+
agentId: ctx.agentId,
|
|
38
|
+
engine: ctx.engine,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -29,8 +29,9 @@ export async function responseSenderMiddleware(ctx, next) {
|
|
|
29
29
|
|
|
30
30
|
// Send via appropriate channel
|
|
31
31
|
if (ctx.platform === 'telegram' && tm) {
|
|
32
|
-
// Send file attachment (pptx,
|
|
32
|
+
// Send file attachment (pptx, excel, pdf, html)
|
|
33
33
|
if (response.filePath) {
|
|
34
|
+
logger.info('sender', 'SENDING FILE: ' + response.filePath);
|
|
34
35
|
try {
|
|
35
36
|
await tm.sendDocument(agentId, contactId, response.filePath, response.fileName, response.messages?.[0] || '');
|
|
36
37
|
await next();
|
package/lib/tools/router.js
CHANGED
|
@@ -79,6 +79,18 @@ export class ToolRouter {
|
|
|
79
79
|
'Example:',
|
|
80
80
|
'---TOOL:pptx_slides:AI Report|dark|## Introduction\n- AI is transforming industries\n- Revenue growing 40% YoY\n\n## Growth [chart:bar]\n- 2020: 50\n- 2021: 80\n- 2022: 120\n- 2023: 200\n\n## Key Stats [stats]\n- š 195 ā Countries using AI\n- š° $500B ā Market size\n- š 40% ā Annual growth---');
|
|
81
81
|
|
|
82
|
+
tools.push('', '### Spawn Sub-Agent Session',
|
|
83
|
+
'---TOOL:session_spawn:task description---',
|
|
84
|
+
'Spawn an isolated AI session to work on a task in the background. Returns when complete.',
|
|
85
|
+
'Use for: research, analysis, writing, any task that needs focused work.',
|
|
86
|
+
'Example: ---TOOL:session_spawn:Research the top 5 AI companies in Saudi Arabia and summarize their products---',
|
|
87
|
+
'', '### List Sessions',
|
|
88
|
+
'---TOOL:session_list:all---',
|
|
89
|
+
'Show all active sessions.',
|
|
90
|
+
'', '### Send to Session',
|
|
91
|
+
'---TOOL:session_send:session_id|message---',
|
|
92
|
+
'Send a follow-up message to an existing session.');
|
|
93
|
+
|
|
82
94
|
tools.push('', '### Create Excel Spreadsheet (SENDS AS FILE!)',
|
|
83
95
|
'---TOOL:excel:Title|theme|sheet content---',
|
|
84
96
|
'Creates .xlsx file and sends it. Theme: blue, green, dark, red, saudi, corporate.',
|
|
@@ -232,6 +244,15 @@ export class ToolRouter {
|
|
|
232
244
|
|
|
233
245
|
tools.push('', '**Important:** Use tools when needed. The tool result will be injected into the conversation automatically. Only use one tool per response.');
|
|
234
246
|
|
|
247
|
+
// Plugin tools
|
|
248
|
+
if (this._engine?.plugins) {
|
|
249
|
+
const pluginTools = this._engine.plugins.getToolDescriptions();
|
|
250
|
+
if (pluginTools.length > 0) {
|
|
251
|
+
tools.push('', '## Plugin Tools');
|
|
252
|
+
tools.push(...pluginTools);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
235
256
|
return tools.join('\n');
|
|
236
257
|
}
|
|
237
258
|
|
|
@@ -246,7 +267,21 @@ export class ToolRouter {
|
|
|
246
267
|
}
|
|
247
268
|
|
|
248
269
|
async processResponse(response, agentId) {
|
|
249
|
-
const toolMatch =
|
|
270
|
+
const toolMatch = (() => {
|
|
271
|
+
const startIdx = response.indexOf('---TOOL:');
|
|
272
|
+
if (startIdx === -1) return null;
|
|
273
|
+
const afterTag = response.slice(startIdx + 8);
|
|
274
|
+
const colonIdx = afterTag.indexOf(':');
|
|
275
|
+
if (colonIdx === -1) return null;
|
|
276
|
+
const toolName = afterTag.slice(0, colonIdx);
|
|
277
|
+
const rest = afterTag.slice(colonIdx + 1);
|
|
278
|
+
// Find the closing --- that's NOT part of markdown (look for last --- or ---END---)
|
|
279
|
+
let endIdx = rest.lastIndexOf('---');
|
|
280
|
+
if (endIdx <= 0) return null;
|
|
281
|
+
const toolArg = rest.slice(0, endIdx);
|
|
282
|
+
const fullMatch = response.slice(startIdx, startIdx + 8 + colonIdx + 1 + endIdx + 3);
|
|
283
|
+
return [fullMatch, toolName, toolArg];
|
|
284
|
+
})();
|
|
250
285
|
if (!toolMatch) return { toolUsed: false, toolResult: null, cleanResponse: response };
|
|
251
286
|
|
|
252
287
|
const [fullMatch, toolName, toolArg] = toolMatch;
|
|
@@ -348,6 +383,43 @@ export class ToolRouter {
|
|
|
348
383
|
}
|
|
349
384
|
break;
|
|
350
385
|
}
|
|
386
|
+
case 'session_spawn':
|
|
387
|
+
case 'spawn_session':
|
|
388
|
+
case 'subagent': {
|
|
389
|
+
try {
|
|
390
|
+
if (!this._engine?.sessions) { toolResult = 'Sessions not available'; break; }
|
|
391
|
+
const result = await this._engine.sessions.runTask(
|
|
392
|
+
agentId, this._currentContactId || 'unknown', toolArg, { timeout: 120000 }
|
|
393
|
+
);
|
|
394
|
+
toolResult = result.status === 'complete' ?
|
|
395
|
+
'Sub-agent result:\n\n' + result.content :
|
|
396
|
+
'Sub-agent failed: ' + result.content;
|
|
397
|
+
} catch (err) { toolResult = 'Spawn failed: ' + err.message; }
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
case 'session_list': {
|
|
401
|
+
try {
|
|
402
|
+
if (!this._engine?.sessions) { toolResult = 'Sessions not available'; break; }
|
|
403
|
+
const sessions = this._engine.sessions.list(agentId, this._currentContactId);
|
|
404
|
+
if (sessions.length === 0) { toolResult = 'No active sessions'; break; }
|
|
405
|
+
toolResult = sessions.map(s =>
|
|
406
|
+
(s.status === 'active' ? 'š¢' : 'ā«') + ' ' + (s.label || s.id) + ' (' + s.type + ')\n ID: ' + s.id
|
|
407
|
+
).join('\n');
|
|
408
|
+
} catch (err) { toolResult = 'Failed: ' + err.message; }
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
case 'session_send': {
|
|
412
|
+
try {
|
|
413
|
+
if (!this._engine?.sessions) { toolResult = 'Sessions not available'; break; }
|
|
414
|
+
const pipeIdx = toolArg.indexOf('|');
|
|
415
|
+
if (pipeIdx === -1) { toolResult = 'Format: session_id|message'; break; }
|
|
416
|
+
const sessId = toolArg.slice(0, pipeIdx).trim();
|
|
417
|
+
const msg = toolArg.slice(pipeIdx + 1).trim();
|
|
418
|
+
const result = await this._engine.sessions.send(sessId, msg);
|
|
419
|
+
toolResult = 'Session response:\n\n' + result.content;
|
|
420
|
+
} catch (err) { toolResult = 'Failed: ' + err.message; }
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
351
423
|
case 'excel':
|
|
352
424
|
case 'xlsx':
|
|
353
425
|
case 'spreadsheet': {
|
|
@@ -742,7 +814,19 @@ export class ToolRouter {
|
|
|
742
814
|
toolResult = `Email sent to ${to}`;
|
|
743
815
|
break;
|
|
744
816
|
|
|
745
|
-
default:
|
|
817
|
+
default: {
|
|
818
|
+
// Check plugins for custom tool handlers
|
|
819
|
+
if (this._engine?.plugins) {
|
|
820
|
+
const pluginResult = await this._engine.plugins.handleTool(toolName, toolArg, agentId);
|
|
821
|
+
if (pluginResult) {
|
|
822
|
+
if (pluginResult.filePath) {
|
|
823
|
+
return { toolUsed: true, toolName, toolResult: pluginResult.result || 'Done', filePath: pluginResult.filePath, fileName: pluginResult.fileName, cleanResponse };
|
|
824
|
+
}
|
|
825
|
+
toolResult = pluginResult.result || pluginResult;
|
|
826
|
+
break;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
746
830
|
toolResult = `Unknown tool: ${toolName}`;
|
|
747
831
|
}
|
|
748
832
|
} catch (err) {
|