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
|
@@ -1,48 +1,25 @@
|
|
|
1
1
|
const { v4: uuidv4 } = require('uuid');
|
|
2
2
|
const fs = require('fs');
|
|
3
|
-
const os = require('os');
|
|
4
|
-
const path = require('path');
|
|
5
3
|
const db = require('../../db/database');
|
|
6
|
-
const {
|
|
7
|
-
const {
|
|
4
|
+
const { compact } = require('./compaction');
|
|
5
|
+
const { getConversationContext, buildSummaryCarrier, refreshConversationSummary } = require('./history');
|
|
6
|
+
const { ensureDefaultAiSettings, getAiSettings } = require('./settings');
|
|
7
|
+
const { selectToolsForTask } = require('./toolSelector');
|
|
8
|
+
const { compactToolResult } = require('./toolResult');
|
|
8
9
|
|
|
9
|
-
const MODEL = 'grok-4-1-fast-reasoning';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Turn a raw task/trigger string into a short, readable run title.
|
|
13
|
-
* Strips messaging-trigger boilerplate so the history panel shows
|
|
14
|
-
* the actual content instead of "You have received a message from: …"
|
|
15
|
-
*/
|
|
16
10
|
function generateTitle(task) {
|
|
17
11
|
if (!task || typeof task !== 'string') return 'Untitled';
|
|
18
|
-
// WhatsApp/messaging pattern: "You have received a message from <sender>: <actual text>"
|
|
19
12
|
const msgMatch = task.match(/received a (?:message|media|image|video|file|audio)[^:]*:\s*(.+)/is);
|
|
20
13
|
if (msgMatch) {
|
|
21
|
-
const body = msgMatch[1].replace(/\n[\s\S]*/s, '').trim();
|
|
14
|
+
const body = msgMatch[1].replace(/\n[\s\S]*/s, '').trim();
|
|
22
15
|
return body.slice(0, 90) || 'Incoming message';
|
|
23
16
|
}
|
|
24
|
-
// Scheduler / sub-agent trigger may start with a [tag]
|
|
25
17
|
const cleaned = task.replace(/^\[.*?\]\s*/i, '').replace(/^(system|task|prompt)[:\s]+/i, '').trim();
|
|
26
18
|
return cleaned.slice(0, 90);
|
|
27
19
|
}
|
|
28
20
|
|
|
29
|
-
/**
|
|
30
|
-
* Returns a human-readable label for a millisecond gap, or null if < 5 min.
|
|
31
|
-
* Injected as system messages between conversation turns so the model stays
|
|
32
|
-
* aware of how much real time has elapsed.
|
|
33
|
-
*/
|
|
34
|
-
function timeDeltaLabel(ms) {
|
|
35
|
-
const s = Math.round(ms / 1000);
|
|
36
|
-
if (s < 900) return null; // < 15 min — not noteworthy
|
|
37
|
-
if (s < 3600) return `${Math.round(s / 60)} minutes later`;
|
|
38
|
-
if (s < 86400) return `${Math.round(s / 3600)} hour${Math.round(s / 3600) === 1 ? '' : 's'} later`;
|
|
39
|
-
if (s < 604800) return `${Math.round(s / 86400)} day${Math.round(s / 86400) === 1 ? '' : 's'} later`;
|
|
40
|
-
return `${Math.round(s / 604800)} week${Math.round(s / 604800) === 1 ? '' : 's'} later`;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
21
|
function getProviderForUser(userId, task = '', isSubagent = false, modelOverride = null) {
|
|
44
22
|
const { SUPPORTED_MODELS, createProviderInstance } = require('./models');
|
|
45
|
-
const db = require('../../db/database');
|
|
46
23
|
|
|
47
24
|
let enabledIds = [];
|
|
48
25
|
let defaultChatModel = 'auto';
|
|
@@ -58,40 +35,29 @@ function getProviderForUser(userId, task = '', isSubagent = false, modelOverride
|
|
|
58
35
|
let parsedVal = row.value;
|
|
59
36
|
try {
|
|
60
37
|
parsedVal = JSON.parse(row.value);
|
|
61
|
-
} catch
|
|
62
|
-
// Expected for older plain-string values, keep parsedVal as the original string
|
|
63
|
-
}
|
|
38
|
+
} catch { }
|
|
64
39
|
|
|
65
|
-
if (row.key === 'enabled_models')
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
defaultChatModel = parsedVal;
|
|
69
|
-
} else if (row.key === 'default_subagent_model') {
|
|
70
|
-
defaultSubagentModel = parsedVal;
|
|
71
|
-
}
|
|
40
|
+
if (row.key === 'enabled_models') enabledIds = parsedVal;
|
|
41
|
+
if (row.key === 'default_chat_model') defaultChatModel = parsedVal;
|
|
42
|
+
if (row.key === 'default_subagent_model') defaultSubagentModel = parsedVal;
|
|
72
43
|
}
|
|
73
44
|
} catch (e) {
|
|
74
|
-
console.error(
|
|
45
|
+
console.error('Failed to fetch model settings:', e.message);
|
|
75
46
|
}
|
|
76
47
|
|
|
77
|
-
// Fallback if settings empty or incorrectly parsed: Use all supported models
|
|
78
48
|
if (!Array.isArray(enabledIds) || enabledIds.length === 0) {
|
|
79
|
-
enabledIds = SUPPORTED_MODELS.map(m => m.id);
|
|
49
|
+
enabledIds = SUPPORTED_MODELS.map((m) => m.id);
|
|
80
50
|
}
|
|
81
51
|
|
|
82
|
-
|
|
83
|
-
const availableModels = SUPPORTED_MODELS.filter(m => enabledIds.includes(m.id));
|
|
84
|
-
|
|
85
|
-
// Absolute fallback in case they disabled everything/corrupted data
|
|
52
|
+
const availableModels = SUPPORTED_MODELS.filter((m) => enabledIds.includes(m.id));
|
|
86
53
|
const fallbackModel = availableModels.length > 0 ? availableModels[0] : SUPPORTED_MODELS[0];
|
|
87
54
|
let selectedModelDef = fallbackModel;
|
|
88
|
-
|
|
89
55
|
const userSelectedDefault = isSubagent ? defaultSubagentModel : defaultChatModel;
|
|
90
56
|
|
|
91
57
|
if (modelOverride && typeof modelOverride === 'string') {
|
|
92
|
-
const
|
|
93
|
-
if (
|
|
94
|
-
selectedModelDef =
|
|
58
|
+
const requested = SUPPORTED_MODELS.find((m) => m.id === modelOverride.trim());
|
|
59
|
+
if (requested && enabledIds.includes(requested.id)) {
|
|
60
|
+
selectedModelDef = requested;
|
|
95
61
|
return {
|
|
96
62
|
provider: createProviderInstance(selectedModelDef.provider),
|
|
97
63
|
model: selectedModelDef.id,
|
|
@@ -101,18 +67,16 @@ function getProviderForUser(userId, task = '', isSubagent = false, modelOverride
|
|
|
101
67
|
}
|
|
102
68
|
|
|
103
69
|
if (userSelectedDefault && userSelectedDefault !== 'auto') {
|
|
104
|
-
selectedModelDef = SUPPORTED_MODELS.find(m => m.id === userSelectedDefault) || fallbackModel;
|
|
70
|
+
selectedModelDef = SUPPORTED_MODELS.find((m) => m.id === userSelectedDefault) || fallbackModel;
|
|
105
71
|
} else {
|
|
106
72
|
const taskStr = String(task || '').toLowerCase();
|
|
107
|
-
const isPlanning =
|
|
108
|
-
|
|
109
|
-
// Intelligent matching
|
|
73
|
+
const isPlanning = /\b(plan|think|analy[sz]e|complex|step by step)\b/.test(taskStr);
|
|
110
74
|
if (isPlanning) {
|
|
111
|
-
selectedModelDef = availableModels.find(m => m.purpose === 'planning') || fallbackModel;
|
|
75
|
+
selectedModelDef = availableModels.find((m) => m.purpose === 'planning') || fallbackModel;
|
|
112
76
|
} else if (isSubagent) {
|
|
113
|
-
selectedModelDef = availableModels.find(m => m.purpose === 'fast') || fallbackModel;
|
|
77
|
+
selectedModelDef = availableModels.find((m) => m.purpose === 'fast') || fallbackModel;
|
|
114
78
|
} else {
|
|
115
|
-
selectedModelDef = availableModels.find(m => m.purpose === 'general') || fallbackModel;
|
|
79
|
+
selectedModelDef = availableModels.find((m) => m.purpose === 'general') || fallbackModel;
|
|
116
80
|
}
|
|
117
81
|
}
|
|
118
82
|
|
|
@@ -123,35 +87,135 @@ function getProviderForUser(userId, task = '', isSubagent = false, modelOverride
|
|
|
123
87
|
};
|
|
124
88
|
}
|
|
125
89
|
|
|
90
|
+
function estimateTokenValue(value) {
|
|
91
|
+
if (!value) return 0;
|
|
92
|
+
if (typeof value === 'string') return Math.ceil(value.length / 4);
|
|
93
|
+
return Math.ceil(JSON.stringify(value).length / 4);
|
|
94
|
+
}
|
|
95
|
+
|
|
126
96
|
class AgentEngine {
|
|
127
97
|
constructor(io, services = {}) {
|
|
128
98
|
this.io = io;
|
|
129
|
-
this.maxIterations =
|
|
99
|
+
this.maxIterations = 12;
|
|
130
100
|
this.activeRuns = new Map();
|
|
131
101
|
this.browserController = services.browserController || null;
|
|
132
102
|
this.messagingManager = services.messagingManager || null;
|
|
133
103
|
this.mcpManager = services.mcpManager || services.mcpClient || null;
|
|
134
104
|
this.skillRunner = services.skillRunner || null;
|
|
135
105
|
this.scheduler = services.scheduler || null;
|
|
106
|
+
this.memoryManager = services.memoryManager || null;
|
|
107
|
+
this.learningManager = services.learningManager || null;
|
|
136
108
|
}
|
|
137
109
|
|
|
138
110
|
async buildSystemPrompt(userId, context = {}) {
|
|
139
111
|
const { buildSystemPrompt } = require('./systemPrompt');
|
|
140
112
|
const { MemoryManager } = require('../memory/manager');
|
|
141
|
-
const memoryManager = new MemoryManager();
|
|
142
|
-
return
|
|
113
|
+
const memoryManager = this.memoryManager || new MemoryManager();
|
|
114
|
+
return buildSystemPrompt(userId, context, memoryManager);
|
|
143
115
|
}
|
|
144
116
|
|
|
145
|
-
getAvailableTools(app) {
|
|
117
|
+
getAvailableTools(app, options = {}) {
|
|
146
118
|
const { getAvailableTools } = require('./tools');
|
|
147
|
-
return getAvailableTools(app);
|
|
119
|
+
return getAvailableTools(app, options);
|
|
148
120
|
}
|
|
149
121
|
|
|
150
122
|
async executeTool(toolName, args, context) {
|
|
151
123
|
const { executeTool } = require('./tools');
|
|
152
|
-
return
|
|
124
|
+
return executeTool(toolName, args, context, this);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
getIterationLimit(triggerType, aiSettings) {
|
|
128
|
+
if (triggerType === 'subagent') return aiSettings.subagent_max_iterations;
|
|
129
|
+
return this.maxIterations;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
getReasoningEffort(providerName, options = {}) {
|
|
133
|
+
if (providerName === 'google') return undefined;
|
|
134
|
+
return options.reasoningEffort || process.env.REASONING_EFFORT || 'low';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
buildContextMessages(systemPrompt, summaryMessage, historyMessages, recallMsg) {
|
|
138
|
+
const messages = [{ role: 'system', content: systemPrompt }];
|
|
139
|
+
if (summaryMessage) messages.push(summaryMessage);
|
|
140
|
+
if (Array.isArray(historyMessages)) messages.push(...historyMessages);
|
|
141
|
+
if (recallMsg) messages.push({ role: 'system', content: recallMsg });
|
|
142
|
+
return messages;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
buildUserMessage(userMessage, options = {}) {
|
|
146
|
+
if (!options.mediaAttachments || options.mediaAttachments.length === 0) {
|
|
147
|
+
return { role: 'user', content: userMessage };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const contentArr = [{ type: 'text', text: userMessage }];
|
|
151
|
+
for (const att of options.mediaAttachments) {
|
|
152
|
+
if ((att.type === 'image' || att.type === 'video') && att.path) {
|
|
153
|
+
try {
|
|
154
|
+
if (fs.existsSync(att.path)) {
|
|
155
|
+
const b64 = fs.readFileSync(att.path).toString('base64');
|
|
156
|
+
const mime = att.path.endsWith('.png') ? 'image/png' : att.path.endsWith('.gif') ? 'image/gif' : 'image/jpeg';
|
|
157
|
+
contentArr.push({ type: 'image_url', image_url: { url: `data:${mime};base64,${b64}` } });
|
|
158
|
+
}
|
|
159
|
+
} catch { }
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return { role: 'user', content: contentArr.length > 1 ? contentArr : userMessage };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
estimatePromptMetrics(messages, tools) {
|
|
167
|
+
const metrics = {
|
|
168
|
+
systemPromptTokens: 0,
|
|
169
|
+
toolSchemaTokens: estimateTokenValue(tools),
|
|
170
|
+
historyTokens: 0,
|
|
171
|
+
recalledMemoryTokens: 0,
|
|
172
|
+
toolReplayTokens: 0,
|
|
173
|
+
totalEstimatedTokens: 0
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
messages.forEach((msg, index) => {
|
|
177
|
+
const contentTokens = estimateTokenValue(msg.content);
|
|
178
|
+
const callTokens = estimateTokenValue(msg.tool_calls);
|
|
179
|
+
const total = contentTokens + callTokens;
|
|
180
|
+
|
|
181
|
+
if (msg.role === 'tool') {
|
|
182
|
+
metrics.toolReplayTokens += total;
|
|
183
|
+
} else if (msg.role === 'system' && index === 0) {
|
|
184
|
+
metrics.systemPromptTokens += total;
|
|
185
|
+
} else if (msg.role === 'system' && /^\[Recalled memory/.test(msg.content || '')) {
|
|
186
|
+
metrics.recalledMemoryTokens += total;
|
|
187
|
+
} else {
|
|
188
|
+
metrics.historyTokens += total;
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
metrics.totalEstimatedTokens = metrics.systemPromptTokens
|
|
193
|
+
+ metrics.toolSchemaTokens
|
|
194
|
+
+ metrics.historyTokens
|
|
195
|
+
+ metrics.recalledMemoryTokens
|
|
196
|
+
+ metrics.toolReplayTokens;
|
|
197
|
+
|
|
198
|
+
return metrics;
|
|
153
199
|
}
|
|
154
200
|
|
|
201
|
+
mergePromptMetrics(summary, metrics, iteration, toolCount) {
|
|
202
|
+
return {
|
|
203
|
+
iterationsObserved: Math.max(summary.iterationsObserved || 0, iteration),
|
|
204
|
+
toolCount,
|
|
205
|
+
maxEstimatedTokens: Math.max(summary.maxEstimatedTokens || 0, metrics.totalEstimatedTokens),
|
|
206
|
+
maxSystemPromptTokens: Math.max(summary.maxSystemPromptTokens || 0, metrics.systemPromptTokens),
|
|
207
|
+
maxToolSchemaTokens: Math.max(summary.maxToolSchemaTokens || 0, metrics.toolSchemaTokens),
|
|
208
|
+
maxHistoryTokens: Math.max(summary.maxHistoryTokens || 0, metrics.historyTokens),
|
|
209
|
+
maxRecalledMemoryTokens: Math.max(summary.maxRecalledMemoryTokens || 0, metrics.recalledMemoryTokens),
|
|
210
|
+
maxToolReplayTokens: Math.max(summary.maxToolReplayTokens || 0, metrics.toolReplayTokens),
|
|
211
|
+
lastEstimate: metrics
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async persistPromptMetrics(runId, metrics) {
|
|
216
|
+
db.prepare('UPDATE agent_runs SET prompt_metrics = ? WHERE id = ?')
|
|
217
|
+
.run(JSON.stringify(metrics), runId);
|
|
218
|
+
}
|
|
155
219
|
|
|
156
220
|
async run(userId, userMessage, options = {}) {
|
|
157
221
|
return this.runWithModel(userId, userMessage, options, null);
|
|
@@ -159,106 +223,54 @@ class AgentEngine {
|
|
|
159
223
|
|
|
160
224
|
async runWithModel(userId, userMessage, options = {}, _modelOverride = null) {
|
|
161
225
|
const triggerType = options.triggerType || 'user';
|
|
162
|
-
|
|
226
|
+
ensureDefaultAiSettings(userId);
|
|
227
|
+
const aiSettings = getAiSettings(userId);
|
|
228
|
+
const { provider, model, providerName } = getProviderForUser(userId, userMessage, triggerType === 'subagent', _modelOverride);
|
|
163
229
|
|
|
164
230
|
const runId = options.runId || uuidv4();
|
|
165
231
|
const conversationId = options.conversationId;
|
|
166
232
|
const app = options.app;
|
|
167
233
|
const triggerSource = options.triggerSource || 'web';
|
|
234
|
+
const historyWindow = aiSettings.chat_history_window;
|
|
235
|
+
const toolReplayBudget = aiSettings.tool_replay_budget_chars;
|
|
236
|
+
const maxIterations = this.getIterationLimit(triggerType, aiSettings);
|
|
168
237
|
|
|
169
238
|
const runTitle = generateTitle(userMessage);
|
|
170
239
|
db.prepare(`INSERT OR REPLACE INTO agent_runs(id, user_id, title, status, trigger_type, trigger_source, model)
|
|
171
|
-
|
|
240
|
+
VALUES(?, ?, ?, 'running', ?, ?, ?)`).run(runId, userId, runTitle, triggerType, triggerSource, model);
|
|
172
241
|
|
|
173
242
|
this.activeRuns.set(runId, { userId, status: 'running', messagingSent: false, lastToolName: null, lastToolTarget: null });
|
|
174
243
|
this.emit(userId, 'run:start', { runId, title: runTitle, model, triggerType, triggerSource });
|
|
175
244
|
|
|
176
245
|
const systemPrompt = await this.buildSystemPrompt(userId, { ...(options.context || {}), userMessage });
|
|
177
|
-
const
|
|
178
|
-
|
|
246
|
+
const builtInTools = this.getAvailableTools(app);
|
|
179
247
|
const mcpManager = app?.locals?.mcpManager || app?.locals?.mcpClient || this.mcpManager;
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
tools.push(...mcpTools);
|
|
183
|
-
}
|
|
248
|
+
const mcpTools = mcpManager ? mcpManager.getAllTools(userId) : [];
|
|
249
|
+
const tools = selectToolsForTask(userMessage, builtInTools, mcpTools, options);
|
|
184
250
|
|
|
185
|
-
// Build recalled-memory context message to inject just before the current user turn.
|
|
186
|
-
// Uses raw message content (not the full prompt wrapper) as the recall query.
|
|
187
251
|
const { MemoryManager } = require('../memory/manager');
|
|
188
|
-
const
|
|
252
|
+
const memoryManager = this.memoryManager || new MemoryManager();
|
|
189
253
|
const recallQuery = options.context?.rawUserMessage || userMessage;
|
|
190
|
-
const recallMsg = await
|
|
254
|
+
const recallMsg = await memoryManager.buildRecallMessage(userId, recallQuery);
|
|
191
255
|
|
|
192
|
-
let
|
|
256
|
+
let summaryMessage = null;
|
|
257
|
+
let historyMessages = [];
|
|
193
258
|
|
|
194
259
|
if (conversationId) {
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
messages = [{ role: 'system', content: systemPrompt }];
|
|
200
|
-
let lastMsgTs = null;
|
|
201
|
-
for (const msg of existingMessages) {
|
|
202
|
-
// Inject a time-gap marker when significant time passed before a user turn
|
|
203
|
-
if (msg.created_at && msg.role === 'user') {
|
|
204
|
-
const msgTs = new Date(msg.created_at).getTime();
|
|
205
|
-
if (lastMsgTs !== null) {
|
|
206
|
-
const label = timeDeltaLabel(msgTs - lastMsgTs);
|
|
207
|
-
if (label) {
|
|
208
|
-
messages.push({ role: 'system', content: `[${label} — now ${new Date(msgTs).toLocaleString('en-GB', { dateStyle: 'medium', timeStyle: 'short' })}]` });
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
const m = { role: msg.role, content: msg.content };
|
|
213
|
-
if (msg.tool_calls) m.tool_calls = JSON.parse(msg.tool_calls);
|
|
214
|
-
if (msg.tool_call_id) m.tool_call_id = msg.tool_call_id;
|
|
215
|
-
if (msg.name) m.name = msg.name;
|
|
216
|
-
messages.push(m);
|
|
217
|
-
if (msg.created_at) lastMsgTs = new Date(msg.created_at).getTime();
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Annotate the incoming message if the conversation has been idle
|
|
221
|
-
const nowTs = Date.now();
|
|
222
|
-
if (lastMsgTs !== null) {
|
|
223
|
-
const label = timeDeltaLabel(nowTs - lastMsgTs);
|
|
224
|
-
if (label) {
|
|
225
|
-
messages.push({ role: 'system', content: `[${label} — now ${new Date(nowTs).toLocaleString('en-GB', { dateStyle: 'medium', timeStyle: 'short' })}]` });
|
|
226
|
-
}
|
|
227
|
-
}
|
|
260
|
+
const conversationContext = getConversationContext(conversationId, historyWindow);
|
|
261
|
+
summaryMessage = buildSummaryCarrier(conversationContext.summary);
|
|
262
|
+
historyMessages = conversationContext.recentMessages;
|
|
228
263
|
} else {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
for (const pm of options.priorMessages) {
|
|
232
|
-
if (pm.role && pm.content) messages.push({ role: pm.role, content: pm.content });
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// Inject recalled memories as a system message immediately before the current user turn
|
|
238
|
-
if (recallMsg) {
|
|
239
|
-
messages.push({ role: 'system', content: recallMsg });
|
|
264
|
+
summaryMessage = buildSummaryCarrier(options.priorSummary || '');
|
|
265
|
+
historyMessages = (options.priorMessages || []).slice(-historyWindow).filter((pm) => pm.role && pm.content);
|
|
240
266
|
}
|
|
241
267
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
for (const att of options.mediaAttachments) {
|
|
245
|
-
if ((att.type === 'image' || att.type === 'video') && att.path) {
|
|
246
|
-
try {
|
|
247
|
-
if (fs.existsSync(att.path)) {
|
|
248
|
-
const b64 = fs.readFileSync(att.path).toString('base64');
|
|
249
|
-
const mime = att.path.endsWith('.png') ? 'image/png' : att.path.endsWith('.gif') ? 'image/gif' : 'image/jpeg';
|
|
250
|
-
contentArr.push({ type: 'image_url', image_url: { url: `data:${mime};base64,${b64}` } });
|
|
251
|
-
}
|
|
252
|
-
} catch { /* skip unreadable */ }
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
messages.push({ role: 'user', content: contentArr.length > 1 ? contentArr : userMessage });
|
|
256
|
-
} else {
|
|
257
|
-
messages.push({ role: 'user', content: userMessage });
|
|
258
|
-
}
|
|
268
|
+
let messages = this.buildContextMessages(systemPrompt, summaryMessage, historyMessages, recallMsg);
|
|
269
|
+
messages.push(this.buildUserMessage(userMessage, options));
|
|
259
270
|
|
|
260
271
|
if (conversationId) {
|
|
261
|
-
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content) VALUES (?, ?, ?)')
|
|
272
|
+
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content) VALUES (?, ?, ?)')
|
|
273
|
+
.run(conversationId, 'user', userMessage);
|
|
262
274
|
}
|
|
263
275
|
|
|
264
276
|
let iteration = 0;
|
|
@@ -266,23 +278,27 @@ class AgentEngine {
|
|
|
266
278
|
let lastContent = '';
|
|
267
279
|
let stepIndex = 0;
|
|
268
280
|
let forcedFinalResponse = false;
|
|
281
|
+
let promptMetrics = {};
|
|
269
282
|
|
|
270
283
|
try {
|
|
271
|
-
while (iteration <
|
|
284
|
+
while (iteration < maxIterations) {
|
|
272
285
|
iteration++;
|
|
273
286
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
287
|
+
let metrics = this.estimatePromptMetrics(messages, tools);
|
|
288
|
+
const contextWindow = provider.getContextWindow(model);
|
|
289
|
+
if (metrics.totalEstimatedTokens > contextWindow * 0.7) {
|
|
277
290
|
messages = await compact(messages, provider, model);
|
|
278
291
|
this.emit(userId, 'run:compaction', { runId, iteration });
|
|
292
|
+
metrics = this.estimatePromptMetrics(messages, tools);
|
|
279
293
|
}
|
|
280
294
|
|
|
295
|
+
promptMetrics = this.mergePromptMetrics(promptMetrics, metrics, iteration, tools.length);
|
|
296
|
+
this.persistPromptMetrics(runId, promptMetrics).catch(() => { });
|
|
281
297
|
this.emit(userId, 'run:thinking', { runId, iteration });
|
|
282
298
|
|
|
283
299
|
let response;
|
|
284
300
|
let streamContent = '';
|
|
285
|
-
const callOptions = { model, reasoningEffort:
|
|
301
|
+
const callOptions = { model, reasoningEffort: this.getReasoningEffort(providerName, options) };
|
|
286
302
|
|
|
287
303
|
if (options.stream !== false) {
|
|
288
304
|
const gen = provider.stream(messages, tools, callOptions);
|
|
@@ -318,19 +334,21 @@ class AgentEngine {
|
|
|
318
334
|
lastContent = response.content || streamContent || '';
|
|
319
335
|
|
|
320
336
|
const assistantMessage = { role: 'assistant', content: lastContent };
|
|
321
|
-
if (response.toolCalls
|
|
322
|
-
assistantMessage.tool_calls = response.toolCalls;
|
|
323
|
-
}
|
|
337
|
+
if (response.toolCalls?.length) assistantMessage.tool_calls = response.toolCalls;
|
|
324
338
|
messages.push(assistantMessage);
|
|
325
339
|
|
|
326
340
|
if (conversationId) {
|
|
327
341
|
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tool_calls, tokens) VALUES (?, ?, ?, ?, ?)')
|
|
328
|
-
.run(
|
|
342
|
+
.run(
|
|
343
|
+
conversationId,
|
|
344
|
+
'assistant',
|
|
345
|
+
lastContent,
|
|
346
|
+
response.toolCalls?.length ? JSON.stringify(response.toolCalls) : null,
|
|
347
|
+
response.usage?.totalTokens || 0
|
|
348
|
+
);
|
|
329
349
|
}
|
|
330
350
|
|
|
331
|
-
if (!response.toolCalls || response.toolCalls.length === 0)
|
|
332
|
-
break;
|
|
333
|
-
}
|
|
351
|
+
if (!response.toolCalls || response.toolCalls.length === 0) break;
|
|
334
352
|
|
|
335
353
|
for (const toolCall of response.toolCalls) {
|
|
336
354
|
stepIndex++;
|
|
@@ -347,40 +365,37 @@ class AgentEngine {
|
|
|
347
365
|
.run(stepId, runId, stepIndex, this.getStepType(toolName), `${toolName}: ${JSON.stringify(toolArgs).slice(0, 200)} `, 'running', toolName, JSON.stringify(toolArgs));
|
|
348
366
|
|
|
349
367
|
this.emit(userId, 'run:tool_start', {
|
|
350
|
-
runId, stepId, stepIndex, toolName, toolArgs
|
|
368
|
+
runId, stepId, stepIndex, toolName, toolArgs,
|
|
351
369
|
type: this.getStepType(toolName)
|
|
352
370
|
});
|
|
353
371
|
|
|
354
372
|
let toolResult;
|
|
355
373
|
try {
|
|
356
|
-
toolResult = await this.executeTool(toolName, toolArgs, {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
|
|
374
|
+
toolResult = await this.executeTool(toolName, toolArgs, {
|
|
375
|
+
userId,
|
|
376
|
+
runId,
|
|
377
|
+
app,
|
|
378
|
+
triggerSource
|
|
379
|
+
});
|
|
380
|
+
const screenshotPath = toolResult?.screenshotPath || null;
|
|
363
381
|
db.prepare('UPDATE agent_steps SET status = ?, result = ?, screenshot_path = ?, completed_at = datetime(\'now\') WHERE id = ?')
|
|
364
382
|
.run('completed', JSON.stringify(toolResult).slice(0, 20000), screenshotPath, stepId);
|
|
365
|
-
|
|
366
|
-
this.emit(userId, 'run:tool_end', {
|
|
367
|
-
runId, stepId, toolName, result: toolResult, screenshotPath,
|
|
368
|
-
status: 'completed'
|
|
369
|
-
});
|
|
383
|
+
this.emit(userId, 'run:tool_end', { runId, stepId, toolName, result: toolResult, screenshotPath, status: 'completed' });
|
|
370
384
|
} catch (err) {
|
|
371
385
|
toolResult = { error: err.message };
|
|
372
386
|
db.prepare('UPDATE agent_steps SET status = ?, error = ?, completed_at = datetime(\'now\') WHERE id = ?')
|
|
373
387
|
.run('failed', err.message, stepId);
|
|
374
|
-
|
|
375
|
-
this.emit(userId, 'run:tool_end', {
|
|
376
|
-
runId, stepId, toolName, error: err.message, status: 'failed'
|
|
377
|
-
});
|
|
388
|
+
this.emit(userId, 'run:tool_end', { runId, stepId, toolName, error: err.message, status: 'failed' });
|
|
378
389
|
}
|
|
379
390
|
|
|
380
391
|
const toolMessage = {
|
|
381
392
|
role: 'tool',
|
|
393
|
+
name: toolName,
|
|
382
394
|
tool_call_id: toolCall.id,
|
|
383
|
-
content:
|
|
395
|
+
content: compactToolResult(toolName, toolArgs, toolResult, {
|
|
396
|
+
softLimit: toolReplayBudget,
|
|
397
|
+
hardLimit: 2000
|
|
398
|
+
})
|
|
384
399
|
};
|
|
385
400
|
messages.push(toolMessage);
|
|
386
401
|
|
|
@@ -392,30 +407,19 @@ class AgentEngine {
|
|
|
392
407
|
const runMeta = this.activeRuns.get(runId);
|
|
393
408
|
if (runMeta) {
|
|
394
409
|
runMeta.lastToolName = toolName;
|
|
395
|
-
runMeta.lastToolTarget =
|
|
410
|
+
runMeta.lastToolTarget = toolName === 'send_message' ? toolArgs.to : null;
|
|
396
411
|
}
|
|
397
412
|
}
|
|
398
413
|
|
|
399
414
|
if (!this.activeRuns.has(runId)) break;
|
|
400
415
|
}
|
|
401
416
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
if ((iteration >= this.maxIterations && messages[messages.length - 1].role === 'tool') ||
|
|
408
|
-
(iteration < this.maxIterations && stepIndex > 0 && !lastContent.trim() && messages[messages.length - 1].role !== 'tool')) {
|
|
409
|
-
|
|
410
|
-
const callOptions = { model, reasoningEffort: options.reasoningEffort || process.env.REASONING_EFFORT || undefined };
|
|
411
|
-
|
|
412
|
-
// Push an explicit instruction to force the model to summarize its tool results
|
|
413
|
-
messages.push({
|
|
414
|
-
role: 'system',
|
|
415
|
-
content: 'You have finished executing your tools, but you did not provide a final text response. Please provide a final, natural-language summary or response to the user based on your findings.'
|
|
417
|
+
if ((iteration >= maxIterations && messages[messages.length - 1]?.role === 'tool')
|
|
418
|
+
|| (iteration < maxIterations && stepIndex > 0 && !lastContent.trim() && messages[messages.length - 1]?.role !== 'tool')) {
|
|
419
|
+
const finalResponse = await provider.chat(messages, [], {
|
|
420
|
+
model,
|
|
421
|
+
reasoningEffort: this.getReasoningEffort(providerName, options)
|
|
416
422
|
});
|
|
417
|
-
|
|
418
|
-
const finalResponse = await provider.chat(messages, [], callOptions);
|
|
419
423
|
lastContent = finalResponse.content || '';
|
|
420
424
|
forcedFinalResponse = true;
|
|
421
425
|
|
|
@@ -433,6 +437,33 @@ class AgentEngine {
|
|
|
433
437
|
if (conversationId) {
|
|
434
438
|
db.prepare('UPDATE conversations SET total_tokens = total_tokens + ?, updated_at = datetime(\'now\') WHERE id = ?')
|
|
435
439
|
.run(totalTokens, conversationId);
|
|
440
|
+
refreshConversationSummary(conversationId, provider, model, historyWindow).catch((err) => {
|
|
441
|
+
console.error('[AI] Conversation summary refresh failed:', err.message);
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
await this.persistPromptMetrics(runId, {
|
|
446
|
+
...promptMetrics,
|
|
447
|
+
finalTotalTokens: totalTokens
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
const autoSkillLearning = aiSettings.auto_skill_learning !== false && aiSettings.auto_skill_learning !== 'false';
|
|
451
|
+
if (autoSkillLearning && this.learningManager) {
|
|
452
|
+
const steps = db.prepare('SELECT * FROM agent_steps WHERE run_id = ? ORDER BY step_index ASC').all(runId);
|
|
453
|
+
try {
|
|
454
|
+
this.learningManager.maybeCaptureDraft({
|
|
455
|
+
userId,
|
|
456
|
+
runId,
|
|
457
|
+
triggerSource,
|
|
458
|
+
triggerType,
|
|
459
|
+
task: userMessage,
|
|
460
|
+
title: runTitle,
|
|
461
|
+
finalContent: lastContent,
|
|
462
|
+
steps
|
|
463
|
+
});
|
|
464
|
+
} catch (learningErr) {
|
|
465
|
+
console.error('[AI] Skill draft capture failed:', learningErr.message);
|
|
466
|
+
}
|
|
436
467
|
}
|
|
437
468
|
|
|
438
469
|
const runMeta = this.activeRuns.get(runId);
|
|
@@ -447,15 +478,15 @@ class AgentEngine {
|
|
|
447
478
|
if (lastContent && lastContent.trim() && lastContent.trim() !== '[NO RESPONSE]') {
|
|
448
479
|
const manager = this.messagingManager;
|
|
449
480
|
if (manager) {
|
|
450
|
-
const chunks = lastContent.split(/\n\s*\n/).filter(c => c.trim().length > 0);
|
|
481
|
+
const chunks = lastContent.split(/\n\s*\n/).filter((c) => c.trim().length > 0);
|
|
451
482
|
(async () => {
|
|
452
483
|
for (let i = 0; i < chunks.length; i++) {
|
|
453
484
|
if (i > 0) {
|
|
454
485
|
const delay = Math.max(1000, Math.min(chunks[i].length * 30, 4000));
|
|
455
486
|
await manager.sendTyping(userId, options.source, options.chatId, true).catch(() => { });
|
|
456
|
-
await new Promise(
|
|
487
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
457
488
|
}
|
|
458
|
-
await manager.sendMessage(userId, options.source, options.chatId, chunks[i]).catch(err =>
|
|
489
|
+
await manager.sendMessage(userId, options.source, options.chatId, chunks[i]).catch((err) =>
|
|
459
490
|
console.error('[Engine] Auto-reply fallback failed:', err.message)
|
|
460
491
|
);
|
|
461
492
|
}
|
|
@@ -496,21 +527,12 @@ class AgentEngine {
|
|
|
496
527
|
if (toolName.startsWith('memory_')) return 'memory';
|
|
497
528
|
if (toolName === 'send_message') return 'messaging';
|
|
498
529
|
if (toolName === 'make_call') return 'messaging';
|
|
499
|
-
if (toolName
|
|
530
|
+
if (toolName.startsWith('mcp_') || toolName.includes('mcp')) return 'mcp';
|
|
531
|
+
if (toolName.includes('scheduled_task') || toolName === 'schedule_run') return 'scheduler';
|
|
500
532
|
if (toolName === 'think') return 'thinking';
|
|
501
|
-
if (toolName.includes('scheduled_task')) return 'scheduler';
|
|
502
533
|
return 'tool';
|
|
503
534
|
}
|
|
504
535
|
|
|
505
|
-
estimateTokens(messages) {
|
|
506
|
-
let total = 0;
|
|
507
|
-
for (const msg of messages) {
|
|
508
|
-
if (msg.content) total += Math.ceil(msg.content.length / 4);
|
|
509
|
-
if (msg.tool_calls) total += Math.ceil(JSON.stringify(msg.tool_calls).length / 4);
|
|
510
|
-
}
|
|
511
|
-
return total;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
536
|
emit(userId, event, data) {
|
|
515
537
|
if (this.io) {
|
|
516
538
|
this.io.to(`user:${userId}`).emit(event, data);
|
|
@@ -518,4 +540,4 @@ class AgentEngine {
|
|
|
518
540
|
}
|
|
519
541
|
}
|
|
520
542
|
|
|
521
|
-
module.exports = { AgentEngine };
|
|
543
|
+
module.exports = { AgentEngine, getProviderForUser };
|